Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/skade

vendor/iceberg-arrow59/src/spec/transform.rs

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

//! Transforms in iceberg.

use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::str::FromStr;

use chrono::Datelike;
use fnv::FnvHashSet;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use super::values::decimal_utils::decimal_from_i128_with_scale;
use super::values::temporal::date;
use super::{Datum, PrimitiveLiteral};
use crate::ErrorKind;
use crate::error::{Error, Result, invalid_data};
use crate::expr::{
    BinaryExpression, BoundPredicate, BoundReference, Predicate, PredicateOperator, Reference,
    SetExpression, UnaryExpression,
};
use crate::spec::Literal;
use crate::spec::datatypes::{PrimitiveType, Type};
use crate::transform::{BoxedTransformFunction, create_transform_function};

/// The year the Unix epoch falls in, which every temporal ordinal counts from.
const UNIX_EPOCH_YEAR: i32 = 1970;

/// Transform is used to transform predicates to partition predicates,
/// in addition to transforming data values.
///
/// Deriving partition predicates from column predicates on the table data
/// is used to separate the logical queries from physical storage: the
/// partitioning can change and the correct partition filters are always
/// derived from column predicates.
///
/// This simplifies queries because users don’t have to supply both logical
/// predicates and partition predicates.
///
/// All transforms must return `null` for a `null` input value.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Transform {
    /// Source value, unmodified
    ///
    /// - Source type could be any type.
    /// - Return type is the same with source type.
    Identity,
    /// Hash of value, mod `N`.
    ///
    /// Bucket partition transforms use a 32-bit hash of the source value.
    /// The 32-bit hash implementation is the 32-bit Murmur3 hash, x86
    /// variant, seeded with 0.
    ///
    /// Transforms are parameterized by a number of buckets, N. The hash mod
    /// N must produce a positive value by first discarding the sign bit of
    /// the hash value. In pseudo-code, the function is:
    ///
    /// ```text
    /// def bucket_N(x) = (murmur3_x86_32_hash(x) & Integer.MAX_VALUE) % N
    /// ```
    ///
    /// - Source type could be `int`, `long`, `decimal`, `date`, `time`,
    ///   `timestamp`, `timestamptz`, `string`, `uuid`, `fixed`, `binary`.
    /// - Return type is `int`.
    Bucket(u32),
    /// Value truncated to width `W`
    ///
    /// For `int`:
    ///
    /// - `v - (v % W)` remainders must be positive
    /// - example: W=10: 1 → 0, -1 → -10
    /// - note: The remainder, v % W, must be positive.
    ///
    /// For `long`:
    ///
    /// - `v - (v % W)` remainders must be positive
    /// - example: W=10: 1 → 0, -1 → -10
    /// - note: The remainder, v % W, must be positive.
    ///
    /// For `decimal`:
    ///
    /// - `scaled_W = decimal(W, scale(v)) v - (v % scaled_W)`
    /// - example: W=50, s=2: 10.65 → 10.50
    ///
    /// For `string`:
    ///
    /// - Substring of length L: `v.substring(0, L)`
    /// - example: L=3: iceberg → ice
    /// - note: Strings are truncated to a valid UTF-8 string with no more
    ///   than L code points.
    ///
    /// - Source type could be `int`, `long`, `decimal`, `string`
    /// - Return type is the same with source type.
    Truncate(u32),
    /// Extract a date or timestamp year, as years from 1970
    ///
    /// - Source type could be `date`, `timestamp`, `timestamptz`
    /// - Return type is `int`
    Year,
    /// Extract a date or timestamp month, as months from 1970-01-01
    ///
    /// - Source type could be `date`, `timestamp`, `timestamptz`
    /// - Return type is `int`
    Month,
    /// Extract a date or timestamp day, as days from 1970-01-01
    ///
    /// - Source type could be `date`, `timestamp`, `timestamptz`
    /// - Return type is `int`
    Day,
    /// Extract a timestamp hour, as hours from 1970-01-01 00:00:00
    ///
    /// - Source type could be `timestamp`, `timestamptz`
    /// - Return type is `int`
    Hour,
    /// Always produces `null`
    ///
    /// The void transform may be used to replace the transform in an
    /// existing partition field so that the field is effectively dropped in
    /// v1 tables.
    ///
    /// - Source type could be any type..
    /// - Return type is Source type.
    Void,
    /// Used to represent some customized transform that can't be recognized or supported now.
    Unknown,
}

impl Transform {
    /// Returns a human-readable String representation of a transformed value.
    ///
    /// The temporal transforms store an ordinal count since the Unix epoch, and
    /// this method renders that count as a date so that partition paths and
    /// snapshot summary keys match the Java reference implementation:
    ///
    /// | Transform | Format          | Example         |
    /// |-----------|-----------------|-----------------|
    /// | `Year`    | `yyyy`          | `2017`          |
    /// | `Month`   | `yyyy-MM`       | `2017-06`       |
    /// | `Day`     | `yyyy-MM-dd`    | `2017-06-15`    |
    /// | `Hour`    | `yyyy-MM-dd-HH` | `2017-06-15-16` |
    ///
    /// `Void` renders as `null`, as does an absent value for any transform.
    ///
    /// # Example
    ///
    /// ```
    /// use iceberg::spec::{Literal, PrimitiveType, Transform, Type};
    ///
    /// let int = Type::Primitive(PrimitiveType::Int);
    /// let date = Type::Primitive(PrimitiveType::Date);
    ///
    /// // A stored value carries no logical type of its own. For transforms that do
    /// // not format it themselves, the declared field type decides how it renders.
    /// let stored = Literal::int(17332);
    /// assert_eq!(
    ///     Transform::Identity.to_human_string(&int, Some(&stored)),
    ///     "17332"
    /// );
    /// assert_eq!(
    ///     Transform::Identity.to_human_string(&date, Some(&stored)),
    ///     "2017-06-15"
    /// );
    ///
    /// // The temporal transforms format their own ordinal and ignore the declared
    /// // type. All four ordinals below are the same instant, 2017-06-15T16:00:00Z,
    /// // counted at four granularities.
    /// assert_eq!(
    ///     Transform::Year.to_human_string(&int, Some(&Literal::int(47))),
    ///     "2017"
    /// );
    /// assert_eq!(
    ///     Transform::Month.to_human_string(&int, Some(&Literal::int(569))),
    ///     "2017-06"
    /// );
    /// assert_eq!(
    ///     Transform::Day.to_human_string(&int, Some(&Literal::int(17332))),
    ///     "2017-06-15"
    /// );
    /// assert_eq!(
    ///     Transform::Hour.to_human_string(&int, Some(&Literal::int(415984))),
    ///     "2017-06-15-16"
    /// );
    ///
    /// // `Void` and an absent value render as `null`.
    /// assert_eq!(
    ///     Transform::Void.to_human_string(&int, Some(&Literal::int(47))),
    ///     "null"
    /// );
    /// assert_eq!(Transform::Year.to_human_string(&int, None), "null");
    /// ```
    pub fn to_human_string(&self, field_type: &Type, value: Option<&Literal>) -> String {
        let Some(value) = value.and_then(Literal::as_primitive_literal) else {
            return "null".to_string();
        };

        match (*self, value) {
            (Self::Void, _) => "null".to_string(),
            // The temporal transforms store an ordinal count since the Unix epoch,
            // which the datum arm below cannot render: it would show the raw count
            // for `Year`, `Month` and `Hour`, and would leave `Day` dependent on the
            // field type happening to be `date`. Java overrides `toHumanString` on
            // each of these four transforms and ignores the declared type, so do the
            // same. Any other literal falls through to the datum.
            (Self::Year, PrimitiveLiteral::Int(ordinal)) => Self::human_year(ordinal),
            (Self::Month, PrimitiveLiteral::Int(ordinal)) => Self::human_month(ordinal),
            (Self::Day, PrimitiveLiteral::Int(ordinal)) => Self::human_day(ordinal),
            (Self::Hour, PrimitiveLiteral::Int(ordinal)) => Self::human_hour(ordinal),
            (_, value) => {
                let field_type = field_type.as_primitive_type().unwrap();
                Datum::new(field_type.clone(), value).to_human_string()
            }
        }
    }

    /// Formats a year ordinal, the number of years since 1970, as `yyyy`.
    ///
    /// Mirrors the output of `TransformUtil.humanYear`.
    fn human_year(year_ordinal: i32) -> String {
        format!("{:04}", UNIX_EPOCH_YEAR + year_ordinal)
    }

    /// Formats a month ordinal, the number of months since 1970-01, as `yyyy-MM`.
    ///
    /// Mirrors the output of `TransformUtil.humanMonth`, which divides with
    /// `Math.floorDiv` and `Math.floorMod` rather than `/` and `%`. Truncating
    /// division rounds toward zero, which is the wrong direction before 1970:
    /// ordinal -1 is 1969-12, but truncating yields 1970-01. `div_euclid` and
    /// `rem_euclid` round toward negative infinity and so agree with Java.
    fn human_month(month_ordinal: i32) -> String {
        format!(
            "{:04}-{:02}",
            UNIX_EPOCH_YEAR + month_ordinal.div_euclid(12),
            1 + month_ordinal.rem_euclid(12)
        )
    }

    /// Formats a day ordinal, the number of days since 1970-01-01, as `yyyy-MM-dd`.
    ///
    /// Mirrors the output of `TransformUtil.humanDay`. Like the Java `Days`
    /// transform, whose signature is `toHumanString(Type alwaysDate, Integer
    /// value)`, this ignores the declared field type rather than relying on it being
    /// `date`.
    fn human_day(day_ordinal: i32) -> String {
        let date = date::days_to_date(day_ordinal);
        format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day())
    }

    /// Formats an hour ordinal, the number of hours since 1970-01-01T00:00:00Z,
    /// as `yyyy-MM-dd-HH`.
    ///
    /// Mirrors the output of `TransformUtil.humanHour`. `div_euclid` and `rem_euclid`
    /// split the ordinal into whole days and the hour within the day so that hours
    /// before 1970 round the way they do in Java.
    fn human_hour(hour_ordinal: i32) -> String {
        format!(
            "{}-{:02}",
            Self::human_day(hour_ordinal.div_euclid(24)),
            hour_ordinal.rem_euclid(24)
        )
    }

    /// Get the return type of transform given the input type.
    /// Returns `None` if it can't be transformed.
    pub fn result_type(&self, input_type: &Type) -> Result<Type> {
        match self {
            Transform::Identity => {
                if matches!(input_type, Type::Primitive(_)) {
                    Ok(input_type.clone())
                } else {
                    Err(invalid_data!(
                        "{input_type} is not a valid input type of identity transform",
                    ))
                }
            }
            Transform::Void => Ok(input_type.clone()),
            Transform::Unknown => Ok(Type::Primitive(PrimitiveType::String)),
            Transform::Bucket(_) => {
                if let Type::Primitive(p) = input_type {
                    match p {
                        PrimitiveType::Int
                        | PrimitiveType::Long
                        | PrimitiveType::Decimal { .. }
                        | PrimitiveType::Date
                        | PrimitiveType::Time
                        | PrimitiveType::Timestamp
                        | PrimitiveType::Timestamptz
                        | PrimitiveType::TimestampNs
                        | PrimitiveType::TimestamptzNs
                        | PrimitiveType::String
                        | PrimitiveType::Uuid
                        | PrimitiveType::Fixed(_)
                        | PrimitiveType::Binary => Ok(Type::Primitive(PrimitiveType::Int)),
                        _ => Err(invalid_data!(
                            "{input_type} is not a valid input type of bucket transform",
                        )),
                    }
                } else {
                    Err(invalid_data!(
                        "{input_type} is not a valid input type of bucket transform",
                    ))
                }
            }
            Transform::Truncate(_) => {
                if let Type::Primitive(p) = input_type {
                    match p {
                        PrimitiveType::Int
                        | PrimitiveType::Long
                        | PrimitiveType::String
                        | PrimitiveType::Binary
                        | PrimitiveType::Decimal { .. } => Ok(input_type.clone()),
                        _ => Err(invalid_data!(
                            "{input_type} is not a valid input type of truncate transform",
                        )),
                    }
                } else {
                    Err(invalid_data!(
                        "{input_type} is not a valid input type of truncate transform",
                    ))
                }
            }
            Transform::Year | Transform::Month => {
                if let Type::Primitive(p) = input_type {
                    match p {
                        PrimitiveType::Timestamp
                        | PrimitiveType::Timestamptz
                        | PrimitiveType::TimestampNs
                        | PrimitiveType::TimestamptzNs
                        | PrimitiveType::Date => Ok(Type::Primitive(PrimitiveType::Int)),
                        _ => Err(invalid_data!(
                            "{input_type} is not a valid input type of {self} transform",
                        )),
                    }
                } else {
                    Err(invalid_data!(
                        "{input_type} is not a valid input type of {self} transform",
                    ))
                }
            }
            Transform::Day => {
                if let Type::Primitive(p) = input_type {
                    match p {
                        PrimitiveType::Timestamp
                        | PrimitiveType::Timestamptz
                        | PrimitiveType::TimestampNs
                        | PrimitiveType::TimestamptzNs
                        | PrimitiveType::Date => Ok(Type::Primitive(PrimitiveType::Date)),
                        _ => Err(invalid_data!(
                            "{input_type} is not a valid input type of {self} transform",
                        )),
                    }
                } else {
                    Err(invalid_data!(
                        "{input_type} is not a valid input type of {self} transform",
                    ))
                }
            }
            Transform::Hour => {
                if let Type::Primitive(p) = input_type {
                    match p {
                        PrimitiveType::Timestamp
                        | PrimitiveType::Timestamptz
                        | PrimitiveType::TimestampNs
                        | PrimitiveType::TimestamptzNs => Ok(Type::Primitive(PrimitiveType::Int)),
                        _ => Err(invalid_data!(
                            "{input_type} is not a valid input type of {self} transform",
                        )),
                    }
                } else {
                    Err(invalid_data!(
                        "{input_type} is not a valid input type of {self} transform",
                    ))
                }
            }
        }
    }

    /// Whether the transform preserves the order of values.
    pub fn preserves_order(&self) -> bool {
        !matches!(
            self,
            Transform::Void | Transform::Bucket(_) | Transform::Unknown
        )
    }

    /// Return the unique transform name to check if similar transforms for the same source field
    /// are added multiple times in partition spec builder.
    pub fn dedup_name(&self) -> String {
        match self {
            Transform::Year | Transform::Month | Transform::Day | Transform::Hour => {
                "time".to_string()
            }
            _ => format!("{self}"),
        }
    }

    /// Whether ordering by this transform's result satisfies the ordering of another transform's
    /// result.
    ///
    /// For example, sorting by day(ts) will produce an ordering that is also by month(ts) or
    ///  year(ts). However, sorting by day(ts) will not satisfy the order of hour(ts) or identity(ts).
    pub fn satisfies_order_of(&self, other: &Self) -> bool {
        match self {
            Transform::Identity => other.preserves_order(),
            Transform::Hour => matches!(
                other,
                Transform::Hour | Transform::Day | Transform::Month | Transform::Year
            ),
            Transform::Day => matches!(other, Transform::Day | Transform::Month | Transform::Year),
            Transform::Month => matches!(other, Transform::Month | Transform::Year),
            _ => self == other,
        }
    }

    /// Strictly projects a given predicate according to the transformation
    /// specified by the `Transform` instance.
    ///
    /// This method ensures that the projected predicate is strictly aligned
    /// with the transformation logic, providing a more precise filtering
    /// mechanism for transformed data.
    ///
    /// # Example
    /// Suppose, we have row filter `a = 10`, and a partition spec
    /// `bucket(a, 37) as bs`, if one row matches `a = 10`, then its partition
    /// value should match `bucket(10, 37) as bs`, and we project `a = 10` to
    /// `bs = bucket(10, 37)`
    pub fn strict_project(
        &self,
        name: &str,
        predicate: &BoundPredicate,
    ) -> Result<Option<Predicate>> {
        let func = create_transform_function(self)?;

        match self {
            Transform::Identity => match predicate {
                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                BoundPredicate::Binary(expr) => Ok(Some(Predicate::Binary(BinaryExpression::new(
                    expr.op(),
                    Reference::new(name),
                    expr.literal().to_owned(),
                )))),
                BoundPredicate::Set(expr) => Ok(Some(Predicate::Set(SetExpression::new(
                    expr.op(),
                    Reference::new(name),
                    expr.literals().to_owned(),
                )))),
                _ => Ok(None),
            },
            Transform::Bucket(_) => match predicate {
                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                BoundPredicate::Binary(expr) => {
                    self.project_binary_expr(name, PredicateOperator::NotEq, expr, &func)
                }
                BoundPredicate::Set(expr) => {
                    self.project_set_expr(expr, PredicateOperator::NotIn, name, &func)
                }
                _ => Ok(None),
            },
            Transform::Truncate(width) => match predicate {
                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                BoundPredicate::Binary(expr) => {
                    if matches!(
                        expr.term().field().field_type.as_primitive_type(),
                        Some(&PrimitiveType::Int)
                            | Some(&PrimitiveType::Long)
                            | Some(&PrimitiveType::Decimal { .. })
                    ) {
                        self.truncate_number_strict(name, expr, &func)
                    } else if expr.op() == PredicateOperator::StartsWith {
                        let len = match expr.literal().literal() {
                            PrimitiveLiteral::String(s) => s.len(),
                            PrimitiveLiteral::Binary(b) => b.len(),
                            _ => {
                                return Err(invalid_data!(
                                    "Expected a string or binary literal, got: {:?}",
                                    expr.literal()
                                ));
                            }
                        };
                        match len.cmp(&(*width as usize)) {
                            Ordering::Less => Ok(Some(Predicate::Binary(BinaryExpression::new(
                                PredicateOperator::StartsWith,
                                Reference::new(name),
                                expr.literal().to_owned(),
                            )))),
                            Ordering::Equal => Ok(Some(Predicate::Binary(BinaryExpression::new(
                                PredicateOperator::Eq,
                                Reference::new(name),
                                expr.literal().to_owned(),
                            )))),
                            Ordering::Greater => Ok(None),
                        }
                    } else if expr.op() == PredicateOperator::NotStartsWith {
                        let len = match expr.literal().literal() {
                            PrimitiveLiteral::String(s) => s.len(),
                            PrimitiveLiteral::Binary(b) => b.len(),
                            _ => {
                                return Err(invalid_data!(
                                    "Expected a string or binary literal, got: {:?}",
                                    expr.literal()
                                ));
                            }
                        };
                        match len.cmp(&(*width as usize)) {
                            Ordering::Less => Ok(Some(Predicate::Binary(BinaryExpression::new(
                                PredicateOperator::NotStartsWith,
                                Reference::new(name),
                                expr.literal().to_owned(),
                            )))),
                            Ordering::Equal => Ok(Some(Predicate::Binary(BinaryExpression::new(
                                PredicateOperator::NotEq,
                                Reference::new(name),
                                expr.literal().to_owned(),
                            )))),
                            Ordering::Greater => {
                                Ok(Some(Predicate::Binary(BinaryExpression::new(
                                    expr.op(),
                                    Reference::new(name),
                                    func.transform_literal_result(expr.literal())?,
                                ))))
                            }
                        }
                    } else {
                        self.truncate_array_strict(name, expr, &func)
                    }
                }
                BoundPredicate::Set(expr) => {
                    self.project_set_expr(expr, PredicateOperator::NotIn, name, &func)
                }
                _ => Ok(None),
            },
            Transform::Year | Transform::Month | Transform::Day | Transform::Hour => {
                match predicate {
                    BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                    BoundPredicate::Binary(expr) => self.truncate_number_strict(name, expr, &func),
                    BoundPredicate::Set(expr) => {
                        self.project_set_expr(expr, PredicateOperator::NotIn, name, &func)
                    }
                    _ => Ok(None),
                }
            }
            _ => Ok(None),
        }
    }

    /// Projects a given predicate according to the transformation
    /// specified by the `Transform` instance.
    ///
    /// This allows predicates to be effectively applied to data
    /// that has undergone transformation, enabling efficient querying
    /// and filtering based on the original, untransformed data.
    ///
    /// # Example
    /// Suppose, we have row filter `a = 10`, and a partition spec
    /// `bucket(a, 37) as bs`, if one row matches `a = 10`, then its partition
    /// value should match `bucket(10, 37) as bs`, and we project `a = 10` to
    /// `bs = bucket(10, 37)`
    pub fn project(&self, name: &str, predicate: &BoundPredicate) -> Result<Option<Predicate>> {
        let func = create_transform_function(self)?;

        match self {
            Transform::Identity => match predicate {
                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                BoundPredicate::Binary(expr) => Ok(Some(Predicate::Binary(BinaryExpression::new(
                    expr.op(),
                    Reference::new(name),
                    expr.literal().to_owned(),
                )))),
                BoundPredicate::Set(expr) => Ok(Some(Predicate::Set(SetExpression::new(
                    expr.op(),
                    Reference::new(name),
                    expr.literals().to_owned(),
                )))),
                _ => Ok(None),
            },
            Transform::Bucket(_) => match predicate {
                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                BoundPredicate::Binary(expr) => {
                    self.project_binary_expr(name, PredicateOperator::Eq, expr, &func)
                }
                BoundPredicate::Set(expr) => {
                    self.project_set_expr(expr, PredicateOperator::In, name, &func)
                }
                _ => Ok(None),
            },
            Transform::Truncate(width) => match predicate {
                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                BoundPredicate::Binary(expr) => {
                    self.project_binary_with_adjusted_boundary(name, expr, &func, Some(*width))
                }
                BoundPredicate::Set(expr) => {
                    self.project_set_expr(expr, PredicateOperator::In, name, &func)
                }
                _ => Ok(None),
            },
            Transform::Year | Transform::Month | Transform::Day | Transform::Hour => {
                match predicate {
                    BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
                    BoundPredicate::Binary(expr) => {
                        self.project_binary_with_adjusted_boundary(name, expr, &func, None)
                    }
                    BoundPredicate::Set(expr) => {
                        self.project_set_expr(expr, PredicateOperator::In, name, &func)
                    }
                    _ => Ok(None),
                }
            }
            _ => Ok(None),
        }
    }

    /// Check if `Transform` is applicable on datum's `PrimitiveType`
    fn can_transform(&self, datum: &Datum) -> bool {
        let input_type = datum.data_type().clone();
        self.result_type(&Type::Primitive(input_type)).is_ok()
    }

    /// Creates a unary predicate from a given operator and a reference name.
    fn project_unary(op: PredicateOperator, name: &str) -> Result<Option<Predicate>> {
        Ok(Some(Predicate::Unary(UnaryExpression::new(
            op,
            Reference::new(name),
        ))))
    }

    /// Attempts to create a binary predicate based on a binary expression,
    /// if applicable.
    ///
    /// This method evaluates a given binary expression and, if the operation
    /// is the given operator and the literal can be transformed, constructs a
    /// `Predicate::Binary`variant representing the binary operation.
    fn project_binary_expr(
        &self,
        name: &str,
        op: PredicateOperator,
        expr: &BinaryExpression<BoundReference>,
        func: &BoxedTransformFunction,
    ) -> Result<Option<Predicate>> {
        if expr.op() != op || !self.can_transform(expr.literal()) {
            return Ok(None);
        }

        Ok(Some(Predicate::Binary(BinaryExpression::new(
            expr.op(),
            Reference::new(name),
            func.transform_literal_result(expr.literal())?,
        ))))
    }

    /// Projects a binary expression to a predicate with an adjusted boundary.
    ///
    /// Checks if the literal within the given binary expression is
    /// transformable. If transformable, it proceeds to potentially adjust
    /// the boundary of the expression based on the comparison operator (`op`).
    /// The potential adjustments involve incrementing or decrementing the
    /// literal value and changing the `PredicateOperator` itself to its
    /// inclusive variant.
    fn project_binary_with_adjusted_boundary(
        &self,
        name: &str,
        expr: &BinaryExpression<BoundReference>,
        func: &BoxedTransformFunction,
        width: Option<u32>,
    ) -> Result<Option<Predicate>> {
        if !self.can_transform(expr.literal()) {
            return Ok(None);
        }

        let op = &expr.op();
        let datum = &expr.literal();

        if let Some(boundary) = Self::adjust_boundary(op, datum)? {
            let transformed_projection = func.transform_literal_result(&boundary)?;

            let adjusted_projection =
                self.adjust_time_projection(op, datum, &transformed_projection);

            let adjusted_operator = Self::adjust_operator(op, datum, width);

            if let Some(op) = adjusted_operator {
                let predicate = match adjusted_projection {
                    None => Predicate::Binary(BinaryExpression::new(
                        op,
                        Reference::new(name),
                        transformed_projection,
                    )),
                    Some(AdjustedProjection::Single(d)) => {
                        Predicate::Binary(BinaryExpression::new(op, Reference::new(name), d))
                    }
                    Some(AdjustedProjection::Set(d)) => Predicate::Set(SetExpression::new(
                        PredicateOperator::In,
                        Reference::new(name),
                        d,
                    )),
                };
                return Ok(Some(predicate));
            }
        };

        Ok(None)
    }

    /// Projects a set expression to a predicate,
    /// applying a transformation to each literal in the set.
    fn project_set_expr(
        &self,
        expr: &SetExpression<BoundReference>,
        op: PredicateOperator,
        name: &str,
        func: &BoxedTransformFunction,
    ) -> Result<Option<Predicate>> {
        if expr.op() != op || expr.literals().iter().any(|d| !self.can_transform(d)) {
            return Ok(None);
        }

        let mut new_set = FnvHashSet::default();

        for lit in expr.literals() {
            let datum = func.transform_literal_result(lit)?;

            if let Some(AdjustedProjection::Single(d)) =
                self.adjust_time_projection(&op, lit, &datum)
            {
                new_set.insert(d);
            };

            new_set.insert(datum);
        }

        Ok(Some(Predicate::Set(SetExpression::new(
            expr.op(),
            Reference::new(name),
            new_set,
        ))))
    }

    /// Adjusts the boundary value for comparison operations
    /// based on the specified `PredicateOperator` and `Datum`.
    ///
    /// This function modifies the boundary value for certain comparison
    /// operators (`LessThan`, `GreaterThan`) by incrementing or decrementing
    /// the literal value within the given `Datum`. For operators that do not
    /// imply a boundary shift (`Eq`, `LessThanOrEq`, `GreaterThanOrEq`,
    /// `StartsWith`, `NotStartsWith`), the original datum is returned
    /// unmodified.
    fn adjust_boundary(op: &PredicateOperator, datum: &Datum) -> Result<Option<Datum>> {
        let adjusted_boundary = match op {
            PredicateOperator::LessThan => match (datum.data_type(), datum.literal()) {
                (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Some(Datum::int(v - 1)),
                (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Some(Datum::long(v - 1)),
                (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
                    Some(Datum::decimal(decimal_from_i128_with_scale(v - 1, 0))?)
                }
                (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Some(Datum::date(v - 1)),
                (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamp_micros(v - 1))
                }
                (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamptz_micros(v - 1))
                }
                (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamp_nanos(v - 1))
                }
                (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamptz_nanos(v - 1))
                }
                _ => Some(datum.to_owned()),
            },
            PredicateOperator::GreaterThan => match (datum.data_type(), datum.literal()) {
                (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Some(Datum::int(v + 1)),
                (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Some(Datum::long(v + 1)),
                (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
                    Some(Datum::decimal(decimal_from_i128_with_scale(v + 1, 0))?)
                }
                (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Some(Datum::date(v + 1)),
                (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamp_micros(v + 1))
                }
                (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamptz_micros(v + 1))
                }
                (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamp_nanos(v + 1))
                }
                (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
                    Some(Datum::timestamptz_nanos(v + 1))
                }
                _ => Some(datum.to_owned()),
            },
            PredicateOperator::Eq
            | PredicateOperator::LessThanOrEq
            | PredicateOperator::GreaterThanOrEq
            | PredicateOperator::StartsWith
            | PredicateOperator::NotStartsWith => Some(datum.to_owned()),
            _ => None,
        };

        Ok(adjusted_boundary)
    }

    /// Adjusts the comparison operator based on the specified datum and an
    /// optional width constraint.
    ///
    /// This function modifies the comparison operator for `LessThan` and
    /// `GreaterThan` cases to their inclusive counterparts (`LessThanOrEq`,
    /// `GreaterThanOrEq`) unconditionally. For `StartsWith` and
    /// `NotStartsWith` operators acting on string literals, the operator may
    /// be adjusted to `Eq` or `NotEq` if the string length matches the
    /// specified width, indicating a precise match rather than a prefix
    /// condition.
    fn adjust_operator(
        op: &PredicateOperator,
        datum: &Datum,
        width: Option<u32>,
    ) -> Option<PredicateOperator> {
        match op {
            PredicateOperator::LessThan => Some(PredicateOperator::LessThanOrEq),
            PredicateOperator::GreaterThan => Some(PredicateOperator::GreaterThanOrEq),
            PredicateOperator::StartsWith => match datum.literal() {
                PrimitiveLiteral::String(s) => {
                    if let Some(w) = width
                        && s.len() == w as usize
                    {
                        return Some(PredicateOperator::Eq);
                    };
                    Some(*op)
                }
                _ => Some(*op),
            },
            PredicateOperator::NotStartsWith => match datum.literal() {
                PrimitiveLiteral::String(s) => {
                    if let Some(w) = width {
                        let w = w as usize;

                        if s.len() == w {
                            return Some(PredicateOperator::NotEq);
                        }

                        if s.len() < w {
                            return Some(*op);
                        }

                        return None;
                    };
                    Some(*op)
                }
                _ => Some(*op),
            },
            _ => Some(*op),
        }
    }

    /// Adjust projection for temporal transforms, align with Java
    /// implementation: https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/transforms/ProjectionUtil.java#L275
    fn adjust_time_projection(
        &self,
        op: &PredicateOperator,
        original: &Datum,
        transformed: &Datum,
    ) -> Option<AdjustedProjection> {
        let should_adjust = match self {
            Transform::Day => matches!(original.data_type(), PrimitiveType::Timestamp),
            Transform::Year | Transform::Month => true,
            _ => false,
        };

        if should_adjust && let &PrimitiveLiteral::Int(v) = transformed.literal() {
            match op {
                PredicateOperator::LessThan
                | PredicateOperator::LessThanOrEq
                | PredicateOperator::In => {
                    if v < 0 {
                        // # TODO
                        // An ugly hack to fix. Refine the increment and decrement logic later.
                        match self {
                            Transform::Day => {
                                return Some(AdjustedProjection::Single(Datum::date(v + 1)));
                            }
                            _ => {
                                return Some(AdjustedProjection::Single(Datum::int(v + 1)));
                            }
                        }
                    };
                }
                PredicateOperator::Eq => {
                    if v < 0 {
                        let new_set = FnvHashSet::from_iter(vec![
                            transformed.to_owned(),
                            // # TODO
                            // An ugly hack to fix. Refine the increment and decrement logic later.
                            {
                                match self {
                                    Transform::Day => Datum::date(v + 1),
                                    _ => Datum::int(v + 1),
                                }
                            },
                        ]);
                        return Some(AdjustedProjection::Set(new_set));
                    }
                }
                _ => {
                    return None;
                }
            }
        };
        None
    }

    // Increment for Int, Long, Decimal, Date, Timestamp
    // Ignore other types
    #[inline]
    fn try_increment_number(datum: &Datum) -> Result<Datum> {
        match (datum.data_type(), datum.literal()) {
            (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Ok(Datum::int(v + 1)),
            (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Ok(Datum::long(v + 1)),
            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
                Datum::decimal(decimal_from_i128_with_scale(v + 1, 0))
            }
            (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Ok(Datum::date(v + 1)),
            (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamp_micros(v + 1))
            }
            (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamp_nanos(v + 1))
            }
            (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamptz_micros(v + 1))
            }
            (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamptz_nanos(v + 1))
            }
            (PrimitiveType::Int, _)
            | (PrimitiveType::Long, _)
            | (PrimitiveType::Decimal { .. }, _)
            | (PrimitiveType::Date, _)
            | (PrimitiveType::Timestamp, _) => Err(Error::new(
                ErrorKind::Unexpected,
                format!(
                    "Unsupported literal increment for type: {:?}",
                    datum.data_type()
                ),
            )),
            _ => Ok(datum.to_owned()),
        }
    }

    // Decrement for Int, Long, Decimal, Date, Timestamp
    // Ignore other types
    #[inline]
    fn try_decrement_number(datum: &Datum) -> Result<Datum> {
        match (datum.data_type(), datum.literal()) {
            (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Ok(Datum::int(v - 1)),
            (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Ok(Datum::long(v - 1)),
            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
                Datum::decimal(decimal_from_i128_with_scale(v - 1, 0))
            }
            (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Ok(Datum::date(v - 1)),
            (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamp_micros(v - 1))
            }
            (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamp_nanos(v - 1))
            }
            (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamptz_micros(v - 1))
            }
            (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
                Ok(Datum::timestamptz_nanos(v - 1))
            }
            (PrimitiveType::Int, _)
            | (PrimitiveType::Long, _)
            | (PrimitiveType::Decimal { .. }, _)
            | (PrimitiveType::Date, _)
            | (PrimitiveType::Timestamp, _) => Err(Error::new(
                ErrorKind::Unexpected,
                format!(
                    "Unsupported literal decrement for type: {:?}",
                    datum.data_type()
                ),
            )),
            _ => Ok(datum.to_owned()),
        }
    }

    fn truncate_number_strict(
        &self,
        name: &str,
        expr: &BinaryExpression<BoundReference>,
        func: &BoxedTransformFunction,
    ) -> Result<Option<Predicate>> {
        let boundary = expr.literal();

        if !matches!(
            boundary.data_type(),
            &PrimitiveType::Int
                | &PrimitiveType::Long
                | &PrimitiveType::Decimal { .. }
                | &PrimitiveType::Date
                | &PrimitiveType::Timestamp
                | &PrimitiveType::Timestamptz
                | &PrimitiveType::TimestampNs
                | &PrimitiveType::TimestamptzNs
        ) {
            return Err(invalid_data!(
                "Expected a numeric literal, got: {boundary:?}"
            ));
        }

        let predicate = match expr.op() {
            PredicateOperator::LessThan => Some(Predicate::Binary(BinaryExpression::new(
                PredicateOperator::LessThan,
                Reference::new(name),
                func.transform_literal_result(boundary)?,
            ))),
            PredicateOperator::LessThanOrEq => Some(Predicate::Binary(BinaryExpression::new(
                PredicateOperator::LessThan,
                Reference::new(name),
                func.transform_literal_result(&Self::try_increment_number(boundary)?)?,
            ))),
            PredicateOperator::GreaterThan => Some(Predicate::Binary(BinaryExpression::new(
                PredicateOperator::GreaterThan,
                Reference::new(name),
                func.transform_literal_result(boundary)?,
            ))),
            PredicateOperator::GreaterThanOrEq => Some(Predicate::Binary(BinaryExpression::new(
                PredicateOperator::GreaterThan,
                Reference::new(name),
                func.transform_literal_result(&Self::try_decrement_number(boundary)?)?,
            ))),
            PredicateOperator::NotEq => Some(Predicate::Binary(BinaryExpression::new(
                PredicateOperator::NotEq,
                Reference::new(name),
                func.transform_literal_result(boundary)?,
            ))),
            _ => None,
        };

        Ok(predicate)
    }

    fn truncate_array_strict(
        &self,
        name: &str,
        expr: &BinaryExpression<BoundReference>,
        func: &BoxedTransformFunction,
    ) -> Result<Option<Predicate>> {
        let boundary = expr.literal();

        match expr.op() {
            PredicateOperator::LessThan | PredicateOperator::LessThanOrEq => {
                Ok(Some(Predicate::Binary(BinaryExpression::new(
                    PredicateOperator::LessThan,
                    Reference::new(name),
                    func.transform_literal_result(boundary)?,
                ))))
            }
            PredicateOperator::GreaterThan | PredicateOperator::GreaterThanOrEq => {
                Ok(Some(Predicate::Binary(BinaryExpression::new(
                    PredicateOperator::GreaterThan,
                    Reference::new(name),
                    func.transform_literal_result(boundary)?,
                ))))
            }
            PredicateOperator::NotEq => Ok(Some(Predicate::Binary(BinaryExpression::new(
                PredicateOperator::NotEq,
                Reference::new(name),
                func.transform_literal_result(boundary)?,
            )))),
            _ => Ok(None),
        }
    }
}

impl Display for Transform {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Transform::Identity => write!(f, "identity"),
            Transform::Year => write!(f, "year"),
            Transform::Month => write!(f, "month"),
            Transform::Day => write!(f, "day"),
            Transform::Hour => write!(f, "hour"),
            Transform::Void => write!(f, "void"),
            Transform::Bucket(length) => write!(f, "bucket[{length}]"),
            Transform::Truncate(width) => write!(f, "truncate[{width}]"),
            Transform::Unknown => write!(f, "unknown"),
        }
    }
}

impl FromStr for Transform {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let t = match s {
            "identity" => Transform::Identity,
            "year" => Transform::Year,
            "month" => Transform::Month,
            "day" => Transform::Day,
            "hour" => Transform::Hour,
            "void" => Transform::Void,
            "unknown" => Transform::Unknown,
            v if v.starts_with("bucket") => {
                let length = v
                    .strip_prefix("bucket")
                    .expect("transform must starts with `bucket`")
                    .trim_start_matches('[')
                    .trim_end_matches(']')
                    .parse()
                    .map_err(|err| {
                        invalid_data!("transform bucket type {v:?} is invalid").with_source(err)
                    })?;

                Transform::Bucket(length)
            }
            v if v.starts_with("truncate") => {
                let width = v
                    .strip_prefix("truncate")
                    .expect("transform must starts with `truncate`")
                    .trim_start_matches('[')
                    .trim_end_matches(']')
                    .parse()
                    .map_err(|err| {
                        invalid_data!("transform truncate type {v:?} is invalid").with_source(err)
                    })?;

                Transform::Truncate(width)
            }
            v => {
                return Err(invalid_data!("transform {v:?} is invalid"));
            }
        };

        Ok(t)
    }
}

impl Serialize for Transform {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where S: Serializer {
        serializer.serialize_str(format!("{self}").as_str())
    }
}

impl<'de> Deserialize<'de> for Transform {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where D: Deserializer<'de> {
        let s = String::deserialize(deserializer)?;
        s.parse().map_err(<D::Error as serde::de::Error>::custom)
    }
}

/// An enum representing the result of the adjusted projection.
/// Either being a single adjusted datum or a set.
#[derive(Debug)]
enum AdjustedProjection {
    Single(Datum),
    Set(FnvHashSet<Datum>),
}

#[cfg(test)]
mod tests {
    use super::*;

    fn check_boundary(op: PredicateOperator, input: Datum, expected: Datum) {
        let result = Transform::adjust_boundary(&op, &input).unwrap().unwrap();
        assert_eq!(result, expected);
    }

    #[test]
    fn test_adjust_boundary_timestamp_types() {
        for (datum, dec, inc) in [
            (
                Datum::timestamptz_micros(1000),
                Datum::timestamptz_micros(999),
                Datum::timestamptz_micros(1001),
            ),
            (
                Datum::timestamp_nanos(5000),
                Datum::timestamp_nanos(4999),
                Datum::timestamp_nanos(5001),
            ),
            (
                Datum::timestamptz_nanos(5000),
                Datum::timestamptz_nanos(4999),
                Datum::timestamptz_nanos(5001),
            ),
        ] {
            check_boundary(PredicateOperator::LessThan, datum.clone(), dec);
            check_boundary(PredicateOperator::GreaterThan, datum.clone(), inc);
            check_boundary(
                PredicateOperator::LessThanOrEq,
                datum.clone(),
                datum.clone(),
            );
            check_boundary(PredicateOperator::GreaterThanOrEq, datum.clone(), datum);
        }
    }

    /// Renders `ordinal` through the public API with the given declared type.
    fn human(transform: Transform, primitive: PrimitiveType, ordinal: i32) -> String {
        transform.to_human_string(&Type::Primitive(primitive), Some(&Literal::int(ordinal)))
    }

    /// Renders `ordinal` for a transform whose result type is `int`.
    fn human_int(transform: Transform, ordinal: i32) -> String {
        human(transform, PrimitiveType::Int, ordinal)
    }

    #[test]
    fn test_to_human_string_year() {
        assert_eq!(human_int(Transform::Year, -1970), "0000");
        assert_eq!(human_int(Transform::Year, -1), "1969");
        assert_eq!(human_int(Transform::Year, 0), "1970");
        assert_eq!(human_int(Transform::Year, 47), "2017");
    }

    #[test]
    fn test_to_human_string_month() {
        assert_eq!(human_int(Transform::Month, -1970 * 12), "0000-01");
        assert_eq!(human_int(Transform::Month, -13), "1968-12");
        assert_eq!(human_int(Transform::Month, -12), "1969-01");
        assert_eq!(human_int(Transform::Month, -1), "1969-12");
        assert_eq!(human_int(Transform::Month, 0), "1970-01");
        assert_eq!(human_int(Transform::Month, 11), "1970-12");
        assert_eq!(human_int(Transform::Month, 12), "1971-01");
        assert_eq!(human_int(Transform::Month, 569), "2017-06");
    }

    #[test]
    fn test_to_human_string_day() {
        assert_eq!(human_int(Transform::Day, -1), "1969-12-31");
        assert_eq!(human_int(Transform::Day, 0), "1970-01-01");
        assert_eq!(human_int(Transform::Day, 31), "1970-02-01");
        assert_eq!(human_int(Transform::Day, 17332), "2017-06-15");
    }

    #[test]
    fn test_to_human_string_hour() {
        assert_eq!(human_int(Transform::Hour, -24), "1969-12-31-00");
        assert_eq!(human_int(Transform::Hour, -1), "1969-12-31-23");
        assert_eq!(human_int(Transform::Hour, 0), "1970-01-01-00");
        assert_eq!(human_int(Transform::Hour, 23), "1970-01-01-23");
        assert_eq!(human_int(Transform::Hour, 24), "1970-01-02-00");
        assert_eq!(human_int(Transform::Hour, 1000), "1970-02-11-16");
        assert_eq!(human_int(Transform::Hour, 415984), "2017-06-15-16");
    }

    /// The temporal transforms ignore the declared field type, matching the Java
    /// signatures `toHumanString(Type alwaysInt, ..)` and
    /// `toHumanString(Type alwaysDate, ..)`.
    #[test]
    fn test_to_human_string_ignores_declared_type_for_temporal_transforms() {
        assert_eq!(human(Transform::Year, PrimitiveType::Date, 47), "2017");
        assert_eq!(human(Transform::Month, PrimitiveType::Date, 569), "2017-06");
        assert_eq!(
            human(Transform::Day, PrimitiveType::Int, 17332),
            "2017-06-15"
        );
        assert_eq!(
            human(Transform::Hour, PrimitiveType::Date, 415984),
            "2017-06-15-16"
        );
    }

    /// Transforms with no temporal format keep deferring to the datum, which
    /// renders according to the declared field type.
    #[test]
    fn test_to_human_string_defers_to_datum_for_other_transforms() {
        assert_eq!(
            human(Transform::Identity, PrimitiveType::Int, 17332),
            "17332"
        );
        assert_eq!(
            human(Transform::Identity, PrimitiveType::Date, 17332),
            "2017-06-15"
        );
        assert_eq!(human(Transform::Bucket(16), PrimitiveType::Int, 5), "5");

        // A literal that is not an `int` also defers to the datum rather than being
        // reported as `null`, so the temporal arms add no second behaviour change.
        assert_eq!(
            Transform::Year.to_human_string(
                &Type::Primitive(PrimitiveType::String),
                Some(&Literal::string("unformatted"))
            ),
            "unformatted"
        );
    }

    #[test]
    fn test_to_human_string_null_cases() {
        assert_eq!(human(Transform::Void, PrimitiveType::Int, 47), "null");
        assert_eq!(human(Transform::Void, PrimitiveType::Date, 17332), "null");
        for transform in [
            Transform::Year,
            Transform::Month,
            Transform::Day,
            Transform::Hour,
            Transform::Identity,
            Transform::Void,
        ] {
            assert_eq!(
                transform.to_human_string(&Type::Primitive(PrimitiveType::Int), None),
                "null"
            );
        }
    }
}