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 10d45378 feat(core): return the commit completion time from writes 
(#877)
10d45378 is described below

commit 10d453786efaeaa3751fc88edb0a43ad34f17486
Author: Y Ethan Guo <[email protected]>
AuthorDate: Thu Oct 29 19:59:30 2026 -0700

    feat(core): return the commit completion time from writes (#877)
    
    On timeline layout v2 an incremental read's bounds are compared against
    completion times, but a write returned only the instant's requested time, so
    a caller could not read strictly after its own write. WriteResult and
    AppendResult now carry completion_time, the value written into the completed
    instant; it is None on layout v1 and when no commit is written.
    
    Closes #876
---
 crates/core/src/write/append.rs        | 10 +++++---
 crates/core/src/write/mod.rs           | 21 ++++++++++++---
 crates/core/src/write/rewrite/cow.rs   |  3 ++-
 crates/core/src/write/rewrite/mod.rs   |  6 ++++-
 crates/core/src/write/rewrite/mor.rs   |  6 +++--
 crates/core/tests/table_write_tests.rs | 47 ++++++++++++++++++++++++++++++++++
 6 files changed, 82 insertions(+), 11 deletions(-)

diff --git a/crates/core/src/write/append.rs b/crates/core/src/write/append.rs
index 813ef058..0fa25c79 100644
--- a/crates/core/src/write/append.rs
+++ b/crates/core/src/write/append.rs
@@ -53,8 +53,10 @@ static LAST_EPOCH_MILLIS: AtomicI64 = AtomicI64::new(0);
 /// What an append wrote.
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct AppendResult {
-    /// Timestamp of the completed instant.
+    /// Timestamp of the completed instant (the requested time).
     pub instant: String,
+    /// Completion time of the instant on timeline layout v2; `None` on v1.
+    pub completion_time: Option<String>,
     /// Path of the completed instant, relative to the table base.
     pub commit_relative_path: String,
     /// Path of the first base file written, relative to the table base.
@@ -327,7 +329,7 @@ pub async fn append_batches(table: &mut Table, batches: 
&[RecordBatch]) -> Resul
 
     let commit_bytes =
         crate::write::commit_metadata_bytes(table, "INSERT", write_stats, 
None, schema.as_ref())?;
-    let commit_relative_path = crate::write::complete_write(
+    let completed = crate::write::complete_write(
         table,
         &request_instant,
         action,
@@ -348,7 +350,8 @@ pub async fn append_batches(table: &mut Table, batches: 
&[RecordBatch]) -> Resul
 
     Ok(AppendResult {
         instant: request_instant,
-        commit_relative_path,
+        completion_time: completed.completion_time,
+        commit_relative_path: completed.commit_relative_path,
         base_file_path: primary_base_path,
         num_rows,
     })
@@ -502,6 +505,7 @@ async fn append_version_6(table: &mut Table, batches: 
&[RecordBatch]) -> Result<
     table.clear_read_caches();
     Ok(AppendResult {
         instant,
+        completion_time: None,
         commit_relative_path,
         base_file_path,
         num_rows,
diff --git a/crates/core/src/write/mod.rs b/crates/core/src/write/mod.rs
index e30b8e83..76719655 100644
--- a/crates/core/src/write/mod.rs
+++ b/crates/core/src/write/mod.rs
@@ -484,7 +484,8 @@ pub(crate) fn commit_metadata_bytes(
 /// effort: a transient failure must not turn a committed write into an error
 /// the caller would retry.
 ///
-/// Returns the completed instant's path relative to the table base.
+/// Returns the completed instant's path relative to the table base and its
+/// completion time (timeline layout v2 only).
 pub(crate) async fn complete_write(
     table: &mut crate::table::Table,
     instant: &str,
@@ -493,7 +494,7 @@ pub(crate) async fn complete_write(
     schema: &arrow_schema::Schema,
     updates: MetadataTableUpdates,
     written_paths: &[String],
-) -> Result<String> {
+) -> Result<CompletedWrite> {
     let storage = table.file_system_view.storage.clone();
     let mut mdt_stats = HashMap::new();
     if table.is_metadata_table_enabled() {
@@ -555,7 +556,7 @@ pub(crate) async fn complete_write(
     } else {
         None
     };
-    let completed = Instant::new_completed(instant.to_string(), action, 
completion)?;
+    let completed = Instant::new_completed(instant.to_string(), action, 
completion.clone())?;
     let commit_relative_path = 
completed.relative_path_with_base(&append::timeline_dir(table))?;
     storage
         .put_file_if_absent(&commit_relative_path, commit_bytes)
@@ -565,7 +566,19 @@ pub(crate) async fn complete_write(
 
     let _ = table.timeline.reload_completed_commits().await;
     table.clear_read_caches();
-    Ok(commit_relative_path)
+    Ok(CompletedWrite {
+        commit_relative_path,
+        completion_time: completion,
+    })
+}
+
+/// What [`complete_write`] stored on the timeline.
+pub(crate) struct CompletedWrite {
+    /// Path of the completed instant, relative to the table base.
+    pub commit_relative_path: String,
+    /// Completion time of the instant on timeline layout v2; `None` on v1,
+    /// which records none.
+    pub completion_time: Option<String>,
 }
 
 #[cfg(test)]
diff --git a/crates/core/src/write/rewrite/cow.rs 
b/crates/core/src/write/rewrite/cow.rs
index e139b119..a0b45e46 100644
--- a/crates/core/src/write/rewrite/cow.rs
+++ b/crates/core/src/write/rewrite/cow.rs
@@ -372,7 +372,7 @@ pub(super) async fn finalize_rewrite_commit(
         },
         schema.as_ref(),
     )?;
-    crate::write::complete_write(
+    let completed = crate::write::complete_write(
         table,
         instant,
         action,
@@ -389,6 +389,7 @@ pub(super) async fn finalize_rewrite_commit(
     .await?;
     Ok(WriteResult {
         instant: instant.to_string(),
+        completion_time: completed.completion_time,
         num_writes,
         num_updates: updates,
         num_inserts: inserts,
diff --git a/crates/core/src/write/rewrite/mod.rs 
b/crates/core/src/write/rewrite/mod.rs
index ecc0d80e..577c1ce7 100644
--- a/crates/core/src/write/rewrite/mod.rs
+++ b/crates/core/src/write/rewrite/mod.rs
@@ -63,8 +63,12 @@ use validate::*;
 /// Result of an upsert, delete, or overwrite write.
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
 pub struct WriteResult {
-    /// Completed instant timestamp.
+    /// Completed instant timestamp (the requested time).
     pub instant: String,
+    /// Completion time of the instant on timeline layout v2, which is what an
+    /// incremental read's start bound is compared against on that layout.
+    /// `None` on layout v1 and when no commit was written.
+    pub completion_time: Option<String>,
     /// Number of rows written to replacement base files.
     pub num_writes: usize,
     /// Number of input keys that replaced existing rows.
diff --git a/crates/core/src/write/rewrite/mor.rs 
b/crates/core/src/write/rewrite/mor.rs
index 921e8fc2..30303736 100644
--- a/crates/core/src/write/rewrite/mor.rs
+++ b/crates/core/src/write/rewrite/mor.rs
@@ -626,7 +626,7 @@ pub(super) async fn mor_upsert_batches(
         None,
         incoming.schema().as_ref(),
     )?;
-    crate::write::complete_write(
+    let completed = crate::write::complete_write(
         table,
         &instant,
         Action::DeltaCommit,
@@ -643,6 +643,7 @@ pub(super) async fn mor_upsert_batches(
     .await?;
     Ok(WriteResult {
         instant,
+        completion_time: completed.completion_time,
         num_writes: incoming.num_rows(),
         num_updates: updates,
         num_inserts: inserts,
@@ -805,7 +806,7 @@ pub(super) async fn mor_delete_keys(
         None,
         &schema,
     )?;
-    crate::write::complete_write(
+    let completed = crate::write::complete_write(
         table,
         &instant,
         Action::DeltaCommit,
@@ -822,6 +823,7 @@ pub(super) async fn mor_delete_keys(
     .await?;
     Ok(WriteResult {
         instant,
+        completion_time: completed.completion_time,
         num_writes: 0,
         num_updates: 0,
         num_inserts: 0,
diff --git a/crates/core/tests/table_write_tests.rs 
b/crates/core/tests/table_write_tests.rs
index a844eb09..533dbd82 100644
--- a/crates/core/tests/table_write_tests.rs
+++ b/crates/core/tests/table_write_tests.rs
@@ -304,6 +304,53 @@ async fn 
test_append_writes_column_stats_and_partition_stats_mdt() {
     );
 }
 
+#[tokio::test]
+async fn test_write_result_completion_time_matches_timeline() {
+    for table_type in [TableTypeValue::CopyOnWrite, 
TableTypeValue::MergeOnRead] {
+        let dir = tempdir().unwrap();
+        let mut table = Table::create(dir.path().to_str().unwrap())
+            .with_table_name("trips")
+            .with_table_type(table_type.clone())
+            .with_record_key_fields(["id"])
+            .create()
+            .await
+            .unwrap();
+        let append = table.append([sample_batch()]).await.unwrap();
+        let upsert = table
+            .upsert([batch(vec![("a", 10), ("d", 4)])])
+            .await
+            .unwrap();
+        let delete = table.delete("id = 'a'").await.unwrap();
+        assert_eq!(delete.num_deletes, 1, "{table_type:?}");
+
+        let timeline = table.get_timeline();
+        let instants = match table_type {
+            TableTypeValue::CopyOnWrite => 
timeline.get_completed_commits(false).await,
+            TableTypeValue::MergeOnRead => 
timeline.get_completed_deltacommits(false).await,
+        }
+        .unwrap();
+        for (verb, instant, completion_time) in [
+            ("append", &append.instant, &append.completion_time),
+            ("upsert", &upsert.instant, &upsert.completion_time),
+            ("delete", &delete.instant, &delete.completion_time),
+        ] {
+            let completed = instants
+                .iter()
+                .find(|i| &i.timestamp == instant)
+                .unwrap_or_else(|| panic!("{table_type:?}: {verb} instant not 
on the timeline"));
+            assert!(completion_time.is_some(), "{table_type:?} {verb}");
+            assert_eq!(
+                completion_time, &completed.completion_timestamp,
+                "{table_type:?} {verb}"
+            );
+        }
+
+        let noop = table.delete("id = 'missing'").await.unwrap();
+        assert_eq!(noop.num_deletes, 0, "{table_type:?}");
+        assert_eq!(noop.completion_time, None, "{table_type:?}");
+    }
+}
+
 #[tokio::test]
 async fn test_upsert_delete_and_overwrite() {
     let dir = tempdir().unwrap();

Reply via email to