Skip to content

Commit 0f76a20

Browse files
committed
fix(reader): keep matching rows when filtering a promoted column
A file written before an int -> long, float -> double, or decimal precision promotion keeps its old physical type, while the predicate is bound to the table type. The row filter cast the literal down to the file's Arrow type, so an out-of-range literal became null and dropped every row, and a double literal was rounded to f32. Cast a numeric column to the table type before comparing instead, as RecordBatchTransformer does for the output. Other columns keep the literal cast. Page-index pruning is left to #3507 and #3521. Part of #3122.
1 parent 26356f5 commit 0f76a20

2 files changed

Lines changed: 118 additions & 11 deletions

File tree

‎crates/iceberg/src/arrow/reader/predicate_visitor.rs‎

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use arrow_string::like::starts_with;
3434
use fnv::FnvHashSet;
3535
use parquet::schema::types::SchemaDescriptor;
3636

37-
use crate::arrow::get_arrow_datum;
37+
use crate::arrow::{get_arrow_datum, type_to_arrow_type};
3838
use crate::error::{Result, invalid_data};
3939
use crate::expr::visitors::bound_predicate_visitor::BoundPredicateVisitor;
4040
use crate::expr::{BoundPredicate, BoundReference};
@@ -278,6 +278,22 @@ fn project_column(
278278
}
279279
}
280280

281+
/// Like `project_column`, but casts a numeric column the file stores in another type to
282+
/// `target_type`, as `RecordBatchTransformer` does. Narrowing the literal to the file type
283+
/// instead can turn it into null or round it.
284+
fn project_promoted_column(
285+
batch: &RecordBatch,
286+
column_idx: usize,
287+
target_type: &DataType,
288+
) -> std::result::Result<ArrayRef, ArrowError> {
289+
let column = project_column(batch, column_idx)?;
290+
if column.data_type().is_numeric() && column.data_type() != target_type {
291+
cast(&column, target_type)
292+
} else {
293+
Ok(column)
294+
}
295+
}
296+
281297
fn compute_is_nan(array: &ArrayRef) -> std::result::Result<BooleanArray, ArrowError> {
282298
// Compute NaN over the contiguous values slice, then fold the null bitmap
283299
// in with a single bitwise AND so that null slots become false.
@@ -425,9 +441,10 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
425441
) -> Result<Box<PredicateResult>> {
426442
if let Some(idx) = self.bound_reference(reference)? {
427443
let literal = get_arrow_datum(literal)?;
444+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
428445

429446
Ok(Box::new(move |batch| {
430-
let left = project_column(&batch, idx)?;
447+
let left = project_promoted_column(&batch, idx, &target_type)?;
431448
let literal = try_cast_literal(&literal, left.data_type())?;
432449
lt(&left, literal.as_ref())
433450
}))
@@ -445,9 +462,10 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
445462
) -> Result<Box<PredicateResult>> {
446463
if let Some(idx) = self.bound_reference(reference)? {
447464
let literal = get_arrow_datum(literal)?;
465+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
448466

449467
Ok(Box::new(move |batch| {
450-
let left = project_column(&batch, idx)?;
468+
let left = project_promoted_column(&batch, idx, &target_type)?;
451469
let literal = try_cast_literal(&literal, left.data_type())?;
452470
lt_eq(&left, literal.as_ref())
453471
}))
@@ -465,9 +483,10 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
465483
) -> Result<Box<PredicateResult>> {
466484
if let Some(idx) = self.bound_reference(reference)? {
467485
let literal = get_arrow_datum(literal)?;
486+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
468487

469488
Ok(Box::new(move |batch| {
470-
let left = project_column(&batch, idx)?;
489+
let left = project_promoted_column(&batch, idx, &target_type)?;
471490
let literal = try_cast_literal(&literal, left.data_type())?;
472491
gt(&left, literal.as_ref())
473492
}))
@@ -485,9 +504,10 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
485504
) -> Result<Box<PredicateResult>> {
486505
if let Some(idx) = self.bound_reference(reference)? {
487506
let literal = get_arrow_datum(literal)?;
507+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
488508

489509
Ok(Box::new(move |batch| {
490-
let left = project_column(&batch, idx)?;
510+
let left = project_promoted_column(&batch, idx, &target_type)?;
491511
let literal = try_cast_literal(&literal, left.data_type())?;
492512
gt_eq(&left, literal.as_ref())
493513
}))
@@ -505,9 +525,10 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
505525
) -> Result<Box<PredicateResult>> {
506526
if let Some(idx) = self.bound_reference(reference)? {
507527
let literal = get_arrow_datum(literal)?;
528+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
508529

509530
Ok(Box::new(move |batch| {
510-
let left = project_column(&batch, idx)?;
531+
let left = project_promoted_column(&batch, idx, &target_type)?;
511532
let literal = try_cast_literal(&literal, left.data_type())?;
512533
eq(&left, literal.as_ref())
513534
}))
@@ -525,9 +546,10 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
525546
) -> Result<Box<PredicateResult>> {
526547
if let Some(idx) = self.bound_reference(reference)? {
527548
let literal = get_arrow_datum(literal)?;
549+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
528550

529551
Ok(Box::new(move |batch| {
530-
let left = project_column(&batch, idx)?;
552+
let left = project_promoted_column(&batch, idx, &target_type)?;
531553
let literal = try_cast_literal(&literal, left.data_type())?;
532554
neq(&left, literal.as_ref())
533555
}))
@@ -589,10 +611,11 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
589611
.iter()
590612
.map(|lit| get_arrow_datum(lit).unwrap())
591613
.collect();
614+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
592615

593616
Ok(Box::new(move |batch| {
594617
// update this if arrow ever adds a native is_in kernel
595-
let left = project_column(&batch, idx)?;
618+
let left = project_promoted_column(&batch, idx, &target_type)?;
596619

597620
let mut acc = constant_bool_array(false, batch.num_rows());
598621
for literal in &literals {
@@ -619,10 +642,11 @@ impl BoundPredicateVisitor for PredicateConverter<'_> {
619642
.iter()
620643
.map(|lit| get_arrow_datum(lit).unwrap())
621644
.collect();
645+
let target_type = type_to_arrow_type(&reference.field().field_type)?;
622646

623647
Ok(Box::new(move |batch| {
624648
// update this if arrow ever adds a native not_in kernel
625-
let left = project_column(&batch, idx)?;
649+
let left = project_promoted_column(&batch, idx, &target_type)?;
626650
let mut acc = constant_bool_array(true, batch.num_rows());
627651
for literal in &literals {
628652
let literal = try_cast_literal(literal, left.data_type())?;

‎crates/iceberg/src/arrow/reader/row_filter.rs‎

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ mod tests {
228228

229229
use arrow_array::cast::AsArray;
230230
use arrow_array::{
231-
ArrayRef, Decimal128Array, Float32Array, Int32Array, Int64Array, LargeStringArray,
232-
RecordBatch, StringArray,
231+
ArrayRef, Decimal128Array, Float32Array, Float64Array, Int32Array, Int64Array,
232+
LargeStringArray, RecordBatch, StringArray,
233233
};
234234
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
235235
use futures::TryStreamExt;
@@ -460,6 +460,89 @@ mod tests {
460460
}
461461
}
462462

463+
/// Writes `column` as field 1, reads it back as `iceberg_type` under each predicate, and
464+
/// checks the values that survive (`None` when no row does).
465+
async fn assert_promoted_reads(
466+
column: ArrayRef,
467+
iceberg_type: PrimitiveType,
468+
cases: Vec<(Predicate, Option<ArrayRef>)>,
469+
) {
470+
let tmp = TempDir::new().unwrap();
471+
let path = format!("{}/column.parquet", tmp.path().to_str().unwrap());
472+
let arrow_schema = Arc::new(ArrowSchema::new(vec![field_with_id(
473+
"v",
474+
column.data_type().clone(),
475+
1,
476+
)]));
477+
let batch = RecordBatch::try_new(arrow_schema.clone(), vec![column]).unwrap();
478+
write_row_groups(&path, arrow_schema, vec![batch], false);
479+
480+
let schema = value_schema(Type::Primitive(iceberg_type));
481+
let file_size = std::fs::metadata(&path).unwrap().len();
482+
let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
483+
for (predicate, expected) in cases {
484+
let task = FileScanTask::builder()
485+
.with_file_size_in_bytes(file_size)
486+
.with_start(0)
487+
.with_length(0)
488+
.with_data_file_path(path.clone())
489+
.with_data_file_format(DataFileFormat::Parquet)
490+
.with_schema(schema.clone())
491+
.with_project_field_ids(vec![1])
492+
.with_predicate(Some(predicate.bind(schema.clone(), true).unwrap()))
493+
.with_case_sensitive(true)
494+
.build()
495+
.unwrap();
496+
let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
497+
let batches: Vec<RecordBatch> = reader
498+
.clone()
499+
.read(tasks)
500+
.unwrap()
501+
.stream()
502+
.try_collect()
503+
.await
504+
.unwrap();
505+
let actual = collapse(&batches).map(|batch| Arc::clone(batch.column(0)));
506+
assert_eq!(actual, expected, "{predicate}");
507+
}
508+
}
509+
510+
/// A file written before a type promotion keeps the old physical type, so the row
511+
/// filter must compare its values promoted to the table type.
512+
#[tokio::test]
513+
async fn test_predicate_on_promoted_column() {
514+
let longs = |values: Vec<i64>| Some(Arc::new(Int64Array::from(values)) as ArrayRef);
515+
let all = longs(vec![1, 2]);
516+
let above = || Datum::long(i64::from(i32::MAX) + 1);
517+
let below = || Datum::long(i64::from(i32::MIN) - 1);
518+
let v = || Reference::new("v");
519+
assert_promoted_reads(
520+
Arc::new(Int32Array::from(vec![1, 2])),
521+
PrimitiveType::Long,
522+
vec![
523+
(v().less_than(above()), all.clone()),
524+
(v().less_than_or_equal_to(above()), all.clone()),
525+
(v().greater_than_or_equal_to(below()), all.clone()),
526+
(v().not_equal_to(above()), all),
527+
(v().is_in([Datum::long(1), above()]), longs(vec![1])),
528+
(v().is_not_in([Datum::long(1), above()]), longs(vec![2])),
529+
],
530+
)
531+
.await;
532+
533+
// 0.1f32 promotes to a double that is greater than 0.1, not equal to it.
534+
let promoted = Arc::new(Float64Array::from(vec![f64::from(0.1f32)])) as ArrayRef;
535+
assert_promoted_reads(
536+
Arc::new(Float32Array::from(vec![0.1])),
537+
PrimitiveType::Double,
538+
vec![
539+
(v().greater_than(Datum::double(0.1)), Some(promoted)),
540+
(v().equal_to(Datum::double(0.1)), None),
541+
],
542+
)
543+
.await;
544+
}
545+
463546
/// Verifies that file splits respect byte ranges and only read specific row groups.
464547
#[tokio::test]
465548
async fn test_file_splits_respect_byte_ranges() {

0 commit comments

Comments
 (0)