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}