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 02b02463 feat: enable DV merge engine and postpone fixed bucket writes
(#1101)
02b02463 is described below
commit 02b02463d2f4f997240512938a7af76de3bd5750
Author: Jingsong Lee <[email protected]>
AuthorDate: Sun Oct 11 14:40:01 2026 +0800
feat: enable DV merge engine and postpone fixed bucket writes (#1101)
---
crates/paimon/src/table/kv_file_reader.rs | 117 +++---
crates/paimon/src/table/kv_file_writer.rs | 52 ---
.../src/table/postpone_fixed_bucket_write.rs | 16 +-
.../table/postpone_fixed_bucket_write_builder.rs | 17 +-
crates/paimon/src/table/table_write.rs | 37 --
crates/paimon/tests/dv_merge_engine_write_test.rs | 405 +++++++++++++++++++++
6 files changed, 484 insertions(+), 160 deletions(-)
diff --git a/crates/paimon/src/table/kv_file_reader.rs
b/crates/paimon/src/table/kv_file_reader.rs
index a9d9cdf6..f50585d6 100644
--- a/crates/paimon/src/table/kv_file_reader.rs
+++ b/crates/paimon/src/table/kv_file_reader.rs
@@ -1732,61 +1732,76 @@ mod tests {
/// key with no replacement is resurrected.
#[tokio::test]
async fn dv_merge_on_read_applies_deletion_vectors_before_key_merge() {
- let file_io = test_file_io();
- let table_path = "memory:/dv_merge_on_read_with_dv";
- setup_dirs(&file_io, table_path).await;
- let table = pk_table(
- &file_io,
- table_path,
- &[
- ("deletion-vectors.enabled", "true"),
- ("deletion-vectors.merge-on-read", "true"),
- ],
- );
+ for (engine, expected) in [
+ ("deduplicate", 11),
+ ("partial-update", 11),
+ ("aggregation", 21),
+ ] {
+ let file_io = test_file_io();
+ let table_path = "memory:/dv_merge_on_read_with_dv";
+ setup_dirs(&file_io, table_path).await;
+ let table = pk_table(
+ &file_io,
+ table_path,
+ &[
+ ("merge-engine", engine),
+ ("deletion-vectors.enabled", "true"),
+ ("deletion-vectors.merge-on-read", "true"),
+ ],
+ );
+ let table = if engine == "aggregation" {
+ table.copy_with_options(HashMap::from([(
+ "fields.value.aggregate-function".into(),
+ "sum".into(),
+ )]))
+ } else {
+ table
+ };
- write_commit(&table, &int_batch(vec![1, 2], vec![Some(10),
Some(20)])).await;
- write_commit(&table, &int_batch(vec![1, 3], vec![Some(11),
Some(30)])).await;
+ write_commit(&table, &int_batch(vec![1, 2], vec![Some(10),
Some(20)])).await;
+ write_commit(&table, &int_batch(vec![1, 3], vec![Some(11),
Some(30)])).await;
- let all_files = table
- .new_read_builder()
- .new_scan()
- .with_scan_all_files()
- .plan()
- .await
- .unwrap();
- let mut files = all_files
- .splits()
- .iter()
- .flat_map(|split| split.data_files().iter().cloned())
- .collect::<Vec<_>>();
- files.sort_by_key(|file| file.min_sequence_number);
- assert_eq!(files.len(), 2);
- files[0].level = 1;
+ let all_files = table
+ .new_read_builder()
+ .new_scan()
+ .with_scan_all_files()
+ .plan()
+ .await
+ .unwrap();
+ let mut files = all_files
+ .splits()
+ .iter()
+ .flat_map(|split| split.data_files().iter().cloned())
+ .collect::<Vec<_>>();
+ files.sort_by_key(|file| file.min_sequence_number);
+ assert_eq!(files.len(), 2);
+ files[0].level = 1;
- let deletion_file = write_deletion_file(&file_io, table_path,
&[1]).await;
- let split = DataSplitBuilder::new()
- .with_snapshot(2)
- .with_partition(BinaryRow::new(0))
- .with_bucket(0)
- .with_bucket_path(format!("{table_path}/bucket-0"))
- .with_total_buckets(1)
- .with_data_files(files)
- .with_data_deletion_files(vec![Some(deletion_file), None])
- .with_raw_convertible(false)
- .build()
- .unwrap();
+ let deletion_file = write_deletion_file(&file_io, table_path,
&[1]).await;
+ let split = DataSplitBuilder::new()
+ .with_snapshot(2)
+ .with_partition(BinaryRow::new(0))
+ .with_bucket(0)
+ .with_bucket_path(format!("{table_path}/bucket-0"))
+ .with_total_buckets(1)
+ .with_data_files(files)
+ .with_data_deletion_files(vec![Some(deletion_file), None])
+ .with_raw_convertible(false)
+ .build()
+ .unwrap();
- let batches = table
- .new_read_builder()
- .new_read()
- .unwrap()
- .to_arrow(&[split])
- .unwrap()
- .try_collect::<Vec<_>>()
- .await
- .unwrap();
- assert_eq!(int_column(&batches, "id"), vec![1, 3]);
- assert_eq!(int_column(&batches, "value"), vec![11, 30]);
+ let batches = table
+ .new_read_builder()
+ .new_read()
+ .unwrap()
+ .to_arrow(&[split])
+ .unwrap()
+ .try_collect::<Vec<_>>()
+ .await
+ .unwrap();
+ assert_eq!(int_column(&batches, "id"), vec![1, 3]);
+ assert_eq!(int_column(&batches, "value"), vec![expected, 30]);
+ }
}
/// Disjoint files in one split are consumed as sequential merge groups. A
diff --git a/crates/paimon/src/table/kv_file_writer.rs
b/crates/paimon/src/table/kv_file_writer.rs
index d12c2b26..33845d4e 100644
--- a/crates/paimon/src/table/kv_file_writer.rs
+++ b/crates/paimon/src/table/kv_file_writer.rs
@@ -110,7 +110,6 @@ pub(crate) struct KeyValueWriteConfig {
pub sequence_field_indices: Vec<usize>,
/// Merge engine for deduplication.
pub merge_engine: MergeEngine,
- pub deletion_vectors_enabled: bool,
/// File indexes follow the sorted, merged data rows and never changelog
rows.
pub file_index_options: Option<Arc<FileIndexOptions>>,
}
@@ -145,29 +144,11 @@ impl KeyValueFileWriter {
partial_update.validate_write_mode(true, &config.table_name)?;
partial_update
.validated_aggregate_functions(&config.value_fields,
&config.primary_keys)?;
-
- if config.deletion_vectors_enabled {
- return Err(crate::Error::Unsupported {
- message: format!(
- "Table '{}' uses merge-engine=partial-update with
deletion-vectors.enabled=true, which is not supported yet",
- config.table_name
- ),
- });
- }
}
if config.merge_engine == MergeEngine::Aggregation {
AggregationConfig::new(&config.table_options)
.validate_runtime_mode(true, &config.table_name)?;
-
- if config.deletion_vectors_enabled {
- return Err(crate::Error::Unsupported {
- message: format!(
- "Table '{}' uses merge-engine=aggregation with
deletion-vectors.enabled=true, which is not supported yet",
- config.table_name
- ),
- });
- }
}
let paths = super::data_file_path_factory::DataFilePathFactory::new(
@@ -1346,7 +1327,6 @@ mod tests {
],
sequence_field_indices: vec![1],
merge_engine,
- deletion_vectors_enabled: false,
file_index_options: None,
}
}
@@ -2518,22 +2498,6 @@ mod tests {
);
}
- #[test]
- fn test_new_rejects_partial_update_with_deletion_vectors() {
- let mut config = test_write_config(MergeEngine::PartialUpdate);
- config.deletion_vectors_enabled = true;
-
- let err =
KeyValueFileWriter::new(FileIOBuilder::new("memory").build().unwrap(), config,
0)
- .err()
- .unwrap();
-
- assert!(matches!(
- err,
- crate::Error::Unsupported { message }
- if message.contains("deletion-vectors.enabled=true")
- ));
- }
-
#[test]
fn test_new_rejects_unsupported_partial_update_options() {
let mut config = test_write_config(MergeEngine::PartialUpdate);
@@ -2814,22 +2778,6 @@ mod tests {
arrow_select::concat::concat_batches(&schema, &batches).unwrap()
}
- #[test]
- fn test_new_rejects_aggregation_with_deletion_vectors() {
- let mut config = test_write_config(MergeEngine::Aggregation);
- config.deletion_vectors_enabled = true;
-
- let err =
KeyValueFileWriter::new(FileIOBuilder::new("memory").build().unwrap(), config,
0)
- .err()
- .unwrap();
-
- assert!(matches!(
- err,
- crate::Error::Unsupported { message }
- if message.contains("deletion-vectors.enabled=true")
- ));
- }
-
#[test]
fn test_new_rejects_unsupported_aggregation_options() {
let mut config = test_write_config(MergeEngine::Aggregation);
diff --git a/crates/paimon/src/table/postpone_fixed_bucket_write.rs
b/crates/paimon/src/table/postpone_fixed_bucket_write.rs
index 6f2f59a9..44565fc6 100644
--- a/crates/paimon/src/table/postpone_fixed_bucket_write.rs
+++ b/crates/paimon/src/table/postpone_fixed_bucket_write.rs
@@ -20,7 +20,6 @@ use super::postpone_fixed_bucket_router::{
validate_postpone_fixed_bucket_table, PostponeFixedBucketRouter,
};
use crate::resource::ResourceContext;
-use crate::spec::CoreOptions;
use crate::table::{CommitMessage, PostponeBucketPlan, Table, TableCommit,
TableWrite};
use crate::Result;
use arrow_array::RecordBatch;
@@ -55,7 +54,7 @@ impl PostponeFixedBucketTableWrite {
overwrite: bool,
resources: Option<ResourceContext>,
) -> Result<Self> {
- validate_postpone_fixed_bucket_write(table)?;
+ validate_postpone_fixed_bucket_table(table)?;
let mut inner = TableWrite::new(table, commit_user)?;
if let Some(resources) = resources {
inner = inner.with_resources(resources);
@@ -231,19 +230,6 @@ impl PostponeFixedBucketTableCommit {
}
}
-fn validate_postpone_fixed_bucket_write(table: &Table) -> Result<()> {
- validate_postpone_fixed_bucket_table(table)?;
- if CoreOptions::new(table.schema().options()).deletion_vectors_enabled() {
- return Err(crate::Error::Unsupported {
- message: format!(
- "Table '{}' cannot use postpone fixed-bucket writes with
deletion-vectors.enabled=true because deletion-vector scans skip the level-0
files produced by batch writers; use the normal postpone writer or disable
deletion vectors",
- table.identifier().full_name()
- ),
- });
- }
- Ok(())
-}
-
#[cfg(test)]
mod tests {
use crate::catalog::Identifier;
diff --git a/crates/paimon/src/table/postpone_fixed_bucket_write_builder.rs
b/crates/paimon/src/table/postpone_fixed_bucket_write_builder.rs
index 6f7e3d3c..530fa496 100644
--- a/crates/paimon/src/table/postpone_fixed_bucket_write_builder.rs
+++ b/crates/paimon/src/table/postpone_fixed_bucket_write_builder.rs
@@ -160,16 +160,23 @@ mod tests {
"deletion-vectors.enabled".to_string(),
"true".to_string(),
)]));
- let error = dv_table
+ let mut write = dv_table
.new_postpone_fixed_bucket_write_builder()
.unwrap()
.with_bucket_plan(bucket_plan(&dv_table, 1))
.new_write()
- .err()
.unwrap();
- assert!(matches!(error, crate::Error::Unsupported { ref message }
- if message.contains("postpone fixed-bucket writes")
- && message.contains("deletion-vectors.enabled=true")));
+ write
+ .write_arrow_batch(&make_batch(vec![5], vec![50]))
+ .await
+ .unwrap();
+ let messages = write.prepare_commit().await.unwrap();
+ assert_eq!(messages.len(), 1);
+ assert_eq!(messages[0].bucket, 0);
+ assert_eq!(messages[0].total_buckets, Some(1));
+ assert_eq!(messages[0].new_files[0].level, 0);
+ assert_eq!(messages[0].new_files[0].row_count, 1);
+ write.close().await;
}
#[tokio::test]
diff --git a/crates/paimon/src/table/table_write.rs
b/crates/paimon/src/table/table_write.rs
index 5c153f25..23675fe1 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -1634,8 +1634,6 @@ impl TableWrite {
value_fields: self.table.schema().fields().to_vec(),
sequence_field_indices:
self.sequence_field_indices.clone(),
merge_engine: self.merge_engine,
- deletion_vectors_enabled:
CoreOptions::new(self.table.schema().options())
- .deletion_vectors_enabled(),
file_index_options: self.file_index_options.clone(),
},
next_seq,
@@ -3629,41 +3627,6 @@ pub(in crate::table) mod tests {
.unwrap();
}
- #[tokio::test]
- async fn
test_rejects_partial_update_with_deletion_vectors_when_creating_writer() {
- let file_io = test_file_io();
- let table_path = "memory:/test_partial_update_dv_table";
- setup_dirs(&file_io, table_path).await;
-
- let table = Table::new(
- file_io,
- Identifier::new("default", "test_partial_update_dv_table"),
- table_path.to_string(),
- TableSchema::new(
- 0,
- &Schema::builder()
- .column("id", DataType::Int(IntType::new()))
- .column("value", DataType::Int(IntType::new()))
- .primary_key(["id"])
- .option("bucket", "1")
- .option("merge-engine", "partial-update")
- .option("deletion-vectors.enabled", "true")
- .build()
- .unwrap(),
- ),
- None,
- );
-
- let mut table_write = TableWrite::new(&table,
"test-user".to_string()).unwrap();
- let err = table_write
- .write_arrow_batch(&make_batch(vec![1], vec![10]))
- .await
- .unwrap_err();
- assert!(
- matches!(err, crate::Error::Unsupported { message } if
message.contains("deletion-vectors.enabled=true"))
- );
- }
-
#[tokio::test]
async fn test_write_partitioned() {
let file_io = test_file_io();
diff --git a/crates/paimon/tests/dv_merge_engine_write_test.rs
b/crates/paimon/tests/dv_merge_engine_write_test.rs
new file mode 100644
index 00000000..3af0e91c
--- /dev/null
+++ b/crates/paimon/tests/dv_merge_engine_write_test.rs
@@ -0,0 +1,405 @@
+// 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 std::sync::Arc;
+
+use arrow_array::{Array, ArrayRef, Int32Array, RecordBatch, StringArray};
+use futures::TryStreamExt;
+use paimon::spec::{DataType, Datum, IntType, PredicateBuilder, Schema,
TableSchema, VarCharType};
+use paimon::table::{CommitMessage, Table, TableCommit};
+
+use common::incremental_helpers::{memory_table, persist_table_schema,
setup_dirs};
+
+type Row = (i32, Option<i32>, Option<String>);
+
+async fn table(engine: &str, bucket: &str, input_changelog: bool) -> Table {
+ let mut builder = Schema::builder()
+ .column("id", DataType::Int(IntType::new()))
+ .column("value", DataType::Int(IntType::new()))
+ .column("text", DataType::VarChar(VarCharType::string_type()))
+ .primary_key(["id"])
+ .option("bucket", bucket)
+ .option("merge-engine", engine)
+ .option("deletion-vectors.enabled", "true")
+ .option("source.split.target-size", "1b")
+ .option("source.split.open-file-cost", "1b");
+ if engine == "aggregation" {
+ builder = builder
+ .option("fields.value.aggregate-function", "sum")
+ .option("fields.text.aggregate-function", "last_non_null_value");
+ }
+ if input_changelog {
+ builder = builder.option("changelog-producer", "input");
+ }
+ let (io, table) = memory_table(
+ "memory:/dv_merge_engine",
+ TableSchema::new(0, &builder.build().unwrap()),
+ );
+ setup_dirs(&io, table.location()).await;
+ persist_table_schema(&io, table.location(), table.schema()).await;
+ table
+}
+
+fn batch(ids: Vec<i32>, values: Vec<Option<i32>>, texts: Vec<Option<&str>>) ->
RecordBatch {
+ RecordBatch::try_from_iter([
+ ("id", Arc::new(Int32Array::from(ids)) as ArrayRef),
+ ("value", Arc::new(Int32Array::from(values)) as ArrayRef),
+ ("text", Arc::new(StringArray::from(texts)) as ArrayRef),
+ ])
+ .unwrap()
+}
+
+async fn write(table: &Table, batches: Vec<RecordBatch>, identifier: i64) ->
Vec<CommitMessage> {
+ let builder = table
+ .new_write_builder()
+ .with_commit_user("dv-test")
+ .unwrap();
+ let mut writer = builder.new_write().unwrap();
+ for batch in batches {
+ writer.write_arrow_batch(&batch).await.unwrap();
+ }
+ let messages = writer.prepare_commit().await.unwrap();
+ TableCommit::new(table.clone(), "dv-test".into())
+ .commit_with_identifier(messages.clone(), identifier)
+ .await
+ .unwrap();
+ writer.close().await;
+ messages
+}
+
+async fn rows(table: &Table) -> Vec<Row> {
+ let builder = table.new_read_builder();
+ let plan = builder.new_scan().plan().await.unwrap();
+ let batches = builder
+ .new_read()
+ .unwrap()
+ .to_arrow(plan.splits())
+ .unwrap()
+ .try_collect::<Vec<_>>()
+ .await
+ .unwrap();
+ let mut rows = Vec::new();
+ for batch in batches {
+ let ids = batch
+ .column(0)
+ .as_any()
+ .downcast_ref::<Int32Array>()
+ .unwrap();
+ let values = batch
+ .column(1)
+ .as_any()
+ .downcast_ref::<Int32Array>()
+ .unwrap();
+ let texts = batch
+ .column(2)
+ .as_any()
+ .downcast_ref::<StringArray>()
+ .unwrap();
+ for row in 0..batch.num_rows() {
+ rows.push((
+ ids.value(row),
+ values.is_valid(row).then(|| values.value(row)),
+ texts.is_valid(row).then(|| texts.value(row).to_string()),
+ ));
+ }
+ }
+ rows.sort();
+ rows
+}
+
+#[tokio::test]
+async fn dv_merge_engines_fold_inputs_and_restore_across_commits() {
+ for engine in ["partial-update", "aggregation"] {
+ for bucket in ["1", "-1"] {
+ for input_changelog in [false, true] {
+ let table = table(engine, bucket, input_changelog).await;
+ let first = write(
+ &table,
+ vec![
+ batch(
+ vec![1, 2],
+ vec![Some(10), Some(20)],
+ vec![Some("first"), Some("second")],
+ ),
+ batch(vec![1], vec![None], vec![Some("patched")]),
+ ],
+ 1,
+ )
+ .await;
+ assert_eq!(
+ first
+ .iter()
+ .flat_map(|m| &m.new_files)
+ .map(|f| f.row_count)
+ .sum::<i64>(),
+ 2
+ );
+ assert!(first
+ .iter()
+ .flat_map(|m| &m.new_files)
+ .all(|f| f.level == 0));
+ assert_eq!(
+ first
+ .iter()
+ .flat_map(|m| &m.new_changelog_files)
+ .map(|f| f.row_count)
+ .sum::<i64>(),
+ if input_changelog { 3 } else { 0 }
+ );
+ // Java does not expose uncompacted level-0 files by default
in DV mode.
+ assert!(rows(&table).await.is_empty());
+ let visible =
table.copy_with_options(std::collections::HashMap::from([(
+ "deletion-vectors.merge-on-read".into(),
+ "true".into(),
+ )]));
+ assert_eq!(
+ rows(&visible).await,
+ vec![
+ (1, Some(10), Some("patched".into())),
+ (2, Some(20), Some("second".into())),
+ ]
+ );
+ write(
+ &table,
+ vec![batch(
+ vec![1, 3],
+ vec![Some(3), Some(30)],
+ vec![None, Some("third")],
+ )],
+ 2,
+ )
+ .await;
+ assert_eq!(
+ rows(&visible).await,
+ vec![
+ (
+ 1,
+ Some(if engine == "aggregation" { 13 } else { 3 }),
+ Some("patched".into())
+ ),
+ (2, Some(20), Some("second".into())),
+ (3, Some(30), Some("third".into())),
+ ]
+ );
+ assert!(rows(&table).await.is_empty());
+ assert_eq!(
+ table
+ .snapshot_manager()
+ .get_latest_snapshot()
+ .await
+ .unwrap()
+ .unwrap()
+ .id(),
+ 2
+ );
+ }
+ }
+ }
+}
+
+#[tokio::test]
+async fn dv_merge_engine_filters_are_applied_after_merging() {
+ for engine in ["partial-update", "aggregation"] {
+ let table = table(engine, "1", false).await;
+ write(
+ &table,
+ vec![batch(vec![1], vec![Some(5)], vec![Some("old")])],
+ 1,
+ )
+ .await;
+ write(&table, vec![batch(vec![1], vec![Some(6)], vec![None])],
2).await;
+ let visible =
table.copy_with_options(std::collections::HashMap::from([(
+ "deletion-vectors.merge-on-read".into(),
+ "true".into(),
+ )]));
+ let predicates = PredicateBuilder::new(visible.schema().fields());
+ for (predicate, expected) in [
+ (
+ predicates.greater_than("value", Datum::Int(10)).unwrap(),
+ if engine == "aggregation" { 1 } else { 0 },
+ ),
+ (
+ predicates
+ .equal("text", Datum::String("old".into()))
+ .unwrap(),
+ 1,
+ ),
+ ] {
+ let mut builder = visible.new_read_builder();
+ builder
+ .with_projection(&["id"])
+ .unwrap()
+ .with_filter(predicate)
+ .with_limit(1);
+ let plan = builder.new_scan().plan().await.unwrap();
+ let batches = builder
+ .new_read()
+ .unwrap()
+ .to_arrow(plan.splits())
+ .unwrap()
+ .try_collect::<Vec<_>>()
+ .await
+ .unwrap();
+ assert_eq!(
+ batches.iter().map(RecordBatch::num_rows).sum::<usize>(),
+ expected
+ );
+ for batch in batches {
+ assert_eq!(batch.schema().fields().len(), 1);
+ let ids = batch
+ .column(0)
+ .as_any()
+ .downcast_ref::<Int32Array>()
+ .unwrap();
+ assert!(ids.values().iter().all(|id| *id == 1));
+ }
+ }
+ }
+}
+
+#[tokio::test]
+async fn dv_merge_engines_keep_inputs_across_buffer_flushes() {
+ for engine in ["partial-update", "aggregation"] {
+ let table =
+ table(engine, "1", true)
+ .await
+ .copy_with_options(std::collections::HashMap::from([
+ ("write.parquet-buffer-size".into(), "1b".into()),
+ ("deletion-vectors.merge-on-read".into(), "true".into()),
+ ]));
+ let messages = write(
+ &table,
+ vec![
+ batch(vec![1], vec![Some(1)], vec![Some("first")]),
+ batch(vec![1], vec![Some(2)], vec![None]),
+ batch(vec![1], vec![Some(3)], vec![Some("last")]),
+ ],
+ 1,
+ )
+ .await;
+ assert_eq!(messages.iter().flat_map(|m| &m.new_files).count(), 3);
+ assert_eq!(
+ messages
+ .iter()
+ .flat_map(|m| &m.new_changelog_files)
+ .map(|f| f.row_count)
+ .sum::<i64>(),
+ 3
+ );
+ assert_eq!(
+ rows(&table).await,
+ vec![(
+ 1,
+ Some(if engine == "aggregation" { 6 } else { 3 }),
+ Some("last".into())
+ )]
+ );
+ }
+}
+
+#[tokio::test]
+async fn dv_postpone_fixed_bucket_writers_merge_and_overwrite() {
+ for engine in ["deduplicate", "partial-update", "aggregation"] {
+ let table =
+ table(engine, "-2", true)
+ .await
+ .copy_with_options(std::collections::HashMap::from([(
+ "postpone.default-bucket-num".into(),
+ "3".into(),
+ )]));
+ for (identifier, inputs, expected) in [
+ (
+ 1,
+ vec![
+ batch(vec![1], vec![Some(10)], vec![Some("first")]),
+ batch(vec![1], vec![Some(20)], vec![None]),
+ ],
+ if engine == "aggregation" { 30 } else { 20 },
+ ),
+ (
+ 2,
+ vec![batch(vec![1], vec![Some(30)], vec![Some("after")])],
+ if engine == "aggregation" { 60 } else { 30 },
+ ),
+ ] {
+ let builder = table
+ .new_postpone_fixed_bucket_write_builder()
+ .unwrap()
+ .with_commit_user("fixed-dv")
+ .unwrap();
+ let mut writer = builder.new_write().unwrap();
+ for data in inputs {
+ writer.write_arrow_batch(&data).await.unwrap();
+ }
+ let messages = writer.prepare_commit().await.unwrap();
+ assert!(messages
+ .iter()
+ .all(|m| m.bucket >= 0 && m.bucket < 3 && m.total_buckets ==
Some(3)));
+ assert!(messages
+ .iter()
+ .flat_map(|m| &m.new_files)
+ .all(|f| f.level == 0 && f.row_count == 1));
+ builder
+ .new_commit()
+ .commit_with_identifier(messages, identifier)
+ .await
+ .unwrap();
+ writer.close().await;
+ assert!(rows(&table).await.is_empty());
+ let visible =
table.copy_with_options(std::collections::HashMap::from([(
+ "deletion-vectors.merge-on-read".into(),
+ "true".into(),
+ )]));
+ let text = if identifier == 2 {
+ Some("after".into())
+ } else if engine == "deduplicate" {
+ None
+ } else {
+ Some("first".into())
+ };
+ assert_eq!(rows(&visible).await, vec![(1, Some(expected), text)]);
+ }
+ let replacement =
table.copy_with_options(std::collections::HashMap::from([(
+ "postpone.default-bucket-num".into(),
+ "5".into(),
+ )]));
+ let builder = replacement
+ .new_postpone_fixed_bucket_write_builder()
+ .unwrap()
+ .with_commit_user("fixed-dv-overwrite")
+ .unwrap()
+ .with_overwrite();
+ let mut writer = builder.new_write().unwrap();
+ writer
+ .write_arrow_batch(&batch(vec![1], vec![Some(100)], vec![None]))
+ .await
+ .unwrap();
+ let messages = writer.prepare_commit().await.unwrap();
+ assert!(messages
+ .iter()
+ .all(|m| m.total_buckets == Some(5) &&
m.new_changelog_files.is_empty()));
+ builder.new_commit().commit(messages).await.unwrap();
+ writer.close().await;
+ let visible =
table.copy_with_options(std::collections::HashMap::from([(
+ "deletion-vectors.merge-on-read".into(),
+ "true".into(),
+ )]));
+ assert_eq!(rows(&visible).await, vec![(1, Some(100), None)]);
+ }
+}