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 ae858eae [core] Align BIGINT and timestamp schema evolution with Java 
(#1107)
ae858eae is described below

commit ae858eae590cc0d0aeeeda9904ec6e974641f7f7
Author: Jingsong Lee <[email protected]>
AuthorDate: Mon Oct 12 10:59:02 2026 +0800

    [core] Align BIGINT and timestamp schema evolution with Java (#1107)
---
 crates/paimon/src/arrow/evolution_cast.rs         |  63 ++++
 crates/paimon/src/arrow/nested_evolution.rs       | 354 ++++++++++++++++++----
 crates/paimon/tests/schema_evolution_cast_test.rs | 173 ++++++++++-
 3 files changed, 528 insertions(+), 62 deletions(-)

diff --git a/crates/paimon/src/arrow/evolution_cast.rs 
b/crates/paimon/src/arrow/evolution_cast.rs
index 5a980386..4648dd6f 100644
--- a/crates/paimon/src/arrow/evolution_cast.rs
+++ b/crates/paimon/src/arrow/evolution_cast.rs
@@ -83,6 +83,69 @@ pub(super) fn numeric_to_integer(source: &ArrayRef, target: 
&ArrowType) -> crate
     Ok(result)
 }
 
+/// TimestampToNumericPrimitiveCastRule returns floor-divided epoch seconds,
+/// rather than Arrow's physical timestamp ticks. LTZ uses Java Timestamp's
+/// local-date-time round trip, whose epoch day is an int.
+pub(super) fn timestamp_to_bigint(source: &ArrayRef, local_zoned: bool) -> 
crate::Result<ArrayRef> {
+    let ArrowType::Timestamp(unit, _) = source.data_type() else {
+        unreachable!("timestamp source validated by evolution dispatch")
+    };
+    let raw = arrow_cast::cast(source, &ArrowType::Int64).map_err(cast_error)?;
+    let values = 
Int64Array::from_iter(raw.as_primitive::<Int64Type>().iter().map(|value| {
+        value.and_then(|value| {
+            if !local_zoned {
+                return Some(value.div_euclid(ticks_per_second(unit)));
+            }
+            let millis = match unit {
+                TimeUnit::Second => value.checked_mul(1_000)?,
+                TimeUnit::Millisecond => value,
+                TimeUnit::Microsecond => value.div_euclid(1_000),
+                TimeUnit::Nanosecond => value.div_euclid(1_000_000),
+            };
+            // Timestamp.toLocalDateTime casts the epoch day to int, then
+            // Timestamp.fromLocalDateTime reconstructs its milliseconds.
+            let day = millis.div_euclid(86_400_000) as i32;
+            let millis = i64::from(day) * 86_400_000 + 
millis.rem_euclid(86_400_000);
+            Some(millis.div_euclid(1_000))
+        })
+    }));
+    Ok(Arc::new(values))
+}
+
+/// Java NumericPrimitiveToTimestamp multiplies long seconds by 1000 with
+/// signed-long wrapping. Only after that conversion do we rebuild Arrow's
+/// declared timestamp unit, returning NULL if its storage cannot hold it.
+pub(super) fn numeric_epoch_seconds_to_timestamp(
+    source: &ArrayRef,
+    target_arrow: &ArrowType,
+) -> crate::Result<ArrayRef> {
+    let ArrowType::Timestamp(unit, None) = target_arrow else {
+        unreachable!("ordinary timestamp target validated by evolution 
dispatch")
+    };
+    let raw = arrow_cast::cast(source, &ArrowType::Int64).map_err(cast_error)?;
+    let values = 
Int64Array::from_iter(raw.as_primitive::<Int64Type>().iter().map(|value| {
+        value.and_then(|value| {
+            let millis = value.wrapping_mul(1_000);
+            match unit {
+                TimeUnit::Second => Some(millis.div_euclid(1_000)),
+                TimeUnit::Millisecond => Some(millis),
+                TimeUnit::Microsecond => millis.checked_mul(1_000),
+                TimeUnit::Nanosecond => millis.checked_mul(1_000_000),
+            }
+        })
+    }));
+    arrow_cast::cast(&(Arc::new(values) as ArrayRef), 
target_arrow).map_err(cast_error)
+}
+
+fn ticks_per_second(unit: &TimeUnit) -> i64 {
+    match unit {
+        TimeUnit::Second => 1,
+        TimeUnit::Millisecond => 1_000,
+        TimeUnit::Microsecond => 1_000_000,
+        TimeUnit::Nanosecond => 1_000_000_000,
+    }
+}
+
 /// Match DateTimeUtils.truncate for the same timestamp kind. Work in i128 so
 /// flooring a pre-epoch value cannot overflow the source's i64 unit before
 /// converting to a coarser target unit. Cross-kind timezone casts are handled
diff --git a/crates/paimon/src/arrow/nested_evolution.rs 
b/crates/paimon/src/arrow/nested_evolution.rs
index e3fad1d3..883b89c3 100644
--- a/crates/paimon/src/arrow/nested_evolution.rs
+++ b/crates/paimon/src/arrow/nested_evolution.rs
@@ -34,8 +34,7 @@
 use std::sync::Arc;
 
 use arrow_array::{
-    new_null_array, Array, ArrayRef, Int64Array, ListArray, MapArray, 
StringArray, StructArray,
-    UInt32Array,
+    new_null_array, Array, ArrayRef, ListArray, MapArray, StringArray, 
StructArray, UInt32Array,
 };
 use arrow_cast::cast;
 use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Fields};
@@ -205,20 +204,24 @@ pub(crate) fn evolve_column(
         _ => {}
     }
 
-    // Java `NumericPrimitiveToTimestamp` reads an INTEGER/BIGINT column as 
epoch
-    // *seconds* (`value.longValue() * 1000` millis, fed to `toLocalDateTime`);
-    // Arrow's Int64->Timestamp cast instead reinterprets the raw value 
directly
-    // in the column's unit, so a BIGINT epoch column evolved to TIMESTAMP(6)
-    // would read 10^6x too small (~1970). Scale seconds into the target unit
-    // first. Scoped to INTEGER/BIGINT into TIMESTAMP *without* local time 
zone:
-    // Java's `create()` returns null for every other numeric source, and its
-    // TIMESTAMP-with-local-zone target resolves the offset with
-    // `ZoneId.systemDefault()` (environment dependent) -- both are left to the
-    // Arrow fallthrough below rather than guessed at here.
-    if matches!(source_type, DataType::Int(_) | DataType::BigInt(_)) {
-        if let DataType::Timestamp(ts) = target_type {
-            return numeric_epoch_seconds_to_timestamp(source, &target_arrow, 
ts.precision());
-        }
+    // Apply the epoch-seconds rule at every leaf, including nested containers.
+    // Numeric -> LTZ depends on Java's system time zone and retains its 
existing
+    // path; ordinary TIMESTAMP has no such runtime dependency.
+    if matches!(source_type, DataType::Int(_) | DataType::BigInt(_))
+        && matches!(target_type, DataType::Timestamp(_))
+    {
+        return 
super::evolution_cast::numeric_epoch_seconds_to_timestamp(source, 
&target_arrow);
+    }
+    if matches!(target_type, DataType::BigInt(_))
+        && matches!(
+            source_type,
+            DataType::Timestamp(_) | DataType::LocalZonedTimestamp(_)
+        )
+    {
+        return super::evolution_cast::timestamp_to_bigint(
+            source,
+            matches!(source_type, DataType::LocalZonedTimestamp(_)),
+        );
     }
 
     if matches!(
@@ -252,49 +255,6 @@ pub(crate) fn evolve_column(
     })
 }
 
-/// Evolve an INTEGER/BIGINT column into a TIMESTAMP (without local time zone)
-/// the way Java `NumericPrimitiveToTimestamp` does: interpret each value as
-/// epoch seconds and scale it into the target time unit. Nulls are preserved,
-/// and a value that overflows `i64` in the target unit becomes NULL, matching
-/// the safe-cast overflow behavior of the surrounding evolution path.
-fn numeric_epoch_seconds_to_timestamp(
-    source: &ArrayRef,
-    target_arrow: &ArrowDataType,
-    precision: u32,
-) -> crate::Result<ArrayRef> {
-    // Ticks per second for the target unit (precision buckets match
-    // `paimon_type_to_arrow`'s `timestamp_time_unit`).
-    let ticks_per_second: i64 = match precision {
-        0 => 1,
-        1..=3 => 1_000,
-        4..=6 => 1_000_000,
-        _ => 1_000_000_000,
-    };
-    let as_i64 =
-        cast(source, &ArrowDataType::Int64).map_err(|e| 
crate::Error::UnexpectedError {
-            message: format!(
-                "failed to widen {:?} to Int64 before scaling epoch seconds to 
timestamp",
-                source.data_type()
-            ),
-            source: Some(Box::new(e)),
-        })?;
-    let seconds = as_i64
-        .as_any()
-        .downcast_ref::<Int64Array>()
-        .expect("cast to Int64 yields an Int64Array");
-    // Scale seconds into the unit; a null stays null and an overflow becomes 
null.
-    let scaled: Int64Array = seconds
-        .iter()
-        .map(|value| value.and_then(|v| v.checked_mul(ticks_per_second)))
-        .collect();
-    // Reinterpret the already-scaled value straight into the timestamp unit
-    // (Arrow's Int64->Timestamp copies it verbatim, no further scaling).
-    cast(&(Arc::new(scaled) as ArrayRef), target_arrow).map_err(|e| 
crate::Error::UnexpectedError {
-        message: format!("failed to build {target_arrow:?} from scaled epoch 
seconds"),
-        source: Some(Box::new(e)),
-    })
-}
-
 /// Match Paimon's constructed-value string form, including nested values and
 /// NULL containers. Arrow's generic cast cannot convert ROW/ARRAY/MAP to Utf8.
 fn render_string_values(
@@ -940,6 +900,282 @@ mod tests {
         assert_eq!(millis.value(0), secs * 1_000);
     }
 
+    #[test]
+    fn evolves_timestamp_to_bigint_as_floored_epoch_seconds() {
+        for (precision, ticks) in [(0, 1_i64), (3, 1_000), (6, 1_000_000), (9, 
1_000_000_000)] {
+            for local in [false, true] {
+                let source_type = if local {
+                    DataType::LocalZonedTimestamp(
+                        
crate::spec::LocalZonedTimestampType::new(precision).unwrap(),
+                    )
+                } else {
+                    DataType::Timestamp(TimestampType::new(precision).unwrap())
+                };
+                let fraction = if precision == 0 { 0 } else { 1 };
+                let raw: ArrayRef = Arc::new(Int64Array::from(vec![
+                    None,
+                    Some(ticks + fraction),
+                    Some(-1),
+                    Some(-ticks - fraction),
+                    Some(0),
+                    Some((i64::from(i32::MAX) + 3) * ticks),
+                    None,
+                ]));
+                let source = cast(&raw, 
&paimon_type_to_arrow(&source_type).unwrap())
+                    .unwrap()
+                    .slice(1, 6);
+                let out =
+                    evolve_column(&source, &source_type, 
&DataType::BigInt(BigIntType::new()))
+                        .unwrap();
+                let values: Vec<_> = out
+                    .as_any()
+                    .downcast_ref::<Int64Array>()
+                    .unwrap()
+                    .iter()
+                    .collect();
+                assert_eq!(
+                    values,
+                    vec![
+                        Some(1),
+                        Some(-1),
+                        Some(if precision == 0 { -1 } else { -2 }),
+                        Some(0),
+                        Some(i64::from(i32::MAX) + 3),
+                        None
+                    ],
+                    "precision={precision} local={local}"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn 
bigint_epoch_seconds_wrap_in_millis_before_timestamp_conversion_like_java() {
+        let source: ArrayRef = Arc::new(Int64Array::from(vec![
+            Some(i64::MAX),
+            Some(i64::MIN),
+            Some(i64::MAX - 1),
+            Some(42),
+            Some(-42),
+            None,
+        ]));
+        for (precision, ticks) in [(0, 1_i64), (3, 1_000), (6, 1_000_000), (9, 
1_000_000_000)] {
+            let target = 
DataType::Timestamp(TimestampType::new(precision).unwrap());
+            let out =
+                evolve_column(&source, &DataType::BigInt(BigIntType::new()), 
&target).unwrap();
+            let raw = cast(&out, &ArrowDataType::Int64).unwrap();
+            let values: Vec<_> = raw
+                .as_any()
+                .downcast_ref::<Int64Array>()
+                .unwrap()
+                .iter()
+                .collect();
+            assert_eq!(
+                values,
+                vec![
+                    Some(-ticks),
+                    Some(0),
+                    Some(-2 * ticks),
+                    Some(42 * ticks),
+                    Some(-42 * ticks),
+                    None
+                ],
+                "precision={precision}"
+            );
+        }
+    }
+
+    #[test]
+    fn timestamp_to_bigint_matches_java_extreme_millis_round_trip() {
+        for (local, expected) in [
+            (
+                false,
+                vec![
+                    Some(9_223_372_036_854_775),
+                    Some(-9_223_372_036_854_776),
+                    None,
+                ],
+            ),
+            (
+                true,
+                vec![Some(-53_757_322_505_225), Some(53_757_322_505_224), 
None],
+            ),
+        ] {
+            let source_type = if local {
+                
DataType::LocalZonedTimestamp(crate::spec::LocalZonedTimestampType::new(3).unwrap())
+            } else {
+                DataType::Timestamp(TimestampType::new(3).unwrap())
+            };
+            let raw: ArrayRef =
+                Arc::new(Int64Array::from(vec![Some(i64::MAX), Some(i64::MIN), 
None]));
+            let source = cast(&raw, 
&paimon_type_to_arrow(&source_type).unwrap()).unwrap();
+            let output =
+                evolve_column(&source, &source_type, 
&DataType::BigInt(BigIntType::new())).unwrap();
+            assert_eq!(
+                output
+                    .as_any()
+                    .downcast_ref::<Int64Array>()
+                    .unwrap()
+                    .iter()
+                    .collect::<Vec<_>>(),
+                expected,
+                "local={local}"
+            );
+        }
+    }
+
+    #[test]
+    fn bigint_to_timestamp_keeps_lenient_arrow_storage_overflow() {
+        let source: ArrayRef = Arc::new(Int64Array::from(vec![
+            Some(9_223_372_036_854_775),
+            Some(-9_223_372_036_854_775),
+            None,
+        ]));
+        for (precision, expected) in [
+            (
+                0,
+                vec![
+                    Some(9_223_372_036_854_775),
+                    Some(-9_223_372_036_854_775),
+                    None,
+                ],
+            ),
+            (
+                3,
+                vec![
+                    Some(9_223_372_036_854_775_000),
+                    Some(-9_223_372_036_854_775_000),
+                    None,
+                ],
+            ),
+            (6, vec![None, None, None]),
+            (9, vec![None, None, None]),
+        ] {
+            let output = evolve_column(
+                &source,
+                &DataType::BigInt(BigIntType::new()),
+                &DataType::Timestamp(TimestampType::new(precision).unwrap()),
+            )
+            .unwrap();
+            let raw = cast(&output, &ArrowDataType::Int64).unwrap();
+            assert_eq!(
+                raw.as_any()
+                    .downcast_ref::<Int64Array>()
+                    .unwrap()
+                    .iter()
+                    .collect::<Vec<_>>(),
+                expected,
+                "precision={precision}"
+            );
+        }
+    }
+
+    #[test]
+    fn temporal_evolution_recurses_through_nullable_rows_lists_and_maps() {
+        for reverse in [false, true] {
+            let timestamp = 
DataType::Timestamp(TimestampType::new(6).unwrap());
+            let bigint = DataType::BigInt(BigIntType::new());
+            let (source_type, target_type, raw, expected) = if reverse {
+                (
+                    bigint,
+                    timestamp,
+                    vec![Some(42), Some(i64::MAX), None],
+                    vec![Some(42_000_000), Some(-1_000_000), None],
+                )
+            } else {
+                (
+                    timestamp,
+                    bigint,
+                    vec![Some(42_000_001), Some(-1), None],
+                    vec![Some(42), Some(-1), None],
+                )
+            };
+            let raw: ArrayRef = Arc::new(Int64Array::from(raw));
+            let leaf = cast(&raw, 
&paimon_type_to_arrow(&source_type).unwrap()).unwrap();
+            let validity = Some(NullBuffer::from(vec![true, false, true]));
+            let values = |array: &ArrayRef| {
+                cast(array, &ArrowDataType::Int64)
+                    .unwrap()
+                    .as_any()
+                    .downcast_ref::<Int64Array>()
+                    .unwrap()
+                    .iter()
+                    .collect::<Vec<_>>()
+            };
+
+            let source_row = row(vec![field(11, "v", source_type.clone())]);
+            let target_row = row(vec![field(11, "v", target_type.clone())]);
+            let ArrowDataType::Struct(fields) = 
paimon_type_to_arrow(&source_row).unwrap() else {
+                panic!()
+            };
+            let source: ArrayRef = Arc::new(
+                StructArray::try_new(fields, vec![leaf.clone()], 
validity.clone()).unwrap(),
+            );
+            let out = evolve_column(&source, &source_row, 
&target_row).unwrap();
+            let out = as_struct(&out);
+            assert_eq!(out.nulls(), source.nulls());
+            assert_eq!(values(out.column(0)), expected, "ROW 
reverse={reverse}");
+
+            let source: ArrayRef = Arc::new(
+                ListArray::try_new(
+                    Arc::new(ArrowField::new("element", 
leaf.data_type().clone(), true)),
+                    arrow_buffer::OffsetBuffer::new(vec![0, 2, 2, 3].into()),
+                    leaf.clone(),
+                    validity.clone(),
+                )
+                .unwrap(),
+            );
+            let out = evolve_column(
+                &source,
+                &DataType::Array(ArrayType::new(source_type.clone())),
+                &DataType::Array(ArrayType::new(target_type.clone())),
+            )
+            .unwrap();
+            let out = out.as_any().downcast_ref::<ListArray>().unwrap();
+            assert_eq!(out.nulls(), source.nulls());
+            assert_eq!(out.value_offsets(), &[0, 2, 2, 3]);
+            assert_eq!(values(out.values()), expected, "ARRAY 
reverse={reverse}");
+
+            let fields = Fields::from(vec![
+                ArrowField::new("key", ArrowDataType::Utf8, false),
+                ArrowField::new("value", leaf.data_type().clone(), true),
+            ]);
+            let entries = StructArray::try_new(
+                fields.clone(),
+                vec![
+                    Arc::new(StringArray::from(vec!["positive", "negative", 
"null"])) as ArrayRef,
+                    leaf,
+                ],
+                None,
+            )
+            .unwrap();
+            let source: ArrayRef = Arc::new(
+                MapArray::try_new(
+                    Arc::new(ArrowField::new(
+                        "entries",
+                        ArrowDataType::Struct(fields),
+                        false,
+                    )),
+                    arrow_buffer::OffsetBuffer::new(vec![0, 2, 2, 3].into()),
+                    entries,
+                    validity,
+                    false,
+                )
+                .unwrap(),
+            );
+            let out = evolve_column(
+                &source,
+                &DataType::Map(MapType::new(string_type(), source_type)),
+                &DataType::Map(MapType::new(string_type(), target_type)),
+            )
+            .unwrap();
+            let out = out.as_any().downcast_ref::<MapArray>().unwrap();
+            assert_eq!(out.nulls(), source.nulls());
+            assert_eq!(out.value_offsets(), &[0, 2, 2, 3]);
+            assert_eq!(values(out.values()), expected, "MAP 
reverse={reverse}");
+        }
+    }
+
     #[test]
     fn evolves_int_epoch_seconds_to_timestamp_like_bigint() {
         // Java switches INTEGER and BIGINT together in the same cast rule.
diff --git a/crates/paimon/tests/schema_evolution_cast_test.rs 
b/crates/paimon/tests/schema_evolution_cast_test.rs
index 6b3b63f5..de4c1ccd 100644
--- a/crates/paimon/tests/schema_evolution_cast_test.rs
+++ b/crates/paimon/tests/schema_evolution_cast_test.rs
@@ -20,12 +20,13 @@ mod common;
 use std::sync::Arc;
 
 use arrow_array::{
-    ArrayRef, Int32Array, Int64Array, Int8Array, RecordBatch, 
TimestampMillisecondArray,
+    Array, ArrayRef, Int32Array, Int64Array, Int8Array, RecordBatch, 
TimestampMicrosecondArray,
+    TimestampMillisecondArray,
 };
 use futures::TryStreamExt;
 use paimon::spec::{
-    BigIntType, DataType, Datum, IntType, Predicate, PredicateBuilder, Schema, 
SchemaChange,
-    TableSchema, TimestampType, TinyIntType,
+    BigIntType, DataType, Datum, IntType, LocalZonedTimestampType, Predicate, 
PredicateBuilder,
+    Schema, SchemaChange, TableSchema, TimestampType, TinyIntType,
 };
 use paimon::table::Table;
 
@@ -178,3 +179,169 @@ async fn 
converted_predicates_filter_after_schema_evolution_for_every_table_mode
         );
     }
 }
+
+#[tokio::test]
+async fn 
epoch_seconds_evolution_reads_mixed_files_and_filters_for_every_table_mode() {
+    for mode in ["append", "pk", "de"] {
+        let mut schema = Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .column("t", DataType::Timestamp(TimestampType::new(6).unwrap()))
+            .column(
+                "l",
+                
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(6).unwrap()),
+            )
+            .column("s", DataType::BigInt(BigIntType::new()))
+            .option("read.batch-size", "1");
+        if mode == "pk" {
+            schema = schema.primary_key(["id"]).option("bucket", "1");
+        }
+        if mode == "de" {
+            schema = schema
+                .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true");
+        }
+        let (io, old) = memory_table(
+            &format!("memory:/epoch_seconds_{mode}"),
+            TableSchema::new(0, &schema.build().unwrap()),
+        );
+        setup_dirs(&io, old.location()).await;
+        persist_table_schema(&io, old.location(), old.schema()).await;
+        let times = TimestampMicrosecondArray::from(vec![
+            Some(42_000_001),
+            Some(-1),
+            None,
+            Some(42_000_001),
+        ]);
+        let batch = RecordBatch::try_from_iter([
+            (
+                "id",
+                Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as ArrayRef,
+            ),
+            ("t", Arc::new(times.clone()) as ArrayRef),
+            ("l", Arc::new(times.with_timezone("UTC")) as ArrayRef),
+            (
+                "s",
+                Arc::new(Int64Array::from(vec![
+                    Some(42),
+                    Some(-42),
+                    None,
+                    Some(i64::MAX),
+                ])) as ArrayRef,
+            ),
+        ])
+        .unwrap();
+        write_batch(&old, &batch.slice(0, 3)).await;
+        // A single-value old file must survive stats pruning in the new units.
+        write_batch(&old, &batch.slice(3, 1)).await;
+        let schema = old
+            .schema()
+            .apply_changes(vec![
+                SchemaChange::update_column_type("t".into(), 
DataType::BigInt(BigIntType::new())),
+                SchemaChange::update_column_type("l".into(), 
DataType::BigInt(BigIntType::new())),
+                SchemaChange::update_column_type(
+                    "s".into(),
+                    DataType::Timestamp(TimestampType::new(6).unwrap()),
+                ),
+            ])
+            .unwrap();
+        persist_table_schema(&io, old.location(), &schema).await;
+        let current = old.copy_with_resolved_schema(schema, "main").unwrap();
+        write_batch(
+            &current,
+            &RecordBatch::try_from_iter([
+                ("id", Arc::new(Int32Array::from(vec![5])) as ArrayRef),
+                ("t", Arc::new(Int64Array::from(vec![42])) as ArrayRef),
+                ("l", Arc::new(Int64Array::from(vec![42])) as ArrayRef),
+                (
+                    "s",
+                    
Arc::new(TimestampMicrosecondArray::from(vec![42_000_000])) as ArrayRef,
+                ),
+            ])
+            .unwrap(),
+        )
+        .await;
+
+        let builder = current.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 raw = batch
+                .columns()
+                .iter()
+                .skip(1)
+                .map(|array| arrow_cast::cast(array, 
&arrow_schema::DataType::Int64).unwrap())
+                .collect::<Vec<_>>();
+            let values = raw
+                .iter()
+                .map(|array| 
array.as_any().downcast_ref::<Int64Array>().unwrap())
+                .collect::<Vec<_>>();
+            let ids = batch
+                .column(0)
+                .as_any()
+                .downcast_ref::<Int32Array>()
+                .unwrap();
+            for index in 0..batch.num_rows() {
+                let value = |column: usize| {
+                    values[column]
+                        .is_valid(index)
+                        .then(|| values[column].value(index))
+                };
+                rows.push((ids.value(index), value(0), value(1), value(2)));
+            }
+        }
+        rows.sort_unstable_by_key(|row| row.0);
+        assert_eq!(
+            rows,
+            vec![
+                (1, Some(42), Some(42), Some(42_000_000)),
+                (2, Some(-1), Some(-1), Some(-42_000_000)),
+                (3, None, None, None),
+                (4, Some(42), Some(42), Some(-1_000_000)),
+                (5, Some(42), Some(42), Some(42_000_000)),
+            ],
+            "{mode}"
+        );
+        let pb = PredicateBuilder::new(current.schema().fields());
+        for name in ["t", "l"] {
+            assert_eq!(
+                ids(&current, pb.equal(name, Datum::Long(42)).unwrap()).await,
+                vec![1, 4, 5],
+                "{mode} {name} predicate-only field"
+            );
+            assert_eq!(
+                ids(&current, pb.less_than(name, 
Datum::Long(0)).unwrap()).await,
+                vec![2],
+                "{mode} {name} pre-epoch"
+            );
+        }
+        assert_eq!(
+            ids(
+                &current,
+                pb.equal(
+                    "s",
+                    Datum::Timestamp {
+                        millis: -1000,
+                        nanos: 0
+                    }
+                )
+                .unwrap()
+            )
+            .await,
+            vec![4],
+            "{mode} wrapped single-value file"
+        );
+        assert_eq!(
+            ids(&current, pb.is_null("s").unwrap()).await,
+            vec![3],
+            "{mode} NULL"
+        );
+    }
+}

Reply via email to