toasty_core/stmt/
ty.rs

1use super::{PathFieldSet, TypeUnion, Value};
2use crate::{
3    Result,
4    schema::app::{FieldId, ModelId},
5    stmt,
6};
7
8/// Statement-level type system for values and expressions within Toasty's query engine.
9///
10/// `stmt::Type` represents types at both the **application level** (models, fields, Rust types)
11/// and the **query engine level** (tables, columns, internal processing). These types are
12/// **internal to Toasty** - they describe how Toasty views and processes data throughout the
13/// entire query pipeline, from user queries to driver execution.
14///
15/// # Distinction from Database Types
16///
17/// Toasty has two distinct type systems:
18///
19/// 1. **`stmt::Type`** (this type): Application and query engine types
20///    - Types of [`stmt::Value`] and [`stmt::Expr`] throughout query processing
21///    - Represents Rust primitive types: `I8`, `I16`, `String`, etc.
22///    - Works at both model level (application) and table/column level (engine)
23///    - Internal to Toasty's query processing pipeline
24///
25/// 2. **[`schema::db::Type`](crate::schema::db::Type)**: Database storage types
26///    - External representation for the target database
27///    - Database-specific types: `Integer(n)`, `Text`, `VarChar(n)`, etc.
28///    - Used only at the driver boundary when generating database queries
29///
30/// The key distinction: `stmt::Type` is how **Toasty** views types internally, while
31/// [`schema::db::Type`](crate::schema::db::Type) is how the **database** stores them externally.
32///
33/// # Query Processing Pipeline
34///
35/// Throughout query processing, all values and expressions are typed using `stmt::Type`,
36/// even as they are transformed and converted:
37///
38/// **Application Level (Model/Field)**
39/// - User writes queries referencing models and fields
40/// - Types like `stmt::Type::Model(UserId)`, `stmt::Type::String`
41/// - Values like `stmt::Value::String("alice")`, `stmt::Value::I64(42)`
42///
43/// **Query Engine Level (Table/Column)**
44/// - During planning, queries are "lowered" from models to tables
45/// - Values may be converted between types (e.g., Model → Record, Id → String)
46/// - All conversions are from `stmt::Type` to `stmt::Type`
47/// - Still using the same type system, now at table/column abstraction level
48///
49/// **Driver Boundary (Database Storage)**
50/// - Statements with `stmt::Value` (typed by `stmt::Type`) passed to drivers
51/// - Driver consults schema to map `stmt::Type` → [`schema::db::Type`](crate::schema::db::Type)
52/// - Same `stmt::Type::String` may map to different database types based on schema configuration
53///
54/// # Schema Representation
55///
56/// Each column in the database schema stores both type representations:
57/// - `column.ty: stmt::Type` - How Toasty views this column internally
58/// - `column.storage_ty: Option<db::Type>` - How the database stores it externally
59///
60/// This dual representation enables flexible mapping. For instance, `stmt::Type::String`
61/// might map to `db::Type::Text` in one column and `db::Type::VarChar(100)` in another,
62/// depending on schema configuration and database capabilities.
63///
64/// # See Also
65///
66/// - [`schema::db::Type`](crate::schema::db::Type) External database storage types
67/// - [`stmt::Value`] - Values typed by this system
68/// - [`stmt::Expr`] - Expressions typed by this system
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71pub enum Type {
72    /// Boolean value
73    Bool,
74
75    /// String type
76    String,
77
78    /// Signed 8-bit integer
79    I8,
80
81    /// Signed 16-bit integer
82    I16,
83
84    /// Signed 32-bit integer
85    I32,
86
87    /// Signed 64-bit integer
88    I64,
89
90    /// Unsigned 8-bit integer
91    U8,
92
93    /// Unsigned 16-bit integer
94    U16,
95
96    /// Unsigned 32-bit integer
97    U32,
98
99    /// Unsigned 64-bit integer
100    U64,
101
102    /// 128-bit universally unique identifier (UUID)
103    Uuid,
104
105    /// An instance of a model key
106    Key(ModelId),
107
108    /// An instance of a model
109    Model(ModelId),
110
111    /// An instance of a foreign key for a specific relation
112    ForeignKey(FieldId),
113
114    /// A list of a single type
115    List(Box<Type>),
116
117    /// A fixed-length tuple where each item can have a different type.
118    Record(Vec<Type>),
119
120    /// A byte array, more efficient than `List(U8)`.
121    Bytes,
122
123    /// A fixed-precision decimal number.
124    /// See [`rust_decimal::Decimal`].
125    #[cfg(feature = "rust_decimal")]
126    Decimal,
127
128    /// An arbitrary-precision decimal number.
129    /// See [`bigdecimal::BigDecimal`].
130    #[cfg(feature = "bigdecimal")]
131    BigDecimal,
132
133    /// An instant in time represented as the number of nanoseconds since the Unix epoch.
134    /// See [`jiff::Timestamp`].
135    #[cfg(feature = "jiff")]
136    Timestamp,
137
138    /// A time zone aware instant in time.
139    /// See [`jiff::Zoned`]
140    #[cfg(feature = "jiff")]
141    Zoned,
142
143    /// A representation of a civil date in the Gregorian calendar.
144    /// See [`jiff::civil::Date`].
145    #[cfg(feature = "jiff")]
146    Date,
147
148    /// A representation of civil “wall clock” time.
149    /// See [`jiff::civil::Time`].
150    #[cfg(feature = "jiff")]
151    Time,
152
153    /// A representation of a civil datetime in the Gregorian calendar.
154    /// See [`jiff::civil::DateTime`].
155    #[cfg(feature = "jiff")]
156    DateTime,
157
158    /// The null type. Represents the type of a null value and is cast-able to
159    /// any type. Also used as the element type of an empty list whose item type
160    /// is not yet known.
161    Null,
162
163    /// A record type where only a subset of fields are populated, identified
164    /// by a [`PathFieldSet`].
165    SparseRecord(PathFieldSet),
166
167    /// Unit type
168    Unit,
169
170    /// A type that could not be inferred (e.g., empty list)
171    Unknown,
172
173    /// A union of possible types.
174    ///
175    /// Used when a match expression's arms can produce values of different types
176    /// (e.g., a mixed enum where unit arms return `I64` and data arms return
177    /// `Record`). A value is compatible with a union if it satisfies any of the
178    /// member types.
179    Union(TypeUnion),
180}
181
182impl Type {
183    /// Creates a [`Type::List`] wrapping the given element type.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// # use toasty_core::stmt::Type;
189    /// let ty = Type::list(Type::String);
190    /// assert!(ty.is_list());
191    /// ```
192    pub fn list(ty: impl Into<Self>) -> Self {
193        Self::List(Box::new(ty.into()))
194    }
195
196    /// Returns the element type of this list type, panicking if this is not
197    /// a [`Type::List`].
198    ///
199    /// # Panics
200    ///
201    /// Panics if the type is not a `List` variant.
202    #[track_caller]
203    pub fn as_list_unwrap(&self) -> &Type {
204        match self {
205            stmt::Type::List(items) => items,
206            _ => panic!("expected stmt::Type::List; actual={self:#?}"),
207        }
208    }
209
210    /// Returns `true` if this is [`Type::Bool`].
211    pub fn is_bool(&self) -> bool {
212        matches!(self, Self::Bool)
213    }
214
215    /// Returns `true` if this is [`Type::Model`].
216    pub fn is_model(&self) -> bool {
217        matches!(self, Self::Model(_))
218    }
219
220    /// Returns `true` if this is [`Type::List`].
221    pub fn is_list(&self) -> bool {
222        matches!(self, Self::List(_))
223    }
224
225    /// Returns `true` if this is [`Type::String`].
226    pub fn is_string(&self) -> bool {
227        matches!(self, Self::String)
228    }
229
230    /// Returns `true` if this is [`Type::Unit`].
231    pub fn is_unit(&self) -> bool {
232        matches!(self, Self::Unit)
233    }
234
235    /// Returns `true` if this is [`Type::Record`].
236    pub fn is_record(&self) -> bool {
237        matches!(self, Self::Record(..))
238    }
239
240    /// Returns `true` if this is [`Type::Bytes`].
241    pub fn is_bytes(&self) -> bool {
242        matches!(self, Self::Bytes)
243    }
244
245    /// Returns `true` if this is [`Type::Decimal`] (requires `rust_decimal` feature).
246    pub fn is_decimal(&self) -> bool {
247        #[cfg(feature = "rust_decimal")]
248        {
249            matches!(self, Self::Decimal)
250        }
251        #[cfg(not(feature = "rust_decimal"))]
252        {
253            false
254        }
255    }
256
257    /// Returns `true` if this is [`Type::BigDecimal`] (requires `bigdecimal` feature).
258    pub fn is_big_decimal(&self) -> bool {
259        #[cfg(feature = "bigdecimal")]
260        {
261            matches!(self, Self::BigDecimal)
262        }
263        #[cfg(not(feature = "bigdecimal"))]
264        {
265            false
266        }
267    }
268
269    /// Returns `true` if this is [`Type::Uuid`].
270    pub fn is_uuid(&self) -> bool {
271        matches!(self, Self::Uuid)
272    }
273
274    /// Returns `true` if this is [`Type::SparseRecord`].
275    pub fn is_sparse_record(&self) -> bool {
276        matches!(self, Self::SparseRecord(..))
277    }
278
279    /// Returns `true` if this type is a numeric integer type.
280    ///
281    /// Numeric types include all signed and unsigned integer types:
282    /// `I8`, `I16`, `I32`, `I64`, `U8`, `U16`, `U32`, `U64`.
283    ///
284    /// This does not include decimal types or floating-point types.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// # use toasty_core::stmt::Type;
290    /// assert!(Type::I32.is_numeric());
291    /// assert!(Type::U64.is_numeric());
292    /// assert!(!Type::String.is_numeric());
293    /// assert!(!Type::Bool.is_numeric());
294    /// ```
295    pub fn is_numeric(&self) -> bool {
296        matches!(
297            self,
298            Self::I8
299                | Self::I16
300                | Self::I32
301                | Self::I64
302                | Self::U8
303                | Self::U16
304                | Self::U32
305                | Self::U64
306        )
307    }
308
309    /// Casts `value` to this type, returning the converted value.
310    ///
311    /// Null values pass through unchanged. Supported conversions include
312    /// identity casts, string/UUID interchange, string/decimal interchange,
313    /// record-to-sparse-record, and integer width conversions.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if the conversion is not supported or if the value
318    /// is out of range for the target type.
319    pub fn cast(&self, value: Value) -> Result<Value> {
320        use stmt::Value;
321
322        // Null values are passed through
323        if value.is_null() {
324            return Ok(value);
325        }
326
327        #[cfg(feature = "jiff")]
328        if let Some(value) = self.cast_jiff(&value)? {
329            return Ok(value);
330        }
331
332        Ok(match (value, self) {
333            // Identity
334            (value @ Value::String(_), Self::String) => value,
335            // String <-> Uuid
336            (Value::Uuid(value), Self::String) => Value::String(value.to_string()),
337            (Value::String(value), Self::Uuid) => {
338                Value::Uuid(value.parse().expect("could not parse uuid"))
339            }
340            // Bytes <-> Uuid
341            (Value::Uuid(value), Self::Bytes) => Value::Bytes(value.as_bytes().to_vec()),
342            (Value::Bytes(value), Self::Uuid) => {
343                let bytes = value.clone();
344                Value::Uuid(
345                    value
346                        .try_into()
347                        .map_err(|_| crate::Error::type_conversion(Value::Bytes(bytes), "Uuid"))?,
348                )
349            }
350            // String <-> Decimal
351            #[cfg(feature = "rust_decimal")]
352            (Value::Decimal(value), Self::String) => Value::String(value.to_string()),
353            #[cfg(feature = "rust_decimal")]
354            (Value::String(value), Self::Decimal) => {
355                Value::Decimal(value.parse().expect("could not parse Decimal"))
356            }
357            // String <-> BigDecimal
358            #[cfg(feature = "bigdecimal")]
359            (Value::BigDecimal(value), Self::String) => Value::String(value.to_string()),
360            #[cfg(feature = "bigdecimal")]
361            (Value::String(value), Self::BigDecimal) => {
362                Value::BigDecimal(value.parse().expect("could not parse BigDecimal"))
363            }
364            // Record <-> SparseRecord
365            (Value::Record(record), Self::SparseRecord(fields)) => {
366                Value::sparse_record(fields.clone(), record)
367            }
368            // Integer conversions - use TryFrom which provides error messages
369            (value, Self::I8) => Value::I8(i8::try_from(value)?),
370            (value, Self::I16) => Value::I16(i16::try_from(value)?),
371            (value, Self::I32) => Value::I32(i32::try_from(value)?),
372            (value, Self::I64) => Value::I64(i64::try_from(value)?),
373            (value, Self::U8) => Value::U8(u8::try_from(value)?),
374            (value, Self::U16) => Value::U16(u16::try_from(value)?),
375            (value, Self::U32) => Value::U32(u32::try_from(value)?),
376            (value, Self::U64) => Value::U64(u64::try_from(value)?),
377            (value, _) => todo!("value={value:#?}; ty={self:#?}"),
378        })
379    }
380
381    /// Checks whether `self` (the actual/inferred type) is assignable to `other`
382    /// (the expected type).
383    ///
384    /// This is a subtype check, NOT strict equality:
385    /// - [`Type::Null`] matches any type (in either direction), since it represents
386    ///   "we don't know what type this is"
387    /// - A concrete type is assignable to a [`Type::Union`] if it matches any member
388    /// - A [`Type::Union`] is assignable to another union if every member of `self`
389    ///   matches some member of `other`
390    /// - Container types ([`Type::List`], [`Type::Record`]) check element/field
391    ///   types recursively
392    ///
393    /// # Examples
394    ///
395    /// - `String.is_subtype_of(String)` -> true
396    /// - `String.is_subtype_of(Null)` -> true
397    /// - `String.is_subtype_of(Bytes)` -> false
398    /// - `Record([...]).is_subtype_of(Union([I64, Record([...])]))` -> true
399    /// - `I64.is_subtype_of(Union([I64, Record(...)]))` -> true
400    /// - `String.is_subtype_of(Union([I64, Record(...)]))` -> false
401    pub fn is_subtype_of(&self, other: &Type) -> bool {
402        // Null matches anything (commutative)
403        if matches!(self, Type::Null) || matches!(other, Type::Null) {
404            return true;
405        }
406
407        match (self, other) {
408            // Simple types must match exactly
409            (Type::Bool, Type::Bool) => true,
410            (Type::String, Type::String) => true,
411            (Type::I8, Type::I8) => true,
412            (Type::I16, Type::I16) => true,
413            (Type::I32, Type::I32) => true,
414            (Type::I64, Type::I64) => true,
415            (Type::U8, Type::U8) => true,
416            (Type::U16, Type::U16) => true,
417            (Type::U32, Type::U32) => true,
418            (Type::U64, Type::U64) => true,
419            (Type::Uuid, Type::Uuid) => true,
420            (Type::Bytes, Type::Bytes) => true,
421            (Type::Unit, Type::Unit) => true,
422            (Type::Unknown, Type::Unknown) => true,
423
424            // Decimal types
425            #[cfg(feature = "rust_decimal")]
426            (Type::Decimal, Type::Decimal) => true,
427            #[cfg(feature = "bigdecimal")]
428            (Type::BigDecimal, Type::BigDecimal) => true,
429
430            // Temporal types
431            #[cfg(feature = "jiff")]
432            (Type::Timestamp, Type::Timestamp) => true,
433            #[cfg(feature = "jiff")]
434            (Type::Zoned, Type::Zoned) => true,
435            #[cfg(feature = "jiff")]
436            (Type::Date, Type::Date) => true,
437            #[cfg(feature = "jiff")]
438            (Type::Time, Type::Time) => true,
439            #[cfg(feature = "jiff")]
440            (Type::DateTime, Type::DateTime) => true,
441
442            // Model-related types must match model IDs
443            (Type::Key(a), Type::Key(b)) => a == b,
444            (Type::Model(a), Type::Model(b)) => a == b,
445            (Type::ForeignKey(a), Type::ForeignKey(b)) => a == b,
446
447            // List types: element type must be assignable
448            (Type::List(a), Type::List(b)) => a.is_subtype_of(b),
449
450            // Record types: same length and all fields recursively assignable
451            (Type::Record(a), Type::Record(b)) => {
452                a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a.is_subtype_of(b))
453            }
454
455            // Sparse records must have the same field set
456            (Type::SparseRecord(a), Type::SparseRecord(b)) => a == b,
457
458            // Union-to-Union: every member of self must be assignable to some member of other
459            (Type::Union(a), Type::Union(b)) => a
460                .iter()
461                .all(|a_ty| b.iter().any(|b_ty| a_ty.is_subtype_of(b_ty))),
462
463            // Concrete type assignable to union if it matches any member
464            (ty, Type::Union(union)) => union.iter().any(|member| ty.is_subtype_of(member)),
465
466            // Union assignable to concrete type if every member is assignable
467            (Type::Union(union), other) => union.iter().all(|member| member.is_subtype_of(other)),
468
469            // Different type variants are not assignable
470            _ => false,
471        }
472    }
473}
474
475impl From<&Self> for Type {
476    fn from(value: &Self) -> Self {
477        value.clone()
478    }
479}
480
481impl From<ModelId> for Type {
482    fn from(value: ModelId) -> Self {
483        Self::Model(value)
484    }
485}