This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new b89aa34e fix(table): accept DataSplit version 9 and mirror 
_WRITE_COLS_SEQUENCES (#745)
b89aa34e is described below

commit b89aa34e76e28b7fc15d42203fd935999ca1ce46
Author: Junrui Lee <[email protected]>
AuthorDate: Wed Aug 26 10:24:40 2026 +0800

    fix(table): accept DataSplit version 9 and mirror _WRITE_COLS_SEQUENCES 
(#745)
---
 bindings/python/tests/test_read.py                 |   4 +-
 .../paimon/src/spec/avro/manifest_entry_decode.rs  |  55 +++++++
 crates/paimon/src/spec/data_file.rs                | 179 +++++++++++++++++++--
 crates/paimon/src/spec/manifest.rs                 |   1 +
 crates/paimon/src/spec/manifest_entry.rs           |   3 +-
 crates/paimon/src/spec/objects_file.rs             |  66 ++++++++
 crates/paimon/src/table/bin_pack.rs                |   1 +
 crates/paimon/src/table/data_evolution_reader.rs   |   1 +
 crates/paimon/src/table/data_evolution_writer.rs   |   1 +
 crates/paimon/src/table/data_file_reader.rs        |   3 +
 crates/paimon/src/table/data_file_writer.rs        |   1 +
 crates/paimon/src/table/format_table_scan.rs       |   1 +
 .../paimon/src/table/global_index_drop_builder.rs  |   1 +
 crates/paimon/src/table/goldens/datasplit_v9.bin   | Bin 0 -> 738 bytes
 crates/paimon/src/table/goldens/split_v1_data.bin  | Bin 896 -> 912 bytes
 .../paimon/src/table/goldens/split_v1_indexed.bin  | Bin 961 -> 977 bytes
 crates/paimon/src/table/kv_file_reader.rs          |   1 +
 crates/paimon/src/table/kv_file_writer.rs          |   1 +
 .../paimon/src/table/lumina_index_build_builder.rs |   1 +
 .../paimon/src/table/merge_tree_split_generator.rs |   1 +
 crates/paimon/src/table/partition_stat.rs          |   1 +
 .../paimon/src/table/pk_full_text_bucket_search.rs |   1 +
 .../paimon/src/table/pk_full_text_bucket_state.rs  |   1 +
 crates/paimon/src/table/pk_full_text_read.rs       |   1 +
 crates/paimon/src/table/pk_full_text_scan.rs       |   1 +
 .../paimon/src/table/pk_vector_data_file_reader.rs |   1 +
 .../src/table/pk_vector_indexed_split_read.rs      |   2 +
 crates/paimon/src/table/pk_vector_orchestrator.rs  |   2 +
 crates/paimon/src/table/pk_vector_position_read.rs |   1 +
 crates/paimon/src/table/pk_vector_scan.rs          |   1 +
 crates/paimon/src/table/postpone_file_writer.rs    |   1 +
 crates/paimon/src/table/referenced_files.rs        |   3 +
 .../src/table/sorted_global_index_build_builder.rs |   1 +
 crates/paimon/src/table/source.rs                  | 106 +++++++++---
 crates/paimon/src/table/table_commit.rs            |   1 +
 crates/paimon/src/table/table_read.rs              |   1 +
 crates/paimon/src/table/table_scan.rs              |   2 +
 crates/paimon/src/table/vector_search_builder.rs   |   2 +
 .../paimon/src/table/vindex_index_build_builder.rs |   1 +
 39 files changed, 412 insertions(+), 39 deletions(-)

diff --git a/bindings/python/tests/test_read.py 
b/bindings/python/tests/test_read.py
index 49ed926d..c53d656a 100644
--- a/bindings/python/tests/test_read.py
+++ b/bindings/python/tests/test_read.py
@@ -736,10 +736,10 @@ def test_split_serialize_produces_split_v1_binary():
         version, type_id = struct.unpack_from(">ii", data, 8)
         assert version == 1
         assert type_id == 1
-        # DataSplit v8 body follows: MAGIC + VERSION(8)
+        # DataSplit body follows: MAGIC + VERSION
         magic, body_version = struct.unpack_from(">qi", data, 16)
         assert magic == -2394839472490812314
-        assert body_version == 8
+        assert body_version == 9
         assert splits[0].serialize() == data  # deterministic
 
 
diff --git a/crates/paimon/src/spec/avro/manifest_entry_decode.rs 
b/crates/paimon/src/spec/avro/manifest_entry_decode.rs
index df251bd3..0de57aae 100644
--- a/crates/paimon/src/spec/avro/manifest_entry_decode.rs
+++ b/crates/paimon/src/spec/avro/manifest_entry_decode.rs
@@ -221,6 +221,7 @@ fn decode_data_file_meta(
     let mut external_path: Option<String> = None;
     let mut first_row_id: Option<i64> = None;
     let mut write_cols: Option<Vec<String>> = None;
+    let mut column_max_sequence_numbers: Option<Vec<i64>> = None;
 
     for field in &writer_schema.fields {
         match field.name.as_str() {
@@ -260,6 +261,9 @@ fn decode_data_file_meta(
             "_EXTERNAL_PATH" => external_path = decode_nullable_string(cursor, 
field.nullable)?,
             "_FIRST_ROW_ID" => first_row_id = decode_nullable_long(cursor, 
field.nullable)?,
             "_WRITE_COLS" => write_cols = decode_nullable_string_array(cursor, 
field.nullable)?,
+            "_WRITE_COLS_SEQUENCES" => {
+                column_max_sequence_numbers = 
decode_nullable_long_array(cursor, field.nullable)?
+            }
             _ => skip_nullable_field(cursor, &field.schema, field.nullable)?,
         }
     }
@@ -285,6 +289,7 @@ fn decode_data_file_meta(
         external_path,
         first_row_id,
         write_cols,
+        column_max_sequence_numbers,
     })
 }
 
@@ -309,6 +314,55 @@ fn decode_string_array(cursor: &mut AvroCursor) -> 
crate::Result<Vec<String>> {
     Ok(result)
 }
 
+/// Decode an Avro `array<long>` whose items are plain (non-union) longs, 
mirroring
+/// [`decode_string_array`]. Paimon declares `_WRITE_COLS_SEQUENCES` as
+/// `array<bigint not null>`, so items carry no per-element union index.
+fn decode_long_array(cursor: &mut AvroCursor) -> crate::Result<Vec<i64>> {
+    let mut result = Vec::new();
+    loop {
+        let count = cursor.read_long()?;
+        if count == 0 {
+            break;
+        }
+        let count = if count < 0 {
+            cursor.skip_long()?;
+            neg_count_to_usize(count)?
+        } else {
+            count as usize
+        };
+        // A zigzag long is at least one byte, so the remaining input bounds 
how many
+        // elements can really follow. Reserving the declared count instead 
would let a
+        // corrupt manifest ask for an arbitrary allocation before the first 
read fails.
+        result.reserve(count.min(cursor.remaining()));
+        for _ in 0..count {
+            result.push(cursor.read_long()?);
+        }
+    }
+    Ok(result)
+}
+
+fn decode_nullable_long_array(
+    cursor: &mut AvroCursor,
+    nullable: bool,
+) -> crate::Result<Option<Vec<i64>>> {
+    if nullable {
+        // A two-branch `["null", array]` union only ever encodes 0 or 1. 
Anything else
+        // means the stream is not where the schema says it is, so stop rather 
than read
+        // the following bytes as an array.
+        match cursor.read_union_index()? {
+            0 => return Ok(None),
+            1 => {}
+            other => {
+                return Err(crate::Error::DataInvalid {
+                    message: format!("invalid union index {other} for 
_WRITE_COLS_SEQUENCES"),
+                    source: None,
+                })
+            }
+        }
+    }
+    Ok(Some(decode_long_array(cursor)?))
+}
+
 fn decode_nullable_long(cursor: &mut AvroCursor, nullable: bool) -> 
crate::Result<Option<i64>> {
     if nullable {
         let idx = cursor.read_union_index()?;
@@ -406,5 +460,6 @@ fn default_data_file_meta() -> DataFileMeta {
         external_path: None,
         first_row_id: None,
         write_cols: None,
+        column_max_sequence_numbers: None,
     }
 }
diff --git a/crates/paimon/src/spec/data_file.rs 
b/crates/paimon/src/spec/data_file.rs
index 314c6d15..97e2d2d0 100644
--- a/crates/paimon/src/spec/data_file.rs
+++ b/crates/paimon/src/spec/data_file.rs
@@ -17,7 +17,8 @@
 
 use crate::spec::stats::BinaryTableStats;
 use crate::spec::{
-    extract_datum, serialize_binary_array_str, BinaryRow, BinaryRowBuilder, 
DataField, Datum,
+    extract_datum, serialize_binary_array_long, serialize_binary_array_str, 
BinaryRow,
+    BinaryRowBuilder, DataField, Datum,
 };
 use chrono::serde::ts_milliseconds_option::deserialize as from_millis_opt;
 use chrono::serde::ts_milliseconds_option::serialize as to_millis_opt;
@@ -122,6 +123,16 @@ pub struct DataFileMeta {
         skip_serializing_if = "Option::is_none"
     )]
     pub write_cols: Option<Vec<String>>,
+
+    /// Per-column maximum sequence numbers, positionally aligned with
+    /// [`Self::write_cols`] (used in data evolution mode). Absent on files
+    /// written before this field existed.
+    #[serde(
+        rename = "_WRITE_COLS_SEQUENCES",
+        default,
+        skip_serializing_if = "Option::is_none"
+    )]
+    pub column_max_sequence_numbers: Option<Vec<i64>>,
 }
 
 impl Display for DataFileMeta {
@@ -132,7 +143,8 @@ impl Display for DataFileMeta {
              minKey={:?}, maxKey={:?}, keyStats={:?}, valueStats={:?}, \
              minSequenceNumber={}, maxSequenceNumber={}, schemaId={}, 
level={}, \
              extraFiles={:?}, creationTime={:?}, deleteRowCount={:?}, 
fileSource={:?}, \
-             valueStatsCols={:?}, externalPath={:?}, firstRowId={:?}, 
writeCols={:?}}}",
+             valueStatsCols={:?}, externalPath={:?}, firstRowId={:?}, 
writeCols={:?}, \
+             columnMaxSequenceNumbers={:?}}}",
             self.file_name,
             self.file_size,
             self.row_count,
@@ -153,10 +165,38 @@ impl Display for DataFileMeta {
             self.external_path,
             self.first_row_id,
             self.write_cols,
+            self.column_max_sequence_numbers,
         )
     }
 }
 
+/// Which `DataFileMeta.SCHEMA` revision a serialized `BinaryRow` follows.
+///
+/// The schema only ever grows by appending nullable fields, so the two layouts
+/// share slots 0..=19 and differ solely in arity. Which one a `DataSplit` body
+/// carries is decided by the split's own version, not by the row bytes.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DataFileMetaRowLayout {
+    /// 20 fields, ending at `_WRITE_COLS`. Mirrors Java
+    /// `DataFileMetaWriteColsLegacySerializer`.
+    WriteCols,
+    /// 21 fields, ending at `_WRITE_COLS_SEQUENCES`. Mirrors Java
+    /// `DataFileMetaSerializer`, and is what this crate writes.
+    WriteColsSequences,
+}
+
+impl DataFileMetaRowLayout {
+    /// The layout this crate serializes to, matching current Java.
+    pub const CURRENT: Self = Self::WriteColsSequences;
+
+    fn arity(self) -> usize {
+        match self {
+            Self::WriteCols => 20,
+            Self::WriteColsSequences => 21,
+        }
+    }
+}
+
 fn opt_long(b: &mut BinaryRowBuilder, pos: usize, v: Option<i64>) {
     match v {
         Some(x) => b.write_long(pos, x),
@@ -164,6 +204,26 @@ fn opt_long(b: &mut BinaryRowBuilder, pos: usize, v: 
Option<i64>) {
     }
 }
 
+/// Write an `array<bigint not null>` field, or a null at `pos`.
+fn opt_long_array(b: &mut BinaryRowBuilder, pos: usize, v: &Option<Vec<i64>>) {
+    match v {
+        Some(a) => {
+            let elements: Vec<Option<i64>> = 
a.iter().copied().map(Some).collect();
+            b.write_bytes(pos, &serialize_binary_array_long(&elements));
+        }
+        None => b.set_null_at(pos),
+    }
+}
+
+/// Read an `array<bigint not null>` field. Java declares the elements 
non-null, so a
+/// null element is malformed rather than something to silently map to a 
placeholder.
+fn read_non_null_long_array(row: &BinaryRow, pos: usize) -> 
crate::Result<Vec<i64>> {
+    crate::spec::deserialize_binary_array_long(row.get_binary(pos)?)?
+        .into_iter()
+        .map(|v| v.ok_or_else(|| data_file_err("array<bigint not null> has a 
null element")))
+        .collect()
+}
+
 fn opt_str_array(b: &mut BinaryRowBuilder, pos: usize, v: 
&Option<Vec<String>>) {
     match v {
         Some(a) => b.write_bytes(pos, &serialize_binary_array_str(a)),
@@ -282,11 +342,15 @@ impl DataFileMeta {
         Some((from, to))
     }
 
-    /// Serialize as a `DataFileMeta.SCHEMA` (version 8) BinaryRow, raw data 
without the
-    /// arity prefix -- the form `DataSplit#serialize` writes per file as 
`writeInt(len) + data`.
-    /// Fields, order and nullability mirror Java 
`DataFileMetaSerializer#toRow`.
+    /// Serialize as a [`DataFileMetaRowLayout::CURRENT`] 
`DataFileMeta.SCHEMA` BinaryRow,
+    /// raw data without the arity prefix -- the form `DataSplit#serialize` 
writes per file
+    /// as `writeInt(len) + data`. Fields, order and nullability mirror Java
+    /// `DataFileMetaSerializer#toRow`.
+    ///
+    /// Only the current layout is written. Reading an older one stays 
supported because a
+    /// `DataSplit` on disk or on the wire may still carry it.
     pub fn to_serialized_row_data(&self) -> crate::Result<Vec<u8>> {
-        let mut b = BinaryRowBuilder::new(20);
+        let mut b = 
BinaryRowBuilder::new(DataFileMetaRowLayout::CURRENT.arity() as i32);
         b.write_bytes(0, self.file_name.as_bytes());
         b.write_long(1, self.file_size);
         b.write_long(2, self.row_count);
@@ -325,14 +389,29 @@ impl DataFileMeta {
         }
         opt_long(&mut b, 18, self.first_row_id);
         opt_str_array(&mut b, 19, &self.write_cols);
+        opt_long_array(&mut b, 20, &self.column_max_sequence_numbers);
         Ok(b.build_row_data())
     }
 
-    /// Reverse of [`DataFileMeta::to_serialized_row_data`]: decode the fixed
-    /// 20-field `DataFileMeta` BinaryRow (version 8 layout).
-    pub fn from_serialized_row_data(data: &[u8]) -> 
crate::Result<DataFileMeta> {
+    /// Reverse of [`DataFileMeta::to_serialized_row_data`]: decode a 
`DataFileMeta`
+    /// BinaryRow written in `layout`.
+    ///
+    /// The row bytes carry no arity of their own, so the caller must supply 
the layout
+    /// that the enclosing container declared.
+    pub fn from_serialized_row_data(
+        data: &[u8],
+        layout: DataFileMetaRowLayout,
+    ) -> crate::Result<DataFileMeta> {
         use crate::spec::deserialize_binary_array_str;
-        let row = BinaryRow::from_bytes(20, data.to_vec());
+        let fixed_part = BinaryRow::cal_fix_part_size_in_bytes(layout.arity() 
as i32) as usize;
+        if data.len() < fixed_part {
+            return Err(data_file_err(&format!(
+                "DataFileMeta row of {} bytes is shorter than the 
{fixed_part}-byte fixed part \
+                 of the {layout:?} layout",
+                data.len()
+            )));
+        }
+        let row = BinaryRow::from_bytes(layout.arity() as i32, data.to_vec());
 
         let file_name = String::from_utf8(row.get_binary(0)?.to_vec())
             .map_err(|_| data_file_err("file_name is not valid UTF-8"))?;
@@ -390,6 +469,11 @@ impl DataFileMeta {
         } else {
             Some(deserialize_binary_array_str(row.get_binary(19)?)?)
         };
+        let column_max_sequence_numbers = match layout {
+            DataFileMetaRowLayout::WriteCols => None,
+            DataFileMetaRowLayout::WriteColsSequences if row.is_null_at(20) => 
None,
+            DataFileMetaRowLayout::WriteColsSequences => 
Some(read_non_null_long_array(&row, 20)?),
+        };
 
         Ok(DataFileMeta {
             file_name,
@@ -412,6 +496,7 @@ impl DataFileMeta {
             external_path,
             first_row_id,
             write_cols,
+            column_max_sequence_numbers,
         })
     }
 
@@ -524,6 +609,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(100),
             write_cols: Some(vec!["k".to_string(), "v".to_string()]),
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -565,6 +651,7 @@ mod tests {
             external_path: Some("s3://bucket/data-full.parquet".to_string()),
             first_row_id: Some(1_000),
             write_cols: Some(vec!["a".to_string(), "b".to_string(), 
"c".to_string()]),
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -591,6 +678,7 @@ mod tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -601,11 +689,79 @@ mod tests {
             sample_minimal_data_file_meta(),
         ] {
             let bytes = meta.to_serialized_row_data().unwrap();
-            let back = DataFileMeta::from_serialized_row_data(&bytes).unwrap();
+            let back =
+                DataFileMeta::from_serialized_row_data(&bytes, 
DataFileMetaRowLayout::CURRENT)
+                    .unwrap();
             assert_eq!(back, meta);
         }
     }
 
+    #[test]
+    fn column_max_sequence_numbers_round_trip() {
+        let mut meta = sample_full_data_file_meta();
+        meta.column_max_sequence_numbers = Some(vec![15, 100, 150, 200]);
+        let bytes = meta.to_serialized_row_data().unwrap();
+        let back =
+            DataFileMeta::from_serialized_row_data(&bytes, 
DataFileMetaRowLayout::CURRENT).unwrap();
+        assert_eq!(
+            back.column_max_sequence_numbers,
+            Some(vec![15, 100, 150, 200])
+        );
+        assert_eq!(back, meta);
+    }
+
+    #[test]
+    fn empty_column_max_sequence_numbers_round_trips() {
+        let mut meta = sample_full_data_file_meta();
+        meta.column_max_sequence_numbers = Some(Vec::new());
+        let bytes = meta.to_serialized_row_data().unwrap();
+        let back =
+            DataFileMeta::from_serialized_row_data(&bytes, 
DataFileMetaRowLayout::CURRENT).unwrap();
+        // An empty array must stay distinguishable from an absent one.
+        assert_eq!(back.column_max_sequence_numbers, Some(Vec::new()));
+        assert_eq!(back, meta);
+    }
+
+    #[test]
+    fn row_shorter_than_the_layout_fixed_part_is_rejected() {
+        let meta = sample_full_data_file_meta();
+        let bytes = meta.to_serialized_row_data().unwrap();
+        let fixed_part =
+            
BinaryRow::cal_fix_part_size_in_bytes(DataFileMetaRowLayout::CURRENT.arity() as 
i32)
+                as usize;
+
+        for truncated in [0, fixed_part - 1] {
+            assert!(
+                DataFileMeta::from_serialized_row_data(
+                    &bytes[..truncated],
+                    DataFileMetaRowLayout::CURRENT
+                )
+                .is_err(),
+                "a {truncated}-byte row must not decode as the current layout"
+            );
+        }
+        assert!(
+            DataFileMeta::from_serialized_row_data(&bytes, 
DataFileMetaRowLayout::CURRENT).is_ok()
+        );
+    }
+
+    /// The legacy layout has no slot for the field, so decoding a row as 
`WriteCols`
+    /// yields `None` rather than reading past the fields that layout 
declares. Java does
+    /// the same: 
`DataFileMetaSerializerTest#testLegacySerializerDropsColumnSequences`
+    /// round-trips through the legacy serializer and expects the field back 
as null.
+    #[test]
+    fn legacy_layout_decodes_without_column_max_sequence_numbers() {
+        let mut meta = sample_full_data_file_meta();
+        meta.column_max_sequence_numbers = Some(vec![7]);
+        let bytes = meta.to_serialized_row_data().unwrap();
+
+        let back = DataFileMeta::from_serialized_row_data(&bytes, 
DataFileMetaRowLayout::WriteCols)
+            .unwrap();
+        assert_eq!(back.column_max_sequence_numbers, None);
+        meta.column_max_sequence_numbers = None;
+        assert_eq!(back, meta);
+    }
+
     #[test]
     fn row_id_range_rejects_non_positive_count_and_overflow() {
         let mut file = data_file("data.parquet");
@@ -707,6 +863,7 @@ mod tests {
             "externalPath=Some(\"s3://bucket/data-1.parquet\")",
             "firstRowId=Some(100)",
             "writeCols=Some([\"k\", \"v\"])",
+            "columnMaxSequenceNumbers=None",
         ] {
             assert!(
                 display.contains(expected),
diff --git a/crates/paimon/src/spec/manifest.rs 
b/crates/paimon/src/spec/manifest.rs
index c25dba9a..ed2fa162 100644
--- a/crates/paimon/src/spec/manifest.rs
+++ b/crates/paimon/src/spec/manifest.rs
@@ -166,6 +166,7 @@ mod tests {
                 external_path: None,
                 first_row_id: None,
                 write_cols: None,
+                column_max_sequence_numbers: None,
             };
             ManifestEntry::new(kind, vec![], 0, 1, file, 2)
         }
diff --git a/crates/paimon/src/spec/manifest_entry.rs 
b/crates/paimon/src/spec/manifest_entry.rs
index 63473247..484f2bd9 100644
--- a/crates/paimon/src/spec/manifest_entry.rs
+++ b/crates/paimon/src/spec/manifest_entry.rs
@@ -231,7 +231,8 @@ pub const MANIFEST_ENTRY_SCHEMA: &str = r#"["null", {
                 {"name": "_VALUE_STATS_COLS", "type": ["null", {"type": 
"array", "items": "string"}], "default": null},
                 {"name": "_EXTERNAL_PATH", "type": ["null", "string"], 
"default": null},
                 {"name": "_FIRST_ROW_ID", "type": ["null", "long"], "default": 
null},
-                {"name": "_WRITE_COLS", "type": ["null", {"type": "array", 
"items": "string"}], "default": null}
+                {"name": "_WRITE_COLS", "type": ["null", {"type": "array", 
"items": "string"}], "default": null},
+                {"name": "_WRITE_COLS_SEQUENCES", "type": ["null", {"type": 
"array", "items": "long"}], "default": null}
             ]
         }}
     ]
diff --git a/crates/paimon/src/spec/objects_file.rs 
b/crates/paimon/src/spec/objects_file.rs
index 7db066fa..d5d0d9fe 100644
--- a/crates/paimon/src/spec/objects_file.rs
+++ b/crates/paimon/src/spec/objects_file.rs
@@ -89,6 +89,12 @@ mod tests {
     use chrono::{DateTime, Utc};
 
     fn manifest_entry() -> ManifestEntry {
+        manifest_entry_with_sequences(None)
+    }
+
+    fn manifest_entry_with_sequences(
+        column_max_sequence_numbers: Option<Vec<i64>>,
+    ) -> ManifestEntry {
         let value_bytes = vec![
             0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 129, 1, 
0, 0, 0, 0, 0, 0, 0,
         ];
@@ -131,6 +137,7 @@ mod tests {
                 external_path: None,
                 file_source: None,
                 value_stats_cols: None,
+                column_max_sequence_numbers,
             },
             2,
         )
@@ -189,6 +196,63 @@ mod tests {
         assert_eq!(original, decoded);
     }
 
+    /// The Avro manifest record carries `_WRITE_COLS_SEQUENCES`, so a 
populated value must
+    /// survive a write/read round trip rather than being dropped on either 
side.
+    #[test]
+    fn test_roundtrip_manifest_entry_with_column_max_sequence_numbers() {
+        for value in [Some(vec![15, 100, 150, 200]), Some(Vec::new()), None] {
+            let original = vec![manifest_entry_with_sequences(value.clone())];
+            let bytes = to_avro_bytes(MANIFEST_ENTRY_SCHEMA, 
&original).unwrap();
+
+            let decoded = from_avro_bytes::<ManifestEntry>(&bytes).unwrap();
+            assert_eq!(decoded, original);
+            assert_eq!(decoded[0].file().column_max_sequence_numbers, value);
+
+            // The hand-written decoder reads the same bytes by field name.
+            let fast = from_avro_bytes_fast::<ManifestEntry>(&bytes).unwrap();
+            assert_eq!(fast, original);
+            assert_eq!(fast[0].file().column_max_sequence_numbers, value);
+        }
+    }
+
+    /// A writer that predates the field omits it entirely; it must read back 
as absent.
+    #[test]
+    fn test_read_manifest_entry_without_column_max_sequence_numbers_field() {
+        let mut schema: serde_json::Value = 
serde_json::from_str(MANIFEST_ENTRY_SCHEMA).unwrap();
+        let fields = schema.as_array_mut().unwrap()[1]
+            .as_object_mut()
+            .unwrap()
+            .get_mut("fields")
+            .unwrap()
+            .as_array_mut()
+            .unwrap();
+        let file_fields = fields
+            .iter_mut()
+            .find(|field| field.get("name").and_then(|name| name.as_str()) == 
Some("_FILE"))
+            .unwrap()
+            .get_mut("type")
+            .unwrap()
+            .get_mut("fields")
+            .unwrap()
+            .as_array_mut()
+            .unwrap();
+        let before = file_fields.len();
+        file_fields.retain(|field| {
+            field.get("name").and_then(|name| name.as_str()) != 
Some("_WRITE_COLS_SEQUENCES")
+        });
+        assert_eq!(
+            file_fields.len(),
+            before - 1,
+            "field must exist to be removed"
+        );
+        let legacy_schema = serde_json::to_string(&schema).unwrap();
+
+        let original = vec![manifest_entry()];
+        let bytes = to_avro_bytes(&legacy_schema, &original).unwrap();
+        let decoded = from_avro_bytes_fast::<ManifestEntry>(&bytes).unwrap();
+        assert_eq!(decoded[0].file().column_max_sequence_numbers, None);
+    }
+
     #[test]
     fn test_read_manifest_entry_with_legacy_rust_schema() {
         let mut schema: serde_json::Value = 
serde_json::from_str(MANIFEST_ENTRY_SCHEMA).unwrap();
@@ -315,6 +379,7 @@ mod tests {
                         external_path: None,
                         file_source: None,
                         value_stats_cols: None,
+                        column_max_sequence_numbers: None,
                     },
                     2
                 ),
@@ -356,6 +421,7 @@ mod tests {
                         external_path: None,
                         file_source: None,
                         value_stats_cols: None,
+                        column_max_sequence_numbers: None,
                     },
                     2
                 ),
diff --git a/crates/paimon/src/table/bin_pack.rs 
b/crates/paimon/src/table/bin_pack.rs
index b4dc178a..8c582695 100644
--- a/crates/paimon/src/table/bin_pack.rs
+++ b/crates/paimon/src/table/bin_pack.rs
@@ -101,6 +101,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/data_evolution_reader.rs 
b/crates/paimon/src/table/data_evolution_reader.rs
index 11a4cb96..a456505a 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -6389,6 +6389,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(first_row_id),
             write_cols: write_cols.map(|cols| 
cols.into_iter().map(str::to_string).collect()),
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/data_evolution_writer.rs 
b/crates/paimon/src/table/data_evolution_writer.rs
index 078a2a12..d8b66e86 100644
--- a/crates/paimon/src/table/data_evolution_writer.rs
+++ b/crates/paimon/src/table/data_evolution_writer.rs
@@ -1218,6 +1218,7 @@ mod tests {
             external_path: None,
             first_row_id,
             write_cols,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/data_file_reader.rs 
b/crates/paimon/src/table/data_file_reader.rs
index c9e2e1a7..526f8c7c 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -1102,6 +1102,7 @@ mod row_tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -1543,6 +1544,7 @@ mod tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -1994,6 +1996,7 @@ mod vector_parquet_tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/data_file_writer.rs 
b/crates/paimon/src/table/data_file_writer.rs
index 75f17c6f..76f49a0d 100644
--- a/crates/paimon/src/table/data_file_writer.rs
+++ b/crates/paimon/src/table/data_file_writer.rs
@@ -276,6 +276,7 @@ impl DataFileWriter {
             external_path: None,
             first_row_id,
             write_cols,
+            column_max_sequence_numbers: None,
         }
     }
 }
diff --git a/crates/paimon/src/table/format_table_scan.rs 
b/crates/paimon/src/table/format_table_scan.rs
index 5a6a00c8..1638c0a6 100644
--- a/crates/paimon/src/table/format_table_scan.rs
+++ b/crates/paimon/src/table/format_table_scan.rs
@@ -669,6 +669,7 @@ fn data_file_meta(file_name: String, file_size: i64, 
schema_id: i64) -> DataFile
         external_path: None,
         first_row_id: None,
         write_cols: None,
+        column_max_sequence_numbers: None,
     }
 }
 
diff --git a/crates/paimon/src/table/global_index_drop_builder.rs 
b/crates/paimon/src/table/global_index_drop_builder.rs
index 80a0ad7f..6bc70e72 100644
--- a/crates/paimon/src/table/global_index_drop_builder.rs
+++ b/crates/paimon/src/table/global_index_drop_builder.rs
@@ -327,6 +327,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/goldens/datasplit_v9.bin 
b/crates/paimon/src/table/goldens/datasplit_v9.bin
new file mode 100644
index 00000000..2bfd4ac1
Binary files /dev/null and b/crates/paimon/src/table/goldens/datasplit_v9.bin 
differ
diff --git a/crates/paimon/src/table/goldens/split_v1_data.bin 
b/crates/paimon/src/table/goldens/split_v1_data.bin
index 6cbade9c..9d2f6c08 100644
Binary files a/crates/paimon/src/table/goldens/split_v1_data.bin and 
b/crates/paimon/src/table/goldens/split_v1_data.bin differ
diff --git a/crates/paimon/src/table/goldens/split_v1_indexed.bin 
b/crates/paimon/src/table/goldens/split_v1_indexed.bin
index 0d20df10..eab6fbe8 100644
Binary files a/crates/paimon/src/table/goldens/split_v1_indexed.bin and 
b/crates/paimon/src/table/goldens/split_v1_indexed.bin differ
diff --git a/crates/paimon/src/table/kv_file_reader.rs 
b/crates/paimon/src/table/kv_file_reader.rs
index d922c267..88b1bf80 100644
--- a/crates/paimon/src/table/kv_file_reader.rs
+++ b/crates/paimon/src/table/kv_file_reader.rs
@@ -1003,6 +1003,7 @@ mod tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/kv_file_writer.rs 
b/crates/paimon/src/table/kv_file_writer.rs
index b5e94f54..bf1cd9aa 100644
--- a/crates/paimon/src/table/kv_file_writer.rs
+++ b/crates/paimon/src/table/kv_file_writer.rs
@@ -533,6 +533,7 @@ impl KeyValueFileWriter {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         })
     }
 
diff --git a/crates/paimon/src/table/lumina_index_build_builder.rs 
b/crates/paimon/src/table/lumina_index_build_builder.rs
index e98aedbe..7bed2cc3 100644
--- a/crates/paimon/src/table/lumina_index_build_builder.rs
+++ b/crates/paimon/src/table/lumina_index_build_builder.rs
@@ -906,6 +906,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/merge_tree_split_generator.rs 
b/crates/paimon/src/table/merge_tree_split_generator.rs
index cceee451..6f33d2be 100644
--- a/crates/paimon/src/table/merge_tree_split_generator.rs
+++ b/crates/paimon/src/table/merge_tree_split_generator.rs
@@ -460,6 +460,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/partition_stat.rs 
b/crates/paimon/src/table/partition_stat.rs
index ae5e5580..caa8c4a7 100644
--- a/crates/paimon/src/table/partition_stat.rs
+++ b/crates/paimon/src/table/partition_stat.rs
@@ -242,6 +242,7 @@ mod tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         };
         ManifestEntry::new(kind, partition, 0, 1, file, 2)
     }
diff --git a/crates/paimon/src/table/pk_full_text_bucket_search.rs 
b/crates/paimon/src/table/pk_full_text_bucket_search.rs
index 298733f0..d774e237 100644
--- a/crates/paimon/src/table/pk_full_text_bucket_search.rs
+++ b/crates/paimon/src/table/pk_full_text_bucket_search.rs
@@ -397,6 +397,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_full_text_bucket_state.rs 
b/crates/paimon/src/table/pk_full_text_bucket_state.rs
index f9122ea1..48ad4b21 100644
--- a/crates/paimon/src/table/pk_full_text_bucket_state.rs
+++ b/crates/paimon/src/table/pk_full_text_bucket_state.rs
@@ -255,6 +255,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_full_text_read.rs 
b/crates/paimon/src/table/pk_full_text_read.rs
index 3c5a34b1..63b58f84 100644
--- a/crates/paimon/src/table/pk_full_text_read.rs
+++ b/crates/paimon/src/table/pk_full_text_read.rs
@@ -719,6 +719,7 @@ mod read_tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_full_text_scan.rs 
b/crates/paimon/src/table/pk_full_text_scan.rs
index f6093858..5a677655 100644
--- a/crates/paimon/src/table/pk_full_text_scan.rs
+++ b/crates/paimon/src/table/pk_full_text_scan.rs
@@ -457,6 +457,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_vector_data_file_reader.rs 
b/crates/paimon/src/table/pk_vector_data_file_reader.rs
index 9fadaf2b..c3087beb 100644
--- a/crates/paimon/src/table/pk_vector_data_file_reader.rs
+++ b/crates/paimon/src/table/pk_vector_data_file_reader.rs
@@ -312,6 +312,7 @@ mod integration_tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_vector_indexed_split_read.rs 
b/crates/paimon/src/table/pk_vector_indexed_split_read.rs
index c6158108..d234ddca 100644
--- a/crates/paimon/src/table/pk_vector_indexed_split_read.rs
+++ b/crates/paimon/src/table/pk_vector_indexed_split_read.rs
@@ -212,6 +212,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -433,6 +434,7 @@ mod e2e_tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_vector_orchestrator.rs 
b/crates/paimon/src/table/pk_vector_orchestrator.rs
index 1a816bd9..ac0d3ab3 100644
--- a/crates/paimon/src/table/pk_vector_orchestrator.rs
+++ b/crates/paimon/src/table/pk_vector_orchestrator.rs
@@ -641,6 +641,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -974,6 +975,7 @@ mod e2e_tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_vector_position_read.rs 
b/crates/paimon/src/table/pk_vector_position_read.rs
index 29446822..06721816 100644
--- a/crates/paimon/src/table/pk_vector_position_read.rs
+++ b/crates/paimon/src/table/pk_vector_position_read.rs
@@ -338,6 +338,7 @@ mod tests {
             external_path: None,
             first_row_id,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/pk_vector_scan.rs 
b/crates/paimon/src/table/pk_vector_scan.rs
index 00bec032..cfc528b2 100644
--- a/crates/paimon/src/table/pk_vector_scan.rs
+++ b/crates/paimon/src/table/pk_vector_scan.rs
@@ -414,6 +414,7 @@ mod tests {
             external_path: None,
             first_row_id: Some(0),
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/postpone_file_writer.rs 
b/crates/paimon/src/table/postpone_file_writer.rs
index 8ee83cba..978649cd 100644
--- a/crates/paimon/src/table/postpone_file_writer.rs
+++ b/crates/paimon/src/table/postpone_file_writer.rs
@@ -306,5 +306,6 @@ fn build_meta(
         external_path: None,
         first_row_id: None,
         write_cols: None,
+        column_max_sequence_numbers: None,
     }
 }
diff --git a/crates/paimon/src/table/referenced_files.rs 
b/crates/paimon/src/table/referenced_files.rs
index b602bbfd..851e8719 100644
--- a/crates/paimon/src/table/referenced_files.rs
+++ b/crates/paimon/src/table/referenced_files.rs
@@ -911,6 +911,7 @@ mod tests {
             external_path: Some("s3://bucket/external/data-0.row".to_string()),
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         };
 
         assert_eq!(
@@ -968,6 +969,7 @@ mod tests {
             external_path: Some(format!("{external_dir}/data-0.row")),
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         };
         let entry = ManifestEntry::new(FileKind::Add, vec![0u8; 12], 0, 1, 
data_file, 0);
         Manifest::write(&file_io, &manifest_path, &[entry])
@@ -1248,6 +1250,7 @@ mod tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         };
         let entry = ManifestEntry::new(FileKind::Add, vec![0u8; 12], 0, 1, 
data_file, 2);
         Manifest::write(&file_io, &manifest_path, &[entry])
diff --git a/crates/paimon/src/table/sorted_global_index_build_builder.rs 
b/crates/paimon/src/table/sorted_global_index_build_builder.rs
index f0657919..94fc3165 100644
--- a/crates/paimon/src/table/sorted_global_index_build_builder.rs
+++ b/crates/paimon/src/table/sorted_global_index_build_builder.rs
@@ -1084,6 +1084,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/source.rs 
b/crates/paimon/src/table/source.rs
index c1e697ec..38bfdb89 100644
--- a/crates/paimon/src/table/source.rs
+++ b/crates/paimon/src/table/source.rs
@@ -19,7 +19,7 @@
 //!
 //! Reference: 
[org.apache.paimon.table.source](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/table/source/).
 
-use crate::spec::{BinaryRow, DataFileMeta};
+use crate::spec::{BinaryRow, DataFileMeta, DataFileMetaRowLayout};
 use crate::table::stats_filter::group_by_overlapping_row_id;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
@@ -211,6 +211,7 @@ mod row_range_tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -682,8 +683,8 @@ impl DataSplit {
         DataSplitBuilder::new()
     }
 
-    /// Serialize the DataSplit fields to Java `DataSplit#serialize` (version 
8) binary.
-    /// Byte-compatible with `compatibility/datasplit-v8`. Row ranges are not 
part of the v8
+    /// Serialize the DataSplit fields to Java `DataSplit#serialize` (version 
9) binary.
+    /// Byte-compatible with `compatibility/datasplit-v9`. Row ranges are not 
part of the
     /// format; `serialize_split_v1` wraps a row-range split as an 
`IndexedSplit` instead.
     pub fn serialize(&self) -> crate::Result<Vec<u8>> {
         let mut out = Vec::new();
@@ -711,11 +712,12 @@ impl DataSplit {
         Ok(out)
     }
 
-    /// Reverse of [`DataSplit::serialize`]: parse a raw v8 
`DataSplit#serialize` body.
+    /// Reverse of [`DataSplit::serialize`]: parse a raw `DataSplit#serialize` 
body.
+    /// Both the current version and the legacy v8 layout are accepted.
     /// Consumes the entire buffer; trailing bytes are an error.
     pub fn deserialize(data: &[u8]) -> crate::Result<DataSplit> {
         let mut cur = data;
-        let split = Self::read_v8_body(&mut cur)?;
+        let split = Self::read_body(&mut cur)?;
         if !cur.is_empty() {
             return Err(crate::Error::DataInvalid {
                 message: format!("{} trailing bytes after DataSplit", 
cur.len()),
@@ -725,9 +727,9 @@ impl DataSplit {
         Ok(split)
     }
 
-    /// Read a v8 `DataSplit` body from the cursor, leaving it positioned 
after the body
+    /// Read a `DataSplit` body from the cursor, leaving it positioned after 
the body
     /// (used both by `deserialize` and the SPLIT_V1 frame reader).
-    fn read_v8_body(cur: &mut &[u8]) -> crate::Result<DataSplit> {
+    fn read_body(cur: &mut &[u8]) -> crate::Result<DataSplit> {
         let magic = read_i64(cur)?;
         if magic != SPLIT_MAGIC {
             return Err(crate::Error::DataInvalid {
@@ -736,17 +738,27 @@ impl DataSplit {
             });
         }
         match read_i32(cur)? {
-            SPLIT_VERSION => Self::read_v8_body_after_version(cur),
+            SPLIT_VERSION => {
+                Self::read_body_after_version(cur, 
DataFileMetaRowLayout::WriteColsSequences)
+            }
+            SPLIT_VERSION_LEGACY_WRITE_COLS => {
+                Self::read_body_after_version(cur, 
DataFileMetaRowLayout::WriteCols)
+            }
             version => Err(crate::Error::Unsupported {
                 message: format!(
-                    "DataSplit version {version} not supported (only 
v{SPLIT_VERSION})"
+                    "DataSplit version {version} not supported \
+                     (only v{SPLIT_VERSION_LEGACY_WRITE_COLS} and 
v{SPLIT_VERSION})"
                 ),
             }),
         }
     }
 
-    /// Read the fields following the magic + version header of a v8 
`DataSplit` body.
-    fn read_v8_body_after_version(cur: &mut &[u8]) -> crate::Result<DataSplit> 
{
+    /// Read the fields following the magic + version header of a `DataSplit` 
body. Only the
+    /// per-file `DataFileMeta` row layout varies between the supported 
versions.
+    fn read_body_after_version(
+        cur: &mut &[u8],
+        file_layout: DataFileMetaRowLayout,
+    ) -> crate::Result<DataSplit> {
         let snapshot_id = read_i64(cur)?;
 
         let part_len = read_i32(cur)?;
@@ -803,7 +815,7 @@ impl DataSplit {
                 });
             }
             let row = take(cur, len as usize)?;
-            data_files.push(DataFileMeta::from_serialized_row_data(row)?);
+            data_files.push(DataFileMeta::from_serialized_row_data(row, 
file_layout)?);
         }
 
         let data_deletion_files = read_deletion_list(cur)?;
@@ -881,7 +893,7 @@ impl DataSplit {
         }
         let type_id = read_i32(&mut cur)?;
         let split = match type_id {
-            SPLIT_SER_TYPE_DATA_SPLIT => Self::read_v8_body(&mut cur)?,
+            SPLIT_SER_TYPE_DATA_SPLIT => Self::read_body(&mut cur)?,
             SPLIT_SER_TYPE_INDEXED_SPLIT => {
                 let im = read_i64(&mut cur)?;
                 if im != INDEXED_SPLIT_MAGIC {
@@ -896,7 +908,7 @@ impl DataSplit {
                         message: format!("IndexedSplit version {iv} not 
supported"),
                     });
                 }
-                let body = Self::read_v8_body(&mut cur)?;
+                let body = Self::read_body(&mut cur)?;
                 let ranges_n = read_i32(&mut cur)?;
                 if ranges_n < 0 {
                     return Err(crate::Error::DataInvalid {
@@ -946,7 +958,11 @@ impl DataSplit {
 
 /// Java `DataSplit#MAGIC` / `VERSION` for the serialize format.
 const SPLIT_MAGIC: i64 = -2394839472490812314;
-const SPLIT_VERSION: i32 = 8;
+const SPLIT_VERSION: i32 = 9;
+/// Java bumped `DataSplit#VERSION` to 9 when it appended 
`_WRITE_COLS_SEQUENCES` to
+/// `DataFileMeta.SCHEMA`. The split body is unchanged; only the per-file row 
grew, so
+/// v8 bodies stay readable by decoding their files with the older row layout.
+const SPLIT_VERSION_LEGACY_WRITE_COLS: i32 = 8;
 
 /// Java `SplitSerializer` frame: magic "SPLIT_V1", version, and the 
`DataSplit` type id.
 const SPLIT_SER_MAGIC: i64 = 0x53504C49545F5631; // "SPLIT_V1"
@@ -1343,6 +1359,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -1444,7 +1461,11 @@ mod tests {
         // 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 = [
+        //
+        // The split carries no data files, so the only byte that the later 
version bump
+        // moved is the version int itself -- which makes this golden a check 
of the split
+        // body rather than of the per-file row layout.
+        let java_golden_v8 = [
             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,
@@ -1462,8 +1483,14 @@ mod tests {
             .build()
             .unwrap();
 
+        let mut expected = java_golden_v8;
+        expected[8..12].copy_from_slice(&SPLIT_VERSION.to_be_bytes());
+
         let bytes = split.serialize().expect("serialize");
-        assert_eq!(bytes, java_golden);
+        assert_eq!(bytes, expected);
+        // A legacy body still reads, and re-serializing it normalizes to the 
current version.
+        let from_v8 = 
DataSplit::deserialize(&java_golden_v8).expect("deserialize v8");
+        assert_eq!(from_v8.serialize().expect("reserialize v8"), expected);
         let restored = DataSplit::deserialize(&bytes).expect("deserialize");
         assert_eq!(restored.snapshot_id(), split.snapshot_id());
         assert_eq!(restored.partition().arity(), 0);
@@ -1747,16 +1774,17 @@ mod tests {
     }
 
     #[test]
-    fn serialize_matches_datasplit_v8() {
+    fn serialize_matches_datasplit_v9() {
         // Golden generated by Java (paimon-core DataSplitCompatibleTest) for 
cross-language parity.
-        let expected = include_bytes!("goldens/datasplit_v8.bin");
-        let split = sample_v8_split();
+        let expected = include_bytes!("goldens/datasplit_v9.bin");
+        let split = sample_v9_split();
         assert_eq!(split.serialize().unwrap().as_slice(), &expected[..]);
     }
 
-    /// The fixture split whose Java-generated bytes live in 
`goldens/datasplit_v8.bin`.
-    /// Shared by the serialize and deserialize golden tests.
-    fn sample_v8_split() -> DataSplit {
+    /// The fixture split behind the Java-generated 
`goldens/datasplit_v*.bin`. The two
+    /// goldens describe the same split; only `_WRITE_COLS_SEQUENCES` differs, 
matching
+    /// Java's `testSerializerCompatibleV8` / `testSerializerCompatibleV9`.
+    fn sample_split(column_max_sequence_numbers: Option<Vec<i64>>) -> 
DataSplit {
         use chrono::DateTime;
 
         let mut pb = crate::spec::BinaryRowBuilder::new(1);
@@ -1796,6 +1824,7 @@ mod tests {
                     .map(|s| s.to_string())
                     .collect(),
             ),
+            column_max_sequence_numbers,
         };
 
         DataSplitBuilder::new()
@@ -1816,6 +1845,18 @@ mod tests {
             .unwrap()
     }
 
+    /// The v8 golden's split: its file predates `_WRITE_COLS_SEQUENCES`.
+    fn sample_v8_split() -> DataSplit {
+        sample_split(None)
+    }
+
+    /// The v9 golden's split: the same file, carrying per-column maximum 
sequence numbers.
+    fn sample_v9_split() -> DataSplit {
+        sample_split(Some(vec![15, 100, 150, 200]))
+    }
+
+    /// A v8 body stays readable: the split is identical apart from the field 
that
+    /// layout has no slot for.
     #[test]
     fn deserialize_matches_datasplit_v8_golden() {
         let golden = include_bytes!("goldens/datasplit_v8.bin");
@@ -1823,9 +1864,23 @@ mod tests {
         assert_eq!(split, sample_v8_split());
     }
 
+    #[test]
+    fn deserialize_matches_datasplit_v9_golden() {
+        let golden = include_bytes!("goldens/datasplit_v9.bin");
+        let split = DataSplit::deserialize(golden).unwrap();
+        assert_eq!(split, sample_v9_split());
+    }
+
+    #[test]
+    fn deserialize_rejects_unsupported_version() {
+        let mut bytes = sample_v9_split().serialize().unwrap();
+        bytes[8..12].copy_from_slice(&7i32.to_be_bytes());
+        assert!(DataSplit::deserialize(&bytes).is_err());
+    }
+
     #[test]
     fn deserialize_round_trips_serialize() {
-        let split = sample_v8_split();
+        let split = sample_v9_split();
         assert_eq!(
             DataSplit::deserialize(&split.serialize().unwrap()).unwrap(),
             split
@@ -1834,7 +1889,7 @@ mod tests {
 
     #[test]
     fn deserialize_rejects_trailing_bytes() {
-        let mut bytes = sample_v8_split().serialize().unwrap();
+        let mut bytes = sample_v9_split().serialize().unwrap();
         bytes.push(0xFF);
         assert!(DataSplit::deserialize(&bytes).is_err());
     }
@@ -2114,6 +2169,7 @@ mod tests {
             external_path: None,
             first_row_id: None,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/table_commit.rs 
b/crates/paimon/src/table/table_commit.rs
index e5f09f9e..7bf70936 100644
--- a/crates/paimon/src/table/table_commit.rs
+++ b/crates/paimon/src/table/table_commit.rs
@@ -3265,6 +3265,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/table_read.rs 
b/crates/paimon/src/table/table_read.rs
index 4ff0c5a0..99c01f7d 100644
--- a/crates/paimon/src/table/table_read.rs
+++ b/crates/paimon/src/table/table_read.rs
@@ -1509,6 +1509,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/table_scan.rs 
b/crates/paimon/src/table/table_scan.rs
index b28915f6..601eeae9 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -2174,6 +2174,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -2556,6 +2557,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/vector_search_builder.rs 
b/crates/paimon/src/table/vector_search_builder.rs
index 4692b6c1..497ccd98 100644
--- a/crates/paimon/src/table/vector_search_builder.rs
+++ b/crates/paimon/src/table/vector_search_builder.rs
@@ -4630,6 +4630,7 @@ mod tests {
             external_path: None,
             first_row_id,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
@@ -7019,6 +7020,7 @@ mod residual_positions_tests {
             external_path: None,
             first_row_id,
             write_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 
diff --git a/crates/paimon/src/table/vindex_index_build_builder.rs 
b/crates/paimon/src/table/vindex_index_build_builder.rs
index a7ef0897..572f4b3c 100644
--- a/crates/paimon/src/table/vindex_index_build_builder.rs
+++ b/crates/paimon/src/table/vindex_index_build_builder.rs
@@ -1221,6 +1221,7 @@ mod tests {
             external_path: None,
             file_source: None,
             value_stats_cols: None,
+            column_max_sequence_numbers: None,
         }
     }
 

Reply via email to