hcrosse opened a new issue, #3617: URL: https://github.com/apache/iceberg-rust/issues/3617
### Apache Iceberg Rust version Main at `896f9d6c50ff52fa9e43aec170d3f14bfb40b512`, tested on Linux amd64 with Rust 1.99.1 and Arrow/Parquet 59.3.2. ### Describe the bug When a Parquet file stores a `timestamp` column as `TIMESTAMP(MILLIS)`, a scan that filters on that column without selecting it silently drops matching rows. Selecting the column instead fails with `DataInvalid => Unsupported Arrow data type: Timestamp(ms)`. I reproduced the row loss with one synthetic row at `1970-01-01T00:00:02`. The file uses `TIMESTAMP(MILLIS)` and the table schema uses `timestamp`. Filtering on that value and selecting only `id` returns no rows with the default reader. Disabling row-group pruning returns one row. Enabling only page-index selection or bloom filtering also drops it. Both [`ArrowReaderBuilder`](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/arrow/reader/mod.rs#L74-L76) and [`TableScanBuilder`](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/scan/mod.rs#L171-L173) enable row-group filtering by default, with page-index selection and bloom filtering off. This does not require opting into pruning. The reader [interprets the raw row-group bounds as microseconds](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/arrow/schema.rs#L1018-L1023). The [page-index conversion preserves the raw integer](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/expr/visitors/page_index_evaluator.rs#L506-L515), and the [bloom check hashes the literal without converting its unit](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/expr/visitors/bloom_filter_evaluator.rs#L271-L272). The stored value is `2000`, but the predicate uses `2000000` microseconds. Selecting `ts` fails even without a predicate. The [projection path calls `arrow_schema_to_schema`](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/arrow/reader/projection.rs#L184-L192), which [rejects the millisecond type](https://github.com/apache/iceberg-rust/blob/896f9d6c50ff52fa9e43aec170d3f14bfb40b512/crates/iceberg/src/arrow/schema.rs#L643-L670). I kept both symptoms in one issue because accepting millisecond columns in projection without fixing pruning would turn this error into silent row loss for scans that select and filter `ts`. Iceberg Java has the same row-group statistics bug, reported in https://github.com/apache/iceberg/issues/18173. Its open fix, https://github.com/apache/iceberg/pull/18321, converts the bounds to microseconds before filtering. ### To Reproduce Create a Rust 2024 binary crate with these dependencies in `Cargo.toml`. Put the program in `src/main.rs` and run `cargo run`. I used Arrow/Parquet 59.3.2, resolved from these version ranges. ```toml [dependencies] arrow-array = "59.2" arrow-schema = "59.2" futures = "0.3" iceberg = { git = "https://github.com/apache/iceberg-rust", rev = "896f9d6c50ff52fa9e43aec170d3f14bfb40b512" } parquet = "59.2" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` ```rust use std::collections::HashMap; use std::error::Error; use std::fs::File; use std::sync::Arc; use arrow_array::{Int32Array, RecordBatch, TimestampMillisecondArray}; use arrow_schema::{DataType, Field, Schema as ArrowSchema, TimeUnit}; use futures::TryStreamExt; use iceberg::Runtime; use iceberg::arrow::ArrowReaderBuilder; use iceberg::expr::{Bind, Predicate, Reference}; use iceberg::io::FileIO; use iceberg::scan::FileScanTask; use iceberg::spec::{DataFileFormat, Datum, NestedField, PrimitiveType, Schema, SchemaRef, Type}; use parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY}; use parquet::file::properties::WriterProperties; #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { let path = std::env::temp_dir().join("ts_millis.parquet"); let path = path.to_str().unwrap(); let field_id = |id: &str| HashMap::from([(PARQUET_FIELD_ID_META_KEY.into(), id.into())]); let millis = DataType::Timestamp(TimeUnit::Millisecond, None); let file_schema = Arc::new(ArrowSchema::new(vec![ Field::new("id", DataType::Int32, false).with_metadata(field_id("1")), Field::new("ts", millis, false).with_metadata(field_id("2")), ])); let batch = RecordBatch::try_new( file_schema.clone(), vec![ Arc::new(Int32Array::from(vec![0])), Arc::new(TimestampMillisecondArray::from(vec![2_000])), ], )?; let props = WriterProperties::builder() .set_bloom_filter_enabled(true) .build(); let mut writer = ArrowWriter::try_new(File::create(path)?, file_schema, Some(props))?; writer.write(&batch)?; writer.close()?; let schema = Arc::new( Schema::builder() .with_fields(vec![ NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(), NestedField::required(2, "ts", Type::Primitive(PrimitiveType::Timestamp)).into(), ]) .build()?, ); let ts_is_2s = Reference::new("ts").equal_to(Datum::timestamp_micros(2_000_000)); let reader = || ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()); let full = rows(reader(), path, &schema, None).await?; println!("full scan: {full} row(s)"); for (label, reader) in [ ("default reader", reader()), ( "row-group filtering off", reader().with_row_group_filtering_enabled(false), ), ( "page index only", reader() .with_row_group_filtering_enabled(false) .with_row_selection_enabled(true), ), ( "bloom filter only", reader() .with_row_group_filtering_enabled(false) .with_bloom_filter_enabled(true), ), ] { let n = rows(reader, path, &schema, Some(&ts_is_2s)).await?; println!("ts = 1970-01-01T00:00:02, {label}: {n} row(s), expected 1"); } Ok(()) } async fn rows( reader: ArrowReaderBuilder, path: &str, schema: &SchemaRef, filter: Option<&Predicate>, ) -> Result<usize, Box<dyn Error>> { let size = std::fs::metadata(path)?.len(); let task = FileScanTask::builder() .with_file_size_in_bytes(size) .with_start(0) .with_length(size) .with_data_file_path(path.to_string()) .with_data_file_format(DataFileFormat::Parquet) .with_schema(schema.clone()) .with_project_field_ids(vec![1]) .with_predicate(filter.map(|p| p.bind(schema.clone(), true)).transpose()?) .with_case_sensitive(true) .build()?; let batches: Vec<RecordBatch> = reader .build() .read(Box::pin(futures::stream::iter([Ok(task)])))? .stream() .try_collect() .await?; Ok(batches.iter().map(RecordBatch::num_rows).sum()) } ``` Output: ```text $ cargo run # iceberg-rust 896f9d6c, parquet/arrow 59.3.2, rustc 1.99.1, linux/amd64 full scan: 1 row(s) ts = 1970-01-01T00:00:02, default reader: 0 row(s), expected 1 ts = 1970-01-01T00:00:02, row-group filtering off: 1 row(s), expected 1 ts = 1970-01-01T00:00:02, page index only: 0 row(s), expected 1 ts = 1970-01-01T00:00:02, bloom filter only: 0 row(s), expected 1 ``` To see the projection error, pass `vec![2]` or `vec![1, 2]` to `with_project_field_ids`. Every mode then fails, with or without the predicate. The reproduction uses `FileScanTask` and local files, not catalog import or a complete `TableScan`. ### Expected behavior The filtered scan returns the matching row in every pruning mode. Pruning either uses the file's timestamp unit or keeps data it cannot safely rule out. Selecting `ts` returns `1970-01-01T00:00:02` as a microsecond timestamp, as Iceberg Java's [Parquet reader](https://github.com/apache/iceberg/blob/5a2ccf5ba830838ecf182bccb516d5ee48f8b8ed/parquet/src/main/java/org/apache/iceberg/data/parquet/GenericParquetReaders.java#L160-L169) does. If millisecond files are out of scope, the reader rejects them in every mode instead of returning incomplete results. ### Willingness to contribute I would be willing to contribute a fix for this bug with guidance from the Iceberg community -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
