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 /// 32-bit floating point number
103 F32,
104
105 /// 64-bit floating point number
106 F64,
107
108 /// 128-bit universally unique identifier (UUID)
109 Uuid,
110
111 /// An instance of a model key
112 Key(ModelId),
113
114 /// An instance of a model
115 Model(ModelId),
116
117 /// An instance of a foreign key for a specific relation
118 ForeignKey(FieldId),
119
120 /// A list of a single type
121 List(Box<Type>),
122
123 /// A fixed-length tuple where each item can have a different type.
124 Record(Vec<Type>),
125
126 /// A byte array, more efficient than `List(U8)`.
127 Bytes,
128
129 /// A fixed-precision decimal number.
130 /// See [`rust_decimal::Decimal`].
131 #[cfg(feature = "rust_decimal")]
132 Decimal,
133
134 /// An arbitrary-precision decimal number.
135 /// See [`bigdecimal::BigDecimal`].
136 #[cfg(feature = "bigdecimal")]
137 BigDecimal,
138
139 /// An instant in time represented as the number of nanoseconds since the Unix epoch.
140 /// See [`jiff::Timestamp`].
141 #[cfg(feature = "jiff")]
142 Timestamp,
143
144 /// A time zone aware instant in time.
145 /// See [`jiff::Zoned`]
146 #[cfg(feature = "jiff")]
147 Zoned,
148
149 /// A representation of a civil date in the Gregorian calendar.
150 /// See [`jiff::civil::Date`].
151 #[cfg(feature = "jiff")]
152 Date,
153
154 /// A representation of civil “wall clock” time.
155 /// See [`jiff::civil::Time`].
156 #[cfg(feature = "jiff")]
157 Time,
158
159 /// A representation of a civil datetime in the Gregorian calendar.
160 /// See [`jiff::civil::DateTime`].
161 #[cfg(feature = "jiff")]
162 DateTime,
163
164 /// The null type. Represents the type of a null value and is cast-able to
165 /// any type. Also used as the element type of an empty list whose item type
166 /// is not yet known.
167 Null,
168
169 /// A record type where only a subset of fields are populated, identified
170 /// by a [`PathFieldSet`].
171 SparseRecord(PathFieldSet),
172
173 /// Unit type
174 Unit,
175
176 /// A type that could not be inferred (e.g., empty list)
177 Unknown,
178
179 /// A union of possible types.
180 ///
181 /// Used when a match expression's arms can produce values of different types
182 /// (e.g., a mixed enum where unit arms return `I64` and data arms return
183 /// `Record`). A value is compatible with a union if it satisfies any of the
184 /// member types.
185 Union(TypeUnion),
186}
187
188impl Type {
189 /// Creates a [`Type::List`] wrapping the given element type.
190 ///
191 /// # Examples
192 ///
193 /// ```
194 /// # use toasty_core::stmt::Type;
195 /// let ty = Type::list(Type::String);
196 /// assert!(ty.is_list());
197 /// ```
198 pub fn list(ty: impl Into<Self>) -> Self {
199 Self::List(Box::new(ty.into()))
200 }
201
202 /// Returns the element type of this list type, panicking if this is not
203 /// a [`Type::List`].
204 ///
205 /// # Panics
206 ///
207 /// Panics if the type is not a `List` variant.
208 #[track_caller]
209 pub fn as_list_unwrap(&self) -> &Type {
210 match self {
211 stmt::Type::List(items) => items,
212 _ => panic!("expected stmt::Type::List; actual={self:#?}"),
213 }
214 }
215
216 /// Returns `true` if this is [`Type::Bool`].
217 pub fn is_bool(&self) -> bool {
218 matches!(self, Self::Bool)
219 }
220
221 /// Returns `true` if this is [`Type::Model`].
222 pub fn is_model(&self) -> bool {
223 matches!(self, Self::Model(_))
224 }
225
226 /// Returns `true` if this is [`Type::List`].
227 pub fn is_list(&self) -> bool {
228 matches!(self, Self::List(_))
229 }
230
231 /// Returns `true` if this is [`Type::String`].
232 pub fn is_string(&self) -> bool {
233 matches!(self, Self::String)
234 }
235
236 /// Returns `true` if this is [`Type::Unit`].
237 pub fn is_unit(&self) -> bool {
238 matches!(self, Self::Unit)
239 }
240
241 /// Returns `true` if this is [`Type::Record`].
242 pub fn is_record(&self) -> bool {
243 matches!(self, Self::Record(..))
244 }
245
246 /// Returns `true` if this is [`Type::Bytes`].
247 pub fn is_bytes(&self) -> bool {
248 matches!(self, Self::Bytes)
249 }
250
251 /// Returns `true` if this is [`Type::Decimal`] (requires `rust_decimal` feature).
252 pub fn is_decimal(&self) -> bool {
253 #[cfg(feature = "rust_decimal")]
254 {
255 matches!(self, Self::Decimal)
256 }
257 #[cfg(not(feature = "rust_decimal"))]
258 {
259 false
260 }
261 }
262
263 /// Returns `true` if this is [`Type::BigDecimal`] (requires `bigdecimal` feature).
264 pub fn is_big_decimal(&self) -> bool {
265 #[cfg(feature = "bigdecimal")]
266 {
267 matches!(self, Self::BigDecimal)
268 }
269 #[cfg(not(feature = "bigdecimal"))]
270 {
271 false
272 }
273 }
274
275 /// Returns `true` if this is [`Type::Uuid`].
276 pub fn is_uuid(&self) -> bool {
277 matches!(self, Self::Uuid)
278 }
279
280 /// Returns `true` if this is [`Type::SparseRecord`].
281 pub fn is_sparse_record(&self) -> bool {
282 matches!(self, Self::SparseRecord(..))
283 }
284
285 /// Returns `true` if this type is a numeric integer type.
286 ///
287 /// Numeric types include all signed and unsigned integer types:
288 /// `I8`, `I16`, `I32`, `I64`, `U8`, `U16`, `U32`, `U64`.
289 ///
290 /// This does not include decimal types or floating-point types.
291 ///
292 /// # Examples
293 ///
294 /// ```
295 /// # use toasty_core::stmt::Type;
296 /// assert!(Type::I32.is_numeric());
297 /// assert!(Type::U64.is_numeric());
298 /// assert!(!Type::String.is_numeric());
299 /// assert!(!Type::Bool.is_numeric());
300 /// ```
301 pub fn is_numeric(&self) -> bool {
302 matches!(
303 self,
304 Self::I8
305 | Self::I16
306 | Self::I32
307 | Self::I64
308 | Self::U8
309 | Self::U16
310 | Self::U32
311 | Self::U64
312 )
313 }
314
315 /// Casts `value` to this type, returning the converted value.
316 ///
317 /// Null values pass through unchanged. Supported conversions include
318 /// identity casts, string/UUID interchange, string/decimal interchange,
319 /// record-to-sparse-record, and integer width conversions.
320 ///
321 /// # Errors
322 ///
323 /// Returns an error if the conversion is not supported or if the value
324 /// is out of range for the target type.
325 pub fn cast(&self, value: Value) -> Result<Value> {
326 use stmt::Value;
327
328 // Null values are passed through
329 if value.is_null() {
330 return Ok(value);
331 }
332
333 #[cfg(feature = "jiff")]
334 if let Some(value) = self.cast_jiff(&value)? {
335 return Ok(value);
336 }
337
338 Ok(match (value, self) {
339 // Identity
340 (value @ Value::String(_), Self::String) => value,
341 // String <-> Uuid
342 (Value::Uuid(value), Self::String) => Value::String(value.to_string()),
343 (Value::String(value), Self::Uuid) => {
344 Value::Uuid(value.parse().expect("could not parse uuid"))
345 }
346 // Bytes <-> Uuid
347 (Value::Uuid(value), Self::Bytes) => Value::Bytes(value.as_bytes().to_vec()),
348 (Value::Bytes(value), Self::Uuid) => {
349 let bytes = value.clone();
350 Value::Uuid(
351 value
352 .try_into()
353 .map_err(|_| crate::Error::type_conversion(Value::Bytes(bytes), "Uuid"))?,
354 )
355 }
356 // String <-> Decimal
357 #[cfg(feature = "rust_decimal")]
358 (Value::Decimal(value), Self::String) => Value::String(value.to_string()),
359 #[cfg(feature = "rust_decimal")]
360 (Value::String(value), Self::Decimal) => {
361 Value::Decimal(value.parse().expect("could not parse Decimal"))
362 }
363 // String <-> BigDecimal
364 #[cfg(feature = "bigdecimal")]
365 (Value::BigDecimal(value), Self::String) => Value::String(value.to_string()),
366 #[cfg(feature = "bigdecimal")]
367 (Value::String(value), Self::BigDecimal) => {
368 Value::BigDecimal(value.parse().expect("could not parse BigDecimal"))
369 }
370 // Record <-> SparseRecord
371 (Value::Record(record), Self::SparseRecord(fields)) => {
372 Value::sparse_record(fields.clone(), record)
373 }
374 // Integer conversions - use TryFrom which provides error messages
375 (value, Self::I8) => Value::I8(i8::try_from(value)?),
376 (value, Self::I16) => Value::I16(i16::try_from(value)?),
377 (value, Self::I32) => Value::I32(i32::try_from(value)?),
378 (value, Self::I64) => Value::I64(i64::try_from(value)?),
379 (value, Self::U8) => Value::U8(u8::try_from(value)?),
380 (value, Self::U16) => Value::U16(u16::try_from(value)?),
381 (value, Self::U32) => Value::U32(u32::try_from(value)?),
382 (value, Self::U64) => Value::U64(u64::try_from(value)?),
383 // Float casts
384 (Value::F32(v), Self::F32) => Value::F32(v),
385 (Value::F64(v), Self::F32) => {
386 let converted = v as f32;
387 if converted.is_infinite() && !v.is_infinite() {
388 return Err(crate::Error::type_conversion(
389 Value::F64(v),
390 "f32 (overflow)",
391 ));
392 }
393 Value::F32(converted)
394 }
395 (Value::F32(v), Self::F64) => Value::F64(v as f64),
396 (Value::F64(v), Self::F64) => Value::F64(v),
397 (value, _) => todo!("value={value:#?}; ty={self:#?}"),
398 })
399 }
400
401 /// Checks whether `self` (the actual/inferred type) is assignable to `other`
402 /// (the expected type).
403 ///
404 /// This is a subtype check, NOT strict equality:
405 /// - [`Type::Null`] matches any type (in either direction), since it represents
406 /// "we don't know what type this is"
407 /// - A concrete type is assignable to a [`Type::Union`] if it matches any member
408 /// - A [`Type::Union`] is assignable to another union if every member of `self`
409 /// matches some member of `other`
410 /// - Container types ([`Type::List`], [`Type::Record`]) check element/field
411 /// types recursively
412 ///
413 /// # Examples
414 ///
415 /// - `String.is_subtype_of(String)` -> true
416 /// - `String.is_subtype_of(Null)` -> true
417 /// - `String.is_subtype_of(Bytes)` -> false
418 /// - `Record([...]).is_subtype_of(Union([I64, Record([...])]))` -> true
419 /// - `I64.is_subtype_of(Union([I64, Record(...)]))` -> true
420 /// - `String.is_subtype_of(Union([I64, Record(...)]))` -> false
421 pub fn is_subtype_of(&self, other: &Type) -> bool {
422 // Null matches anything (commutative)
423 if matches!(self, Type::Null) || matches!(other, Type::Null) {
424 return true;
425 }
426
427 match (self, other) {
428 // Simple types must match exactly
429 (Type::Bool, Type::Bool) => true,
430 (Type::String, Type::String) => true,
431 (Type::I8, Type::I8) => true,
432 (Type::I16, Type::I16) => true,
433 (Type::I32, Type::I32) => true,
434 (Type::I64, Type::I64) => true,
435 (Type::U8, Type::U8) => true,
436 (Type::U16, Type::U16) => true,
437 (Type::U32, Type::U32) => true,
438 (Type::U64, Type::U64) => true,
439 (Type::F32, Type::F32) => true,
440 (Type::F64, Type::F64) => true,
441 (Type::Uuid, Type::Uuid) => true,
442 (Type::Bytes, Type::Bytes) => true,
443 (Type::Unit, Type::Unit) => true,
444 (Type::Unknown, Type::Unknown) => true,
445
446 // Decimal types
447 #[cfg(feature = "rust_decimal")]
448 (Type::Decimal, Type::Decimal) => true,
449 #[cfg(feature = "bigdecimal")]
450 (Type::BigDecimal, Type::BigDecimal) => true,
451
452 // Temporal types
453 #[cfg(feature = "jiff")]
454 (Type::Timestamp, Type::Timestamp) => true,
455 #[cfg(feature = "jiff")]
456 (Type::Zoned, Type::Zoned) => true,
457 #[cfg(feature = "jiff")]
458 (Type::Date, Type::Date) => true,
459 #[cfg(feature = "jiff")]
460 (Type::Time, Type::Time) => true,
461 #[cfg(feature = "jiff")]
462 (Type::DateTime, Type::DateTime) => true,
463
464 // Model-related types must match model IDs
465 (Type::Key(a), Type::Key(b)) => a == b,
466 (Type::Model(a), Type::Model(b)) => a == b,
467 (Type::ForeignKey(a), Type::ForeignKey(b)) => a == b,
468
469 // List types: element type must be assignable
470 (Type::List(a), Type::List(b)) => a.is_subtype_of(b),
471
472 // Record types: same length and all fields recursively assignable
473 (Type::Record(a), Type::Record(b)) => {
474 a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a.is_subtype_of(b))
475 }
476
477 // Sparse records must have the same field set
478 (Type::SparseRecord(a), Type::SparseRecord(b)) => a == b,
479
480 // Union-to-Union: every member of self must be assignable to some member of other
481 (Type::Union(a), Type::Union(b)) => a
482 .iter()
483 .all(|a_ty| b.iter().any(|b_ty| a_ty.is_subtype_of(b_ty))),
484
485 // Concrete type assignable to union if it matches any member
486 (ty, Type::Union(union)) => union.iter().any(|member| ty.is_subtype_of(member)),
487
488 // Union assignable to concrete type if every member is assignable
489 (Type::Union(union), other) => union.iter().all(|member| member.is_subtype_of(other)),
490
491 // Different type variants are not assignable
492 _ => false,
493 }
494 }
495}
496
497impl From<&Self> for Type {
498 fn from(value: &Self) -> Self {
499 value.clone()
500 }
501}
502
503impl From<ModelId> for Type {
504 fn from(value: ModelId) -> Self {
505 Self::Model(value)
506 }
507}