iceberg/spec/values/primitive.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//! Primitive literal types
19
20use std::cmp::Ordering;
21
22use ordered_float::{FloatCore, OrderedFloat};
23
24use crate::spec::PrimitiveType;
25
26/// Values present in iceberg type
27///
28/// `Float` and `Double` compare as Java's `Float.compare` and `Double.compare` do: `-0.0` is
29/// less than `0.0`, and every NaN is the same value, greater than all others. iceberg-java
30/// compares partition values this way, so `-0.0` and `0.0` are different partitions. The
31/// [spec](https://iceberg.apache.org/spec/#scan-planning) states the same rule: floating point
32/// partition values are equal if their IEEE 754 bit layouts are equal, with NaNs normalized.
33// The derived `Hash` gives `-0.0` and `0.0` the same hash. That is coarser than `eq`, which is
34// allowed: equal values still hash alike.
35#[allow(clippy::derived_hash_with_manual_eq)]
36#[derive(Clone, Debug, Hash, Eq)]
37pub enum PrimitiveLiteral {
38 /// 0x00 for false, non-zero byte for true
39 Boolean(bool),
40 /// Stored as 4-byte little-endian
41 Int(i32),
42 /// Stored as 8-byte little-endian
43 Long(i64),
44 /// Stored as 4-byte little-endian
45 Float(OrderedFloat<f32>),
46 /// Stored as 8-byte little-endian
47 Double(OrderedFloat<f64>),
48 /// UTF-8 bytes (without length)
49 String(String),
50 /// Binary value (without length)
51 Binary(Vec<u8>),
52 /// Stored as 16-byte big-endian
53 Int128(i128),
54 /// Stored as 16-byte big-endian
55 UInt128(u128),
56 /// When a number is larger than it can hold
57 AboveMax,
58 /// When a number is smaller than it can hold
59 BelowMin,
60}
61
62impl PartialEq for PrimitiveLiteral {
63 fn eq(&self, other: &Self) -> bool {
64 self.partial_cmp(other) == Some(Ordering::Equal)
65 }
66}
67
68impl PartialOrd for PrimitiveLiteral {
69 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
70 match (self, other) {
71 (Self::Boolean(a), Self::Boolean(b)) => a.partial_cmp(b),
72 (Self::Int(a), Self::Int(b)) => a.partial_cmp(b),
73 (Self::Long(a), Self::Long(b)) => a.partial_cmp(b),
74 (Self::Float(a), Self::Float(b)) => Some(float_cmp(a, b)),
75 (Self::Double(a), Self::Double(b)) => Some(float_cmp(a, b)),
76 (Self::String(a), Self::String(b)) => a.partial_cmp(b),
77 (Self::Binary(a), Self::Binary(b)) => a.partial_cmp(b),
78 (Self::Int128(a), Self::Int128(b)) => a.partial_cmp(b),
79 (Self::UInt128(a), Self::UInt128(b)) => a.partial_cmp(b),
80 (Self::AboveMax, Self::AboveMax) | (Self::BelowMin, Self::BelowMin) => {
81 Some(Ordering::Equal)
82 }
83 // Different variants order by declaration, as the derived impl did. A variant that
84 // lacks an arm above lands here against itself; fail closed instead of calling the
85 // two values equal.
86 _ => {
87 debug_assert_ne!(std::mem::discriminant(self), std::mem::discriminant(other));
88 match self.variant_index().cmp(&other.variant_index()) {
89 Ordering::Equal => None,
90 ordering => Some(ordering),
91 }
92 }
93 }
94 }
95}
96
97/// Compares floats as Java's `Float.compare` and `Double.compare` do. `OrderedFloat` already
98/// treats every NaN as one value above all others; it only lacks `-0.0` before `0.0`.
99fn float_cmp<T: FloatCore>(a: &OrderedFloat<T>, b: &OrderedFloat<T>) -> Ordering {
100 a.cmp(b).then_with(|| {
101 if a.is_nan() {
102 Ordering::Equal
103 } else {
104 b.is_sign_negative().cmp(&a.is_sign_negative())
105 }
106 })
107}
108
109impl PrimitiveLiteral {
110 /// Widens this literal to its promotion ceiling (`int` -> `long`, `float` ->
111 /// `double`), returning `None` for any other literal. Used when the target type
112 /// is unknown (the partition source column was dropped) so a value and its
113 /// deletes still normalize to the same type.
114 pub(crate) fn widen(&self) -> Option<PrimitiveLiteral> {
115 match self {
116 PrimitiveLiteral::Int(value) => Some(PrimitiveLiteral::Long(i64::from(*value))),
117 PrimitiveLiteral::Float(value) => {
118 Some(PrimitiveLiteral::Double(OrderedFloat(f64::from(value.0))))
119 }
120 _ => None,
121 }
122 }
123
124 /// Widens this literal to `target` for the value promotions `int` -> `long`
125 /// and `float` -> `double`, returning `None` when `target` does not promote it.
126 pub(crate) fn promote_to(&self, target: &PrimitiveType) -> Option<PrimitiveLiteral> {
127 match (self, target) {
128 (PrimitiveLiteral::Int(_), PrimitiveType::Long)
129 | (PrimitiveLiteral::Float(_), PrimitiveType::Double) => self.widen(),
130 _ => None,
131 }
132 }
133
134 /// Must follow the declaration order of the variants: `partial_cmp` uses it to order
135 /// different variants the way the derived `PartialOrd` did.
136 fn variant_index(&self) -> u8 {
137 match self {
138 Self::Boolean(_) => 0,
139 Self::Int(_) => 1,
140 Self::Long(_) => 2,
141 Self::Float(_) => 3,
142 Self::Double(_) => 4,
143 Self::String(_) => 5,
144 Self::Binary(_) => 6,
145 Self::Int128(_) => 7,
146 Self::UInt128(_) => 8,
147 Self::AboveMax => 9,
148 Self::BelowMin => 10,
149 }
150 }
151
152 /// Returns true if the Literal represents a primitive type
153 /// that can be a NaN, and that it's value is NaN
154 pub fn is_nan(&self) -> bool {
155 match self {
156 PrimitiveLiteral::Double(val) => val.is_nan(),
157 PrimitiveLiteral::Float(val) => val.is_nan(),
158 _ => false,
159 }
160 }
161}