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    /// - `Vector` types require dimension check (done separately)
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use alopex_sql::planner::types::ResolvedType;
125    ///
126    /// // Same type
127    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Integer));
128    ///
129    /// // Null can cast to any type
130    /// assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Integer));
131    /// assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Text));
132    ///
133    /// // Numeric widening
134    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::BigInt));
135    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Float));
136    /// assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Double));
137    /// assert!(ResolvedType::BigInt.can_cast_to(&ResolvedType::Double));
138    /// assert!(ResolvedType::Float.can_cast_to(&ResolvedType::Double));
139    ///
140    /// // Incompatible types
141    /// assert!(!ResolvedType::Text.can_cast_to(&ResolvedType::Integer));
142    /// assert!(!ResolvedType::BigInt.can_cast_to(&ResolvedType::Integer));
143    /// ```
144    pub fn can_cast_to(&self, target: &ResolvedType) -> bool {
145        use ResolvedType::*;
146
147        match (self, target) {
148            // Same type is always compatible
149            (a, b) if a == b => true,
150
151            // Null can be cast to any type
152            (Null, _) => true,
153
154            // Numeric widening conversions
155            (Integer, BigInt | Float | Double) => true,
156            (BigInt, Double) => true,
157            (Float, Double) => true,
158
159            // Vector types require dimension check (done separately in TypeChecker)
160            (Vector { .. }, Vector { .. }) => false,
161
162            // All other conversions are not allowed
163            _ => false,
164        }
165    }
166
167    /// Get a human-readable name for this type.
168    ///
169    /// Used for error messages.
170    pub fn type_name(&self) -> &'static str {
171        match self {
172            Self::Integer => "Integer",
173            Self::BigInt => "BigInt",
174            Self::Float => "Float",
175            Self::Double => "Double",
176            Self::Text => "Text",
177            Self::Blob => "Blob",
178            Self::Boolean => "Boolean",
179            Self::Timestamp => "Timestamp",
180            Self::Vector { .. } => "Vector",
181            Self::Null => "Null",
182        }
183    }
184}
185
186impl std::fmt::Display for ResolvedType {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            Self::Integer => write!(f, "INTEGER"),
190            Self::BigInt => write!(f, "BIGINT"),
191            Self::Float => write!(f, "FLOAT"),
192            Self::Double => write!(f, "DOUBLE"),
193            Self::Text => write!(f, "TEXT"),
194            Self::Blob => write!(f, "BLOB"),
195            Self::Boolean => write!(f, "BOOLEAN"),
196            Self::Timestamp => write!(f, "TIMESTAMP"),
197            Self::Vector { dimension, metric } => {
198                write!(f, "VECTOR({}, {:?})", dimension, metric)
199            }
200            Self::Null => write!(f, "NULL"),
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_from_ast_integer() {
211        assert_eq!(
212            ResolvedType::from_ast(&DataType::Integer),
213            ResolvedType::Integer
214        );
215        assert_eq!(
216            ResolvedType::from_ast(&DataType::Int),
217            ResolvedType::Integer
218        );
219    }
220
221    #[test]
222    fn test_from_ast_boolean() {
223        assert_eq!(
224            ResolvedType::from_ast(&DataType::Boolean),
225            ResolvedType::Boolean
226        );
227        assert_eq!(
228            ResolvedType::from_ast(&DataType::Bool),
229            ResolvedType::Boolean
230        );
231    }
232
233    #[test]
234    fn test_from_ast_vector_with_metric() {
235        let dt = DataType::Vector {
236            dimension: 128,
237            metric: Some(VectorMetric::L2),
238        };
239        assert_eq!(
240            ResolvedType::from_ast(&dt),
241            ResolvedType::Vector {
242                dimension: 128,
243                metric: VectorMetric::L2,
244            }
245        );
246    }
247
248    #[test]
249    fn test_from_ast_vector_default_metric() {
250        let dt = DataType::Vector {
251            dimension: 256,
252            metric: None,
253        };
254        assert_eq!(
255            ResolvedType::from_ast(&dt),
256            ResolvedType::Vector {
257                dimension: 256,
258                metric: VectorMetric::Cosine,
259            }
260        );
261    }
262
263    #[test]
264    fn test_can_cast_same_type() {
265        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Integer));
266        assert!(ResolvedType::Text.can_cast_to(&ResolvedType::Text));
267    }
268
269    #[test]
270    fn test_can_cast_null() {
271        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Integer));
272        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Text));
273        assert!(ResolvedType::Null.can_cast_to(&ResolvedType::Boolean));
274    }
275
276    #[test]
277    fn test_can_cast_numeric_widening() {
278        // Integer → BigInt/Float/Double
279        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::BigInt));
280        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Float));
281        assert!(ResolvedType::Integer.can_cast_to(&ResolvedType::Double));
282
283        // BigInt → Double
284        assert!(ResolvedType::BigInt.can_cast_to(&ResolvedType::Double));
285
286        // Float → Double
287        assert!(ResolvedType::Float.can_cast_to(&ResolvedType::Double));
288    }
289
290    #[test]
291    fn test_can_cast_incompatible() {
292        // Text cannot cast to numeric
293        assert!(!ResolvedType::Text.can_cast_to(&ResolvedType::Integer));
294
295        // Numeric narrowing not allowed
296        assert!(!ResolvedType::BigInt.can_cast_to(&ResolvedType::Integer));
297        assert!(!ResolvedType::Double.can_cast_to(&ResolvedType::Float));
298    }
299
300    #[test]
301    fn test_can_cast_vector() {
302        let vec1 = ResolvedType::Vector {
303            dimension: 128,
304            metric: VectorMetric::Cosine,
305        };
306        let vec2 = ResolvedType::Vector {
307            dimension: 128,
308            metric: VectorMetric::L2,
309        };
310        // Vector dimension check is done separately
311        assert!(!vec1.can_cast_to(&vec2));
312    }
313
314    #[test]
315    fn test_display() {
316        assert_eq!(format!("{}", ResolvedType::Integer), "INTEGER");
317        assert_eq!(format!("{}", ResolvedType::Text), "TEXT");
318        assert_eq!(
319            format!(
320                "{}",
321                ResolvedType::Vector {
322                    dimension: 128,
323                    metric: VectorMetric::Cosine
324                }
325            ),
326            "VECTOR(128, Cosine)"
327        );
328    }
329}