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}