Skip to main content

timelake_format/
bounds.rs

1//! Iceberg single-value serialization of column bounds (manifest `lower_bounds`/`upper_bounds`).
2//! <https://iceberg.apache.org/spec/#binary-single-value-serialization>
3
4/// A decoded bound. Timestamps and dates keep Iceberg's integer encoding (µs / days since epoch).
5#[derive(Debug, Clone, PartialEq, PartialOrd)]
6pub enum Bound {
7    /// `boolean`.
8    Bool(bool),
9    /// `int`, `long`, `date`, `time` and timestamps.
10    Int(i64),
11    /// `float` and `double`.
12    Float(f64),
13    /// `string`.
14    Str(String),
15}
16
17/// Decodes a bound for an Iceberg primitive type name (`int`, `long`, `timestamptz`, `double`, …).
18/// Unsupported types return `None` (and are then simply not used for pruning).
19pub fn decode(iceberg_type: &str, bytes: &[u8]) -> Option<Bound> {
20    let le4 = || bytes.first_chunk::<4>().copied();
21    let le8 = || bytes.first_chunk::<8>().copied();
22    Some(match iceberg_type {
23        "boolean" => Bound::Bool(*bytes.first()? != 0),
24        "int" | "date" => Bound::Int(i32::from_le_bytes(le4()?).into()),
25        "long" | "time" | "timestamp" | "timestamptz" | "timestamp_ns" | "timestamptz_ns" => {
26            Bound::Int(i64::from_le_bytes(le8()?))
27        }
28        "float" => Bound::Float(f32::from_le_bytes(le4()?).into()),
29        "double" => Bound::Float(f64::from_le_bytes(le8()?)),
30        "string" => Bound::Str(String::from_utf8(bytes.to_vec()).ok()?),
31        _ => return None,
32    })
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    #[test]
40    fn decodes_primitives() {
41        assert_eq!(decode("int", &7i32.to_le_bytes()), Some(Bound::Int(7)));
42        assert_eq!(decode("date", &(-1i32).to_le_bytes()), Some(Bound::Int(-1)));
43        assert_eq!(
44            decode("timestamptz", &1_700_000_000_000_000i64.to_le_bytes()),
45            Some(Bound::Int(1_700_000_000_000_000))
46        );
47        assert_eq!(decode("double", &1.5f64.to_le_bytes()), Some(Bound::Float(1.5)));
48        assert_eq!(decode("float", &0.5f32.to_le_bytes()), Some(Bound::Float(0.5)));
49        assert_eq!(decode("boolean", &[1]), Some(Bound::Bool(true)));
50        assert_eq!(decode("string", b"truck_1"), Some(Bound::Str("truck_1".into())));
51        assert_eq!(decode("decimal(10,2)", &[0]), None);
52    }
53
54    #[test]
55    fn short_input_is_none() {
56        assert_eq!(decode("long", &[1, 2, 3]), None);
57        assert_eq!(decode("int", &[]), None);
58        assert_eq!(decode("boolean", &[]), None);
59        assert_eq!(decode("string", &[0xff]), None);
60    }
61}