Skip to content

Commit 20cdfe9

Browse files
authored
feat: write value stats for append data files (#477)
1 parent 8e44700 commit 20cdfe9

16 files changed

Lines changed: 1493 additions & 257 deletions

crates/integrations/datafusion/src/merge_into.rs

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ use std::collections::{HashMap, HashSet};
2626
use std::sync::atomic::{AtomicU64, Ordering};
2727
use std::sync::Arc;
2828

29-
use datafusion::arrow::array::{Array, Int32Array, RecordBatch, UInt32Array, UInt64Array};
29+
use datafusion::arrow::array::{
30+
Array, ArrayRef, Int32Array, RecordBatch, UInt32Array, UInt64Array,
31+
};
3032
use datafusion::arrow::compute;
3133
use datafusion::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
3234
use datafusion::error::{DataFusionError, Result as DFResult};
@@ -37,7 +39,7 @@ use datafusion::sql::sqlparser::ast::{
3739
};
3840
use futures::TryStreamExt;
3941

40-
use paimon::spec::{datums_to_binary_row, extract_datum_from_arrow, CoreOptions};
42+
use paimon::spec::{datums_to_binary_row, extract_datum_from_arrow, CoreOptions, DataField};
4143
use paimon::table::{CopyOnWriteMergeWriter, DataSplitBuilder, Table, WriteBuilder};
4244

4345
use crate::error::to_datafusion_error;
@@ -569,13 +571,6 @@ async fn execute_cow_merge_inner(
569571

570572
// Handle NOT MATCHED → INSERT
571573
if !clauses.inserts.is_empty() {
572-
let table_fields: Vec<String> = table
573-
.schema()
574-
.fields()
575-
.iter()
576-
.map(|f| f.name().to_string())
577-
.collect();
578-
579574
let insert_sql = if has_target_data {
580575
format!(
581576
"SELECT {s_alias}.* FROM {source_ref} AS {s_alias} \
@@ -595,7 +590,7 @@ async fn execute_cow_merge_inner(
595590
&clauses.inserts,
596591
s_alias,
597592
&[],
598-
&table_fields,
593+
table.schema().fields(),
599594
temp_tracker,
600595
)
601596
.await?;
@@ -734,21 +729,14 @@ async fn execute_merge_into_once(
734729
injected_columns.push(format!("__upd_{col}"));
735730
}
736731
}
737-
// Table schema field names for reordering INSERT columns
738-
let table_fields: Vec<String> = table
739-
.schema()
740-
.fields()
741-
.iter()
742-
.map(|f| f.name().to_string())
743-
.collect();
744732
let mut temp_tracker = TempTableTracker::new(ctx);
745733
let insert_batches = build_insert_batches(
746734
ctx,
747735
&not_matched_batches,
748736
&parsed.inserts,
749737
s_alias,
750738
&injected_columns,
751-
&table_fields,
739+
table.schema().fields(),
752740
&mut temp_tracker,
753741
)
754742
.await?;
@@ -854,7 +842,7 @@ async fn build_insert_batches(
854842
inserts: &[MergeInsertClause],
855843
s_alias: &str,
856844
injected_columns: &[String],
857-
table_fields: &[String],
845+
table_fields: &[DataField],
858846
temp_tracker: &mut TempTableTracker<'_>,
859847
) -> DFResult<Vec<RecordBatch>> {
860848
if not_matched_batches.is_empty() || not_matched_batches.iter().all(|b| b.num_rows() == 0) {
@@ -883,7 +871,7 @@ async fn build_insert_batches_inner(
883871
inserts: &[MergeInsertClause],
884872
s_alias: &str,
885873
tmp_name: &str,
886-
table_fields: &[String],
874+
table_fields: &[DataField],
887875
) -> DFResult<Vec<RecordBatch>> {
888876
let mut all_batches = Vec::new();
889877
let mut consumed_predicates: Vec<String> = Vec::new();
@@ -910,12 +898,61 @@ async fn build_insert_batches_inner(
910898
let sql = format!("SELECT {select_clause} FROM {tmp_name} AS {s_alias}{where_clause}");
911899

912900
let batches = ctx.ctx().sql(&sql).await?.collect().await?;
913-
all_batches.extend(batches);
901+
for batch in batches {
902+
all_batches.push(normalize_insert_batch_to_table_schema(
903+
&batch,
904+
table_fields,
905+
)?);
906+
}
914907
}
915908

916909
Ok(all_batches)
917910
}
918911

912+
fn normalize_insert_batch_to_table_schema(
913+
batch: &RecordBatch,
914+
table_fields: &[DataField],
915+
) -> DFResult<RecordBatch> {
916+
if batch.num_columns() != table_fields.len() {
917+
return Err(DataFusionError::Plan(format!(
918+
"MERGE INSERT output has {} columns but target table has {}",
919+
batch.num_columns(),
920+
table_fields.len()
921+
)));
922+
}
923+
924+
let target_schema =
925+
paimon::arrow::build_target_arrow_schema(table_fields).map_err(to_datafusion_error)?;
926+
let mut columns = Vec::with_capacity(table_fields.len());
927+
928+
for (target_idx, field) in table_fields.iter().enumerate() {
929+
let column = batch.column(target_idx).clone();
930+
let target_type = target_schema.field(target_idx).data_type();
931+
let column = cast_insert_column(field.name(), column, target_type)?;
932+
columns.push(column);
933+
}
934+
935+
RecordBatch::try_new(target_schema, columns).map_err(DataFusionError::from)
936+
}
937+
938+
fn cast_insert_column(
939+
name: &str,
940+
column: ArrayRef,
941+
target_type: &ArrowDataType,
942+
) -> DFResult<ArrayRef> {
943+
if column.data_type() == target_type {
944+
return Ok(column);
945+
}
946+
947+
compute::cast(column.as_ref(), target_type).map_err(|e| {
948+
DataFusionError::Plan(format!(
949+
"Cannot cast MERGE INSERT column '{name}' from {:?} to {:?}: {e}",
950+
column.data_type(),
951+
target_type
952+
))
953+
})
954+
}
955+
919956
/// Remove injected columns from batches, keeping only source columns.
920957
fn strip_non_source_columns(
921958
batches: &[RecordBatch],
@@ -946,7 +983,7 @@ fn strip_non_source_columns(
946983
/// When the INSERT specifies explicit columns (`INSERT (col2, col1) VALUES (expr2, expr1)`),
947984
/// the output must be reordered to match the table schema so that `write_arrow_batch`
948985
/// (which reads columns by positional index) maps them correctly.
949-
fn insert_select_clause(ins: &MergeInsertClause, table_fields: &[String]) -> String {
986+
fn insert_select_clause(ins: &MergeInsertClause, table_fields: &[DataField]) -> String {
950987
if ins.columns.is_empty() && ins.value_exprs.is_empty() {
951988
"*".to_string()
952989
} else {
@@ -962,11 +999,11 @@ fn insert_select_clause(ins: &MergeInsertClause, table_fields: &[String]) -> Str
962999
table_fields
9631000
.iter()
9641001
.map(|field| {
965-
let key = field.to_lowercase();
1002+
let key = field.name().to_lowercase();
9661003
match col_expr_map.get(&key) {
967-
Some(expr) => format!("{expr} AS {}", quote_identifier(field)),
1004+
Some(expr) => format!("{expr} AS {}", quote_identifier(field.name())),
9681005
// Column not in INSERT list — fill with NULL
969-
None => format!("NULL AS {}", quote_identifier(field)),
1006+
None => format!("NULL AS {}", quote_identifier(field.name())),
9701007
}
9711008
})
9721009
.collect::<Vec<_>>()
@@ -1546,7 +1583,7 @@ mod tests {
15461583
use datafusion::sql::sqlparser::parser::Parser;
15471584
use paimon::catalog::{Catalog, Identifier};
15481585
use paimon::io::FileIOBuilder;
1549-
use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema};
1586+
use paimon::spec::{DataField, DataType, IntType, Schema as PaimonSchema, TableSchema};
15501587
use paimon::{CatalogOptions, FileSystemCatalog, Options};
15511588
use tempfile::TempDir;
15521589

@@ -1613,6 +1650,42 @@ mod tests {
16131650
}
16141651
}
16151652

1653+
#[test]
1654+
fn test_normalize_merge_insert_batch_uses_position() {
1655+
let table_fields = vec![
1656+
DataField::new(0, "a".to_string(), DataType::Int(IntType::new())),
1657+
DataField::new(1, "b".to_string(), DataType::Int(IntType::new())),
1658+
];
1659+
let batch = RecordBatch::try_new(
1660+
Arc::new(Schema::new(vec![
1661+
Field::new("b", ArrowDataType::Int32, false),
1662+
Field::new("x", ArrowDataType::Int32, false),
1663+
])),
1664+
vec![
1665+
Arc::new(Int32Array::from(vec![100])),
1666+
Arc::new(Int32Array::from(vec![7])),
1667+
],
1668+
)
1669+
.unwrap();
1670+
1671+
let normalized = normalize_insert_batch_to_table_schema(&batch, &table_fields).unwrap();
1672+
let first = normalized
1673+
.column(0)
1674+
.as_any()
1675+
.downcast_ref::<Int32Array>()
1676+
.unwrap();
1677+
let second = normalized
1678+
.column(1)
1679+
.as_any()
1680+
.downcast_ref::<Int32Array>()
1681+
.unwrap();
1682+
1683+
assert_eq!(normalized.schema().field(0).name(), "a");
1684+
assert_eq!(normalized.schema().field(1).name(), "b");
1685+
assert_eq!(first.value(0), 100);
1686+
assert_eq!(second.value(0), 7);
1687+
}
1688+
16161689
#[test]
16171690
fn test_source_partition_pruning_requires_partition_equality() {
16181691
let merge = parse_merge(

crates/paimon/src/arrow/format/blob.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use super::{FilePredicates, FormatFileReader, FormatFileWriter};
18+
use super::{FilePredicates, FormatFileReader, FormatFileWriter, FormatWriteResult};
1919
use crate::arrow::build_target_arrow_schema;
2020
use crate::io::{FileRead, FileWrite};
2121
use crate::spec::{BlobDescriptor, DataField, DataType};
@@ -725,7 +725,7 @@ impl FormatFileWriter for BlobFormatWriter {
725725
Ok(())
726726
}
727727

728-
async fn close(mut self: Box<Self>) -> crate::Result<u64> {
728+
async fn close(mut self: Box<Self>) -> crate::Result<FormatWriteResult> {
729729
let index_bytes = encode_delta_varints_write(&self.lengths);
730730
let index_length = index_bytes.len() as i32;
731731

@@ -739,7 +739,7 @@ impl FormatFileWriter for BlobFormatWriter {
739739

740740
let total = self.bytes_written + index_length as u64 + BLOB_FOOTER_SIZE;
741741
self.writer.close().await?;
742-
Ok(total)
742+
Ok(FormatWriteResult::new(total))
743743
}
744744
}
745745

crates/paimon/src/arrow/format/mod.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ mod avro;
1919
pub(crate) mod blob;
2020
mod mosaic;
2121
mod orc;
22-
mod parquet;
22+
pub(crate) mod parquet;
2323
mod row;
2424
mod shredding;
2525
#[cfg(feature = "vortex")]
@@ -29,6 +29,7 @@ mod vortex;
2929
pub(crate) use parquet::ParquetFormatWriter;
3030

3131
use crate::io::{FileRead, OutputFile};
32+
use crate::spec::stats::BinaryTableStats;
3233
use crate::spec::{DataField, Predicate};
3334
use crate::table::{ArrowRecordBatchStream, RowRange};
3435
use crate::Error;
@@ -102,8 +103,40 @@ pub(crate) trait FormatFileWriter: Send {
102103
async fn flush(&mut self) -> crate::Result<()>;
103104

104105
/// Flush and close the writer, finalizing the file on storage.
105-
/// Returns the total number of bytes written.
106-
async fn close(self: Box<Self>) -> crate::Result<u64>;
106+
async fn close(self: Box<Self>) -> crate::Result<FormatWriteResult>;
107+
}
108+
109+
pub(crate) struct FormatWriteResult {
110+
pub(crate) file_size: u64,
111+
pub(crate) value_stats: Option<FormatValueStats>,
112+
}
113+
114+
pub(crate) struct FormatValueStats {
115+
pub(crate) stats: BinaryTableStats,
116+
pub(crate) columns: Option<Vec<String>>,
117+
}
118+
119+
impl FormatWriteResult {
120+
pub(crate) fn new(file_size: u64) -> Self {
121+
Self {
122+
file_size,
123+
value_stats: None,
124+
}
125+
}
126+
127+
pub(crate) fn with_value_stats(
128+
file_size: u64,
129+
value_stats: BinaryTableStats,
130+
columns: Option<Vec<String>>,
131+
) -> Self {
132+
Self {
133+
file_size,
134+
value_stats: Some(FormatValueStats {
135+
stats: value_stats,
136+
columns,
137+
}),
138+
}
139+
}
107140
}
108141

109142
/// Create a format reader based on the file extension.
@@ -180,6 +213,7 @@ pub(crate) async fn create_format_writer(
180213
output,
181214
compression,
182215
zstd_level,
216+
format_options.cloned().unwrap_or_default(),
183217
));
184218
shredding::ShreddingFormatWriter::create(
185219
writer_factory,

0 commit comments

Comments
 (0)