Skip to main content

iceberg/spec/
transform.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Transforms in iceberg.
19
20use std::cmp::Ordering;
21use std::fmt::{Display, Formatter};
22use std::str::FromStr;
23
24use chrono::Datelike;
25use fnv::FnvHashSet;
26use serde::{Deserialize, Deserializer, Serialize, Serializer};
27
28use super::values::decimal_utils::decimal_from_i128_with_scale;
29use super::values::temporal::date;
30use super::{Datum, PrimitiveLiteral};
31use crate::ErrorKind;
32use crate::error::{Error, Result, invalid_data};
33use crate::expr::{
34    BinaryExpression, BoundPredicate, BoundReference, Predicate, PredicateOperator, Reference,
35    SetExpression, UnaryExpression,
36};
37use crate::spec::Literal;
38use crate::spec::datatypes::{PrimitiveType, Type};
39use crate::transform::{BoxedTransformFunction, create_transform_function};
40
41/// The year the Unix epoch falls in, which every temporal ordinal counts from.
42const UNIX_EPOCH_YEAR: i32 = 1970;
43
44/// Transform is used to transform predicates to partition predicates,
45/// in addition to transforming data values.
46///
47/// Deriving partition predicates from column predicates on the table data
48/// is used to separate the logical queries from physical storage: the
49/// partitioning can change and the correct partition filters are always
50/// derived from column predicates.
51///
52/// This simplifies queries because users don’t have to supply both logical
53/// predicates and partition predicates.
54///
55/// All transforms must return `null` for a `null` input value.
56#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
57pub enum Transform {
58    /// Source value, unmodified
59    ///
60    /// - Source type could be any type.
61    /// - Return type is the same with source type.
62    Identity,
63    /// Hash of value, mod `N`.
64    ///
65    /// Bucket partition transforms use a 32-bit hash of the source value.
66    /// The 32-bit hash implementation is the 32-bit Murmur3 hash, x86
67    /// variant, seeded with 0.
68    ///
69    /// Transforms are parameterized by a number of buckets, N. The hash mod
70    /// N must produce a positive value by first discarding the sign bit of
71    /// the hash value. In pseudo-code, the function is:
72    ///
73    /// ```text
74    /// def bucket_N(x) = (murmur3_x86_32_hash(x) & Integer.MAX_VALUE) % N
75    /// ```
76    ///
77    /// - Source type could be `int`, `long`, `decimal`, `date`, `time`,
78    ///   `timestamp`, `timestamptz`, `string`, `uuid`, `fixed`, `binary`.
79    /// - Return type is `int`.
80    Bucket(u32),
81    /// Value truncated to width `W`
82    ///
83    /// For `int`:
84    ///
85    /// - `v - (v % W)` remainders must be positive
86    /// - example: W=10: 1 → 0, -1 → -10
87    /// - note: The remainder, v % W, must be positive.
88    ///
89    /// For `long`:
90    ///
91    /// - `v - (v % W)` remainders must be positive
92    /// - example: W=10: 1 → 0, -1 → -10
93    /// - note: The remainder, v % W, must be positive.
94    ///
95    /// For `decimal`:
96    ///
97    /// - `scaled_W = decimal(W, scale(v)) v - (v % scaled_W)`
98    /// - example: W=50, s=2: 10.65 → 10.50
99    ///
100    /// For `string`:
101    ///
102    /// - Substring of length L: `v.substring(0, L)`
103    /// - example: L=3: iceberg → ice
104    /// - note: Strings are truncated to a valid UTF-8 string with no more
105    ///   than L code points.
106    ///
107    /// - Source type could be `int`, `long`, `decimal`, `string`
108    /// - Return type is the same with source type.
109    Truncate(u32),
110    /// Extract a date or timestamp year, as years from 1970
111    ///
112    /// - Source type could be `date`, `timestamp`, `timestamptz`
113    /// - Return type is `int`
114    Year,
115    /// Extract a date or timestamp month, as months from 1970-01-01
116    ///
117    /// - Source type could be `date`, `timestamp`, `timestamptz`
118    /// - Return type is `int`
119    Month,
120    /// Extract a date or timestamp day, as days from 1970-01-01
121    ///
122    /// - Source type could be `date`, `timestamp`, `timestamptz`
123    /// - Return type is `int`
124    Day,
125    /// Extract a timestamp hour, as hours from 1970-01-01 00:00:00
126    ///
127    /// - Source type could be `timestamp`, `timestamptz`
128    /// - Return type is `int`
129    Hour,
130    /// Always produces `null`
131    ///
132    /// The void transform may be used to replace the transform in an
133    /// existing partition field so that the field is effectively dropped in
134    /// v1 tables.
135    ///
136    /// - Source type could be any type..
137    /// - Return type is Source type.
138    Void,
139    /// Used to represent some customized transform that can't be recognized or supported now.
140    Unknown,
141}
142
143impl Transform {
144    /// Returns a human-readable String representation of a transformed value.
145    ///
146    /// The temporal transforms store an ordinal count since the Unix epoch, and
147    /// this method renders that count as a date so that partition paths and
148    /// snapshot summary keys match the Java reference implementation:
149    ///
150    /// | Transform | Format          | Example         |
151    /// |-----------|-----------------|-----------------|
152    /// | `Year`    | `yyyy`          | `2017`          |
153    /// | `Month`   | `yyyy-MM`       | `2017-06`       |
154    /// | `Day`     | `yyyy-MM-dd`    | `2017-06-15`    |
155    /// | `Hour`    | `yyyy-MM-dd-HH` | `2017-06-15-16` |
156    ///
157    /// `Void` renders as `null`, as does an absent value for any transform.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// use iceberg::spec::{Literal, PrimitiveType, Transform, Type};
163    ///
164    /// let int = Type::Primitive(PrimitiveType::Int);
165    /// let date = Type::Primitive(PrimitiveType::Date);
166    ///
167    /// // A stored value carries no logical type of its own. For transforms that do
168    /// // not format it themselves, the declared field type decides how it renders.
169    /// let stored = Literal::int(17332);
170    /// assert_eq!(
171    ///     Transform::Identity.to_human_string(&int, Some(&stored)),
172    ///     "17332"
173    /// );
174    /// assert_eq!(
175    ///     Transform::Identity.to_human_string(&date, Some(&stored)),
176    ///     "2017-06-15"
177    /// );
178    ///
179    /// // The temporal transforms format their own ordinal and ignore the declared
180    /// // type. All four ordinals below are the same instant, 2017-06-15T16:00:00Z,
181    /// // counted at four granularities.
182    /// assert_eq!(
183    ///     Transform::Year.to_human_string(&int, Some(&Literal::int(47))),
184    ///     "2017"
185    /// );
186    /// assert_eq!(
187    ///     Transform::Month.to_human_string(&int, Some(&Literal::int(569))),
188    ///     "2017-06"
189    /// );
190    /// assert_eq!(
191    ///     Transform::Day.to_human_string(&int, Some(&Literal::int(17332))),
192    ///     "2017-06-15"
193    /// );
194    /// assert_eq!(
195    ///     Transform::Hour.to_human_string(&int, Some(&Literal::int(415984))),
196    ///     "2017-06-15-16"
197    /// );
198    ///
199    /// // `Void` and an absent value render as `null`.
200    /// assert_eq!(
201    ///     Transform::Void.to_human_string(&int, Some(&Literal::int(47))),
202    ///     "null"
203    /// );
204    /// assert_eq!(Transform::Year.to_human_string(&int, None), "null");
205    /// ```
206    pub fn to_human_string(&self, field_type: &Type, value: Option<&Literal>) -> String {
207        let Some(value) = value.and_then(Literal::as_primitive_literal) else {
208            return "null".to_string();
209        };
210
211        match (*self, value) {
212            (Self::Void, _) => "null".to_string(),
213            // The temporal transforms store an ordinal count since the Unix epoch,
214            // which the datum arm below cannot render: it would show the raw count
215            // for `Year`, `Month` and `Hour`, and would leave `Day` dependent on the
216            // field type happening to be `date`. Java overrides `toHumanString` on
217            // each of these four transforms and ignores the declared type, so do the
218            // same. Any other literal falls through to the datum.
219            (Self::Year, PrimitiveLiteral::Int(ordinal)) => Self::human_year(ordinal),
220            (Self::Month, PrimitiveLiteral::Int(ordinal)) => Self::human_month(ordinal),
221            (Self::Day, PrimitiveLiteral::Int(ordinal)) => Self::human_day(ordinal),
222            (Self::Hour, PrimitiveLiteral::Int(ordinal)) => Self::human_hour(ordinal),
223            (_, value) => {
224                let field_type = field_type.as_primitive_type().unwrap();
225                Datum::new(field_type.clone(), value).to_human_string()
226            }
227        }
228    }
229
230    /// Formats a year ordinal, the number of years since 1970, as `yyyy`.
231    ///
232    /// Mirrors the output of `TransformUtil.humanYear`.
233    fn human_year(year_ordinal: i32) -> String {
234        format!("{:04}", UNIX_EPOCH_YEAR + year_ordinal)
235    }
236
237    /// Formats a month ordinal, the number of months since 1970-01, as `yyyy-MM`.
238    ///
239    /// Mirrors the output of `TransformUtil.humanMonth`, which divides with
240    /// `Math.floorDiv` and `Math.floorMod` rather than `/` and `%`. Truncating
241    /// division rounds toward zero, which is the wrong direction before 1970:
242    /// ordinal -1 is 1969-12, but truncating yields 1970-01. `div_euclid` and
243    /// `rem_euclid` round toward negative infinity and so agree with Java.
244    fn human_month(month_ordinal: i32) -> String {
245        format!(
246            "{:04}-{:02}",
247            UNIX_EPOCH_YEAR + month_ordinal.div_euclid(12),
248            1 + month_ordinal.rem_euclid(12)
249        )
250    }
251
252    /// Formats a day ordinal, the number of days since 1970-01-01, as `yyyy-MM-dd`.
253    ///
254    /// Mirrors the output of `TransformUtil.humanDay`. Like the Java `Days`
255    /// transform, whose signature is `toHumanString(Type alwaysDate, Integer
256    /// value)`, this ignores the declared field type rather than relying on it being
257    /// `date`.
258    fn human_day(day_ordinal: i32) -> String {
259        let date = date::days_to_date(day_ordinal);
260        format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day())
261    }
262
263    /// Formats an hour ordinal, the number of hours since 1970-01-01T00:00:00Z,
264    /// as `yyyy-MM-dd-HH`.
265    ///
266    /// Mirrors the output of `TransformUtil.humanHour`. `div_euclid` and `rem_euclid`
267    /// split the ordinal into whole days and the hour within the day so that hours
268    /// before 1970 round the way they do in Java.
269    fn human_hour(hour_ordinal: i32) -> String {
270        format!(
271            "{}-{:02}",
272            Self::human_day(hour_ordinal.div_euclid(24)),
273            hour_ordinal.rem_euclid(24)
274        )
275    }
276
277    /// Get the return type of transform given the input type.
278    /// Returns `None` if it can't be transformed.
279    pub fn result_type(&self, input_type: &Type) -> Result<Type> {
280        match self {
281            Transform::Identity => {
282                if matches!(input_type, Type::Primitive(_)) {
283                    Ok(input_type.clone())
284                } else {
285                    Err(invalid_data!(
286                        "{input_type} is not a valid input type of identity transform",
287                    ))
288                }
289            }
290            Transform::Void => Ok(input_type.clone()),
291            Transform::Unknown => Ok(Type::Primitive(PrimitiveType::String)),
292            Transform::Bucket(_) => {
293                if let Type::Primitive(p) = input_type {
294                    match p {
295                        PrimitiveType::Int
296                        | PrimitiveType::Long
297                        | PrimitiveType::Decimal { .. }
298                        | PrimitiveType::Date
299                        | PrimitiveType::Time
300                        | PrimitiveType::Timestamp
301                        | PrimitiveType::Timestamptz
302                        | PrimitiveType::TimestampNs
303                        | PrimitiveType::TimestamptzNs
304                        | PrimitiveType::String
305                        | PrimitiveType::Uuid
306                        | PrimitiveType::Fixed(_)
307                        | PrimitiveType::Binary => Ok(Type::Primitive(PrimitiveType::Int)),
308                        _ => Err(invalid_data!(
309                            "{input_type} is not a valid input type of bucket transform",
310                        )),
311                    }
312                } else {
313                    Err(invalid_data!(
314                        "{input_type} is not a valid input type of bucket transform",
315                    ))
316                }
317            }
318            Transform::Truncate(_) => {
319                if let Type::Primitive(p) = input_type {
320                    match p {
321                        PrimitiveType::Int
322                        | PrimitiveType::Long
323                        | PrimitiveType::String
324                        | PrimitiveType::Binary
325                        | PrimitiveType::Decimal { .. } => Ok(input_type.clone()),
326                        _ => Err(invalid_data!(
327                            "{input_type} is not a valid input type of truncate transform",
328                        )),
329                    }
330                } else {
331                    Err(invalid_data!(
332                        "{input_type} is not a valid input type of truncate transform",
333                    ))
334                }
335            }
336            Transform::Year | Transform::Month => {
337                if let Type::Primitive(p) = input_type {
338                    match p {
339                        PrimitiveType::Timestamp
340                        | PrimitiveType::Timestamptz
341                        | PrimitiveType::TimestampNs
342                        | PrimitiveType::TimestamptzNs
343                        | PrimitiveType::Date => Ok(Type::Primitive(PrimitiveType::Int)),
344                        _ => Err(invalid_data!(
345                            "{input_type} is not a valid input type of {self} transform",
346                        )),
347                    }
348                } else {
349                    Err(invalid_data!(
350                        "{input_type} is not a valid input type of {self} transform",
351                    ))
352                }
353            }
354            Transform::Day => {
355                if let Type::Primitive(p) = input_type {
356                    match p {
357                        PrimitiveType::Timestamp
358                        | PrimitiveType::Timestamptz
359                        | PrimitiveType::TimestampNs
360                        | PrimitiveType::TimestamptzNs
361                        | PrimitiveType::Date => Ok(Type::Primitive(PrimitiveType::Date)),
362                        _ => Err(invalid_data!(
363                            "{input_type} is not a valid input type of {self} transform",
364                        )),
365                    }
366                } else {
367                    Err(invalid_data!(
368                        "{input_type} is not a valid input type of {self} transform",
369                    ))
370                }
371            }
372            Transform::Hour => {
373                if let Type::Primitive(p) = input_type {
374                    match p {
375                        PrimitiveType::Timestamp
376                        | PrimitiveType::Timestamptz
377                        | PrimitiveType::TimestampNs
378                        | PrimitiveType::TimestamptzNs => Ok(Type::Primitive(PrimitiveType::Int)),
379                        _ => Err(invalid_data!(
380                            "{input_type} is not a valid input type of {self} transform",
381                        )),
382                    }
383                } else {
384                    Err(invalid_data!(
385                        "{input_type} is not a valid input type of {self} transform",
386                    ))
387                }
388            }
389        }
390    }
391
392    /// Whether the transform preserves the order of values.
393    pub fn preserves_order(&self) -> bool {
394        !matches!(
395            self,
396            Transform::Void | Transform::Bucket(_) | Transform::Unknown
397        )
398    }
399
400    /// Return the unique transform name to check if similar transforms for the same source field
401    /// are added multiple times in partition spec builder.
402    pub fn dedup_name(&self) -> String {
403        match self {
404            Transform::Year | Transform::Month | Transform::Day | Transform::Hour => {
405                "time".to_string()
406            }
407            _ => format!("{self}"),
408        }
409    }
410
411    /// Whether ordering by this transform's result satisfies the ordering of another transform's
412    /// result.
413    ///
414    /// For example, sorting by day(ts) will produce an ordering that is also by month(ts) or
415    ///  year(ts). However, sorting by day(ts) will not satisfy the order of hour(ts) or identity(ts).
416    pub fn satisfies_order_of(&self, other: &Self) -> bool {
417        match self {
418            Transform::Identity => other.preserves_order(),
419            Transform::Hour => matches!(
420                other,
421                Transform::Hour | Transform::Day | Transform::Month | Transform::Year
422            ),
423            Transform::Day => matches!(other, Transform::Day | Transform::Month | Transform::Year),
424            Transform::Month => matches!(other, Transform::Month | Transform::Year),
425            _ => self == other,
426        }
427    }
428
429    /// Strictly projects a given predicate according to the transformation
430    /// specified by the `Transform` instance.
431    ///
432    /// This method ensures that the projected predicate is strictly aligned
433    /// with the transformation logic, providing a more precise filtering
434    /// mechanism for transformed data.
435    ///
436    /// # Example
437    /// Suppose, we have row filter `a = 10`, and a partition spec
438    /// `bucket(a, 37) as bs`, if one row matches `a = 10`, then its partition
439    /// value should match `bucket(10, 37) as bs`, and we project `a = 10` to
440    /// `bs = bucket(10, 37)`
441    pub fn strict_project(
442        &self,
443        name: &str,
444        predicate: &BoundPredicate,
445    ) -> Result<Option<Predicate>> {
446        let func = create_transform_function(self)?;
447
448        match self {
449            Transform::Identity => match predicate {
450                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
451                BoundPredicate::Binary(expr) => Ok(Some(Predicate::Binary(BinaryExpression::new(
452                    expr.op(),
453                    Reference::new(name),
454                    expr.literal().to_owned(),
455                )))),
456                BoundPredicate::Set(expr) => Ok(Some(Predicate::Set(SetExpression::new(
457                    expr.op(),
458                    Reference::new(name),
459                    expr.literals().to_owned(),
460                )))),
461                _ => Ok(None),
462            },
463            Transform::Bucket(_) => match predicate {
464                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
465                BoundPredicate::Binary(expr) => {
466                    self.project_binary_expr(name, PredicateOperator::NotEq, expr, &func)
467                }
468                BoundPredicate::Set(expr) => {
469                    self.project_set_expr(expr, PredicateOperator::NotIn, name, &func)
470                }
471                _ => Ok(None),
472            },
473            Transform::Truncate(width) => match predicate {
474                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
475                BoundPredicate::Binary(expr) => {
476                    if matches!(
477                        expr.term().field().field_type.as_primitive_type(),
478                        Some(&PrimitiveType::Int)
479                            | Some(&PrimitiveType::Long)
480                            | Some(&PrimitiveType::Decimal { .. })
481                    ) {
482                        self.truncate_number_strict(name, expr, &func)
483                    } else if expr.op() == PredicateOperator::StartsWith {
484                        let len = match expr.literal().literal() {
485                            PrimitiveLiteral::String(s) => s.len(),
486                            PrimitiveLiteral::Binary(b) => b.len(),
487                            _ => {
488                                return Err(invalid_data!(
489                                    "Expected a string or binary literal, got: {:?}",
490                                    expr.literal()
491                                ));
492                            }
493                        };
494                        match len.cmp(&(*width as usize)) {
495                            Ordering::Less => Ok(Some(Predicate::Binary(BinaryExpression::new(
496                                PredicateOperator::StartsWith,
497                                Reference::new(name),
498                                expr.literal().to_owned(),
499                            )))),
500                            Ordering::Equal => Ok(Some(Predicate::Binary(BinaryExpression::new(
501                                PredicateOperator::Eq,
502                                Reference::new(name),
503                                expr.literal().to_owned(),
504                            )))),
505                            Ordering::Greater => Ok(None),
506                        }
507                    } else if expr.op() == PredicateOperator::NotStartsWith {
508                        let len = match expr.literal().literal() {
509                            PrimitiveLiteral::String(s) => s.len(),
510                            PrimitiveLiteral::Binary(b) => b.len(),
511                            _ => {
512                                return Err(invalid_data!(
513                                    "Expected a string or binary literal, got: {:?}",
514                                    expr.literal()
515                                ));
516                            }
517                        };
518                        match len.cmp(&(*width as usize)) {
519                            Ordering::Less => Ok(Some(Predicate::Binary(BinaryExpression::new(
520                                PredicateOperator::NotStartsWith,
521                                Reference::new(name),
522                                expr.literal().to_owned(),
523                            )))),
524                            Ordering::Equal => Ok(Some(Predicate::Binary(BinaryExpression::new(
525                                PredicateOperator::NotEq,
526                                Reference::new(name),
527                                expr.literal().to_owned(),
528                            )))),
529                            Ordering::Greater => {
530                                Ok(Some(Predicate::Binary(BinaryExpression::new(
531                                    expr.op(),
532                                    Reference::new(name),
533                                    func.transform_literal_result(expr.literal())?,
534                                ))))
535                            }
536                        }
537                    } else {
538                        self.truncate_array_strict(name, expr, &func)
539                    }
540                }
541                BoundPredicate::Set(expr) => {
542                    self.project_set_expr(expr, PredicateOperator::NotIn, name, &func)
543                }
544                _ => Ok(None),
545            },
546            Transform::Year | Transform::Month | Transform::Day | Transform::Hour => {
547                match predicate {
548                    BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
549                    BoundPredicate::Binary(expr) => self.truncate_number_strict(name, expr, &func),
550                    BoundPredicate::Set(expr) => {
551                        self.project_set_expr(expr, PredicateOperator::NotIn, name, &func)
552                    }
553                    _ => Ok(None),
554                }
555            }
556            _ => Ok(None),
557        }
558    }
559
560    /// Projects a given predicate according to the transformation
561    /// specified by the `Transform` instance.
562    ///
563    /// This allows predicates to be effectively applied to data
564    /// that has undergone transformation, enabling efficient querying
565    /// and filtering based on the original, untransformed data.
566    ///
567    /// # Example
568    /// Suppose, we have row filter `a = 10`, and a partition spec
569    /// `bucket(a, 37) as bs`, if one row matches `a = 10`, then its partition
570    /// value should match `bucket(10, 37) as bs`, and we project `a = 10` to
571    /// `bs = bucket(10, 37)`
572    pub fn project(&self, name: &str, predicate: &BoundPredicate) -> Result<Option<Predicate>> {
573        let func = create_transform_function(self)?;
574
575        match self {
576            Transform::Identity => match predicate {
577                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
578                BoundPredicate::Binary(expr) => Ok(Some(Predicate::Binary(BinaryExpression::new(
579                    expr.op(),
580                    Reference::new(name),
581                    expr.literal().to_owned(),
582                )))),
583                BoundPredicate::Set(expr) => Ok(Some(Predicate::Set(SetExpression::new(
584                    expr.op(),
585                    Reference::new(name),
586                    expr.literals().to_owned(),
587                )))),
588                _ => Ok(None),
589            },
590            Transform::Bucket(_) => match predicate {
591                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
592                BoundPredicate::Binary(expr) => {
593                    self.project_binary_expr(name, PredicateOperator::Eq, expr, &func)
594                }
595                BoundPredicate::Set(expr) => {
596                    self.project_set_expr(expr, PredicateOperator::In, name, &func)
597                }
598                _ => Ok(None),
599            },
600            Transform::Truncate(width) => match predicate {
601                BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
602                BoundPredicate::Binary(expr) => {
603                    self.project_binary_with_adjusted_boundary(name, expr, &func, Some(*width))
604                }
605                BoundPredicate::Set(expr) => {
606                    self.project_set_expr(expr, PredicateOperator::In, name, &func)
607                }
608                _ => Ok(None),
609            },
610            Transform::Year | Transform::Month | Transform::Day | Transform::Hour => {
611                match predicate {
612                    BoundPredicate::Unary(expr) => Self::project_unary(expr.op(), name),
613                    BoundPredicate::Binary(expr) => {
614                        self.project_binary_with_adjusted_boundary(name, expr, &func, None)
615                    }
616                    BoundPredicate::Set(expr) => {
617                        self.project_set_expr(expr, PredicateOperator::In, name, &func)
618                    }
619                    _ => Ok(None),
620                }
621            }
622            _ => Ok(None),
623        }
624    }
625
626    /// Check if `Transform` is applicable on datum's `PrimitiveType`
627    fn can_transform(&self, datum: &Datum) -> bool {
628        let input_type = datum.data_type().clone();
629        self.result_type(&Type::Primitive(input_type)).is_ok()
630    }
631
632    /// Creates a unary predicate from a given operator and a reference name.
633    fn project_unary(op: PredicateOperator, name: &str) -> Result<Option<Predicate>> {
634        Ok(Some(Predicate::Unary(UnaryExpression::new(
635            op,
636            Reference::new(name),
637        ))))
638    }
639
640    /// Attempts to create a binary predicate based on a binary expression,
641    /// if applicable.
642    ///
643    /// This method evaluates a given binary expression and, if the operation
644    /// is the given operator and the literal can be transformed, constructs a
645    /// `Predicate::Binary`variant representing the binary operation.
646    fn project_binary_expr(
647        &self,
648        name: &str,
649        op: PredicateOperator,
650        expr: &BinaryExpression<BoundReference>,
651        func: &BoxedTransformFunction,
652    ) -> Result<Option<Predicate>> {
653        if expr.op() != op || !self.can_transform(expr.literal()) {
654            return Ok(None);
655        }
656
657        Ok(Some(Predicate::Binary(BinaryExpression::new(
658            expr.op(),
659            Reference::new(name),
660            func.transform_literal_result(expr.literal())?,
661        ))))
662    }
663
664    /// Projects a binary expression to a predicate with an adjusted boundary.
665    ///
666    /// Checks if the literal within the given binary expression is
667    /// transformable. If transformable, it proceeds to potentially adjust
668    /// the boundary of the expression based on the comparison operator (`op`).
669    /// The potential adjustments involve incrementing or decrementing the
670    /// literal value and changing the `PredicateOperator` itself to its
671    /// inclusive variant.
672    fn project_binary_with_adjusted_boundary(
673        &self,
674        name: &str,
675        expr: &BinaryExpression<BoundReference>,
676        func: &BoxedTransformFunction,
677        width: Option<u32>,
678    ) -> Result<Option<Predicate>> {
679        if !self.can_transform(expr.literal()) {
680            return Ok(None);
681        }
682
683        let op = &expr.op();
684        let datum = &expr.literal();
685
686        if let Some(boundary) = Self::adjust_boundary(op, datum)? {
687            let transformed_projection = func.transform_literal_result(&boundary)?;
688
689            let adjusted_projection =
690                self.adjust_time_projection(op, datum, &transformed_projection);
691
692            let adjusted_operator = Self::adjust_operator(op, datum, width);
693
694            if let Some(op) = adjusted_operator {
695                let predicate = match adjusted_projection {
696                    None => Predicate::Binary(BinaryExpression::new(
697                        op,
698                        Reference::new(name),
699                        transformed_projection,
700                    )),
701                    Some(AdjustedProjection::Single(d)) => {
702                        Predicate::Binary(BinaryExpression::new(op, Reference::new(name), d))
703                    }
704                    Some(AdjustedProjection::Set(d)) => Predicate::Set(SetExpression::new(
705                        PredicateOperator::In,
706                        Reference::new(name),
707                        d,
708                    )),
709                };
710                return Ok(Some(predicate));
711            }
712        };
713
714        Ok(None)
715    }
716
717    /// Projects a set expression to a predicate,
718    /// applying a transformation to each literal in the set.
719    fn project_set_expr(
720        &self,
721        expr: &SetExpression<BoundReference>,
722        op: PredicateOperator,
723        name: &str,
724        func: &BoxedTransformFunction,
725    ) -> Result<Option<Predicate>> {
726        if expr.op() != op || expr.literals().iter().any(|d| !self.can_transform(d)) {
727            return Ok(None);
728        }
729
730        let mut new_set = FnvHashSet::default();
731
732        for lit in expr.literals() {
733            let datum = func.transform_literal_result(lit)?;
734
735            if let Some(AdjustedProjection::Single(d)) =
736                self.adjust_time_projection(&op, lit, &datum)
737            {
738                new_set.insert(d);
739            };
740
741            new_set.insert(datum);
742        }
743
744        Ok(Some(Predicate::Set(SetExpression::new(
745            expr.op(),
746            Reference::new(name),
747            new_set,
748        ))))
749    }
750
751    /// Adjusts the boundary value for comparison operations
752    /// based on the specified `PredicateOperator` and `Datum`.
753    ///
754    /// This function modifies the boundary value for certain comparison
755    /// operators (`LessThan`, `GreaterThan`) by incrementing or decrementing
756    /// the literal value within the given `Datum`. For operators that do not
757    /// imply a boundary shift (`Eq`, `LessThanOrEq`, `GreaterThanOrEq`,
758    /// `StartsWith`, `NotStartsWith`), the original datum is returned
759    /// unmodified.
760    fn adjust_boundary(op: &PredicateOperator, datum: &Datum) -> Result<Option<Datum>> {
761        let adjusted_boundary = match op {
762            PredicateOperator::LessThan => match (datum.data_type(), datum.literal()) {
763                (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Some(Datum::int(v - 1)),
764                (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Some(Datum::long(v - 1)),
765                (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
766                    Some(Datum::decimal(decimal_from_i128_with_scale(v - 1, 0))?)
767                }
768                (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Some(Datum::date(v - 1)),
769                (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
770                    Some(Datum::timestamp_micros(v - 1))
771                }
772                (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
773                    Some(Datum::timestamptz_micros(v - 1))
774                }
775                (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
776                    Some(Datum::timestamp_nanos(v - 1))
777                }
778                (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
779                    Some(Datum::timestamptz_nanos(v - 1))
780                }
781                _ => Some(datum.to_owned()),
782            },
783            PredicateOperator::GreaterThan => match (datum.data_type(), datum.literal()) {
784                (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Some(Datum::int(v + 1)),
785                (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Some(Datum::long(v + 1)),
786                (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
787                    Some(Datum::decimal(decimal_from_i128_with_scale(v + 1, 0))?)
788                }
789                (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Some(Datum::date(v + 1)),
790                (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
791                    Some(Datum::timestamp_micros(v + 1))
792                }
793                (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
794                    Some(Datum::timestamptz_micros(v + 1))
795                }
796                (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
797                    Some(Datum::timestamp_nanos(v + 1))
798                }
799                (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
800                    Some(Datum::timestamptz_nanos(v + 1))
801                }
802                _ => Some(datum.to_owned()),
803            },
804            PredicateOperator::Eq
805            | PredicateOperator::LessThanOrEq
806            | PredicateOperator::GreaterThanOrEq
807            | PredicateOperator::StartsWith
808            | PredicateOperator::NotStartsWith => Some(datum.to_owned()),
809            _ => None,
810        };
811
812        Ok(adjusted_boundary)
813    }
814
815    /// Adjusts the comparison operator based on the specified datum and an
816    /// optional width constraint.
817    ///
818    /// This function modifies the comparison operator for `LessThan` and
819    /// `GreaterThan` cases to their inclusive counterparts (`LessThanOrEq`,
820    /// `GreaterThanOrEq`) unconditionally. For `StartsWith` and
821    /// `NotStartsWith` operators acting on string literals, the operator may
822    /// be adjusted to `Eq` or `NotEq` if the string length matches the
823    /// specified width, indicating a precise match rather than a prefix
824    /// condition.
825    fn adjust_operator(
826        op: &PredicateOperator,
827        datum: &Datum,
828        width: Option<u32>,
829    ) -> Option<PredicateOperator> {
830        match op {
831            PredicateOperator::LessThan => Some(PredicateOperator::LessThanOrEq),
832            PredicateOperator::GreaterThan => Some(PredicateOperator::GreaterThanOrEq),
833            PredicateOperator::StartsWith => match datum.literal() {
834                PrimitiveLiteral::String(s) => {
835                    if let Some(w) = width
836                        && s.len() == w as usize
837                    {
838                        return Some(PredicateOperator::Eq);
839                    };
840                    Some(*op)
841                }
842                _ => Some(*op),
843            },
844            PredicateOperator::NotStartsWith => match datum.literal() {
845                PrimitiveLiteral::String(s) => {
846                    if let Some(w) = width {
847                        let w = w as usize;
848
849                        if s.len() == w {
850                            return Some(PredicateOperator::NotEq);
851                        }
852
853                        if s.len() < w {
854                            return Some(*op);
855                        }
856
857                        return None;
858                    };
859                    Some(*op)
860                }
861                _ => Some(*op),
862            },
863            _ => Some(*op),
864        }
865    }
866
867    /// Adjust projection for temporal transforms, align with Java
868    /// implementation: https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/transforms/ProjectionUtil.java#L275
869    fn adjust_time_projection(
870        &self,
871        op: &PredicateOperator,
872        original: &Datum,
873        transformed: &Datum,
874    ) -> Option<AdjustedProjection> {
875        let should_adjust = match self {
876            Transform::Day => matches!(
877                original.data_type(),
878                PrimitiveType::Timestamp
879                    | PrimitiveType::Timestamptz
880                    | PrimitiveType::TimestampNs
881                    | PrimitiveType::TimestamptzNs
882            ),
883            Transform::Year | Transform::Month => true,
884            _ => false,
885        };
886
887        if should_adjust && let &PrimitiveLiteral::Int(v) = transformed.literal() {
888            match op {
889                PredicateOperator::LessThan
890                | PredicateOperator::LessThanOrEq
891                | PredicateOperator::In => {
892                    if v < 0 {
893                        // # TODO
894                        // An ugly hack to fix. Refine the increment and decrement logic later.
895                        match self {
896                            Transform::Day => {
897                                return Some(AdjustedProjection::Single(Datum::date(v + 1)));
898                            }
899                            _ => {
900                                return Some(AdjustedProjection::Single(Datum::int(v + 1)));
901                            }
902                        }
903                    };
904                }
905                PredicateOperator::Eq => {
906                    if v < 0 {
907                        let new_set = FnvHashSet::from_iter(vec![
908                            transformed.to_owned(),
909                            // # TODO
910                            // An ugly hack to fix. Refine the increment and decrement logic later.
911                            {
912                                match self {
913                                    Transform::Day => Datum::date(v + 1),
914                                    _ => Datum::int(v + 1),
915                                }
916                            },
917                        ]);
918                        return Some(AdjustedProjection::Set(new_set));
919                    }
920                }
921                _ => {
922                    return None;
923                }
924            }
925        };
926        None
927    }
928
929    // Increment for Int, Long, Decimal, Date, Timestamp
930    // Ignore other types
931    #[inline]
932    fn try_increment_number(datum: &Datum) -> Result<Datum> {
933        match (datum.data_type(), datum.literal()) {
934            (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Ok(Datum::int(v + 1)),
935            (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Ok(Datum::long(v + 1)),
936            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
937                Datum::decimal(decimal_from_i128_with_scale(v + 1, 0))
938            }
939            (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Ok(Datum::date(v + 1)),
940            (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
941                Ok(Datum::timestamp_micros(v + 1))
942            }
943            (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
944                Ok(Datum::timestamp_nanos(v + 1))
945            }
946            (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
947                Ok(Datum::timestamptz_micros(v + 1))
948            }
949            (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
950                Ok(Datum::timestamptz_nanos(v + 1))
951            }
952            (PrimitiveType::Int, _)
953            | (PrimitiveType::Long, _)
954            | (PrimitiveType::Decimal { .. }, _)
955            | (PrimitiveType::Date, _)
956            | (PrimitiveType::Timestamp, _) => Err(Error::new(
957                ErrorKind::Unexpected,
958                format!(
959                    "Unsupported literal increment for type: {:?}",
960                    datum.data_type()
961                ),
962            )),
963            _ => Ok(datum.to_owned()),
964        }
965    }
966
967    // Decrement for Int, Long, Decimal, Date, Timestamp
968    // Ignore other types
969    #[inline]
970    fn try_decrement_number(datum: &Datum) -> Result<Datum> {
971        match (datum.data_type(), datum.literal()) {
972            (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Ok(Datum::int(v - 1)),
973            (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => Ok(Datum::long(v - 1)),
974            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
975                Datum::decimal(decimal_from_i128_with_scale(v - 1, 0))
976            }
977            (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => Ok(Datum::date(v - 1)),
978            (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
979                Ok(Datum::timestamp_micros(v - 1))
980            }
981            (PrimitiveType::TimestampNs, PrimitiveLiteral::Long(v)) => {
982                Ok(Datum::timestamp_nanos(v - 1))
983            }
984            (PrimitiveType::Timestamptz, PrimitiveLiteral::Long(v)) => {
985                Ok(Datum::timestamptz_micros(v - 1))
986            }
987            (PrimitiveType::TimestamptzNs, PrimitiveLiteral::Long(v)) => {
988                Ok(Datum::timestamptz_nanos(v - 1))
989            }
990            (PrimitiveType::Int, _)
991            | (PrimitiveType::Long, _)
992            | (PrimitiveType::Decimal { .. }, _)
993            | (PrimitiveType::Date, _)
994            | (PrimitiveType::Timestamp, _) => Err(Error::new(
995                ErrorKind::Unexpected,
996                format!(
997                    "Unsupported literal decrement for type: {:?}",
998                    datum.data_type()
999                ),
1000            )),
1001            _ => Ok(datum.to_owned()),
1002        }
1003    }
1004
1005    fn truncate_number_strict(
1006        &self,
1007        name: &str,
1008        expr: &BinaryExpression<BoundReference>,
1009        func: &BoxedTransformFunction,
1010    ) -> Result<Option<Predicate>> {
1011        let boundary = expr.literal();
1012
1013        if !matches!(
1014            boundary.data_type(),
1015            &PrimitiveType::Int
1016                | &PrimitiveType::Long
1017                | &PrimitiveType::Decimal { .. }
1018                | &PrimitiveType::Date
1019                | &PrimitiveType::Timestamp
1020                | &PrimitiveType::Timestamptz
1021                | &PrimitiveType::TimestampNs
1022                | &PrimitiveType::TimestamptzNs
1023        ) {
1024            return Err(invalid_data!(
1025                "Expected a numeric literal, got: {boundary:?}"
1026            ));
1027        }
1028
1029        let predicate = match expr.op() {
1030            PredicateOperator::LessThan => Some(Predicate::Binary(BinaryExpression::new(
1031                PredicateOperator::LessThan,
1032                Reference::new(name),
1033                func.transform_literal_result(boundary)?,
1034            ))),
1035            PredicateOperator::LessThanOrEq => Some(Predicate::Binary(BinaryExpression::new(
1036                PredicateOperator::LessThan,
1037                Reference::new(name),
1038                func.transform_literal_result(&Self::try_increment_number(boundary)?)?,
1039            ))),
1040            PredicateOperator::GreaterThan => Some(Predicate::Binary(BinaryExpression::new(
1041                PredicateOperator::GreaterThan,
1042                Reference::new(name),
1043                func.transform_literal_result(boundary)?,
1044            ))),
1045            PredicateOperator::GreaterThanOrEq => Some(Predicate::Binary(BinaryExpression::new(
1046                PredicateOperator::GreaterThan,
1047                Reference::new(name),
1048                func.transform_literal_result(&Self::try_decrement_number(boundary)?)?,
1049            ))),
1050            PredicateOperator::NotEq => Some(Predicate::Binary(BinaryExpression::new(
1051                PredicateOperator::NotEq,
1052                Reference::new(name),
1053                func.transform_literal_result(boundary)?,
1054            ))),
1055            _ => None,
1056        };
1057
1058        Ok(predicate)
1059    }
1060
1061    fn truncate_array_strict(
1062        &self,
1063        name: &str,
1064        expr: &BinaryExpression<BoundReference>,
1065        func: &BoxedTransformFunction,
1066    ) -> Result<Option<Predicate>> {
1067        let boundary = expr.literal();
1068
1069        match expr.op() {
1070            PredicateOperator::LessThan | PredicateOperator::LessThanOrEq => {
1071                Ok(Some(Predicate::Binary(BinaryExpression::new(
1072                    PredicateOperator::LessThan,
1073                    Reference::new(name),
1074                    func.transform_literal_result(boundary)?,
1075                ))))
1076            }
1077            PredicateOperator::GreaterThan | PredicateOperator::GreaterThanOrEq => {
1078                Ok(Some(Predicate::Binary(BinaryExpression::new(
1079                    PredicateOperator::GreaterThan,
1080                    Reference::new(name),
1081                    func.transform_literal_result(boundary)?,
1082                ))))
1083            }
1084            PredicateOperator::NotEq => Ok(Some(Predicate::Binary(BinaryExpression::new(
1085                PredicateOperator::NotEq,
1086                Reference::new(name),
1087                func.transform_literal_result(boundary)?,
1088            )))),
1089            _ => Ok(None),
1090        }
1091    }
1092}
1093
1094impl Display for Transform {
1095    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1096        match self {
1097            Transform::Identity => write!(f, "identity"),
1098            Transform::Year => write!(f, "year"),
1099            Transform::Month => write!(f, "month"),
1100            Transform::Day => write!(f, "day"),
1101            Transform::Hour => write!(f, "hour"),
1102            Transform::Void => write!(f, "void"),
1103            Transform::Bucket(length) => write!(f, "bucket[{length}]"),
1104            Transform::Truncate(width) => write!(f, "truncate[{width}]"),
1105            Transform::Unknown => write!(f, "unknown"),
1106        }
1107    }
1108}
1109
1110impl FromStr for Transform {
1111    type Err = Error;
1112
1113    fn from_str(s: &str) -> Result<Self> {
1114        let t = match s {
1115            "identity" => Transform::Identity,
1116            "year" => Transform::Year,
1117            "month" => Transform::Month,
1118            "day" => Transform::Day,
1119            "hour" => Transform::Hour,
1120            "void" => Transform::Void,
1121            "unknown" => Transform::Unknown,
1122            v if v.starts_with("bucket") => {
1123                let length = v
1124                    .strip_prefix("bucket")
1125                    .expect("transform must starts with `bucket`")
1126                    .trim_start_matches('[')
1127                    .trim_end_matches(']')
1128                    .parse()
1129                    .map_err(|err| {
1130                        invalid_data!("transform bucket type {v:?} is invalid").with_source(err)
1131                    })?;
1132
1133                Transform::Bucket(length)
1134            }
1135            v if v.starts_with("truncate") => {
1136                let width = v
1137                    .strip_prefix("truncate")
1138                    .expect("transform must starts with `truncate`")
1139                    .trim_start_matches('[')
1140                    .trim_end_matches(']')
1141                    .parse()
1142                    .map_err(|err| {
1143                        invalid_data!("transform truncate type {v:?} is invalid").with_source(err)
1144                    })?;
1145
1146                Transform::Truncate(width)
1147            }
1148            v => {
1149                return Err(invalid_data!("transform {v:?} is invalid"));
1150            }
1151        };
1152
1153        Ok(t)
1154    }
1155}
1156
1157impl Serialize for Transform {
1158    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1159    where S: Serializer {
1160        serializer.serialize_str(format!("{self}").as_str())
1161    }
1162}
1163
1164impl<'de> Deserialize<'de> for Transform {
1165    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1166    where D: Deserializer<'de> {
1167        let s = String::deserialize(deserializer)?;
1168        s.parse().map_err(<D::Error as serde::de::Error>::custom)
1169    }
1170}
1171
1172/// An enum representing the result of the adjusted projection.
1173/// Either being a single adjusted datum or a set.
1174#[derive(Debug)]
1175enum AdjustedProjection {
1176    Single(Datum),
1177    Set(FnvHashSet<Datum>),
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182    use super::*;
1183
1184    fn check_boundary(op: PredicateOperator, input: Datum, expected: Datum) {
1185        let result = Transform::adjust_boundary(&op, &input).unwrap().unwrap();
1186        assert_eq!(result, expected);
1187    }
1188
1189    #[test]
1190    fn test_adjust_boundary_timestamp_types() {
1191        for (datum, dec, inc) in [
1192            (
1193                Datum::timestamptz_micros(1000),
1194                Datum::timestamptz_micros(999),
1195                Datum::timestamptz_micros(1001),
1196            ),
1197            (
1198                Datum::timestamp_nanos(5000),
1199                Datum::timestamp_nanos(4999),
1200                Datum::timestamp_nanos(5001),
1201            ),
1202            (
1203                Datum::timestamptz_nanos(5000),
1204                Datum::timestamptz_nanos(4999),
1205                Datum::timestamptz_nanos(5001),
1206            ),
1207        ] {
1208            check_boundary(PredicateOperator::LessThan, datum.clone(), dec);
1209            check_boundary(PredicateOperator::GreaterThan, datum.clone(), inc);
1210            check_boundary(
1211                PredicateOperator::LessThanOrEq,
1212                datum.clone(),
1213                datum.clone(),
1214            );
1215            check_boundary(PredicateOperator::GreaterThanOrEq, datum.clone(), datum);
1216        }
1217    }
1218
1219    /// Renders `ordinal` through the public API with the given declared type.
1220    fn human(transform: Transform, primitive: PrimitiveType, ordinal: i32) -> String {
1221        transform.to_human_string(&Type::Primitive(primitive), Some(&Literal::int(ordinal)))
1222    }
1223
1224    /// Renders `ordinal` for a transform whose result type is `int`.
1225    fn human_int(transform: Transform, ordinal: i32) -> String {
1226        human(transform, PrimitiveType::Int, ordinal)
1227    }
1228
1229    #[test]
1230    fn test_to_human_string_year() {
1231        assert_eq!(human_int(Transform::Year, -1970), "0000");
1232        assert_eq!(human_int(Transform::Year, -1), "1969");
1233        assert_eq!(human_int(Transform::Year, 0), "1970");
1234        assert_eq!(human_int(Transform::Year, 47), "2017");
1235    }
1236
1237    #[test]
1238    fn test_to_human_string_month() {
1239        assert_eq!(human_int(Transform::Month, -1970 * 12), "0000-01");
1240        assert_eq!(human_int(Transform::Month, -13), "1968-12");
1241        assert_eq!(human_int(Transform::Month, -12), "1969-01");
1242        assert_eq!(human_int(Transform::Month, -1), "1969-12");
1243        assert_eq!(human_int(Transform::Month, 0), "1970-01");
1244        assert_eq!(human_int(Transform::Month, 11), "1970-12");
1245        assert_eq!(human_int(Transform::Month, 12), "1971-01");
1246        assert_eq!(human_int(Transform::Month, 569), "2017-06");
1247    }
1248
1249    #[test]
1250    fn test_to_human_string_day() {
1251        assert_eq!(human_int(Transform::Day, -1), "1969-12-31");
1252        assert_eq!(human_int(Transform::Day, 0), "1970-01-01");
1253        assert_eq!(human_int(Transform::Day, 31), "1970-02-01");
1254        assert_eq!(human_int(Transform::Day, 17332), "2017-06-15");
1255    }
1256
1257    #[test]
1258    fn test_to_human_string_hour() {
1259        assert_eq!(human_int(Transform::Hour, -24), "1969-12-31-00");
1260        assert_eq!(human_int(Transform::Hour, -1), "1969-12-31-23");
1261        assert_eq!(human_int(Transform::Hour, 0), "1970-01-01-00");
1262        assert_eq!(human_int(Transform::Hour, 23), "1970-01-01-23");
1263        assert_eq!(human_int(Transform::Hour, 24), "1970-01-02-00");
1264        assert_eq!(human_int(Transform::Hour, 1000), "1970-02-11-16");
1265        assert_eq!(human_int(Transform::Hour, 415984), "2017-06-15-16");
1266    }
1267
1268    /// The temporal transforms ignore the declared field type, matching the Java
1269    /// signatures `toHumanString(Type alwaysInt, ..)` and
1270    /// `toHumanString(Type alwaysDate, ..)`.
1271    #[test]
1272    fn test_to_human_string_ignores_declared_type_for_temporal_transforms() {
1273        assert_eq!(human(Transform::Year, PrimitiveType::Date, 47), "2017");
1274        assert_eq!(human(Transform::Month, PrimitiveType::Date, 569), "2017-06");
1275        assert_eq!(
1276            human(Transform::Day, PrimitiveType::Int, 17332),
1277            "2017-06-15"
1278        );
1279        assert_eq!(
1280            human(Transform::Hour, PrimitiveType::Date, 415984),
1281            "2017-06-15-16"
1282        );
1283    }
1284
1285    /// Transforms with no temporal format keep deferring to the datum, which
1286    /// renders according to the declared field type.
1287    #[test]
1288    fn test_to_human_string_defers_to_datum_for_other_transforms() {
1289        assert_eq!(
1290            human(Transform::Identity, PrimitiveType::Int, 17332),
1291            "17332"
1292        );
1293        assert_eq!(
1294            human(Transform::Identity, PrimitiveType::Date, 17332),
1295            "2017-06-15"
1296        );
1297        assert_eq!(human(Transform::Bucket(16), PrimitiveType::Int, 5), "5");
1298
1299        // A literal that is not an `int` also defers to the datum rather than being
1300        // reported as `null`, so the temporal arms add no second behaviour change.
1301        assert_eq!(
1302            Transform::Year.to_human_string(
1303                &Type::Primitive(PrimitiveType::String),
1304                Some(&Literal::string("unformatted"))
1305            ),
1306            "unformatted"
1307        );
1308    }
1309
1310    #[test]
1311    fn test_to_human_string_null_cases() {
1312        assert_eq!(human(Transform::Void, PrimitiveType::Int, 47), "null");
1313        assert_eq!(human(Transform::Void, PrimitiveType::Date, 17332), "null");
1314        for transform in [
1315            Transform::Year,
1316            Transform::Month,
1317            Transform::Day,
1318            Transform::Hour,
1319            Transform::Identity,
1320            Transform::Void,
1321        ] {
1322            assert_eq!(
1323                transform.to_human_string(&Type::Primitive(PrimitiveType::Int), None),
1324                "null"
1325            );
1326        }
1327    }
1328}