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    /// Vector type with dimension and metric
46    /// Metric is always populated (defaults to Cosine if omitted in AST)
47    Vector {
48        dimension: u32,
49        metric: VectorMetric,
50    },
51    /// NULL type (for NULL literals)
52    Null,
53}
54
55impl ResolvedType {
56    /// Convert from AST [`DataType`] to [`ResolvedType`].
57    ///
58    /// For `VECTOR` types, if metric is omitted in the AST, it defaults to `Cosine`.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use alopex_sql::planner::types::ResolvedType;
64    /// use alopex_sql::ast::ddl::{DataType, VectorMetric};
65    ///
66    /// // INTEGER and INT both resolve to Integer
67    /// assert_eq!(ResolvedType::from_ast(&DataType::Integer), ResolvedType::Integer);
68    /// assert_eq!(ResolvedType::from_ast(&DataType::Int), ResolvedType::Integer);
69    ///
70    /// // BOOLEAN and BOOL both resolve to Boolean
71    /// assert_eq!(ResolvedType::from_ast(&DataType::Boolean), ResolvedType::Boolean);
72    /// assert_eq!(ResolvedType::from_ast(&DataType::Bool), ResolvedType::Boolean);
73    ///
74    /// // VECTOR with metric
75    /// let vec_with_metric = ResolvedType::from_ast(&DataType::Vector {
76    ///     dimension: 128,
77    ///     metric: Some(VectorMetric::L2),
78    /// });
79    /// assert_eq!(vec_with_metric, ResolvedType::Vector {
80    ///     dimension: 128,
81    ///     metric: VectorMetric::L2,
82    /// });
83    ///
84    /// // VECTOR without metric defaults to Cosine
85    /// let vec_default = ResolvedType::from_ast(&DataType::Vector {
86    ///     dimension: 256,
87    ///     metric: None,
88    /// });
89    /// assert_eq!(vec_default, ResolvedType::Vector {
90    ///     dimension: 256,
91    ///     metric: VectorMetric::Cosine,
92    /// });
93    /// ```
94    pub fn from_ast(dt: &DataType) -> Self {
95        match dt {
96            DataType::Integer | DataType::Int => Self::Integer,
97            DataType::BigInt => Self::BigInt,
98            DataType::Float => Self::Float,
99            DataType::Double => Self::Double,
100            DataType::Text => Self::Text,
101            DataType::Blob => Self::Blob,
102            DataType::Boolean | DataType::Bool => Self::Boolean,
103            DataType::Timestamp => Self::Timestamp,
104            DataType::Vector { dimension, metric } => Self::Vector {
105                dimension: *dimension,
106                metric: metric.unwrap_or(VectorMetric::Cosine),
107            },
108        }
109    }
110
111    /// Check if this type can be implicitly cast to the target type.
112    ///
113    /// Implicit conversion rules:
114    /// - Same types are always compatible
115    /// - `Null` can be cast to any type
116    /// - Numeric widening: `Integer` → `BigInt`, `Float`, `Double`
117    /// - Numeric widening: `BigInt` → `Double`
118    /// - Numeric widening: `Float` → `Double`
119    /// - TIMESTAMP accepts canonical timestamp text and integral epoch microseconds
120    /// - `Vector` types require dimension check (done separately)
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use alopex_sql::planner::types::ResolvedType;
126    ///
127    /// // Same type
128    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Integer));
129    ///
130    /// // Null can cast to any type
131    /// assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Integer));
132    /// assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Text));
133    ///
134    /// // Numeric widening
135    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::BigInt));
136    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Float));
137    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Double));
138    /// assert!(ResolvedType::BigInt.can_cast_to(&ResolvedType::Double));
139    /// assert!(ResolvedType::Float.can_cast_to(&ResolvedType::Double));
140    ///
141    /// // Incompatible types
142    /// assert!(!ResolvedType::Text.can_cast_to(&ResolvedType::Integer));
143    /// assert!(!ResolvedType::BigInt.can_cast_to(&ResolvedType::Integer));
144    /// ```
145    pub fn can_cast_to(&self, target: &ResolvedType) -> bool {
146        use ResolvedType::*;
147
148        match (self, target) {
149            // Same type is always compatible
150            (a, b) if a == b => true,
151
152            // Null can be cast to any type
153            (Null, _) => true,
154
155            // Numeric widening conversions
156            (Integer, BigInt | Float | Double) => true,
157            (BigInt, Double) => true,
158            (Float, Double) => true,
159
160            // A decimal literal is typed DOUBLE, so assigning one to a FLOAT
161            // column needs this narrowing; the value is rounded to f32 at
162            // execution time.
163            (Double, Float) => true,
164
165            // TIMESTAMP is stored as epoch microseconds. Text is parsed as a
166            // canonical UTC timestamp and numeric values must be integral
167            // microseconds at execution time.
168            (Text | Integer | BigInt | Float | Double, Timestamp) => true,
169
170            // Vector types require dimension check (done separately in TypeChecker)
171            (Vector { .. }, Vector { .. }) => false,
172
173            // All other conversions are not allowed
174            _ => false,
175        }
176    }
177
178    /// Get a human-readable name for this type.
179    ///
180    /// Used for error messages.
181    pub fn type_name(&self) -> &'static str {
182        match self {
183            Self::Integer => "Integer",
184            Self::BigInt => "BigInt",
185            Self::Float => "Float",
186            Self::Double => "Double",
187            Self::Text => "Text",
188            Self::Blob => "Blob",
189            Self::Boolean => "Boolean",
190            Self::Timestamp => "Timestamp",
191            Self::Vector { .. } => "Vector",
192            Self::Null => "Null",
193        }
194    }
195}
196
197impl std::fmt::Display for ResolvedType {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        match self {
200            Self::Integer => write!(f, "INTEGER"),
201            Self::BigInt => write!(f, "BIGINT"),
202            Self::Float => write!(f, "FLOAT"),
203            Self::Double => write!(f, "DOUBLE"),
204            Self::Text => write!(f, "TEXT"),
205            Self::Blob => write!(f, "BLOB"),
206            Self::Boolean => write!(f, "BOOLEAN"),
207            Self::Timestamp => write!(f, "TIMESTAMP"),
208            Self::Vector { dimension, metric } => {
209                write!(f, "VECTOR({}, {:?})", dimension, metric)
210            }
211            Self::Null => write!(f, "NULL"),
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_from_ast_integer() {
222        assert_eq!(
223            ResolvedType::from_ast(&DataType::Integer),
224            ResolvedType::Integer
225        );
226        assert_eq!(
227            ResolvedType::from_ast(&DataType::Int),
228            ResolvedType::Integer
229        );
230    }
231
232    #[test]
233    fn test_from_ast_boolean() {
234        assert_eq!(
235            ResolvedType::from_ast(&DataType::Boolean),
236            ResolvedType::Boolean
237        );
238        assert_eq!(
239            ResolvedType::from_ast(&DataType::Bool),
240            ResolvedType::Boolean
241        );
242    }
243
244    #[test]
245    fn test_from_ast_vector_with_metric() {
246        let dt = DataType::Vector {
247            dimension: 128,
248            metric: Some(VectorMetric::L2),
249        };
250        assert_eq!(
251            ResolvedType::from_ast(&dt),
252            ResolvedType::Vector {
253                dimension: 128,
254                metric: VectorMetric::L2,
255            }
256        );
257    }
258
259    #[test]
260    fn test_from_ast_vector_default_metric() {
261        let dt = DataType::Vector {
262            dimension: 256,
263            metric: None,
264        };
265        assert_eq!(
266            ResolvedType::from_ast(&dt),
267            ResolvedType::Vector {
268                dimension: 256,
269                metric: VectorMetric::Cosine,
270            }
271        );
272    }
273
274    #[test]
275    fn test_can_cast_same_type() {
276        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Integer));
277        assert!(ResolvedType::Text.can_cast_to(&ResolvedType::Text));
278    }
279
280    #[test]
281    fn test_can_cast_null() {
282        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Integer));
283        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Text));
284        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Boolean));
285    }
286
287    #[test]
288    fn test_can_cast_numeric_widening() {
289        // Integer → BigInt/Float/Double
290        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::BigInt));
291        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Float));
292        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Double));
293
294        // BigInt → Double
295        assert!(ResolvedType::BigInt.can_cast_to(&ResolvedType::Double));
296
297        // Float → Double
298        assert!(ResolvedType::Float.can_cast_to(&ResolvedType::Double));
299    }
300
301    #[test]
302    fn test_can_cast_incompatible() {
303        // Text cannot cast to numeric
304        assert!(!ResolvedType::Text.can_cast_to(&ResolvedType::Integer));
305
306        // Numeric narrowing not allowed
307        assert!(!ResolvedType::BigInt.can_cast_to(&ResolvedType::Integer));
308    }
309
310    #[test]
311    fn double_narrows_to_float_because_decimal_literals_are_double() {
312        // The lexer types every decimal literal as DOUBLE, so rejecting this
313        // narrowing would make FLOAT columns impossible to populate.
314        assert!(ResolvedType::Double.can_cast_to(&ResolvedType::Float));
315    }
316
317    #[test]
318    fn test_can_cast_vector() {
319        let vec1 = ResolvedType::Vector {
320            dimension: 128,
321            metric: VectorMetric::Cosine,
322        };
323        let vec2 = ResolvedType::Vector {
324            dimension: 128,
325            metric: VectorMetric::L2,
326        };
327        // Vector dimension check is done separately
328        assert!(!vec1.can_cast_to(&vec2));
329    }
330
331    #[test]
332    fn test_display() {
333        assert_eq!(format!("{}", ResolvedType::Integer), "INTEGER");
334        assert_eq!(format!("{}", ResolvedType::Text), "TEXT");
335        assert_eq!(
336            format!(
337                "{}",
338                ResolvedType::Vector {
339                    dimension: 128,
340                    metric: VectorMetric::Cosine
341                }
342            ),
343            "VECTOR(128, Cosine)"
344        );
345    }
346}