vibesql/types/
sql_type.rs

1//! SQL type system for semantic analysis.
2//!
3//! This module defines the runtime type system used during query analysis
4//! and execution planning. Type names follow ISO SQL standards.
5
6use std::fmt;
7
8/// A SQL data type used during semantic analysis.
9///
10/// Type names follow ISO SQL standards:
11/// - `Int32` displays as "INTEGER"
12/// - `Int64` displays as "BIGINT"
13/// - `Float32` displays as "REAL"
14/// - `Float64` displays as "DOUBLE PRECISION"
15/// - `Varchar` displays as "VARCHAR"
16/// - `Varbinary` displays as "VARBINARY"
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub enum SqlType {
19    /// Boolean type (BOOLEAN)
20    Bool,
21
22    /// 32-bit signed integer (INTEGER)
23    Int32,
24
25    /// 64-bit signed integer (BIGINT)
26    Int64,
27
28    /// 32-bit unsigned integer (UINTEGER) - extension
29    Uint32,
30
31    /// 64-bit unsigned integer (UBIGINT) - extension
32    Uint64,
33
34    /// 32-bit floating point (REAL)
35    Float32,
36
37    /// 64-bit floating point (DOUBLE PRECISION)
38    Float64,
39
40    /// Fixed-precision decimal (NUMERIC/DECIMAL)
41    Numeric {
42        precision: Option<u8>,
43        scale: Option<u8>,
44    },
45
46    /// Variable-length character string (VARCHAR)
47    Varchar,
48
49    /// Variable-length binary data (VARBINARY)
50    Varbinary,
51
52    /// Date (year, month, day)
53    Date,
54
55    /// Time of day
56    Time,
57
58    /// Date and time without timezone (TIMESTAMP WITHOUT TIME ZONE)
59    Datetime,
60
61    /// Date and time with timezone (TIMESTAMP WITH TIME ZONE)
62    Timestamp,
63
64    /// Time interval
65    Interval,
66
67    /// Array of elements
68    Array(Box<SqlType>),
69
70    /// Struct with named fields (ROW type in standard SQL)
71    Struct(Vec<StructField>),
72
73    /// JSON data
74    Json,
75
76    /// Range of values
77    Range(Box<SqlType>),
78
79    /// Universally unique identifier (UUID)
80    Uuid,
81
82    /// Unknown type (for unresolved expressions)
83    Unknown,
84
85    /// Any type (for polymorphic functions)
86    Any,
87}
88
89/// A field in a struct type.
90#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct StructField {
92    pub name: Option<String>,
93    pub data_type: SqlType,
94}
95
96impl SqlType {
97    /// Check if this type is numeric.
98    pub fn is_numeric(&self) -> bool {
99        matches!(
100            self,
101            SqlType::Int32
102                | SqlType::Int64
103                | SqlType::Uint32
104                | SqlType::Uint64
105                | SqlType::Float32
106                | SqlType::Float64
107                | SqlType::Numeric { .. }
108        )
109    }
110
111    /// Check if this type is an integer type (signed or unsigned).
112    pub fn is_integer(&self) -> bool {
113        matches!(
114            self,
115            SqlType::Int32 | SqlType::Int64 | SqlType::Uint32 | SqlType::Uint64
116        )
117    }
118
119    /// Check if this type is a signed integer type.
120    pub fn is_signed_integer(&self) -> bool {
121        matches!(self, SqlType::Int32 | SqlType::Int64)
122    }
123
124    /// Check if this type is an unsigned integer type.
125    pub fn is_unsigned_integer(&self) -> bool {
126        matches!(self, SqlType::Uint32 | SqlType::Uint64)
127    }
128
129    /// Check if this type is a floating-point type.
130    pub fn is_floating_point(&self) -> bool {
131        matches!(self, SqlType::Float32 | SqlType::Float64)
132    }
133
134    /// Check if this type is a string type.
135    pub fn is_string(&self) -> bool {
136        matches!(self, SqlType::Varchar)
137    }
138
139    /// Check if this type is a date/time type.
140    pub fn is_datetime(&self) -> bool {
141        matches!(
142            self,
143            SqlType::Date | SqlType::Time | SqlType::Datetime | SqlType::Timestamp
144        )
145    }
146
147    /// Check if this type is comparable with another type.
148    pub fn is_comparable_with(&self, other: &SqlType) -> bool {
149        match (self, other) {
150            // Same types are always comparable
151            (a, b) if a == b => true,
152
153            // Numeric types are comparable with each other
154            (a, b) if a.is_numeric() && b.is_numeric() => true,
155
156            // Date/time types are comparable with each other (with caveats)
157            (a, b) if a.is_datetime() && b.is_datetime() => true,
158
159            // Unknown/Any can be compared with anything
160            (SqlType::Unknown, _) | (_, SqlType::Unknown) => true,
161            (SqlType::Any, _) | (_, SqlType::Any) => true,
162
163            _ => false,
164        }
165    }
166
167    /// Check if this type can be implicitly coerced to another type.
168    pub fn can_coerce_to(&self, target: &SqlType) -> bool {
169        match (self, target) {
170            // Same types
171            (a, b) if a == b => true,
172
173            // Integer widening: smaller -> larger
174            (SqlType::Int32, SqlType::Int64) => true,
175            (SqlType::Uint32, SqlType::Uint64) => true,
176            (SqlType::Uint32, SqlType::Int64) => true,
177
178            // Integers can coerce to floating point
179            (SqlType::Int32, SqlType::Float32) => true,
180            (SqlType::Int32, SqlType::Float64) => true,
181            (SqlType::Int64, SqlType::Float64) => true,
182            (SqlType::Uint32, SqlType::Float32) => true,
183            (SqlType::Uint32, SqlType::Float64) => true,
184            (SqlType::Uint64, SqlType::Float64) => true,
185
186            // Float32 can coerce to Float64
187            (SqlType::Float32, SqlType::Float64) => true,
188
189            // Integers can coerce to Numeric
190            (SqlType::Int32, SqlType::Numeric { .. }) => true,
191            (SqlType::Int64, SqlType::Numeric { .. }) => true,
192            (SqlType::Uint32, SqlType::Numeric { .. }) => true,
193            (SqlType::Uint64, SqlType::Numeric { .. }) => true,
194
195            // Float can coerce to Numeric
196            (SqlType::Float32, SqlType::Numeric { .. }) => true,
197            (SqlType::Float64, SqlType::Numeric { .. }) => true,
198
199            // Date can coerce to Datetime/Timestamp
200            (SqlType::Date, SqlType::Datetime) => true,
201            (SqlType::Date, SqlType::Timestamp) => true,
202
203            // Datetime can coerce to Timestamp
204            (SqlType::Datetime, SqlType::Timestamp) => true,
205
206            // Unknown can coerce to anything
207            (SqlType::Unknown, _) => true,
208
209            // Anything can coerce to Any
210            (_, SqlType::Any) => true,
211
212            // Array coercion if element types can coerce
213            (SqlType::Array(a), SqlType::Array(b)) => a.can_coerce_to(b),
214
215            _ => false,
216        }
217    }
218
219    /// Get the common supertype of two types.
220    pub fn common_supertype(&self, other: &SqlType) -> Option<SqlType> {
221        match (self, other) {
222            // Same types
223            (a, b) if a == b => Some(a.clone()),
224
225            // Integer promotions
226            (SqlType::Int32, SqlType::Int64) | (SqlType::Int64, SqlType::Int32) => {
227                Some(SqlType::Int64)
228            }
229            (SqlType::Uint32, SqlType::Uint64) | (SqlType::Uint64, SqlType::Uint32) => {
230                Some(SqlType::Uint64)
231            }
232            (SqlType::Int32, SqlType::Uint32) | (SqlType::Uint32, SqlType::Int32) => {
233                Some(SqlType::Int64) // Promote to signed 64-bit to avoid overflow
234            }
235            (SqlType::Int32, SqlType::Uint64) | (SqlType::Uint64, SqlType::Int32) => {
236                Some(SqlType::Float64) // No safe integer type, use float
237            }
238            (SqlType::Int64, SqlType::Uint64) | (SqlType::Uint64, SqlType::Int64) => {
239                Some(SqlType::Float64) // No safe integer type, use float
240            }
241            (SqlType::Uint32, SqlType::Int64) | (SqlType::Int64, SqlType::Uint32) => {
242                Some(SqlType::Int64)
243            }
244
245            // Float promotions
246            (SqlType::Float32, SqlType::Float64) | (SqlType::Float64, SqlType::Float32) => {
247                Some(SqlType::Float64)
248            }
249
250            // Integer to float promotions
251            (SqlType::Int32, SqlType::Float32) | (SqlType::Float32, SqlType::Int32) => {
252                Some(SqlType::Float32)
253            }
254            (SqlType::Int32, SqlType::Float64) | (SqlType::Float64, SqlType::Int32) => {
255                Some(SqlType::Float64)
256            }
257            (SqlType::Int64, SqlType::Float32) | (SqlType::Float32, SqlType::Int64) => {
258                Some(SqlType::Float64)
259            }
260            (SqlType::Int64, SqlType::Float64) | (SqlType::Float64, SqlType::Int64) => {
261                Some(SqlType::Float64)
262            }
263            (SqlType::Uint32, SqlType::Float32) | (SqlType::Float32, SqlType::Uint32) => {
264                Some(SqlType::Float32)
265            }
266            (SqlType::Uint32, SqlType::Float64) | (SqlType::Float64, SqlType::Uint32) => {
267                Some(SqlType::Float64)
268            }
269            (SqlType::Uint64, SqlType::Float32) | (SqlType::Float32, SqlType::Uint64) => {
270                Some(SqlType::Float64)
271            }
272            (SqlType::Uint64, SqlType::Float64) | (SqlType::Float64, SqlType::Uint64) => {
273                Some(SqlType::Float64)
274            }
275
276            // Numeric supertypes (any integer or float with Numeric)
277            (t, SqlType::Numeric { .. }) | (SqlType::Numeric { .. }, t)
278                if t.is_integer() || t.is_floating_point() =>
279            {
280                Some(SqlType::Numeric {
281                    precision: None,
282                    scale: None,
283                })
284            }
285
286            // Date/time types
287            (SqlType::Date, SqlType::Datetime) | (SqlType::Datetime, SqlType::Date) => {
288                Some(SqlType::Datetime)
289            }
290            (SqlType::Date, SqlType::Timestamp) | (SqlType::Timestamp, SqlType::Date) => {
291                Some(SqlType::Timestamp)
292            }
293            (SqlType::Datetime, SqlType::Timestamp) | (SqlType::Timestamp, SqlType::Datetime) => {
294                Some(SqlType::Timestamp)
295            }
296
297            // Unknown resolves to the other type
298            (SqlType::Unknown, other) | (other, SqlType::Unknown) => Some(other.clone()),
299
300            // Any stays Any
301            (SqlType::Any, _) | (_, SqlType::Any) => Some(SqlType::Any),
302
303            // Arrays - find common element type
304            (SqlType::Array(a), SqlType::Array(b)) => {
305                a.common_supertype(b).map(|t| SqlType::Array(Box::new(t)))
306            }
307
308            _ => None,
309        }
310    }
311
312    /// Get the element type if this is an array.
313    pub fn element_type(&self) -> Option<&SqlType> {
314        match self {
315            SqlType::Array(elem) => Some(elem),
316            _ => None,
317        }
318    }
319
320    /// Get struct fields if this is a struct.
321    pub fn struct_fields(&self) -> Option<&[StructField]> {
322        match self {
323            SqlType::Struct(fields) => Some(fields),
324            _ => None,
325        }
326    }
327}
328
329impl fmt::Display for SqlType {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        match self {
332            SqlType::Bool => write!(f, "BOOLEAN"),
333            SqlType::Int32 => write!(f, "INTEGER"),
334            SqlType::Int64 => write!(f, "BIGINT"),
335            SqlType::Uint32 => write!(f, "UINTEGER"),
336            SqlType::Uint64 => write!(f, "UBIGINT"),
337            SqlType::Float32 => write!(f, "REAL"),
338            SqlType::Float64 => write!(f, "DOUBLE PRECISION"),
339            SqlType::Numeric { precision, scale } => {
340                write!(f, "NUMERIC")?;
341                if let Some(p) = precision {
342                    write!(f, "({}", p)?;
343                    if let Some(s) = scale {
344                        write!(f, ", {}", s)?;
345                    }
346                    write!(f, ")")?;
347                }
348                Ok(())
349            }
350            SqlType::Varchar => write!(f, "VARCHAR"),
351            SqlType::Varbinary => write!(f, "VARBINARY"),
352            SqlType::Date => write!(f, "DATE"),
353            SqlType::Time => write!(f, "TIME"),
354            SqlType::Datetime => write!(f, "DATETIME"),
355            SqlType::Timestamp => write!(f, "TIMESTAMP"),
356            SqlType::Interval => write!(f, "INTERVAL"),
357            SqlType::Array(elem) => write!(f, "ARRAY<{}>", elem),
358            SqlType::Struct(fields) => {
359                write!(f, "STRUCT<")?;
360                for (i, field) in fields.iter().enumerate() {
361                    if i > 0 {
362                        write!(f, ", ")?;
363                    }
364                    if let Some(name) = &field.name {
365                        write!(f, "{} ", name)?;
366                    }
367                    write!(f, "{}", field.data_type)?;
368                }
369                write!(f, ">")
370            }
371            SqlType::Json => write!(f, "JSON"),
372            SqlType::Range(elem) => write!(f, "RANGE<{}>", elem),
373            SqlType::Uuid => write!(f, "UUID"),
374            SqlType::Unknown => write!(f, "UNKNOWN"),
375            SqlType::Any => write!(f, "ANY"),
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_numeric_classification() {
386        assert!(SqlType::Int32.is_numeric());
387        assert!(SqlType::Int64.is_numeric());
388        assert!(SqlType::Uint32.is_numeric());
389        assert!(SqlType::Uint64.is_numeric());
390        assert!(SqlType::Float32.is_numeric());
391        assert!(SqlType::Float64.is_numeric());
392        assert!(SqlType::Numeric {
393            precision: None,
394            scale: None
395        }
396        .is_numeric());
397        assert!(!SqlType::Varchar.is_numeric());
398    }
399
400    #[test]
401    fn test_coercion() {
402        // Integer widening
403        assert!(SqlType::Int32.can_coerce_to(&SqlType::Int64));
404        assert!(SqlType::Uint32.can_coerce_to(&SqlType::Uint64));
405        assert!(SqlType::Uint32.can_coerce_to(&SqlType::Int64));
406
407        // Integer to float
408        assert!(SqlType::Int32.can_coerce_to(&SqlType::Float32));
409        assert!(SqlType::Int32.can_coerce_to(&SqlType::Float64));
410        assert!(SqlType::Int64.can_coerce_to(&SqlType::Float64));
411
412        // Float widening
413        assert!(SqlType::Float32.can_coerce_to(&SqlType::Float64));
414
415        // To Numeric
416        assert!(SqlType::Int64.can_coerce_to(&SqlType::Numeric {
417            precision: None,
418            scale: None
419        }));
420        assert!(SqlType::Float32.can_coerce_to(&SqlType::Numeric {
421            precision: None,
422            scale: None
423        }));
424
425        // No coercion
426        assert!(!SqlType::Varchar.can_coerce_to(&SqlType::Int64));
427        assert!(!SqlType::Int64.can_coerce_to(&SqlType::Int32)); // No narrowing
428    }
429
430    #[test]
431    fn test_common_supertype() {
432        // Integer promotions
433        assert_eq!(
434            SqlType::Int32.common_supertype(&SqlType::Int64),
435            Some(SqlType::Int64)
436        );
437        assert_eq!(
438            SqlType::Uint32.common_supertype(&SqlType::Uint64),
439            Some(SqlType::Uint64)
440        );
441        assert_eq!(
442            SqlType::Int32.common_supertype(&SqlType::Uint32),
443            Some(SqlType::Int64) // Promote to signed 64-bit
444        );
445
446        // Float promotions
447        assert_eq!(
448            SqlType::Float32.common_supertype(&SqlType::Float64),
449            Some(SqlType::Float64)
450        );
451
452        // Integer to float
453        assert_eq!(
454            SqlType::Int64.common_supertype(&SqlType::Float64),
455            Some(SqlType::Float64)
456        );
457        assert_eq!(
458            SqlType::Int32.common_supertype(&SqlType::Float32),
459            Some(SqlType::Float32)
460        );
461
462        // Date/time
463        assert_eq!(
464            SqlType::Date.common_supertype(&SqlType::Timestamp),
465            Some(SqlType::Timestamp)
466        );
467
468        // No common supertype
469        assert_eq!(SqlType::Varchar.common_supertype(&SqlType::Int64), None);
470    }
471
472    #[test]
473    fn test_is_integer() {
474        assert!(SqlType::Int32.is_integer());
475        assert!(SqlType::Int64.is_integer());
476        assert!(SqlType::Uint32.is_integer());
477        assert!(SqlType::Uint64.is_integer());
478        assert!(!SqlType::Float32.is_integer());
479        assert!(!SqlType::Float64.is_integer());
480        assert!(!SqlType::Numeric {
481            precision: None,
482            scale: None
483        }
484        .is_integer());
485        assert!(!SqlType::Varchar.is_integer());
486    }
487
488    #[test]
489    fn test_is_signed_unsigned_integer() {
490        // Signed
491        assert!(SqlType::Int32.is_signed_integer());
492        assert!(SqlType::Int64.is_signed_integer());
493        assert!(!SqlType::Uint32.is_signed_integer());
494        assert!(!SqlType::Uint64.is_signed_integer());
495
496        // Unsigned
497        assert!(SqlType::Uint32.is_unsigned_integer());
498        assert!(SqlType::Uint64.is_unsigned_integer());
499        assert!(!SqlType::Int32.is_unsigned_integer());
500        assert!(!SqlType::Int64.is_unsigned_integer());
501    }
502
503    #[test]
504    fn test_is_floating_point() {
505        assert!(SqlType::Float32.is_floating_point());
506        assert!(SqlType::Float64.is_floating_point());
507        assert!(!SqlType::Int32.is_floating_point());
508        assert!(!SqlType::Int64.is_floating_point());
509        assert!(!SqlType::Numeric {
510            precision: None,
511            scale: None
512        }
513        .is_floating_point());
514        assert!(!SqlType::Varchar.is_floating_point());
515    }
516
517    #[test]
518    fn test_is_string() {
519        assert!(SqlType::Varchar.is_string());
520        assert!(!SqlType::Int64.is_string());
521        assert!(!SqlType::Varbinary.is_string());
522        assert!(!SqlType::Json.is_string());
523    }
524
525    #[test]
526    fn test_is_datetime() {
527        assert!(SqlType::Date.is_datetime());
528        assert!(SqlType::Time.is_datetime());
529        assert!(SqlType::Datetime.is_datetime());
530        assert!(SqlType::Timestamp.is_datetime());
531        assert!(!SqlType::Int64.is_datetime());
532        assert!(!SqlType::Varchar.is_datetime());
533        assert!(!SqlType::Interval.is_datetime());
534    }
535
536    #[test]
537    fn test_is_comparable_with() {
538        // Same types
539        assert!(SqlType::Int64.is_comparable_with(&SqlType::Int64));
540        assert!(SqlType::Varchar.is_comparable_with(&SqlType::Varchar));
541
542        // Numeric types are comparable
543        assert!(SqlType::Int64.is_comparable_with(&SqlType::Float64));
544        assert!(SqlType::Float64.is_comparable_with(&SqlType::Numeric {
545            precision: None,
546            scale: None
547        }));
548
549        // Date/time types are comparable
550        assert!(SqlType::Date.is_comparable_with(&SqlType::Timestamp));
551        assert!(SqlType::Time.is_comparable_with(&SqlType::Datetime));
552
553        // Unknown/Any are comparable with anything
554        assert!(SqlType::Unknown.is_comparable_with(&SqlType::Int64));
555        assert!(SqlType::Int64.is_comparable_with(&SqlType::Unknown));
556        assert!(SqlType::Any.is_comparable_with(&SqlType::Varchar));
557
558        // Incompatible types
559        assert!(!SqlType::Varchar.is_comparable_with(&SqlType::Int64));
560        assert!(!SqlType::Bool.is_comparable_with(&SqlType::Float64));
561    }
562
563    #[test]
564    fn test_element_type() {
565        let array_int = SqlType::Array(Box::new(SqlType::Int64));
566        assert_eq!(array_int.element_type(), Some(&SqlType::Int64));
567
568        let nested = SqlType::Array(Box::new(SqlType::Array(Box::new(SqlType::Varchar))));
569        assert_eq!(
570            nested.element_type(),
571            Some(&SqlType::Array(Box::new(SqlType::Varchar)))
572        );
573
574        assert_eq!(SqlType::Int64.element_type(), None);
575        assert_eq!(SqlType::Varchar.element_type(), None);
576    }
577
578    #[test]
579    fn test_struct_fields() {
580        let struct_type = SqlType::Struct(vec![
581            StructField {
582                name: Some("a".to_string()),
583                data_type: SqlType::Int64,
584            },
585            StructField {
586                name: Some("b".to_string()),
587                data_type: SqlType::Varchar,
588            },
589        ]);
590        let fields = struct_type.struct_fields().unwrap();
591        assert_eq!(fields.len(), 2);
592        assert_eq!(fields[0].name, Some("a".to_string()));
593        assert_eq!(fields[0].data_type, SqlType::Int64);
594
595        assert_eq!(SqlType::Int64.struct_fields(), None);
596    }
597
598    #[test]
599    fn test_display() {
600        assert_eq!(format!("{}", SqlType::Bool), "BOOLEAN");
601        assert_eq!(format!("{}", SqlType::Int32), "INTEGER");
602        assert_eq!(format!("{}", SqlType::Int64), "BIGINT");
603        assert_eq!(format!("{}", SqlType::Uint32), "UINTEGER");
604        assert_eq!(format!("{}", SqlType::Uint64), "UBIGINT");
605        assert_eq!(format!("{}", SqlType::Float32), "REAL");
606        assert_eq!(format!("{}", SqlType::Float64), "DOUBLE PRECISION");
607        assert_eq!(
608            format!(
609                "{}",
610                SqlType::Numeric {
611                    precision: None,
612                    scale: None
613                }
614            ),
615            "NUMERIC"
616        );
617        assert_eq!(
618            format!(
619                "{}",
620                SqlType::Numeric {
621                    precision: Some(10),
622                    scale: None
623                }
624            ),
625            "NUMERIC(10)"
626        );
627        assert_eq!(
628            format!(
629                "{}",
630                SqlType::Numeric {
631                    precision: Some(10),
632                    scale: Some(2)
633                }
634            ),
635            "NUMERIC(10, 2)"
636        );
637        assert_eq!(format!("{}", SqlType::Varchar), "VARCHAR");
638        assert_eq!(format!("{}", SqlType::Varbinary), "VARBINARY");
639        assert_eq!(
640            format!("{}", SqlType::Array(Box::new(SqlType::Int64))),
641            "ARRAY<BIGINT>"
642        );
643        assert_eq!(format!("{}", SqlType::Uuid), "UUID");
644        assert_eq!(format!("{}", SqlType::Unknown), "UNKNOWN");
645        assert_eq!(format!("{}", SqlType::Any), "ANY");
646    }
647}