toasty_core/driver/capability.rs
1use crate::{schema::db, stmt};
2
3/// Describes what a database driver supports.
4///
5/// The query planner reads these flags to decide which [`Operation`](super::Operation)
6/// variants to generate. For example, a SQL driver sets `sql: true` and
7/// receives `QuerySql` operations, while DynamoDB sets `sql: false` and
8/// receives key-value operations like `GetByKey` and `QueryPk`.
9///
10/// Pre-built configurations are available as associated constants:
11/// [`SQLITE`](Self::SQLITE), [`POSTGRESQL`](Self::POSTGRESQL),
12/// [`MYSQL`](Self::MYSQL), and [`DYNAMODB`](Self::DYNAMODB).
13///
14/// # Examples
15///
16/// ```
17/// use toasty_core::driver::Capability;
18///
19/// let cap = &Capability::SQLITE;
20/// assert!(cap.sql);
21/// assert!(cap.returning_from_mutation);
22/// assert!(!cap.select_for_update);
23/// ```
24#[derive(Debug)]
25pub struct Capability {
26 /// When `true`, the database uses a SQL-based query language and the
27 /// planner will emit [`QuerySql`](super::operation::QuerySql) operations.
28 pub sql: bool,
29
30 /// Column storage types supported by the database.
31 pub storage_types: StorageTypes,
32
33 /// Schema mutation capabilities supported by the datbase.
34 pub schema_mutations: SchemaMutations,
35
36 /// SQL: supports update statements in CTE queries.
37 pub cte_with_update: bool,
38
39 /// SQL: Supports row-level locking. If false, then the driver is expected
40 /// to serializable transaction-level isolation.
41 pub select_for_update: bool,
42
43 /// SQL: Mysql doesn't support returning clauses from insert / update queries
44 pub returning_from_mutation: bool,
45
46 /// DynamoDB does not support != predicates on the primary key.
47 pub primary_key_ne_predicate: bool,
48
49 /// Whether the database has an auto increment modifier for integer columns.
50 pub auto_increment: bool,
51
52 /// Whether the database supports `VARCHAR(n)` column types natively.
53 ///
54 /// Must be consistent with [`StorageTypes::varchar`]: when `true`,
55 /// `varchar` must be `Some`; when `false`, `varchar` must be `None`.
56 /// Use [`Capability::validate`] to check this invariant.
57 pub native_varchar: bool,
58
59 /// Whether the database has native support for Timestamp types.
60 pub native_timestamp: bool,
61
62 /// Whether the database has native support for Date types.
63 pub native_date: bool,
64
65 /// Whether the database has native support for Time types.
66 pub native_time: bool,
67
68 /// Whether the database has native support for DateTime types.
69 pub native_datetime: bool,
70
71 /// Whether the database has native support for Decimal types.
72 pub native_decimal: bool,
73
74 /// Whether BigDecimal driver support is implemented.
75 /// TODO: Remove this flag when PostgreSQL BigDecimal support is implemented.
76 /// Currently only MySQL has implemented BigDecimal driver support.
77 pub bigdecimal_implemented: bool,
78
79 /// Whether the database's decimal type supports arbitrary precision.
80 /// When false, the decimal type requires fixed precision and scale to be specified upfront.
81 /// - PostgreSQL: true (NUMERIC supports arbitrary precision)
82 /// - MySQL: false (DECIMAL requires fixed precision/scale)
83 /// - SQLite/DynamoDB: false (no native decimal support, stored as TEXT)
84 pub decimal_arbitrary_precision: bool,
85
86 /// Whether OR is supported in index key conditions (e.g. DynamoDB KeyConditionExpression).
87 /// DynamoDB: false. All other backends: true (SQL backends never use index key conditions).
88 pub index_or_predicate: bool,
89
90 /// Whether to test connection pool behavior.
91 /// TODO: We only need this for the `connection_per_clone.rs` test, come up with a better way.
92 pub test_connection_pool: bool,
93}
94
95/// Maps application-level types to the concrete database column types used for
96/// storage.
97///
98/// Each database has different native type support. For example, PostgreSQL has
99/// a native `UUID` type while SQLite stores UUIDs as `BLOB`. This struct
100/// captures those mappings so the schema layer can generate correct DDL and the
101/// driver can encode/decode values appropriately.
102///
103/// Pre-built configurations: [`SQLITE`](Self::SQLITE),
104/// [`POSTGRESQL`](Self::POSTGRESQL), [`MYSQL`](Self::MYSQL),
105/// [`DYNAMODB`](Self::DYNAMODB).
106///
107/// # Examples
108///
109/// ```
110/// use toasty_core::driver::StorageTypes;
111///
112/// let st = &StorageTypes::POSTGRESQL;
113/// // PostgreSQL stores UUIDs natively
114/// assert!(matches!(st.default_uuid_type, toasty_core::schema::db::Type::Uuid));
115/// ```
116#[derive(Debug)]
117pub struct StorageTypes {
118 /// The default storage type for a string.
119 pub default_string_type: db::Type,
120
121 /// When `Some` the database supports varchar types with the specified upper
122 /// limit.
123 pub varchar: Option<u64>,
124
125 /// The default storage type for a UUID.
126 pub default_uuid_type: db::Type,
127
128 /// The default storage type for Bytes (Vec<u8>).
129 pub default_bytes_type: db::Type,
130
131 /// The default storage type for a Decimal (fixed-precision decimal).
132 pub default_decimal_type: db::Type,
133
134 /// The default storage type for a BigDecimal (arbitrary-precision decimal).
135 pub default_bigdecimal_type: db::Type,
136
137 /// The default storage type for a Timestamp (instant in time).
138 pub default_timestamp_type: db::Type,
139
140 /// The default storage type for a Zoned (timezone-aware instant).
141 pub default_zoned_type: db::Type,
142
143 /// The default storage type for a Date (civil date).
144 pub default_date_type: db::Type,
145
146 /// The default storage type for a Time (wall clock time).
147 pub default_time_type: db::Type,
148
149 /// The default storage type for a DateTime (civil datetime).
150 pub default_datetime_type: db::Type,
151
152 /// Maximum value for unsigned integers. When `Some`, unsigned integers
153 /// are limited to this value. When `None`, full u64 range is supported.
154 pub max_unsigned_integer: Option<u64>,
155}
156
157/// The database's capabilities to mutate the schema (tables, columns, indices).
158///
159/// Used by the migration generator to decide how to express schema changes.
160/// For example, SQLite cannot alter column types so migrations must recreate
161/// the table instead.
162///
163/// Pre-built configurations: [`SQLITE`](Self::SQLITE),
164/// [`POSTGRESQL`](Self::POSTGRESQL), [`MYSQL`](Self::MYSQL),
165/// [`DYNAMODB`](Self::DYNAMODB).
166///
167/// # Examples
168///
169/// Access through [`Capability::schema_mutations`]:
170///
171/// ```
172/// use toasty_core::driver::Capability;
173///
174/// let cap = &Capability::POSTGRESQL;
175/// assert!(cap.schema_mutations.alter_column_type);
176/// assert!(!cap.schema_mutations.alter_column_properties_atomic);
177/// ```
178#[derive(Debug)]
179pub struct SchemaMutations {
180 /// Whether the database can change the type of an existing column.
181 pub alter_column_type: bool,
182
183 /// Whether the database can change name, type and constraints of a column all
184 /// withing a single statement.
185 pub alter_column_properties_atomic: bool,
186}
187
188impl Capability {
189 /// Validates the consistency of the capability configuration.
190 ///
191 /// This performs sanity checks to ensure the capability fields are
192 /// internally consistent. For example, if `native_varchar` is true,
193 /// then `storage_types.varchar` must be Some, and vice versa.
194 ///
195 /// Returns an error if any inconsistencies are found.
196 pub fn validate(&self) -> crate::Result<()> {
197 // Validate varchar consistency
198 if self.native_varchar && self.storage_types.varchar.is_none() {
199 return Err(crate::Error::invalid_driver_configuration(
200 "native_varchar is true but storage_types.varchar is None",
201 ));
202 }
203
204 if !self.native_varchar && self.storage_types.varchar.is_some() {
205 return Err(crate::Error::invalid_driver_configuration(
206 "native_varchar is false but storage_types.varchar is Some",
207 ));
208 }
209
210 Ok(())
211 }
212
213 /// Returns the default string length limit for this database.
214 ///
215 /// This is useful for tests and applications that need to respect
216 /// database-specific string length constraints.
217 pub fn default_string_max_length(&self) -> Option<u64> {
218 match &self.storage_types.default_string_type {
219 db::Type::VarChar(len) => Some(*len),
220 _ => None, // Handle other types gracefully
221 }
222 }
223
224 /// Returns the native database type for an application-level type.
225 ///
226 /// If the database supports the type natively, returns the same type.
227 /// Otherwise, returns the bridge/storage type that the application type
228 /// maps to in this database.
229 ///
230 /// This uses the existing `db::Type::bridge_type()` method to determine
231 /// the appropriate bridge type based on the database's storage capabilities.
232 pub fn native_type_for(&self, ty: &stmt::Type) -> stmt::Type {
233 match ty {
234 stmt::Type::Uuid => self.storage_types.default_uuid_type.bridge_type(ty),
235 #[cfg(feature = "jiff")]
236 stmt::Type::Timestamp => self.storage_types.default_timestamp_type.bridge_type(ty),
237 #[cfg(feature = "jiff")]
238 stmt::Type::Zoned => self.storage_types.default_zoned_type.bridge_type(ty),
239 #[cfg(feature = "jiff")]
240 stmt::Type::Date => self.storage_types.default_date_type.bridge_type(ty),
241 #[cfg(feature = "jiff")]
242 stmt::Type::Time => self.storage_types.default_time_type.bridge_type(ty),
243 #[cfg(feature = "jiff")]
244 stmt::Type::DateTime => self.storage_types.default_datetime_type.bridge_type(ty),
245 _ => ty.clone(),
246 }
247 }
248
249 /// SQLite capabilities.
250 pub const SQLITE: Self = Self {
251 sql: true,
252 storage_types: StorageTypes::SQLITE,
253 schema_mutations: SchemaMutations::SQLITE,
254 cte_with_update: false,
255 select_for_update: false,
256 returning_from_mutation: true,
257 primary_key_ne_predicate: true,
258 auto_increment: true,
259 bigdecimal_implemented: false,
260
261 native_varchar: true,
262
263 // SQLite does not have native date/time types
264 native_timestamp: false,
265 native_date: false,
266 native_time: false,
267 native_datetime: false,
268
269 // SQLite does not have native decimal types
270 native_decimal: false,
271 decimal_arbitrary_precision: false,
272
273 index_or_predicate: true,
274
275 test_connection_pool: false,
276 };
277
278 /// PostgreSQL capabilities
279 pub const POSTGRESQL: Self = Self {
280 cte_with_update: true,
281 storage_types: StorageTypes::POSTGRESQL,
282 schema_mutations: SchemaMutations::POSTGRESQL,
283 select_for_update: true,
284 auto_increment: true,
285 bigdecimal_implemented: false,
286
287 // PostgreSQL has native date/time types
288 native_timestamp: true,
289 native_date: true,
290 native_time: true,
291 native_datetime: true,
292
293 // PostgreSQL has native NUMERIC type with arbitrary precision
294 native_decimal: true,
295 decimal_arbitrary_precision: true,
296
297 test_connection_pool: true,
298
299 ..Self::SQLITE
300 };
301
302 /// MySQL capabilities
303 pub const MYSQL: Self = Self {
304 cte_with_update: false,
305 storage_types: StorageTypes::MYSQL,
306 schema_mutations: SchemaMutations::MYSQL,
307 select_for_update: true,
308 returning_from_mutation: false,
309 auto_increment: true,
310 bigdecimal_implemented: true,
311
312 // MySQL has native date/time types
313 native_timestamp: true,
314 native_date: true,
315 native_time: true,
316 native_datetime: true,
317
318 // MySQL has DECIMAL type but requires fixed precision/scale upfront
319 native_decimal: true,
320 decimal_arbitrary_precision: false,
321
322 test_connection_pool: true,
323
324 ..Self::SQLITE
325 };
326
327 /// DynamoDB capabilities
328 pub const DYNAMODB: Self = Self {
329 sql: false,
330 storage_types: StorageTypes::DYNAMODB,
331 schema_mutations: SchemaMutations::DYNAMODB,
332 cte_with_update: false,
333 select_for_update: false,
334 returning_from_mutation: false,
335 primary_key_ne_predicate: false,
336 auto_increment: false,
337 bigdecimal_implemented: false,
338 native_varchar: false,
339
340 // DynamoDB does not have native date/time types
341 native_timestamp: false,
342 native_date: false,
343 native_time: false,
344 native_datetime: false,
345
346 // DynamoDB does not have native decimal types
347 native_decimal: false,
348 decimal_arbitrary_precision: false,
349
350 index_or_predicate: false,
351
352 test_connection_pool: false,
353 };
354}
355
356impl StorageTypes {
357 /// SQLite storage types
358 pub const SQLITE: StorageTypes = StorageTypes {
359 default_string_type: db::Type::Text,
360
361 // SQLite doesn't really enforce the "N" in VARCHAR(N) at all – it
362 // treats any type containing "CHAR", "CLOB", or "TEXT" as having TEXT
363 // affinity, and simply ignores the length specifier. In other words,
364 // whether you declare a column as VARCHAR(10), VARCHAR(1000000), or
365 // just TEXT, SQLite won't truncate or complain based on that number.
366 //
367 // Instead, the only hard limit on how big a string (or BLOB) can be is
368 // the SQLITE_MAX_LENGTH parameter, which is set to 1 billion by default.
369 varchar: Some(1_000_000_000),
370
371 // SQLite does not have an inbuilt UUID type. The binary blob type is more
372 // difficult to read than Text but likely has better performance characteristics.
373 default_uuid_type: db::Type::Blob,
374
375 default_bytes_type: db::Type::Blob,
376
377 // SQLite does not have a native decimal type. Store as TEXT.
378 default_decimal_type: db::Type::Text,
379 default_bigdecimal_type: db::Type::Text,
380
381 // SQLite does not have native date/time types. Store as TEXT in ISO 8601 format.
382 default_timestamp_type: db::Type::Text,
383 default_zoned_type: db::Type::Text,
384 default_date_type: db::Type::Text,
385 default_time_type: db::Type::Text,
386 default_datetime_type: db::Type::Text,
387
388 // SQLite INTEGER is a signed 64-bit integer, so unsigned integers
389 // are limited to i64::MAX to prevent overflow
390 max_unsigned_integer: Some(i64::MAX as u64),
391 };
392
393 /// PostgreSQL storage types.
394 pub const POSTGRESQL: StorageTypes = StorageTypes {
395 default_string_type: db::Type::Text,
396
397 // The maximum n you can specify is 10 485 760 characters. Attempts to
398 // declare varchar with a larger typmod will be rejected at
399 // table‐creation time.
400 varchar: Some(10_485_760),
401
402 default_uuid_type: db::Type::Uuid,
403
404 default_bytes_type: db::Type::Blob,
405
406 // PostgreSQL has native NUMERIC type for fixed and arbitrary-precision decimals.
407 default_decimal_type: db::Type::Numeric(None),
408 // TODO: PostgreSQL has native NUMERIC type for arbitrary-precision decimals,
409 // but the encoding is complicated and has to be done separately in the future.
410 default_bigdecimal_type: db::Type::Text,
411
412 // PostgreSQL has native support for temporal types with microsecond precision (6 digits)
413 default_timestamp_type: db::Type::Timestamp(6),
414 default_zoned_type: db::Type::Text,
415 default_date_type: db::Type::Date,
416 default_time_type: db::Type::Time(6),
417 default_datetime_type: db::Type::DateTime(6),
418
419 // PostgreSQL BIGINT is signed 64-bit, so unsigned integers are limited
420 // to i64::MAX. While NUMERIC could theoretically support larger values,
421 // we prefer explicit limits over implicit type switching.
422 max_unsigned_integer: Some(i64::MAX as u64),
423 };
424
425 /// MySQL storage types.
426 pub const MYSQL: StorageTypes = StorageTypes {
427 default_string_type: db::Type::VarChar(191),
428
429 // Values in VARCHAR columns are variable-length strings. The length can
430 // be specified as a value from 0 to 65,535. The effective maximum
431 // length of a VARCHAR is subject to the maximum row size (65,535 bytes,
432 // which is shared among all columns) and the character set used.
433 varchar: Some(65_535),
434
435 // MySQL does not have an inbuilt UUID type. The binary blob type is
436 // more difficult to read than Text but likely has better performance
437 // characteristics. However, limitations in the engine make it easier to
438 // use VarChar for now.
439 default_uuid_type: db::Type::VarChar(36),
440
441 default_bytes_type: db::Type::Blob,
442
443 // MySQL does not have an arbitrary-precision decimal type. The DECIMAL type
444 // requires a fixed precision and scale to be specified upfront. Store as TEXT.
445 default_decimal_type: db::Type::Text,
446 default_bigdecimal_type: db::Type::Text,
447
448 // MySQL has native support for temporal types with microsecond precision (6 digits)
449 // The `TIMESTAMP` time only supports a limited range (1970-2038), so we default to
450 // DATETIME and let Toasty do the UTC conversion.
451 default_timestamp_type: db::Type::DateTime(6),
452 default_zoned_type: db::Type::Text,
453 default_date_type: db::Type::Date,
454 default_time_type: db::Type::Time(6),
455 default_datetime_type: db::Type::DateTime(6),
456
457 // MySQL supports full u64 range via BIGINT UNSIGNED
458 max_unsigned_integer: None,
459 };
460
461 /// DynamoDB storage types.
462 pub const DYNAMODB: StorageTypes = StorageTypes {
463 default_string_type: db::Type::Text,
464
465 // DynamoDB does not support varchar types
466 varchar: None,
467
468 default_uuid_type: db::Type::Text,
469
470 default_bytes_type: db::Type::Blob,
471
472 // DynamoDB does not have a native decimal type. Store as TEXT.
473 default_decimal_type: db::Type::Text,
474 default_bigdecimal_type: db::Type::Text,
475
476 // DynamoDB does not have native date/time types. Store as TEXT (strings).
477 default_timestamp_type: db::Type::Text,
478 default_zoned_type: db::Type::Text,
479 default_date_type: db::Type::Text,
480 default_time_type: db::Type::Text,
481 default_datetime_type: db::Type::Text,
482
483 // DynamoDB supports full u64 range (numbers stored as strings)
484 max_unsigned_integer: None,
485 };
486}
487
488impl SchemaMutations {
489 /// SQLite schema mutation capabilities. SQLite cannot alter column types.
490 pub const SQLITE: Self = Self {
491 alter_column_type: false,
492 alter_column_properties_atomic: false,
493 };
494
495 /// PostgreSQL schema mutation capabilities. Supports altering column types
496 /// but not atomically changing multiple column properties.
497 pub const POSTGRESQL: Self = Self {
498 alter_column_type: true,
499 alter_column_properties_atomic: false,
500 };
501
502 /// MySQL schema mutation capabilities. Supports altering column types and
503 /// atomically changing multiple column properties in a single statement.
504 pub const MYSQL: Self = Self {
505 alter_column_type: true,
506 alter_column_properties_atomic: true,
507 };
508
509 /// DynamoDB schema mutation capabilities. Migrations are not currently supported.
510 pub const DYNAMODB: Self = Self {
511 alter_column_type: false,
512 alter_column_properties_atomic: false,
513 };
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_validate_sqlite_capability() {
522 // SQLite has native_varchar=true and varchar=Some, should pass
523 assert!(Capability::SQLITE.validate().is_ok());
524 }
525
526 #[test]
527 fn test_validate_postgresql_capability() {
528 // PostgreSQL has native_varchar=true and varchar=Some, should pass
529 assert!(Capability::POSTGRESQL.validate().is_ok());
530 }
531
532 #[test]
533 fn test_validate_mysql_capability() {
534 // MySQL has native_varchar=true and varchar=Some, should pass
535 assert!(Capability::MYSQL.validate().is_ok());
536 }
537
538 #[test]
539 fn test_validate_dynamodb_capability() {
540 // DynamoDB has native_varchar=false and varchar=None, should pass
541 assert!(Capability::DYNAMODB.validate().is_ok());
542 }
543
544 #[test]
545 fn test_validate_fails_when_native_varchar_true_but_no_varchar() {
546 let invalid = Capability {
547 native_varchar: true,
548 storage_types: StorageTypes {
549 varchar: None, // Invalid: native_varchar is true but varchar is None
550 ..StorageTypes::SQLITE
551 },
552 ..Capability::SQLITE
553 };
554
555 let result = invalid.validate();
556 assert!(result.is_err());
557 assert!(
558 result
559 .unwrap_err()
560 .to_string()
561 .contains("native_varchar is true but storage_types.varchar is None")
562 );
563 }
564
565 #[test]
566 fn test_validate_fails_when_native_varchar_false_but_has_varchar() {
567 let invalid = Capability {
568 native_varchar: false,
569 storage_types: StorageTypes {
570 varchar: Some(1000), // Invalid: native_varchar is false but varchar is Some
571 ..StorageTypes::DYNAMODB
572 },
573 ..Capability::DYNAMODB
574 };
575
576 let result = invalid.validate();
577 assert!(result.is_err());
578 assert!(
579 result
580 .unwrap_err()
581 .to_string()
582 .contains("native_varchar is false but storage_types.varchar is Some")
583 );
584 }
585}