toasty_core/stmt/
ty.rs

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