Skip to main content

alopex_sql/planner/
types.rs

1//! Type definitions for the Alopex SQL planner.
2//!
3//! This module defines [`ResolvedType`], the normalized type representation
4//! used during type checking and planning phases.
5
6use crate::ast::ddl::{DataType, VectorMetric};
7
8/// Normalized type information for type checking.
9///
10/// This enum represents the resolved type after normalization from AST types.
11/// For example, `INTEGER` and `INT` both resolve to [`ResolvedType::Integer`].
12///
13/// # Examples
14///
15/// ```
16/// use alopex_sql::planner::types::ResolvedType;
17/// use alopex_sql::ast::ddl::{DataType, VectorMetric};
18///
19/// // Convert from AST DataType
20/// let int_type = ResolvedType::from_ast(&DataType::Integer);
21/// assert_eq!(int_type, ResolvedType::Integer);
22///
23/// // VECTOR with omitted metric defaults to Cosine
24/// let vec_type = ResolvedType::from_ast(&DataType::Vector { dimension: 128, metric: None });
25/// assert_eq!(vec_type, ResolvedType::Vector { dimension: 128, metric: VectorMetric::Cosine });
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ResolvedType {
29    /// Integer type (INTEGER, INT → Integer)
30    Integer,
31    /// Big integer type
32    BigInt,
33    /// Single-precision floating point
34    Float,
35    /// Double-precision floating point
36    Double,
37    /// Text/string type
38    Text,
39    /// Binary data type
40    Blob,
41    /// Boolean type (BOOLEAN, BOOL → Boolean)
42    Boolean,
43    /// Timestamp type
44    Timestamp,
45    /// Calendar date without a time zone, stored as days from 1970-01-01.
46    Date,
47    /// Time of day without a time zone, stored as microseconds after midnight.
48    Time,
49    /// Calendar interval stored as independent month, day, and microsecond parts.
50    Interval,
51    /// Exact fixed-precision decimal with up to 38 digits.
52    Decimal { precision: u8, scale: u8 },
53    /// Canonical native JSON value (`JSONB` is a dialect alias).
54    Json,
55    /// Variable-length homogeneous nested values (`LIST` is a dialect alias).
56    Array(Box<ResolvedType>),
57    /// Ordered key/value entries with homogeneous key and value types.
58    Map {
59        key: Box<ResolvedType>,
60        value: Box<ResolvedType>,
61    },
62    /// Named heterogeneous nested fields.
63    Struct(Vec<(String, ResolvedType)>),
64    /// Vector type with dimension and metric
65    /// Metric is always populated (defaults to Cosine if omitted in AST)
66    Vector {
67        dimension: u32,
68        metric: VectorMetric,
69    },
70    /// NULL type (for NULL literals)
71    Null,
72}
73
74impl ResolvedType {
75    /// Convert from AST [`DataType`] to [`ResolvedType`].
76    ///
77    /// For `VECTOR` types, if metric is omitted in the AST, it defaults to `Cosine`.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use alopex_sql::planner::types::ResolvedType;
83    /// use alopex_sql::ast::ddl::{DataType, VectorMetric};
84    ///
85    /// // INTEGER and INT both resolve to Integer
86    /// assert_eq!(ResolvedType::from_ast(&DataType::Integer), ResolvedType::Integer);
87    /// assert_eq!(ResolvedType::from_ast(&DataType::Int), ResolvedType::Integer);
88    ///
89    /// // BOOLEAN and BOOL both resolve to Boolean
90    /// assert_eq!(ResolvedType::from_ast(&DataType::Boolean), ResolvedType::Boolean);
91    /// assert_eq!(ResolvedType::from_ast(&DataType::Bool), ResolvedType::Boolean);
92    ///
93    /// // VECTOR with metric
94    /// let vec_with_metric = ResolvedType::from_ast(&DataType::Vector {
95    ///     dimension: 128,
96    ///     metric: Some(VectorMetric::L2),
97    /// });
98    /// assert_eq!(vec_with_metric, ResolvedType::Vector {
99    ///     dimension: 128,
100    ///     metric: VectorMetric::L2,
101    /// });
102    ///
103    /// // VECTOR without metric defaults to Cosine
104    /// let vec_default = ResolvedType::from_ast(&DataType::Vector {
105    ///     dimension: 256,
106    ///     metric: None,
107    /// });
108    /// assert_eq!(vec_default, ResolvedType::Vector {
109    ///     dimension: 256,
110    ///     metric: VectorMetric::Cosine,
111    /// });
112    /// ```
113    pub fn from_ast(dt: &DataType) -> Self {
114        match dt {
115            DataType::Integer | DataType::Int => Self::Integer,
116            DataType::BigInt => Self::BigInt,
117            DataType::Float => Self::Float,
118            DataType::Double => Self::Double,
119            DataType::Text => Self::Text,
120            DataType::Blob => Self::Blob,
121            DataType::Boolean | DataType::Bool => Self::Boolean,
122            DataType::Timestamp => Self::Timestamp,
123            DataType::Date => Self::Date,
124            DataType::Time => Self::Time,
125            DataType::Interval => Self::Interval,
126            DataType::Decimal { precision, scale } => Self::Decimal {
127                precision: *precision,
128                scale: *scale,
129            },
130            DataType::Json => Self::Json,
131            DataType::Array { element } => Self::Array(Box::new(Self::from_ast(element))),
132            DataType::Map { key, value } => Self::Map {
133                key: Box::new(Self::from_ast(key)),
134                value: Box::new(Self::from_ast(value)),
135            },
136            DataType::Struct { fields } => Self::Struct(
137                fields
138                    .iter()
139                    .map(|field| (field.name.clone(), Self::from_ast(&field.data_type)))
140                    .collect(),
141            ),
142            DataType::Vector { dimension, metric } => Self::Vector {
143                dimension: *dimension,
144                metric: metric.unwrap_or(VectorMetric::Cosine),
145            },
146        }
147    }
148
149    /// Check if this type can be implicitly cast to the target type.
150    ///
151    /// Implicit conversion rules:
152    /// - Same types are always compatible
153    /// - `Null` can be cast to any type
154    /// - Numeric widening: `Integer` → `BigInt`, `Float`, `Double`
155    /// - Numeric widening: `BigInt` → `Double`
156    /// - Numeric widening: `Float` → `Double`
157    /// - TIMESTAMP accepts canonical timestamp text and integral epoch microseconds
158    /// - `Vector` types require dimension check (done separately)
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use alopex_sql::planner::types::ResolvedType;
164    ///
165    /// // Same type
166    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Integer));
167    ///
168    /// // Null can cast to any type
169    /// assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Integer));
170    /// assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Text));
171    ///
172    /// // Numeric widening
173    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::BigInt));
174    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Float));
175    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Double));
176    /// assert!(ResolvedType::BigInt.can_cast_to(&ResolvedType::Double));
177    /// assert!(ResolvedType::Float.can_cast_to(&ResolvedType::Double));
178    ///
179    /// // Incompatible types
180    /// assert!(!ResolvedType::Text.can_cast_to(&ResolvedType::Integer));
181    /// assert!(!ResolvedType::BigInt.can_cast_to(&ResolvedType::Integer));
182    /// ```
183    pub fn can_cast_to(&self, target: &ResolvedType) -> bool {
184        use ResolvedType::*;
185
186        match (self, target) {
187            // Same type is always compatible
188            (a, b) if a == b => true,
189
190            // Null can be cast to any type
191            (Null, _) => true,
192
193            // Numeric widening conversions
194            (Integer, BigInt | Float | Double | Decimal { .. }) => true,
195            (BigInt, Double) => true,
196            (Float, Double) => true,
197            (BigInt | Float | Double | Text | Decimal { .. }, Decimal { .. }) => true,
198
199            // A decimal literal is typed DOUBLE, so assigning one to a FLOAT
200            // column needs this narrowing; the value is rounded to f32 at
201            // execution time.
202            (Double, Float) => true,
203
204            // TIMESTAMP is stored as epoch microseconds. Text is parsed as a
205            // canonical UTC timestamp and numeric values must be integral
206            // microseconds at execution time.
207            (Text | Integer | BigInt | Float | Double, Timestamp) => true,
208            (Text, Date | Time | Interval) => true,
209            (Text, Json) | (Json, Text) => true,
210            (Array(source), Array(target)) => source.can_cast_to(target),
211            (
212                Map {
213                    key: source_key,
214                    value: source_value,
215                },
216                Map {
217                    key: target_key,
218                    value: target_value,
219                },
220            ) => source_key.can_cast_to(target_key) && source_value.can_cast_to(target_value),
221            (Struct(source), Struct(target)) if source.len() == target.len() => source
222                .iter()
223                .zip(target)
224                .all(|((source_name, source_type), (target_name, target_type))| {
225                    source_name == target_name && source_type.can_cast_to(target_type)
226                }),
227
228            // Vector types require dimension check (done separately in TypeChecker)
229            (Vector { .. }, Vector { .. }) => false,
230
231            // All other conversions are not allowed
232            _ => false,
233        }
234    }
235
236    /// Get a human-readable name for this type.
237    ///
238    /// Used for error messages.
239    pub fn type_name(&self) -> &'static str {
240        match self {
241            Self::Integer => "Integer",
242            Self::BigInt => "BigInt",
243            Self::Float => "Float",
244            Self::Double => "Double",
245            Self::Text => "Text",
246            Self::Blob => "Blob",
247            Self::Boolean => "Boolean",
248            Self::Timestamp => "Timestamp",
249            Self::Date => "Date",
250            Self::Time => "Time",
251            Self::Interval => "Interval",
252            Self::Decimal { .. } => "Decimal",
253            Self::Json => "Json",
254            Self::Array(_) => "Array",
255            Self::Map { .. } => "Map",
256            Self::Struct(_) => "Struct",
257            Self::Vector { .. } => "Vector",
258            Self::Null => "Null",
259        }
260    }
261}
262
263impl std::fmt::Display for ResolvedType {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        match self {
266            Self::Integer => write!(f, "INTEGER"),
267            Self::BigInt => write!(f, "BIGINT"),
268            Self::Float => write!(f, "FLOAT"),
269            Self::Double => write!(f, "DOUBLE"),
270            Self::Text => write!(f, "TEXT"),
271            Self::Blob => write!(f, "BLOB"),
272            Self::Boolean => write!(f, "BOOLEAN"),
273            Self::Timestamp => write!(f, "TIMESTAMP"),
274            Self::Date => write!(f, "DATE"),
275            Self::Time => write!(f, "TIME"),
276            Self::Interval => write!(f, "INTERVAL"),
277            Self::Decimal { precision, scale } => write!(f, "DECIMAL({precision},{scale})"),
278            Self::Json => write!(f, "JSON"),
279            Self::Array(element) => write!(f, "ARRAY<{element}>"),
280            Self::Map { key, value } => write!(f, "MAP<{key},{value}>"),
281            Self::Struct(fields) => {
282                write!(f, "STRUCT<")?;
283                for (index, (name, data_type)) in fields.iter().enumerate() {
284                    if index > 0 {
285                        write!(f, ",")?;
286                    }
287                    write!(f, "{name} {data_type}")?;
288                }
289                write!(f, ">")
290            }
291            Self::Vector { dimension, metric } => {
292                write!(f, "VECTOR({}, {:?})", dimension, metric)
293            }
294            Self::Null => write!(f, "NULL"),
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_from_ast_integer() {
305        assert_eq!(
306            ResolvedType::from_ast(&DataType::Integer),
307            ResolvedType::Integer
308        );
309        assert_eq!(
310            ResolvedType::from_ast(&DataType::Int),
311            ResolvedType::Integer
312        );
313    }
314
315    #[test]
316    fn test_from_ast_boolean() {
317        assert_eq!(
318            ResolvedType::from_ast(&DataType::Boolean),
319            ResolvedType::Boolean
320        );
321        assert_eq!(
322            ResolvedType::from_ast(&DataType::Bool),
323            ResolvedType::Boolean
324        );
325    }
326
327    #[test]
328    fn test_from_ast_vector_with_metric() {
329        let dt = DataType::Vector {
330            dimension: 128,
331            metric: Some(VectorMetric::L2),
332        };
333        assert_eq!(
334            ResolvedType::from_ast(&dt),
335            ResolvedType::Vector {
336                dimension: 128,
337                metric: VectorMetric::L2,
338            }
339        );
340    }
341
342    #[test]
343    fn test_from_ast_vector_default_metric() {
344        let dt = DataType::Vector {
345            dimension: 256,
346            metric: None,
347        };
348        assert_eq!(
349            ResolvedType::from_ast(&dt),
350            ResolvedType::Vector {
351                dimension: 256,
352                metric: VectorMetric::Cosine,
353            }
354        );
355    }
356
357    #[test]
358    fn test_can_cast_same_type() {
359        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Integer));
360        assert!(ResolvedType::Text.can_cast_to(&ResolvedType::Text));
361    }
362
363    #[test]
364    fn test_can_cast_null() {
365        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Integer));
366        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Text));
367        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Boolean));
368    }
369
370    #[test]
371    fn test_can_cast_numeric_widening() {
372        // Integer → BigInt/Float/Double
373        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::BigInt));
374        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Float));
375        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Double));
376
377        // BigInt → Double
378        assert!(ResolvedType::BigInt.can_cast_to(&ResolvedType::Double));
379
380        // Float → Double
381        assert!(ResolvedType::Float.can_cast_to(&ResolvedType::Double));
382
383        assert!(
384            ResolvedType::Decimal {
385                precision: 5,
386                scale: 3,
387            }
388            .can_cast_to(&ResolvedType::Decimal {
389                precision: 10,
390                scale: 2,
391            })
392        );
393    }
394
395    #[test]
396    fn test_can_cast_incompatible() {
397        // Text cannot cast to numeric
398        assert!(!ResolvedType::Text.can_cast_to(&ResolvedType::Integer));
399
400        // Numeric narrowing not allowed
401        assert!(!ResolvedType::BigInt.can_cast_to(&ResolvedType::Integer));
402    }
403
404    #[test]
405    fn double_narrows_to_float_because_decimal_literals_are_double() {
406        // The lexer types every decimal literal as DOUBLE, so rejecting this
407        // narrowing would make FLOAT columns impossible to populate.
408        assert!(ResolvedType::Double.can_cast_to(&ResolvedType::Float));
409    }
410
411    #[test]
412    fn test_can_cast_vector() {
413        let vec1 = ResolvedType::Vector {
414            dimension: 128,
415            metric: VectorMetric::Cosine,
416        };
417        let vec2 = ResolvedType::Vector {
418            dimension: 128,
419            metric: VectorMetric::L2,
420        };
421        // Vector dimension check is done separately
422        assert!(!vec1.can_cast_to(&vec2));
423    }
424
425    #[test]
426    fn test_display() {
427        assert_eq!(format!("{}", ResolvedType::Integer), "INTEGER");
428        assert_eq!(format!("{}", ResolvedType::Text), "TEXT");
429        assert_eq!(
430            format!(
431                "{}",
432                ResolvedType::Vector {
433                    dimension: 128,
434                    metric: VectorMetric::Cosine
435                }
436            ),
437            "VECTOR(128, Cosine)"
438        );
439    }
440}