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 089861ff fix(table): keep PK sequence numbers advancing across 
TableWrite reuse (#925)
089861ff is described below

commit 089861fff210376a6f977af889f165265aca7d00
Author: jackylee <[email protected]>
AuthorDate: Thu Sep 24 11:18:44 2026 +0800

    fix(table): keep PK sequence numbers advancing across TableWrite reuse 
(#925)
---
 crates/paimon/src/table/table_write.rs         | 10 ++++
 crates/paimon/tests/pk_write_reuse_seq_test.rs | 82 ++++++++++++++++++++++++++
 2 files changed, 92 insertions(+)

diff --git a/crates/paimon/src/table/table_write.rs 
b/crates/paimon/src/table/table_write.rs
index 77b186c4..54eb6375 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -925,8 +925,18 @@ impl TableWrite {
 
     /// Close all writers and collect CommitMessages for use with TableCommit.
     /// Writers are cleared after this call, allowing the TableWrite to be 
reused.
+    ///
+    /// The per-bucket primary-key sequence cache is invalidated here too: it
+    /// memoizes `max_sequence_number + 1` scanned from the latest snapshot, 
which
+    /// this commit advances. Keeping it would make the next reuse cycle 
re-seed
+    /// from the pre-commit value and assign sequence numbers that overlap the
+    /// just-written files, so the highest-sequence-wins merge would silently 
drop
+    /// the newer rows -- Java's `MergeTreeWriter` advances its counter across
+    /// commits. (`sequence_snapshot` is pinned only by the postpone path, 
which
+    /// forbids reuse, so it is left untouched.)
     pub async fn prepare_commit(&mut self) -> Result<Vec<CommitMessage>> {
         self.ensure_active()?;
+        self.partition_seq_cache.clear();
         if self.file_index_options.is_some() {
             return self.prepare_indexed_append_commit().await;
         }
diff --git a/crates/paimon/tests/pk_write_reuse_seq_test.rs 
b/crates/paimon/tests/pk_write_reuse_seq_test.rs
new file mode 100644
index 00000000..de6584fb
--- /dev/null
+++ b/crates/paimon/tests/pk_write_reuse_seq_test.rs
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+mod common;
+
+use arrow_array::{Int32Array, RecordBatch};
+use common::incremental_helpers::{
+    make_batch, memory_table, persist_table_schema, pk_schema, setup_dirs,
+};
+use futures::TryStreamExt;
+use paimon::table::{Plan, ReadBuilder};
+
+async fn read_pairs(builder: &ReadBuilder<'_>, plan: &Plan) -> Vec<(i32, i32)> 
{
+    let batches: Vec<RecordBatch> = builder
+        .new_read()
+        .unwrap()
+        .to_arrow(plan.splits())
+        .unwrap()
+        .try_collect()
+        .await
+        .unwrap();
+    let mut out = Vec::new();
+    for b in &batches {
+        let ids = b.column(0).as_any().downcast_ref::<Int32Array>().unwrap();
+        let vals = b.column(1).as_any().downcast_ref::<Int32Array>().unwrap();
+        for i in 0..b.num_rows() {
+            out.push((ids.value(i), vals.value(i)));
+        }
+    }
+    out.sort();
+    out
+}
+
+/// Reusing one `TableWrite` across commit cycles must not restart per-bucket
+/// primary-key sequence numbers. A later update has to win the 
highest-sequence
+/// dedup, matching Java `MergeTreeWriter` (whose counter advances across
+/// commits). Before the fix the second cycle re-seeded from the stale cache 
and
+/// the update was silently dropped on read.
+#[tokio::test]
+async fn reused_writer_update_wins_over_earlier_row() {
+    let path = "memory:/pk_write_reuse_seq";
+    let (io, table) = memory_table(path, pk_schema(&[("bucket", "1")]));
+    setup_dirs(&io, path).await;
+    persist_table_schema(&io, path, table.schema()).await;
+
+    let builder = table.new_write_builder();
+    let mut writer = builder.new_write().unwrap();
+
+    // Cycle 1: id=1 is written after id=7, so it takes a non-zero sequence.
+    writer
+        .write_arrow_batch(&make_batch(vec![7, 1], vec![70, 10]))
+        .await
+        .unwrap();
+    let m1 = writer.prepare_commit().await.unwrap();
+    builder.new_commit().commit(m1).await.unwrap();
+
+    // Cycle 2: reuse the same writer to update id=1.
+    writer
+        .write_arrow_batch(&make_batch(vec![1], vec![999]))
+        .await
+        .unwrap();
+    let m2 = writer.prepare_commit().await.unwrap();
+    builder.new_commit().commit(m2).await.unwrap();
+
+    let rb = table.new_read_builder();
+    let plan = rb.new_scan().plan().await.unwrap();
+    assert_eq!(read_pairs(&rb, &plan).await, vec![(1, 999), (7, 70)]);
+}

Reply via email to