llkv_expr/
literal.rs

1//! Untyped literal values plus helpers for converting them into native types.
2//!
3//! Literals capture query parameters before a table knows the concrete Arrow
4//! type of each column. Conversion helpers here defer type checking until the
5//! caller can perform schema-aware coercion.
6use arrow::datatypes::ArrowPrimitiveType;
7use std::ops::Bound;
8
9/// A literal value that has not yet been coerced into a specific native
10/// type. This allows for type inference to be deferred until the column
11/// type is known.
12#[derive(Debug, Clone, PartialEq)]
13pub enum Literal {
14    Null,
15    Integer(i128),
16    Float(f64),
17    String(String),
18    Boolean(bool),
19    /// Struct literal with field names and nested literals
20    Struct(Vec<(String, Box<Literal>)>),
21    // Other types like Bool, Bytes can be added here.
22}
23
24macro_rules! impl_from_for_literal {
25    ($variant:ident, $($t:ty),*) => {
26        $(
27            impl From<$t> for Literal {
28                fn from(v: $t) -> Self {
29                    Literal::$variant(v.into())
30                }
31            }
32        )*
33    };
34}
35
36impl_from_for_literal!(Integer, i8, i16, i32, i64, i128, u8, u16, u32, u64);
37impl_from_for_literal!(Float, f32, f64);
38
39impl From<&str> for Literal {
40    fn from(v: &str) -> Self {
41        Literal::String(v.to_string())
42    }
43}
44
45impl From<bool> for Literal {
46    fn from(v: bool) -> Self {
47        Literal::Boolean(v)
48    }
49}
50
51/// Error converting a `Literal` into a concrete native type.
52#[derive(Debug, Clone, PartialEq)]
53pub enum LiteralCastError {
54    /// Tried to coerce a non-integer literal into an integer native type.
55    TypeMismatch {
56        expected: &'static str,
57        got: &'static str,
58    },
59    /// Integer value does not fit in the destination type.
60    OutOfRange { target: &'static str, value: i128 },
61    /// Float value does not fit in the destination type.
62    FloatOutOfRange { target: &'static str, value: f64 },
63}
64
65impl std::fmt::Display for LiteralCastError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            LiteralCastError::TypeMismatch { expected, got } => {
69                write!(f, "expected {}, got {}", expected, got)
70            }
71            LiteralCastError::OutOfRange { target, value } => {
72                write!(f, "value {} out of range for {}", value, target)
73            }
74            LiteralCastError::FloatOutOfRange { target, value } => {
75                write!(f, "value {} out of range for {}", value, target)
76            }
77        }
78    }
79}
80
81impl std::error::Error for LiteralCastError {}
82
83/// Helper trait implemented for primitive types that can be produced from a `Literal`.
84pub trait FromLiteral: Sized {
85    fn from_literal(lit: &Literal) -> Result<Self, LiteralCastError>;
86}
87
88macro_rules! impl_from_literal_int {
89    ($($ty:ty),* $(,)?) => {
90        $(
91            impl FromLiteral for $ty {
92                fn from_literal(lit: &Literal) -> Result<Self, LiteralCastError> {
93                    match lit {
94                        Literal::Integer(i) => <$ty>::try_from(*i).map_err(|_| {
95                            LiteralCastError::OutOfRange {
96                                target: std::any::type_name::<$ty>(),
97                                value: *i,
98                            }
99                        }),
100                        Literal::Float(_) => Err(LiteralCastError::TypeMismatch {
101                            expected: "integer",
102                            got: "float",
103                        }),
104                        Literal::Boolean(_) => Err(LiteralCastError::TypeMismatch {
105                            expected: "integer",
106                            got: "boolean",
107                        }),
108                        Literal::String(_) => Err(LiteralCastError::TypeMismatch {
109                            expected: "integer",
110                            got: "string",
111                        }),
112                        Literal::Struct(_) => Err(LiteralCastError::TypeMismatch {
113                            expected: "integer",
114                            got: "struct",
115                        }),
116                        Literal::Null => Err(LiteralCastError::TypeMismatch {
117                            expected: "integer",
118                            got: "null",
119                        }),
120                    }
121                }
122            }
123        )*
124    };
125}
126
127impl_from_literal_int!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, usize);
128
129impl FromLiteral for f32 {
130    fn from_literal(lit: &Literal) -> Result<Self, LiteralCastError> {
131        let value = match lit {
132            Literal::Float(f) => *f,
133            Literal::Integer(i) => *i as f64,
134            Literal::Boolean(_) => {
135                return Err(LiteralCastError::TypeMismatch {
136                    expected: "float",
137                    got: "boolean",
138                });
139            }
140            Literal::String(_) => {
141                return Err(LiteralCastError::TypeMismatch {
142                    expected: "float",
143                    got: "string",
144                });
145            }
146            Literal::Struct(_) => {
147                return Err(LiteralCastError::TypeMismatch {
148                    expected: "float",
149                    got: "struct",
150                });
151            }
152            Literal::Null => {
153                return Err(LiteralCastError::TypeMismatch {
154                    expected: "float",
155                    got: "null",
156                });
157            }
158        };
159        let cast = value as f32;
160        if value.is_finite() && !cast.is_finite() {
161            return Err(LiteralCastError::FloatOutOfRange {
162                target: "f32",
163                value,
164            });
165        }
166        Ok(cast)
167    }
168}
169
170impl FromLiteral for bool {
171    fn from_literal(lit: &Literal) -> Result<Self, LiteralCastError> {
172        match lit {
173            Literal::Boolean(b) => Ok(*b),
174            Literal::Integer(i) => match *i {
175                0 => Ok(false),
176                1 => Ok(true),
177                value => Err(LiteralCastError::OutOfRange {
178                    target: "bool",
179                    value,
180                }),
181            },
182            Literal::Float(_) => Err(LiteralCastError::TypeMismatch {
183                expected: "bool",
184                got: "float",
185            }),
186            Literal::String(s) => {
187                let normalized = s.trim().to_ascii_lowercase();
188                match normalized.as_str() {
189                    "true" | "t" | "1" => Ok(true),
190                    "false" | "f" | "0" => Ok(false),
191                    _ => Err(LiteralCastError::TypeMismatch {
192                        expected: "bool",
193                        got: "string",
194                    }),
195                }
196            }
197            Literal::Struct(_) => Err(LiteralCastError::TypeMismatch {
198                expected: "bool",
199                got: "struct",
200            }),
201            Literal::Null => Err(LiteralCastError::TypeMismatch {
202                expected: "bool",
203                got: "null",
204            }),
205        }
206    }
207}
208
209impl FromLiteral for f64 {
210    fn from_literal(lit: &Literal) -> Result<Self, LiteralCastError> {
211        match lit {
212            Literal::Float(f) => Ok(*f),
213            Literal::Integer(i) => Ok(*i as f64),
214            Literal::Boolean(_) => Err(LiteralCastError::TypeMismatch {
215                expected: "float",
216                got: "boolean",
217            }),
218            Literal::String(_) => Err(LiteralCastError::TypeMismatch {
219                expected: "float",
220                got: "string",
221            }),
222            Literal::Struct(_) => Err(LiteralCastError::TypeMismatch {
223                expected: "float",
224                got: "struct",
225            }),
226            Literal::Null => Err(LiteralCastError::TypeMismatch {
227                expected: "float",
228                got: "null",
229            }),
230        }
231    }
232}
233
234fn literal_type_name(lit: &Literal) -> &'static str {
235    match lit {
236        Literal::Integer(_) => "integer",
237        Literal::Float(_) => "float",
238        Literal::String(_) => "string",
239        Literal::Boolean(_) => "boolean",
240        Literal::Null => "null",
241        Literal::Struct(_) => "struct",
242    }
243}
244
245/// Convert a `Literal` into an owned `String`.
246pub fn literal_to_string(lit: &Literal) -> Result<String, LiteralCastError> {
247    match lit {
248        Literal::String(s) => Ok(s.clone()),
249        Literal::Null => Err(LiteralCastError::TypeMismatch {
250            expected: "string",
251            got: "null",
252        }),
253        _ => Err(LiteralCastError::TypeMismatch {
254            expected: "string",
255            got: literal_type_name(lit),
256        }),
257    }
258}
259
260/// Convert a `Literal` into a concrete native type `T`.
261pub fn literal_to_native<T>(lit: &Literal) -> Result<T, LiteralCastError>
262where
263    T: FromLiteral + Copy + 'static,
264{
265    T::from_literal(lit)
266}
267
268/// Convert a bound of `Literal` into a bound of `T::Native`.
269///
270/// Kept generic over `T: ArrowPrimitiveType` so callers (like the table
271/// crate) can use `bound_to_native::<T>()` with `T::Native` inferred.
272/// Error type is `LiteralCastError` since this crate is independent of
273/// table-layer errors.
274pub fn bound_to_native<T>(bound: &Bound<Literal>) -> Result<Bound<T::Native>, LiteralCastError>
275where
276    T: ArrowPrimitiveType,
277    T::Native: FromLiteral + Copy,
278{
279    Ok(match bound {
280        Bound::Unbounded => Bound::Unbounded,
281        Bound::Included(l) => Bound::Included(literal_to_native::<T::Native>(l)?),
282        Bound::Excluded(l) => Bound::Excluded(literal_to_native::<T::Native>(l)?),
283    })
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn boolean_literal_roundtrip() {
292        let lit = Literal::from(true);
293        assert_eq!(lit, Literal::Boolean(true));
294        assert!(literal_to_native::<bool>(&lit).unwrap());
295        assert!(!literal_to_native::<bool>(&Literal::Boolean(false)).unwrap());
296    }
297
298    #[test]
299    fn boolean_literal_rejects_integer_cast() {
300        let lit = Literal::Boolean(true);
301        let err = literal_to_native::<i32>(&lit).unwrap_err();
302        assert!(matches!(
303            err,
304            LiteralCastError::TypeMismatch {
305                expected: "integer",
306                got: "boolean",
307            }
308        ));
309    }
310}