sesteves opened a new issue, #3637:
URL: https://github.com/apache/iceberg-rust/issues/3637

   ### Apache Iceberg Rust version
   
   main (`5994664`, 2026-11-06)
   
   ### Describe the bug
   
   **1. `year` and `month` of dates and timestamps beyond chrono's range.**
   
   The spec defines `year` and `month` for every `date` and `timestamp` value. 
Iceberg Java's `DateTimeUtil` computes them through `java.time`, whose range 
covers every `int` day and `long` microsecond. iceberg-rust goes through 
chrono, whose dates end at about ±262,000 years, so:
   
   - The array transforms (`Year::transform`, `Month::transform`, via Arrow's 
`date_part`) return **NULL for non-null inputs** outside that range. A writer 
that uses them files such rows under a `null` partition. A predicate on the 
source column then projects to a non-null partition value and prunes the file 
that holds the row.
   - `transform_literal` returns `DataInvalid` (`Fail to convert timestamp to 
date in year transform`) for such timestamps, and **panics** for such dates 
(`Date32Type::to_naive_date_opt(*v).unwrap()`, `transform/temporal.rs:80` and 
`:173`).
   
   | Input | Expected year | Expected month | Array transform | 
`transform_literal` |
   |---|---|---|---|---|
   | timestamp `i64::MAX` µs (294247-01-10) | 292277 | 3507324 | `null` | 
`DataInvalid` |
   | timestamp `i64::MIN` µs (-290308-12-22) | -292278 | -3507325 | `null` | 
not tested |
   | date `i32::MAX` (5881580-07-11) | 5879610 | 70555326 | `null` | panic |
   | date `i32::MIN` (-5877641-06-24) | -5879611 | -70555327 | `null` | not 
tested |
   
   The expected values use floor division to days and an exact civil-from-days 
conversion. `i64::MAX` microseconds is a common "end of time" sentinel.
   
   **2. `Transform::adjust_boundary` overflows at type extremes.**
   
   `adjust_boundary` (`spec/transform.rs:775` onwards) computes `v - 1` for `<` 
and `v + 1` for `>` unchecked. With overflow checks (debug builds, tests) `x < 
i32::MIN` (int or date) and `x > i64::MAX` panic (`spec/transform.rs:778`). 
Without them the boundary wraps to one that matches every value, which only 
makes the projection over-inclusive.
   
   For context: the related day floor, nanosecond bucket, decimal truncate 
literal, truncate overflow and nanosecond hour issues are already fixed on main 
(#3323, #3243, #3573, #3026), and `LargeBinary` truncate has an open PR (#3120).
   
   ### To Reproduce
   
   Append to `crates/iceberg/src/spec/transform.rs` and run `cargo test -p 
iceberg --lib audit_out_of_range -- --nocapture`:
   
   ```rust
   #[cfg(test)]
   mod audit_out_of_range {
       use std::panic::{AssertUnwindSafe, catch_unwind};
       use std::sync::Arc;
   
       use arrow_array::{ArrayRef, Date32Array, TimestampMicrosecondArray};
   
       use super::*;
   
       #[test]
       fn audit_out_of_range() {
           for t in [Transform::Year, Transform::Month] {
               let f = create_transform_function(&t).unwrap();
               let ts: ArrayRef = 
Arc::new(TimestampMicrosecondArray::from(vec![i64::MAX, i64::MIN, 0]));
               println!("{t:?} timestamp array [MAX, MIN, 0]: {:?}", 
f.transform(ts));
               let d: ArrayRef = Arc::new(Date32Array::from(vec![i32::MAX, 
i32::MIN, 0]));
               println!("{t:?} date array [MAX, MIN, 0]: {:?}", f.transform(d));
               println!(
                   "{t:?} timestamp literal MAX: {:?}",
                   f.transform_literal(&Datum::timestamp_micros(i64::MAX))
               );
               let p = catch_unwind(AssertUnwindSafe(|| 
f.transform_literal(&Datum::date(i32::MAX))));
               println!("{t:?} date literal MAX panicked: {}", p.is_err());
           }
           for (op, d) in [
               (PredicateOperator::LessThan, Datum::int(i32::MIN)),
               (PredicateOperator::GreaterThan, Datum::long(i64::MAX)),
               (PredicateOperator::LessThan, Datum::date(i32::MIN)),
           ] {
               let label = format!("{op:?} {d:?}");
               let p = catch_unwind(AssertUnwindSafe(|| 
Transform::adjust_boundary(&op, &d)));
               println!("adjust_boundary {label} panicked: {}", p.is_err());
           }
       }
   }
   ```
   
   Output: both array transforms print `[null, null, 0]` for timestamps and for 
dates; the timestamp literals print `Err(DataInvalid ...)`; the date literals 
and all three `adjust_boundary` calls panic.
   
   ### Expected behavior
   
   1. `year` and `month` return the spec value for every input, for example 
with floor division to days and an exact civil-from-days conversion instead of 
chrono, in both the array transforms and `transform_literal`. Failing that, 
both paths should return an error, never NULL for a non-null input and never a 
panic.
   2. `adjust_boundary` uses checked arithmetic. `x < MIN` and `x > MAX` are 
always false, so the projection can return an always-false predicate (or no 
boundary).
   


-- 
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]

Reply via email to