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}