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 334c7e02 refactor(core): return one WriteResult from every table write 
(#945)
334c7e02 is described below

commit 334c7e0215b5e08d9843804ddf6a89ec482e90af
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Oct 30 13:04:29 2026 -0700

    refactor(core): return one WriteResult from every table write (#945)
    
    * refactor(core): return one WriteResult from every table write
    
    Table::append returns WriteResult like the other write verbs, and
    AppendResult is removed. WriteResult.instant becomes
    requested_time: Option<String>, None when the write made no commit,
    next to completion_time. The Python binding maps it to
    HudiWriteResult.instant_time directly.
    
    * Read requested_time in the compaction tests and keep the Python 
compaction instant mapping
    
    * Give CompactionResult requested_time, move WriteResult to the write 
module, and pin the upsert benchmark's ruff rules
    
    * Describe the empty compaction result by its requested time
---
 README.md                                          |  4 +-
 benchmark/upsert/ruff.toml                         | 21 ++++++
 crates/core/src/table/mod.rs                       |  8 +--
 crates/core/src/write/append.rs                    | 45 ++++--------
 crates/core/src/write/mod.rs                       | 28 ++++++--
 crates/core/src/write/rewrite/compaction.rs        | 53 ++++++++------
 crates/core/src/write/rewrite/cow.rs               |  2 +-
 crates/core/src/write/rewrite/mod.rs               | 22 +-----
 crates/core/src/write/rewrite/mor.rs               |  4 +-
 crates/core/tests/spark_parity_tests.rs            |  2 +-
 crates/core/tests/table_bulk_insert_tests.rs       | 70 ++++++++++---------
 crates/core/tests/table_write_archival_tests.rs    |  3 +-
 .../core/tests/table_write_composite_keys_tests.rs |  6 +-
 crates/core/tests/table_write_lifecycle_tests.rs   | 18 ++---
 crates/core/tests/table_write_rollback_tests.rs    | 11 +--
 crates/core/tests/table_write_tests.rs             | 81 +++++++++++++++-------
 python/src/internal.rs                             | 56 ++-------------
 17 files changed, 223 insertions(+), 211 deletions(-)

diff --git a/README.md b/README.md
index 72c80d1a..b4cd1fa6 100644
--- a/README.md
+++ b/README.md
@@ -464,7 +464,9 @@ table.upsert(batch, update_columns=["fare"])
 let result = table.upsert([batch]).await?;
 println!(
     "instant={} updates={} inserts={}",
-    result.instant, result.num_updates, result.num_inserts
+    result.requested_time.as_deref().unwrap_or("none"),
+    result.num_updates,
+    result.num_inserts
 );
 ```
 
diff --git a/benchmark/upsert/ruff.toml b/benchmark/upsert/ruff.toml
new file mode 100644
index 00000000..1739702d
--- /dev/null
+++ b/benchmark/upsert/ruff.toml
@@ -0,0 +1,21 @@
+# 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.
+
+# Pin the rule set rather than follow ruff's built-in defaults, which change
+# across ruff releases.
+target-version = "py310"
+lint.select = ["E4", "E7", "E9", "F"]
diff --git a/crates/core/src/table/mod.rs b/crates/core/src/table/mod.rs
index 4bbda1cf..cd61dcfd 100644
--- a/crates/core/src/table/mod.rs
+++ b/crates/core/src/table/mod.rs
@@ -98,7 +98,6 @@ pub(crate) mod validation;
 pub use crate::config::read_options::{QueryType, ReadOptions};
 pub use crate::index::{HoodieKey, SimpleIndex};
 pub use crate::write::TableCreateBuilder;
-pub use crate::write::append::AppendResult;
 pub use crate::write::{CompactionResult, UpsertOptions, WriteResult};
 
 use crate::Result;
@@ -461,8 +460,9 @@ impl Table {
     /// under one compaction instant that completes as a `commit`.
     ///
     /// Rows keep their `_hoodie_commit_time`, so incremental reads see no new
-    /// rows from a compaction. Returns a result with an empty instant when no
-    /// file group had log files, in which case nothing is written.
+    /// rows from a compaction. Returns a result whose `requested_time` is
+    /// `None` when no file group had log files, in which case nothing is
+    /// written.
     ///
     /// Compaction also runs on its own after a delta commit when
     /// `hoodie.compact.inline` is `true` and the completed delta commits since
@@ -1590,7 +1590,7 @@ impl Table {
     pub async fn append(
         &mut self,
         batches: impl IntoIterator<Item = RecordBatch>,
-    ) -> Result<AppendResult> {
+    ) -> Result<WriteResult> {
         let batches: Vec<RecordBatch> = batches.into_iter().collect();
         crate::write::append_batches(self, &batches).await
     }
diff --git a/crates/core/src/write/append.rs b/crates/core/src/write/append.rs
index 2aff8fd3..823a321b 100644
--- a/crates/core/src/write/append.rs
+++ b/crates/core/src/write/append.rs
@@ -45,6 +45,7 @@ use crate::metadata::meta_field::MetaField;
 use crate::metadata::table::encode::RecordIndexEntry;
 use crate::table::Table;
 use crate::timeline::instant::{Action, Instant};
+use crate::write::WriteResult;
 use crate::write::keygen::{
     hoodie_keys_for_batch, hoodie_keys_for_batch_with_offset, 
relative_data_path,
 };
@@ -52,23 +53,8 @@ use crate::write::metadata::is_column_stats_enabled;
 
 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 (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.
-    pub base_file_path: String,
-    /// Rows written.
-    pub num_rows: usize,
-}
-
 /// Append one or more record batches as a single INSERT commit.
-pub async fn append_batches(table: &mut Table, batches: &[RecordBatch]) -> 
Result<AppendResult> {
+pub async fn append_batches(table: &mut Table, batches: &[RecordBatch]) -> 
Result<WriteResult> {
     let batches = &crate::write::normalize_for_write(batches)?;
     if table_version(table) < 8 {
         return append_version_6(table, batches).await;
@@ -131,7 +117,6 @@ pub async fn append_batches(table: &mut Table, batches: 
&[RecordBatch]) -> Resul
         .then(|| crate::write::metadata::instant_millis(&table.hudi_configs, 
&request_instant));
     let mut stats_updates = Vec::new();
     let mut written_paths: Vec<String> = Vec::new();
-    let mut primary_base_path = String::new();
     let max_records = max_records_per_file(table);
     // Monotonic auto-key row id across partitions and size-split chunks in 
this commit.
     let mut auto_key_row_offset = 0usize;
@@ -227,9 +212,6 @@ pub async fn append_batches(table: &mut Table, batches: 
&[RecordBatch]) -> Resul
                     ))
                 })?;
             let base_file_path = relative_data_path(&partition_path, 
&file_name);
-            if primary_base_path.is_empty() {
-                primary_base_path = base_file_path.clone();
-            }
             let prepared = prepare_batches_for_write_with_offset(
                 &table.hudi_configs,
                 &[chunk],
@@ -357,12 +339,12 @@ pub async fn append_batches(table: &mut Table, batches: 
&[RecordBatch]) -> Resul
         crate::write::maybe_compact_inline(table).await;
     }
 
-    Ok(AppendResult {
-        instant: request_instant,
+    Ok(WriteResult {
+        requested_time: Some(request_instant),
         completion_time: completed.completion_time,
-        commit_relative_path: completed.commit_relative_path,
-        base_file_path: primary_base_path,
-        num_rows,
+        num_writes: num_rows,
+        num_inserts: num_rows,
+        ..Default::default()
     })
 }
 
@@ -457,7 +439,7 @@ fn ensure_rows_present(batches: &[RecordBatch]) -> 
Result<usize> {
 /// failed write, and writes no markers and no archival, so it does not share
 /// `complete_write` with the version 8 path, and a crashed write leaves
 /// nothing on the timeline for a later writer to roll back.
-async fn append_version_6(table: &mut Table, batches: &[RecordBatch]) -> 
Result<AppendResult> {
+async fn append_version_6(table: &mut Table, batches: &[RecordBatch]) -> 
Result<WriteResult> {
     ensure_batches_share_schema(batches)?;
     let batches = &match_table_schema(table, batches).await?;
     let schema = batches[0].schema();
@@ -525,12 +507,11 @@ async fn append_version_6(table: &mut Table, batches: 
&[RecordBatch]) -> Result<
     // The commit is durable; a failed refresh must not turn it into an error.
     let _ = table.timeline.reload_completed_commits().await;
     table.clear_read_caches();
-    Ok(AppendResult {
-        instant,
-        completion_time: None,
-        commit_relative_path,
-        base_file_path,
-        num_rows,
+    Ok(WriteResult {
+        requested_time: Some(instant),
+        num_writes: num_rows,
+        num_inserts: num_rows,
+        ..Default::default()
     })
 }
 
diff --git a/crates/core/src/write/mod.rs b/crates/core/src/write/mod.rs
index 2ed9bfbe..c81505d7 100644
--- a/crates/core/src/write/mod.rs
+++ b/crates/core/src/write/mod.rs
@@ -39,7 +39,7 @@ pub(crate) use keygen::{
 };
 pub use lock::{InProcessLockProvider, LockLease, LockProvider};
 pub(crate) use rewrite::ensure_writable_table_version;
-pub use rewrite::{CompactionResult, UpsertOptions, WriteResult};
+pub use rewrite::{CompactionResult, UpsertOptions};
 pub(crate) use rewrite::{
     await_async_compaction, compact_table, delete_filter, delete_keys,
     dynamic_partition_overwrite_batches, maybe_compact_inline, 
overwrite_batches, update_filter,
@@ -761,15 +761,35 @@ pub(crate) async fn complete_write(
     let _ = table.timeline.reload_completed_commits().await;
     table.clear_read_caches();
     Ok(CompletedWrite {
-        commit_relative_path,
         completion_time: completion,
     })
 }
 
+/// Result of a write on [`Table`](crate::table::Table), shared by every write 
verb.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct WriteResult {
+    /// Requested time of the completed commit's instant, or `None` when the
+    /// write matched nothing and made no commit.
+    pub requested_time: Option<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>,
+    /// Rows in the data files this write produced: every row of each base
+    /// file it wrote, or each row appended to a merge-on-read log (deletes
+    /// append none).
+    pub num_writes: usize,
+    /// Number of input keys that replaced existing rows.
+    pub num_updates: usize,
+    /// Number of input keys not previously present. An append looks up no
+    /// keys and counts every row.
+    pub num_inserts: usize,
+    /// Number of rows deleted.
+    pub num_deletes: usize,
+}
+
 /// 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>,
diff --git a/crates/core/src/write/rewrite/compaction.rs 
b/crates/core/src/write/rewrite/compaction.rs
index ce09d30d..803a4db6 100644
--- a/crates/core/src/write/rewrite/compaction.rs
+++ b/crates/core/src/write/rewrite/compaction.rs
@@ -36,9 +36,9 @@ use crate::write::markers::Marker;
 /// Result of a compaction.
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
 pub struct CompactionResult {
-    /// Completed instant timestamp (the requested time); empty when no file
-    /// group carried log files and nothing was written.
-    pub instant: String,
+    /// Requested time of the compaction commit's instant, or `None` when no
+    /// file group carried log files and nothing was written.
+    pub requested_time: Option<String>,
     /// Completion time of the instant on timeline layout v2; `None` on
     /// layout v1 and when nothing was written.
     pub completion_time: Option<String>,
@@ -185,13 +185,15 @@ pub(crate) async fn maybe_compact_inline(table: &mut 
Table) {
     }
     if inline {
         match compact_table(table).await {
-            Ok(result) if result.instant.is_empty() => {}
-            Ok(result) => log::info!(
-                "inline compaction {} folded {} log files of {} file groups",
-                result.instant,
-                result.num_log_files,
-                result.num_file_groups
+            Ok(CompactionResult {
+                requested_time: Some(instant),
+                num_file_groups,
+                num_log_files,
+                ..
+            }) => log::info!(
+                "inline compaction {instant} folded {num_log_files} log files 
of {num_file_groups} file groups"
             ),
+            Ok(_) => {}
             Err(error) => log::warn!("inline compaction failed: {error}"),
         }
         return;
@@ -499,7 +501,7 @@ async fn execute_compaction(
     )
     .await?;
     Ok(CompactionResult {
-        instant: instant.to_string(),
+        requested_time: Some(instant.to_string()),
         completion_time: completed.completion_time,
         num_file_groups: groups.len(),
         num_log_files,
@@ -668,7 +670,10 @@ mod tests {
         assert_eq!(with_logs.len(), 2, "a's group in sf and c's group in ny");
 
         let result = table.compact().await.unwrap();
-        assert!(!result.instant.is_empty());
+        let instant = result
+            .requested_time
+            .as_deref()
+            .expect("compaction commits");
         assert!(result.completion_time.is_some());
         assert_eq!(result.num_file_groups, 2);
         assert_eq!(result.num_log_files, 2);
@@ -680,7 +685,11 @@ mod tests {
         let after = snapshot(&table).await;
         assert_eq!(data_only(&after), data_only(&before));
         let row_a = after.iter().find(|r| r.0 == "a").unwrap();
-        assert_eq!(row_a.4, upsert.instant, "compaction preserves commit 
time");
+        assert_eq!(
+            Some(row_a.4.clone()),
+            upsert.requested_time,
+            "compaction preserves commit time"
+        );
         assert_eq!(
             row_a.5,
             format!(
@@ -690,7 +699,7 @@ mod tests {
                     .find(|s| s.partition_path == "city=sf")
                     .unwrap()
                     .file_id(),
-                result.instant
+                instant
             ),
             "compaction stamps the new base file name"
         );
@@ -699,7 +708,7 @@ mod tests {
         assert_eq!(
             slices
                 .iter()
-                .filter(|s| s.creation_instant_time() == result.instant)
+                .filter(|s| s.creation_instant_time() == instant)
                 .count(),
             2
         );
@@ -707,8 +716,7 @@ mod tests {
         // Timeline: plan in the requested file, empty inflight, compacted 
commit.
         let timeline = dir.path().join(".hoodie/timeline");
         let plan = CompactionPlan::from_avro_bytes(
-            &std::fs::read(timeline.join(format!("{}.compaction.requested", 
result.instant)))
-                .unwrap(),
+            &std::fs::read(timeline.join(format!("{}.compaction.requested", 
instant))).unwrap(),
         )
         .unwrap();
         let operations = plan.operations.clone().unwrap();
@@ -735,13 +743,13 @@ mod tests {
         assert_eq!(plan.preserve_hoodie_metadata, Some(true));
         assert!(plan.is_scheduled_by_hudi_rs());
         assert!(
-            std::fs::read(timeline.join(format!("{}.compaction.inflight", 
result.instant)))
+            std::fs::read(timeline.join(format!("{}.compaction.inflight", 
instant)))
                 .unwrap()
                 .is_empty()
         );
         let commit_name = format!(
             "{}_{}.commit",
-            result.instant,
+            instant,
             result.completion_time.as_deref().unwrap()
         );
         assert_eq!(completed_commits(dir.path()), vec![commit_name.clone()]);
@@ -779,14 +787,17 @@ mod tests {
             .into_iter()
             .find(|s| s.partition_path == "city=sf")
             .unwrap();
-        assert_eq!(sf.creation_instant_time(), result.instant);
+        assert_eq!(
+            Some(sf.creation_instant_time()),
+            result.requested_time.as_deref()
+        );
         assert_eq!(sf.log_files.len(), 1);
         let row_b = snapshot(&table)
             .await
             .into_iter()
             .find(|r| r.0 == "b")
             .unwrap();
-        assert_eq!((row_b.3, row_b.4), (21, later.instant));
+        assert_eq!((row_b.3, row_b.4), (21, later.requested_time.unwrap()));
     }
 
     #[tokio::test]
@@ -1065,7 +1076,7 @@ mod tests {
         assert_eq!(slices[0].log_files.len(), 1, "a pending log is not 
planned");
         finish_delta_commit_unlocked(&table, before).await;
         let during = table.upsert([batch(&[("b", "sf", 2, 
21)])]).await.unwrap();
-        assert!(during.instant > instant);
+        assert!(during.requested_time.unwrap() > instant);
         execute_compaction(&mut table, &instant, slices)
             .await
             .unwrap();
diff --git a/crates/core/src/write/rewrite/cow.rs 
b/crates/core/src/write/rewrite/cow.rs
index ab688126..6fbf1e22 100644
--- a/crates/core/src/write/rewrite/cow.rs
+++ b/crates/core/src/write/rewrite/cow.rs
@@ -317,7 +317,7 @@ pub(super) async fn finalize_rewrite_commit(
     )
     .await?;
     Ok(WriteResult {
-        instant: instant.to_string(),
+        requested_time: Some(instant.to_string()),
         completion_time: completed.completion_time,
         num_writes,
         num_updates: updates,
diff --git a/crates/core/src/write/rewrite/mod.rs 
b/crates/core/src/write/rewrite/mod.rs
index 087f6496..c692d229 100644
--- a/crates/core/src/write/rewrite/mod.rs
+++ b/crates/core/src/write/rewrite/mod.rs
@@ -45,6 +45,7 @@ use crate::metadata::meta_field::MetaField;
 use crate::metadata::table::encode::RecordIndexEntry;
 use crate::table::{ReadOptions, Table};
 use crate::timeline::instant::Action;
+use crate::write::WriteResult;
 use crate::write::append::{
     ensure_copy_on_write, generate_instant_time, is_layout_two, 
prepare_batches_for_write,
     timeline_dir,
@@ -68,27 +69,6 @@ use rows::*;
 pub(crate) use validate::ensure_writable_table_version;
 use validate::*;
 
-/// Result of an upsert, delete, or overwrite write.
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct WriteResult {
-    /// 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>,
-    /// Rows in the data files this write produced: every row of each
-    /// rewritten copy-on-write base file, or each row appended to a
-    /// merge-on-read log (deletes append none).
-    pub num_writes: usize,
-    /// Number of input keys that replaced existing rows.
-    pub num_updates: usize,
-    /// Number of input keys not previously present.
-    pub num_inserts: usize,
-    /// Number of rows deleted.
-    pub num_deletes: usize,
-}
-
 /// Options controlling an upsert.
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
 pub struct UpsertOptions {
diff --git a/crates/core/src/write/rewrite/mor.rs 
b/crates/core/src/write/rewrite/mor.rs
index cacfc897..8e03e75a 100644
--- a/crates/core/src/write/rewrite/mor.rs
+++ b/crates/core/src/write/rewrite/mor.rs
@@ -594,7 +594,7 @@ pub(super) async fn mor_upsert_batches(
     .await?;
     crate::write::maybe_compact_inline(table).await;
     Ok(WriteResult {
-        instant,
+        requested_time: Some(instant),
         completion_time: completed.completion_time,
         num_writes: incoming.num_rows(),
         num_updates: updates,
@@ -771,7 +771,7 @@ pub(super) async fn mor_delete_keys(
     .await?;
     crate::write::maybe_compact_inline(table).await;
     Ok(WriteResult {
-        instant,
+        requested_time: Some(instant),
         completion_time: completed.completion_time,
         num_writes: 0,
         num_updates: 0,
diff --git a/crates/core/tests/spark_parity_tests.rs 
b/crates/core/tests/spark_parity_tests.rs
index e9b600b3..77bb2049 100644
--- a/crates/core/tests/spark_parity_tests.rs
+++ b/crates/core/tests/spark_parity_tests.rs
@@ -1369,7 +1369,7 @@ async fn parity_k_rs_compaction_spark_interop() {
     );
     assert_eq!(rs_read(path).await, expected, "rs read after rs compaction");
     assert_eq!(
-        spark_read_as_of(&env, path, &delete.instant),
+        spark_read_as_of(&env, path, 
delete.requested_time.as_deref().unwrap()),
         expected,
         "spark time travel to the delta commit before the compaction"
     );
diff --git a/crates/core/tests/table_bulk_insert_tests.rs 
b/crates/core/tests/table_bulk_insert_tests.rs
index 17f449ae..e7c7cc7a 100644
--- a/crates/core/tests/table_bulk_insert_tests.rs
+++ b/crates/core/tests/table_bulk_insert_tests.rs
@@ -107,16 +107,13 @@ async fn append_only_writes_records_and_commit() -> 
Result<()> {
     // Instants other writers mint before this write must order before it.
     let earlier_instant = 
chrono::Local::now().format("%Y%m%d%H%M%S%3f").to_string();
     let result = table.append([batch]).await?;
-    assert_eq!(result.num_rows, 3);
-    assert_eq!(
-        result.commit_relative_path,
-        format!(".hoodie/{}.commit", result.instant)
-    );
+    assert_eq!((result.num_writes, result.num_inserts), (3, 3));
+    assert_eq!(result.completion_time, None);
+    let instant = result.requested_time.as_deref().expect("append commits");
     assert!(
-        chrono::NaiveDateTime::parse_from_str(&result.instant, 
"%Y%m%d%H%M%S%3f").is_ok()
-            && result.instant >= earlier_instant,
-        "instant {} must be a yyyyMMddHHmmssSSS time at or after 
{earlier_instant}",
-        result.instant
+        chrono::NaiveDateTime::parse_from_str(instant, 
"%Y%m%d%H%M%S%3f").is_ok()
+            && instant >= earlier_instant.as_str(),
+        "instant {instant} must be a yyyyMMddHHmmssSSS time at or after 
{earlier_instant}"
     );
 
     let reopened = Table::new(temp_dir.path().to_str().expect("valid temp 
path")).await?;
@@ -140,7 +137,7 @@ async fn append_only_writes_records_and_commit() -> 
Result<()> {
     assert_eq!(names.value(1), "bob");
     assert_eq!(names.value(2), "carol");
 
-    let commit_file = temp_dir.path().join(&result.commit_relative_path);
+    let commit_file = 
temp_dir.path().join(format!(".hoodie/{instant}.commit"));
     assert!(commit_file.exists(), "commit file should exist");
 
     Ok(())
@@ -185,10 +182,14 @@ async fn append_only_writes_layout_v2_commit() -> 
Result<()> {
     )?;
 
     let result = table.append([batch]).await?;
-    assert!(result.commit_relative_path.starts_with(".hoodie/timeline/"));
-    assert!(result.commit_relative_path.ends_with(".commit"));
-
-    let commit_file = temp_dir.path().join(&result.commit_relative_path);
+    let commit_file = temp_dir.path().join(format!(
+        ".hoodie/timeline/{}_{}.commit",
+        result.requested_time.as_deref().expect("append commits"),
+        result
+            .completion_time
+            .as_deref()
+            .expect("layout v2 records completion")
+    ));
     assert!(commit_file.exists(), "layout v2 commit file should exist");
 
     let reopened = Table::new(temp_dir.path().to_str().expect("valid temp 
path")).await?;
@@ -236,7 +237,7 @@ async fn append_only_writes_multiple_batches_as_one_file() 
-> Result<()> {
     )?;
 
     let result = table.append([first, second]).await?;
-    assert_eq!(result.num_rows, 3);
+    assert_eq!((result.num_writes, result.num_inserts), (3, 3));
 
     let reopened = Table::new(temp_dir.path().to_str().expect("valid temp 
path")).await?;
     let records = reopened.read(&ReadOptions::new()).await?;
@@ -404,7 +405,7 @@ async fn 
append_only_accepts_cleared_metadata_partitions_property() -> Result<()
     let result = table
         .append([id_name_batch(vec![1], vec!["alice"])])
         .await?;
-    assert_eq!(result.num_rows, 1);
+    assert_eq!((result.num_writes, result.num_inserts), (1, 1));
 
     Ok(())
 }
@@ -419,8 +420,9 @@ async fn append_only_writes_version_6_commit_metadata() -> 
Result<()> {
         .append([id_name_batch(vec![1, 2, 3], vec!["alice", "bob", "carol"])])
         .await?;
 
+    let instant = result.requested_time.as_deref().expect("append commits");
     let commit: serde_json::Value = serde_json::from_slice(&fs::read(
-        temp_dir.path().join(&result.commit_relative_path),
+        temp_dir.path().join(format!(".hoodie/{instant}.commit")),
     )?)
     .expect("v1 commit metadata is JSON");
     assert_eq!(commit["operationType"], "BULK_INSERT");
@@ -430,15 +432,12 @@ async fn append_only_writes_version_6_commit_metadata() 
-> Result<()> {
     assert_eq!(stats.len(), 1);
     let stat = &stats[0];
     let file_id = stat["fileId"].as_str().expect("fileId");
-    assert_eq!(
-        result.base_file_path,
-        format!("{file_id}_0-0-0_{}.parquet", result.instant)
-    );
-    assert_eq!(stat["path"], result.base_file_path.as_str());
+    let base_file = format!("{file_id}_0-0-0_{instant}.parquet");
+    assert_eq!(stat["path"], base_file.as_str());
     assert_eq!(stat["partitionPath"], "");
     assert_eq!(stat["numWrites"], 3);
     assert_eq!(stat["numInserts"], 3);
-    let file_size = 
fs::metadata(temp_dir.path().join(&result.base_file_path))?.len();
+    let file_size = fs::metadata(temp_dir.path().join(&base_file))?.len();
     assert_eq!(stat["fileSizeInBytes"], file_size);
 
     let schema: serde_json::Value = serde_json::from_str(
@@ -573,13 +572,16 @@ async fn 
append_only_version_6_commits_support_time_travel_and_incremental_reads
         .await?;
 
     let as_of = |instant: &str| 
ReadOptions::new().with_as_of_timestamp(instant);
-    assert_eq!(ids_read_with(uri, as_of(&first.instant)).await?, vec![1, 2]);
     assert_eq!(
-        ids_read_with(uri, as_of(&second.instant)).await?,
+        ids_read_with(uri, 
as_of(first.requested_time.as_deref().unwrap())).await?,
+        vec![1, 2]
+    );
+    assert_eq!(
+        ids_read_with(uri, 
as_of(second.requested_time.as_deref().unwrap())).await?,
         vec![1, 2, 3]
     );
     assert_eq!(
-        ids_read_with(uri, as_of(&third.instant)).await?,
+        ids_read_with(uri, 
as_of(third.requested_time.as_deref().unwrap())).await?,
         vec![1, 2, 3, 4, 5]
     );
 
@@ -591,17 +593,18 @@ async fn 
append_only_version_6_commits_support_time_travel_and_incremental_reads
     assert_eq!(
         ids_read_with(
             uri,
-            incremental(&first.instant).with_end_timestamp(&second.instant)
+            incremental(first.requested_time.as_deref().unwrap())
+                .with_end_timestamp(second.requested_time.as_deref().unwrap())
         )
         .await?,
         vec![3]
     );
     assert_eq!(
-        ids_read_with(uri, incremental(&first.instant)).await?,
+        ids_read_with(uri, 
incremental(first.requested_time.as_deref().unwrap())).await?,
         vec![3, 4, 5]
     );
     assert_eq!(
-        ids_read_with(uri, incremental(&third.instant)).await?,
+        ids_read_with(uri, 
incremental(third.requested_time.as_deref().unwrap())).await?,
         Vec::<i32>::new()
     );
 
@@ -735,9 +738,12 @@ async fn 
append_only_without_table_version_takes_the_version_6_path() -> Result<
     let result = table
         .append([id_name_batch(vec![1, 2], vec!["alice", "bob"])])
         .await?;
-    assert_eq!(
-        result.commit_relative_path,
-        format!(".hoodie/{}.commit", result.instant)
+    let instant = result.requested_time.as_deref().expect("append commits");
+    assert!(
+        temp_dir
+            .path()
+            .join(format!(".hoodie/{instant}.commit"))
+            .is_file()
     );
     let rows: usize = table
         .read(&ReadOptions::new())
diff --git a/crates/core/tests/table_write_archival_tests.rs 
b/crates/core/tests/table_write_archival_tests.rs
index 04ede12e..9335b810 100644
--- a/crates/core/tests/table_write_archival_tests.rs
+++ b/crates/core/tests/table_write_archival_tests.rs
@@ -456,7 +456,8 @@ async fn plant_pending_then_archive(
         .append([batch(vec![("k0", "sf", 0)])])
         .await
         .unwrap()
-        .instant;
+        .requested_time
+        .unwrap();
 
     let pending_ts = next_instant(&first);
     let timeline = dir.join(".hoodie/timeline");
diff --git a/crates/core/tests/table_write_composite_keys_tests.rs 
b/crates/core/tests/table_write_composite_keys_tests.rs
index 73687f5f..bb977698 100644
--- a/crates/core/tests/table_write_composite_keys_tests.rs
+++ b/crates/core/tests/table_write_composite_keys_tests.rs
@@ -243,7 +243,7 @@ async fn test_simple_index_tags_virtual_composite_keys() {
         .create()
         .await
         .unwrap();
-    let append = table
+    table
         .append([order_batch(&[(1, "a", 10), (1, "b", 20)])])
         .await
         .unwrap();
@@ -254,7 +254,9 @@ async fn test_simple_index_tags_virtual_composite_keys() {
         .await
         .unwrap();
     let location = locations[&stored].as_ref().expect("stored key is tagged");
-    assert!(append.base_file_path.contains(&location.file_id));
+    let slices = table.get_file_slices(&ReadOptions::new()).await.unwrap();
+    assert_eq!(slices.len(), 1);
+    assert_eq!(slices[0].file_id(), location.file_id);
     assert!(locations[&absent].is_none());
 }
 
diff --git a/crates/core/tests/table_write_lifecycle_tests.rs 
b/crates/core/tests/table_write_lifecycle_tests.rs
index 936a599e..63969f58 100644
--- a/crates/core/tests/table_write_lifecycle_tests.rs
+++ b/crates/core/tests/table_write_lifecycle_tests.rs
@@ -209,7 +209,7 @@ async fn 
test_cow_lifecycle_append_upsert_delete_overwrite_with_mdt() {
             .is_file()
     );
 
-    let append = table
+    table
         .append([batch(vec![("a", 1), ("b", 2), ("c", 3)])])
         .await
         .unwrap();
@@ -223,11 +223,9 @@ async fn 
test_cow_lifecycle_append_upsert_delete_overwrite_with_mdt() {
             replacecommits: 0,
         }
     );
-    assert!(
-        mdt_active_files(&table)
-            .await
-            .contains(&append.base_file_path)
-    );
+    let slices = table.get_file_slices(&ReadOptions::new()).await.unwrap();
+    let base_file = slices[0].base_file_relative_path().unwrap().unwrap();
+    assert!(mdt_active_files(&table).await.contains(&base_file));
     assert_snapshot_async(&table, vec![("a", 1), ("b", 2), ("c", 3)]).await;
 
     // 2) Upsert over time: update + insert; state stays consistent.
@@ -299,11 +297,10 @@ async fn 
test_mor_lifecycle_asserts_base_and_log_file_counts() {
         .unwrap();
 
     // Append → base parquet + deltacommit, no logs yet.
-    let append = table
+    table
         .append([ordered_batch(vec![("a", 1, 1), ("b", 2, 1)])])
         .await
         .unwrap();
-    assert!(append.commit_relative_path.ends_with(".deltacommit"));
     assert_eq!(
         data_layout(root),
         FileLayout {
@@ -341,7 +338,10 @@ async fn 
test_mor_lifecycle_asserts_base_and_log_file_counts() {
             .unwrap()
             .to_str()
             .unwrap()
-            .contains(&format!("_{}.log.", update.instant)),
+            .contains(&format!(
+                "_{}.log.",
+                update.requested_time.as_deref().unwrap()
+            )),
         "log file should include upsert instant: {:?}",
         logs[0]
     );
diff --git a/crates/core/tests/table_write_rollback_tests.rs 
b/crates/core/tests/table_write_rollback_tests.rs
index 8c5433c2..a6ad38f2 100644
--- a/crates/core/tests/table_write_rollback_tests.rs
+++ b/crates/core/tests/table_write_rollback_tests.rs
@@ -117,7 +117,10 @@ async fn test_marker_dir_removed_after_successful_commit() 
{
     let mut table = create_partitioned(dir.path()).await;
     let result = table.append([batch(vec![("a", "sf", 1)])]).await.unwrap();
 
-    let marker_dir = dir.path().join(".hoodie/.temp").join(&result.instant);
+    let marker_dir = dir
+        .path()
+        .join(".hoodie/.temp")
+        .join(result.requested_time.as_deref().unwrap());
     let leftovers = std::fs::read_dir(&marker_dir)
         .map(|entries| entries.count())
         .unwrap_or(0);
@@ -789,13 +792,13 @@ async fn 
test_pending_rollback_of_completed_instant_is_refused() {
     let rollback_ts = "30000101000000017";
     let plan = HoodieRollbackPlan {
         instant_to_rollback: Some(HoodieInstantInfo {
-            commit_time: committed.instant.clone(),
+            commit_time: committed.requested_time.clone().unwrap(),
             action: "commit".to_string(),
         }),
         rollback_requests: Some(vec![HoodieRollbackRequest {
             partition_path: "city=sf".to_string(),
             file_id: Some(data_file.split('_').next().unwrap().to_string()),
-            latest_base_instant: Some(committed.instant.clone()),
+            latest_base_instant: 
Some(committed.requested_time.clone().unwrap()),
             files_to_be_deleted: vec![absolute(&table, &relative)],
             log_blocks_to_be_deleted: Some(Default::default()),
         }]),
@@ -815,7 +818,7 @@ async fn 
test_pending_rollback_of_completed_instant_is_refused() {
     let err = result.unwrap_err();
     assert!(
         matches!(&err, CoreError::Unsupported(msg)
-            if msg.contains(rollback_ts) && msg.contains(&committed.instant)),
+            if msg.contains(rollback_ts) && 
msg.contains(committed.requested_time.as_deref().unwrap())),
         "{err}"
     );
     assert_eq!(pending_rollbacks(&timeline), vec![rollback_ts.to_string()]);
diff --git a/crates/core/tests/table_write_tests.rs 
b/crates/core/tests/table_write_tests.rs
index 41a00d3e..78169026 100644
--- a/crates/core/tests/table_write_tests.rs
+++ b/crates/core/tests/table_write_tests.rs
@@ -137,6 +137,13 @@ fn rows_by_id(batches: &[RecordBatch]) -> Vec<(String, 
i64)> {
     rows
 }
 
+/// The base file of the table's only file slice, relative to the table base.
+async fn only_base_file(table: &Table) -> String {
+    let slices = table.get_file_slices(&ReadOptions::new()).await.unwrap();
+    assert_eq!(slices.len(), 1);
+    slices[0].base_file_relative_path().unwrap().unwrap()
+}
+
 #[tokio::test]
 async fn test_create_and_append_then_read() {
     let dir = tempdir().unwrap();
@@ -150,28 +157,31 @@ async fn test_create_and_append_then_read() {
         .unwrap();
 
     let result = table.append([sample_batch()]).await.unwrap();
-    assert_eq!(result.num_rows, 3);
+    assert_eq!((result.num_writes, result.num_inserts), (3, 3));
+    let instant = result.requested_time.as_deref().unwrap();
+    let base_file = only_base_file(&table).await;
     assert!(
-        dir.path().join(&result.base_file_path).is_file(),
-        "base file should exist at {}",
-        result.base_file_path
+        dir.path().join(&base_file).is_file(),
+        "base file should exist at {base_file}"
     );
+    let timeline = dir.path().join(".hoodie/timeline");
+    let commit_file = timeline.join(format!(
+        "{instant}_{}.commit",
+        result.completion_time.as_deref().unwrap()
+    ));
     assert!(
-        dir.path().join(&result.commit_relative_path).is_file(),
+        commit_file.is_file(),
         "commit file should exist at {}",
-        result.commit_relative_path
+        commit_file.display()
     );
-    let timeline = dir.path().join(".hoodie/timeline");
     assert!(
         timeline
-            .join(format!("{}.commit.requested", result.instant))
+            .join(format!("{instant}.commit.requested"))
             .is_file(),
         "data timeline should fence with .commit.requested"
     );
     assert!(
-        timeline
-            .join(format!("{}.inflight", result.instant))
-            .is_file(),
+        timeline.join(format!("{instant}.inflight")).is_file(),
         "data timeline should fence with .inflight"
     );
 
@@ -395,7 +405,7 @@ async fn 
test_create_with_metadata_and_append_updates_files_partition() {
         .create()
         .await
         .unwrap();
-    let result = table.append([sample_batch()]).await.unwrap();
+    table.append([sample_batch()]).await.unwrap();
     assert!(table.is_metadata_table_enabled());
 
     let reopened = Table::new(base_uri).await.unwrap();
@@ -407,7 +417,13 @@ async fn 
test_create_with_metadata_and_append_updates_files_partition() {
         .await
         .unwrap();
     let files = records.get("").unwrap();
-    assert!(files.has_active_file(&result.base_file_path));
+    let written: Vec<String> = std::fs::read_dir(dir.path())
+        .unwrap()
+        .map(|entry| entry.unwrap().file_name().into_string().unwrap())
+        .filter(|name| name.ends_with(".parquet"))
+        .collect();
+    assert_eq!(written.len(), 1);
+    assert!(files.has_active_file(&written[0]));
 }
 
 #[tokio::test]
@@ -477,13 +493,13 @@ async fn 
test_write_result_completion_time_matches_timeline() {
         }
         .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),
+            ("append", &append.requested_time, &append.completion_time),
+            ("upsert", &upsert.requested_time, &upsert.completion_time),
+            ("delete", &delete.requested_time, &delete.completion_time),
         ] {
             let completed = instants
                 .iter()
-                .find(|i| &i.timestamp == instant)
+                .find(|i| Some(&i.timestamp) == instant.as_ref())
                 .unwrap_or_else(|| panic!("{table_type:?}: {verb} instant not 
on the timeline"));
             assert!(completion_time.is_some(), "{table_type:?} {verb}");
             assert_eq!(
@@ -1443,8 +1459,16 @@ async fn 
test_merge_on_read_writes_parquet_logs_and_merges_snapshot() {
         .append([ordered_batch(vec![("a", 1, 1), ("b", 2, 1)])])
         .await
         .unwrap();
-    assert!(append.commit_relative_path.ends_with(".deltacommit"));
-    assert!(dir.path().join(&append.base_file_path).is_file());
+    assert!(
+        dir.path()
+            .join(format!(
+                ".hoodie/timeline/{}_{}.deltacommit",
+                append.requested_time.as_deref().unwrap(),
+                append.completion_time.as_deref().unwrap()
+            ))
+            .is_file()
+    );
+    assert!(dir.path().join(only_base_file(&table).await).is_file());
     let incremental = table
         .read(
             &ReadOptions::new()
@@ -1593,7 +1617,7 @@ async fn 
test_upsert_deduplicates_keys_and_delete_missing_key_is_noop() {
         .await
         .unwrap();
     assert_eq!(result.num_deletes, 0);
-    assert!(result.instant.is_empty());
+    assert!(result.requested_time.is_none());
 }
 
 #[tokio::test]
@@ -1890,7 +1914,11 @@ async fn test_input_meta_columns_are_replaced() {
             .as_any()
             .downcast_ref::<StringArray>()
             .unwrap();
-        assert_eq!(commit_times.value(0), result.instant, "{table_type:?}");
+        assert_eq!(
+            Some(commit_times.value(0)),
+            result.requested_time.as_deref(),
+            "{table_type:?}"
+        );
 
         let mut fields = vec![Field::new("_hoodie_record_key", DataType::Utf8, 
true)];
         fields.extend(
@@ -3232,7 +3260,7 @@ async fn test_mor_unsupported_verbs_error_clearly() {
         .count();
     let result = table.delete("value = 12345").await.unwrap();
     assert_eq!(result.num_deletes, 0);
-    assert_eq!(result.instant, "");
+    assert_eq!(result.requested_time, None);
     let commits_after = std::fs::read_dir(dir.path().join(".hoodie/timeline"))
         .unwrap()
         .count();
@@ -4247,7 +4275,12 @@ async fn 
upserts_into_spark_written_table_read_through_metadata_table() {
         // Spark leaves the timeline timezone unset, which Java reads as LOCAL.
         let local_now = || 
chrono::Local::now().format("%Y%m%d%H%M%S%3f").to_string();
         let before = local_now();
-        let instant = table.upsert([rows.clone()]).await.unwrap().instant;
+        let instant = table
+            .upsert([rows.clone()])
+            .await
+            .unwrap()
+            .requested_time
+            .unwrap();
         let after = local_now();
         assert!(
             before <= instant && instant <= after,
@@ -4539,7 +4572,7 @@ async fn test_delete_keys_in_wrong_partition_is_noop() {
             .await
             .unwrap();
         assert_eq!(result.num_deletes, 0, "{table_type:?}");
-        assert!(result.instant.is_empty(), "{table_type:?}");
+        assert!(result.requested_time.is_none(), "{table_type:?}");
         assert_eq!(timeline_file_count(dir.path()), before, "{table_type:?}");
     }
 }
diff --git a/python/src/internal.rs b/python/src/internal.rs
index 524c614b..293ef66a 100644
--- a/python/src/internal.rs
+++ b/python/src/internal.rs
@@ -50,7 +50,7 @@ use hudi::file_group::reader::FileGroupReader;
 use hudi::storage::error::StorageError;
 use hudi::table::CompactionResult;
 use hudi::table::builder::TableBuilder;
-use hudi::table::{AppendResult, QueryType, ReadOptions, Table};
+use hudi::table::{QueryType, ReadOptions, Table};
 use hudi::timeline::Timeline;
 use hudi::timeline::instant::Instant;
 use hudi::{HoodieKey, UpsertOptions, WriteResult};
@@ -1088,7 +1088,7 @@ impl HudiCompactionResult {
 impl From<CompactionResult> for HudiCompactionResult {
     fn from(r: CompactionResult) -> Self {
         Self {
-            instant_time: committed(r.instant),
+            instant_time: r.requested_time,
             completion_time: r.completion_time,
             num_file_groups: r.num_file_groups,
             num_log_files: r.num_log_files,
@@ -1132,7 +1132,7 @@ impl HudiWriteResult {
 impl From<WriteResult> for HudiWriteResult {
     fn from(r: WriteResult) -> Self {
         Self {
-            instant_time: committed(r.instant),
+            instant_time: r.requested_time,
             completion_time: r.completion_time,
             num_writes: r.num_writes,
             num_updates: r.num_updates,
@@ -1142,24 +1142,6 @@ impl From<WriteResult> for HudiWriteResult {
     }
 }
 
-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,
-            num_deletes: 0,
-        }
-    }
-}
-
-/// The core reports a write that made no commit with an empty instant.
-fn committed(instant: String) -> Option<String> {
-    (!instant.is_empty()).then_some(instant)
-}
-
 /// A Python argument that is either one string or a list of strings.
 #[cfg(not(tarpaulin_include))]
 #[derive(FromPyObject)]
@@ -1821,7 +1803,6 @@ mod tests {
     use arrow::datatypes::{DataType, Field, Fields, Schema, TimeUnit};
     use arrow::record_batch::RecordBatch;
     use hudi::WriteResult;
-    use hudi::table::AppendResult;
     use std::collections::HashMap;
     use std::sync::Arc;
 
@@ -2077,7 +2058,7 @@ mod tests {
     #[test]
     fn write_results_convert_to_python_counts() {
         let rewrite = HudiWriteResult::from(WriteResult {
-            instant: "20240101000000000".to_string(),
+            requested_time: Some("20240101000000000".to_string()),
             completion_time: None,
             num_writes: 4,
             num_updates: 1,
@@ -2094,35 +2075,6 @@ mod tests {
             ),
             (4, 1, 2, 3)
         );
-
-        let append = HudiWriteResult::from(AppendResult {
-            instant: "20240101000000001".to_string(),
-            completion_time: None,
-            commit_relative_path: String::new(),
-            base_file_path: String::new(),
-            num_rows: 5,
-        });
-        assert_eq!(append.instant_time.as_deref(), Some("20240101000000001"));
-        assert_eq!(
-            (
-                append.num_writes,
-                append.num_updates,
-                append.num_inserts,
-                append.num_deletes
-            ),
-            (5, 0, 5, 0)
-        );
-
-        // A write that matched nothing reports no instant.
-        let noop = HudiWriteResult::from(WriteResult {
-            instant: String::new(),
-            completion_time: None,
-            num_writes: 0,
-            num_updates: 0,
-            num_inserts: 0,
-            num_deletes: 0,
-        });
-        assert_eq!(noop.instant_time, None);
     }
 
     #[test]

Reply via email to