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}