diff --git a/crates/paimon/src/spec/binary_row.rs b/crates/paimon/src/spec/binary_row.rs index ce08ad8b..236cf91e 100644 --- a/crates/paimon/src/spec/binary_row.rs +++ b/crates/paimon/src/spec/binary_row.rs @@ -119,6 +119,14 @@ impl BinaryRow { /// Serialize this BinaryRow to bytes (arity prefix + data), the inverse of `from_serialized_bytes`. pub fn to_serialized_bytes(&self) -> Vec { + // Java's BinaryRow.EMPTY_ROW points to its 8-byte fixed part, so the + // schemaless wire representation is 4 bytes of arity plus that body. + // BinaryRow::new(0) is an in-memory stub without backing data; normalize + // it here instead of emitting a truncated row that the strict decoder + // correctly rejects. + if self.arity == 0 && self.data.is_empty() { + return EMPTY_SERIALIZED_ROW.clone(); + } let mut buf = Vec::with_capacity(4 + self.data.len()); buf.extend_from_slice(&self.arity.to_be_bytes()); buf.extend_from_slice(&self.data); @@ -1556,6 +1564,17 @@ mod tests { assert_eq!(row.data(), &[] as &[u8]); } + #[test] + fn test_empty_binary_row_serializes_to_java_wire_format() { + let serialized = BinaryRow::new(0).to_serialized_bytes(); + assert_eq!(serialized, *EMPTY_SERIALIZED_ROW); + assert_eq!(serialized.len(), 12); + assert_eq!( + BinaryRow::from_serialized_bytes(&serialized).unwrap(), + BinaryRow::from_bytes(0, vec![0; 8]) + ); + } + #[test] fn test_binary_row_constants() { assert_eq!(BinaryRow::cal_bit_set_width_in_bytes(0), 8); diff --git a/crates/paimon/src/table/source.rs b/crates/paimon/src/table/source.rs index a8d6b785..41836580 100644 --- a/crates/paimon/src/table/source.rs +++ b/crates/paimon/src/table/source.rs @@ -1325,6 +1325,41 @@ mod tests { assert_eq!(restored.bucket_path(), split.bucket_path()); } + #[test] + fn data_split_native_round_trip_with_empty_partition() { + // Generated by Apache Paimon Java 1.4.2 using BinaryRow.EMPTY_ROW and + // DataSplit#serialize. The partition field is length 12: arity=0 plus + // the 8-byte fixed part initialized by Java's BinaryRow static block. + let java_golden = [ + 0xde, 0xc3, 0xd2, 0x30, 0x2c, 0x19, 0xec, 0x66, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, + 0x66, 0x69, 0x6c, 0x65, 0x3a, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x62, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x2d, 0x30, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + let split = DataSplit::builder() + .with_snapshot(1) + .with_partition(BinaryRow::new(0)) + .with_bucket(0) + .with_bucket_path("file:/tmp/bucket-0".to_string()) + .with_total_buckets(1) + .with_data_files(vec![]) + .build() + .unwrap(); + + let bytes = split.serialize().expect("serialize"); + assert_eq!(bytes, java_golden); + let restored = DataSplit::deserialize(&bytes).expect("deserialize"); + assert_eq!(restored.snapshot_id(), split.snapshot_id()); + assert_eq!(restored.partition().arity(), 0); + assert_eq!( + restored.partition().to_serialized_bytes(), + *crate::spec::EMPTY_SERIALIZED_ROW + ); + assert_eq!(restored.serialize().expect("reserialize"), bytes); + } + /// Raw convertible split without deletion files: physical sum is exact. #[test] fn test_merged_row_count_raw_convertible_sums_physical_rows() {