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

yihua pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hudi-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 54ce1fc0 feat(python): expose commit completion time on write results 
(#887)
54ce1fc0 is described below

commit 54ce1fc0c9e29a26a0da78230d76adc7596116e7
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Oct 30 02:09:07 2026 -0700

    feat(python): expose commit completion time on write results (#887)
    
    HudiWriteResult now carries completion_time, the core write result's
    completion time of the committed instant. It is None when no commit is
    written or the timeline records no completion times. Passing it to
    with_start_timestamp reads the commits after the write; passing
    instant_time would include the write itself, since incremental bounds
    compare against completion times on timeline layout v2.
---
 python/hudi/_internal.pyi        |  6 ++++++
 python/src/internal.rs           | 20 ++++++++++++++------
 python/tests/test_table_write.py | 31 ++++++++++++++++++++++++++++++-
 3 files changed, 50 insertions(+), 7 deletions(-)

diff --git a/python/hudi/_internal.pyi b/python/hudi/_internal.pyi
index c5d3d9df..dfb7ae24 100644
--- a/python/hudi/_internal.pyi
+++ b/python/hudi/_internal.pyi
@@ -627,6 +627,11 @@ class HudiWriteResult:
     Attributes:
         instant_time (Optional[str]): Instant time of the completed commit, or 
``None``
             when no commit was made.
+        completion_time (Optional[str]): When the commit completed, or 
``None`` when no
+            commit was made or the table's timeline records no completion 
times (table
+            version 6). When set, incremental reads compare their start bound 
against
+            completion times, so ``with_start_timestamp(completion_time)`` 
returns the
+            commits after this one while passing ``instant_time`` would 
include this commit.
         num_writes (int): Rows written to data files.
         num_updates (int): Input rows that replaced existing records.
         num_inserts (int): Input rows that were new records.
@@ -634,6 +639,7 @@ class HudiWriteResult:
     """
 
     instant_time: Optional[str]
+    completion_time: Optional[str]
     num_writes: int
     num_updates: int
     num_inserts: int
diff --git a/python/src/internal.rs b/python/src/internal.rs
index 8c3adbf7..9d807362 100644
--- a/python/src/internal.rs
+++ b/python/src/internal.rs
@@ -1043,6 +1043,7 @@ impl HudiTable {
 #[pyclass(frozen, get_all)]
 pub struct HudiWriteResult {
     instant_time: Option<String>,
+    completion_time: Option<String>,
     num_writes: usize,
     num_updates: usize,
     num_inserts: usize,
@@ -1053,13 +1054,18 @@ pub struct HudiWriteResult {
 #[pymethods]
 impl HudiWriteResult {
     fn __repr__(&self) -> String {
-        let instant_time = self
-            .instant_time
-            .as_ref()
-            .map_or_else(|| "None".to_string(), |t| format!("'{t}'"));
+        let quoted = |t: &Option<String>| {
+            t.as_ref()
+                .map_or_else(|| "None".to_string(), |t| format!("'{t}'"))
+        };
         format!(
-            "HudiWriteResult(instant_time={instant_time}, num_writes={}, 
num_updates={}, num_inserts={}, num_deletes={})",
-            self.num_writes, self.num_updates, self.num_inserts, 
self.num_deletes,
+            "HudiWriteResult(instant_time={}, completion_time={}, 
num_writes={}, num_updates={}, num_inserts={}, num_deletes={})",
+            quoted(&self.instant_time),
+            quoted(&self.completion_time),
+            self.num_writes,
+            self.num_updates,
+            self.num_inserts,
+            self.num_deletes,
         )
     }
 }
@@ -1068,6 +1074,7 @@ impl From<WriteResult> for HudiWriteResult {
     fn from(r: WriteResult) -> Self {
         Self {
             instant_time: committed(r.instant),
+            completion_time: r.completion_time,
             num_writes: r.num_writes,
             num_updates: r.num_updates,
             num_inserts: r.num_inserts,
@@ -1080,6 +1087,7 @@ impl From<AppendResult> for HudiWriteResult {
     fn from(r: AppendResult) -> Self {
         Self {
             instant_time: committed(r.instant),
+            completion_time: r.completion_time,
             num_writes: r.num_rows,
             num_updates: 0,
             num_inserts: r.num_rows,
diff --git a/python/tests/test_table_write.py b/python/tests/test_table_write.py
index 64da9426..7ce5b1a5 100644
--- a/python/tests/test_table_write.py
+++ b/python/tests/test_table_write.py
@@ -24,7 +24,7 @@ from decimal import Decimal
 import pyarrow as pa
 import pytest
 
-from hudi import HudiTable, HudiWriteResult
+from hudi import HudiQueryType, HudiReadOptions, HudiTable, HudiWriteResult
 
 SCHEMA = pa.schema(
     [
@@ -210,6 +210,7 @@ def test_append_then_read(tmp_path, table_type):
     )
     assert "num_inserts=2" in repr(res)
     assert f"instant_time='{res.instant_time}'" in repr(res)
+    assert f"completion_time='{res.completion_time}'" in repr(res)
     with pytest.raises(AttributeError):
         res.num_inserts = 0
     assert snapshot(table) == {"t1": ("sf", 1, 30.0), "t2": ("nyc", 1, 45.0)}
@@ -243,6 +244,34 @@ def test_upsert_update_columns(tmp_path):
     assert snapshot(table)["t1"] == ("sf", 1, 31.0)
 
 
+def test_completion_time_bounds_incremental_reads(tmp_path):
+    table = HudiTable.create(str(tmp_path / "t"), record_key="id", 
ordering_fields="ts")
+    base = table.append(trips(("t1", "sf", 1, 30.0), ("t2", "sf", 1, 45.0)))
+    assert base.completion_time is not None
+    assert base.completion_time > base.instant_time
+    upsert = table.upsert(trips(("t2", "sf", 2, 50.0), ("t3", "nyc", 2, 15.0)))
+    assert upsert.completion_time > base.completion_time
+    no_op = table.delete_keys(["missing"])
+    assert (no_op.instant_time, no_op.completion_time) == (None, None)
+    assert "completion_time=None" in repr(no_op)
+
+    def changed_after(bound):
+        batches = table.read(
+            HudiReadOptions()
+            .with_query_type(HudiQueryType.Incremental)
+            .with_start_timestamp(bound)
+        )
+        return sorted(
+            id_ for batch in batches for id_ in batch.column("id").to_pylist()
+        )
+
+    # The bound is compared against completion times: the requested time of
+    # the base commit admits the base commit itself.
+    assert changed_after(base.completion_time) == ["t2", "t3"]
+    assert changed_after(base.instant_time) == ["t1", "t2", "t3"]
+    assert changed_after(upsert.completion_time) == []
+
+
 @pytest.mark.parametrize("table_type", ["cow", "mor"])
 def test_update_with_dict(tmp_path, table_type):
     table = new_table(tmp_path, table_type)

Reply via email to