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 /// What the database is able to change about its own schema. See
34 /// [`SchemaMutations`] for the individual fields; the migration
35 /// generator branches on them to choose between an in-place
36 /// `ALTER COLUMN` and a table rebuild, and between one combined
37 /// alter statement and several single-property ones.
38 pub schema_mutations: SchemaMutations,
39
40 /// SQL: supports update statements in CTE queries.
41 pub cte_with_update: bool,
42
43 /// SQL: Supports row-level locking. If false, then the driver is expected
44 /// to serializable transaction-level isolation.
45 pub select_for_update: bool,
46
47 /// SQL: Mysql doesn't support returning clauses from insert / update queries
48 pub returning_from_mutation: bool,
49
50 /// DynamoDB does not support != predicates on the primary key.
51 pub primary_key_ne_predicate: bool,
52
53 /// Whether the database has an auto increment modifier for integer columns.
54 pub auto_increment: bool,
55
56 /// Whether the database supports `VARCHAR(n)` column types natively.
57 ///
58 /// Must be consistent with [`StorageTypes::varchar`]: when `true`,
59 /// `varchar` must be `Some`; when `false`, `varchar` must be `None`.
60 /// Use [`Capability::validate`] to check this invariant.
61 pub native_varchar: bool,
62
63 /// Whether the database has native support for Timestamp types.
64 pub native_timestamp: bool,
65
66 /// Whether the database has native support for Date types.
67 pub native_date: bool,
68
69 /// Whether the database has native support for Time types.
70 pub native_time: bool,
71
72 /// Whether the database has native support for DateTime types.
73 pub native_datetime: bool,
74
75 /// Whether the database supports native enum types.
76 ///
77 /// - PostgreSQL: `true` — `CREATE TYPE ... AS ENUM`
78 /// - MySQL: `true` — inline `ENUM('a', 'b')` column type
79 /// - SQLite: `false` — uses `TEXT` + `CHECK` constraint
80 /// - DynamoDB: `false` — plain string attribute
81 pub native_enum: bool,
82
83 /// Whether enum types are standalone named objects requiring separate DDL.
84 ///
85 /// When `true`, migrations must emit `CREATE TYPE` / `ALTER TYPE` for enum
86 /// types. When `false`, enum definitions are inline in column types.
87 ///
88 /// - PostgreSQL: `true` — `CREATE TYPE <name> AS ENUM (...)`
89 /// - MySQL: `false` — inline `ENUM('a', 'b')` on the column
90 /// - SQLite: `false`
91 /// - DynamoDB: `false`
92 pub named_enum_types: bool,
93
94 /// Whether the database has native support for Decimal types.
95 pub native_decimal: bool,
96
97 /// Whether BigDecimal driver support is implemented.
98 /// TODO: Remove this flag when PostgreSQL BigDecimal support is implemented.
99 /// Currently only MySQL has implemented BigDecimal driver support.
100 pub bigdecimal_implemented: bool,
101
102 /// Whether the database's decimal type supports arbitrary precision.
103 /// When false, the decimal type requires fixed precision and scale to be specified upfront.
104 /// - PostgreSQL: true (NUMERIC supports arbitrary precision)
105 /// - MySQL: false (DECIMAL requires fixed precision/scale)
106 /// - SQLite/DynamoDB: false (no native decimal support, stored as TEXT)
107 pub decimal_arbitrary_precision: bool,
108
109 /// Whether OR is supported in index key conditions (e.g. DynamoDB KeyConditionExpression).
110 /// DynamoDB: false. All other backends: true (SQL backends never use index key conditions).
111 pub index_or_predicate: bool,
112
113 /// Whether the database has a native prefix-match operator that does not
114 /// require LIKE-style escaping. When `true`, `starts_with` is left in the
115 /// AST and the driver renders it natively (DynamoDB's `begins_with()`,
116 /// PostgreSQL's `^@`). When `false`, the lowering rewrites it to a
117 /// `LIKE` expression — which requires `native_like` to be `true`.
118 pub native_starts_with: bool,
119
120 /// Whether the database has a native `LIKE` expression. When `false`,
121 /// `Expr::Like` cannot be sent to the driver; `starts_with` lowering
122 /// will not produce one.
123 pub native_like: bool,
124
125 /// Whether the database has a native case-insensitive `LIKE` operator
126 /// (`ILIKE`). Only PostgreSQL has one.
127 ///
128 /// Toasty does not emulate `ILIKE` on backends that lack it: `.ilike()`
129 /// is a pass-through to the database's own operator. When `native_ilike`
130 /// is `false`, the query-verify pass rejects a case-insensitive
131 /// `Expr::Like` with an
132 /// [`unsupported_feature`](crate::Error::unsupported_feature) error rather
133 /// than silently degrading to plain `LIKE`, whose case behavior differs.
134 ///
135 /// Implies `native_like`.
136 pub native_ilike: bool,
137
138 /// Whether the driver can answer queries that don't match any primary key
139 /// or index — i.e. supports unindexed full-table reads.
140 ///
141 /// SQL drivers set this to `true`: unindexed queries go through
142 /// [`QuerySql`](super::operation::QuerySql), so the SQL engine handles
143 /// them transparently. DynamoDB also sets this to `true`; the planner
144 /// emits [`Operation::Scan`](super::Operation::Scan) for the unindexed
145 /// case. A hypothetical pure key-value store with no full-scan capability
146 /// would set this to `false`.
147 pub scan: bool,
148
149 /// Whether scan operations support ordering results.
150 ///
151 /// SQL drivers do not use `Operation::Scan`, so this is `true` for them
152 /// (ordering is handled inside `QuerySql`). DynamoDB's `Scan` API returns
153 /// items in an arbitrary order with no server-side sort, so this is `false`
154 /// for DynamoDB. When `false`, the planner rejects queries that combine a
155 /// scan path with `ORDER BY`.
156 pub scan_supports_sort: bool,
157
158 /// Whether to test connection pool behavior.
159 /// TODO: We only need this for the `connection_per_clone.rs` test, come up with a better way.
160 pub test_connection_pool: bool,
161
162 /// Whether the driver honors non-`Default`
163 /// [`TransactionMode`](super::operation::TransactionMode) variants
164 /// (`Immediate`, `Exclusive`). Currently `true` only for SQLite, which
165 /// maps them to `BEGIN IMMEDIATE` / `BEGIN EXCLUSIVE`. Drivers that
166 /// leave this `false` reject non-`Default` modes with
167 /// [`Error::unsupported_feature`](crate::Error::unsupported_feature).
168 pub transaction_lock_mode: bool,
169
170 /// Whether the backend can walk a paginated query in reverse from a
171 /// cursor.
172 ///
173 /// Gates the `prev_cursor` field on a `Page` returned to user code.
174 /// When `true`, the executor extracts a previous-page cursor from the
175 /// first row of every page (see `apply_sql_pagination` in
176 /// `toasty/src/engine/exec/exec_statement.rs`). When `false`, the
177 /// executor leaves `prev_cursor` as `None`, so
178 /// `Page::has_prev()` returns `false` and `Page::prev(&db)` resolves
179 /// to `Ok(None)` without issuing a query. `Paginate::before(cursor)`
180 /// itself is not rejected — users who already hold a cursor can walk
181 /// backwards explicitly — but a driver that returns `false` is
182 /// declaring that it has no way to *produce* such a cursor.
183 ///
184 /// Drivers should set this to `true` when the backend can answer a
185 /// query equivalent to "rows ordered by K, descending from K = c,
186 /// limited to N" — i.e. the same `ORDER BY` clause reversed plus a
187 /// strict inequality on the cursor key. SQL backends meet this
188 /// trivially. DynamoDB does not: a `Query` with `ScanIndexForward =
189 /// false` returns rows in the opposite direction but cannot be
190 /// rooted at an arbitrary client-supplied cursor without an extra
191 /// `KeyConditionExpression`, and `Scan` has no order guarantee at
192 /// all.
193 pub backward_pagination: bool,
194
195 /// The driver's bind layer accepts a single parameter whose value is
196 /// `Value::List(items)` and type is `Type::List(elem)`, sending it as
197 /// one protocol-level parameter (not N separate scalars).
198 /// Property of the driver bind impl, not the SQL dialect.
199 pub bind_list_param: bool,
200
201 /// The SQL dialect parses `expr <op> ANY(<array>)` and `expr <op> ALL(<array>)`
202 /// as predicates against an array-valued operand.
203 /// Property of the dialect, not the bind layer.
204 pub predicate_match_any: bool,
205
206 /// Whether the database can store a `Vec<scalar>` model field as a native
207 /// array column (e.g. PostgreSQL `text[]`, `int8[]`).
208 ///
209 /// When `true`, schema build maps `Type::List(elem)` to `db::Type::List(elem)`
210 /// and the driver's bind layer accepts `Value::List(items)` as a single
211 /// array-valued parameter.
212 ///
213 /// When `false`, `Vec<T>` model fields use whatever fallback the backend
214 /// provides (JSON column on MySQL/SQLite, native List `L` on DynamoDB).
215 /// See [`Self::vec_scalar`] for the schema-build gate.
216 pub native_array: bool,
217
218 /// Whether the driver supports `Vec<scalar>` model fields, by whatever
219 /// representation (native typed array column, JSON column, key-value
220 /// list attribute, ...). Used by the schema builder as the gate for
221 /// accepting `stmt::Type::List(_)` fields.
222 pub vec_scalar: bool,
223
224 /// Whether the driver natively renders `IsSuperset` / `Intersects` array
225 /// predicates over an arbitrary right-hand-side expression.
226 ///
227 /// SQL drivers set this to `true`: each dialect has a single operator
228 /// (`@>` on PostgreSQL, `JSON_CONTAINS` on MySQL, a `json_each`
229 /// subquery on SQLite) that takes the rhs as a bound expression
230 /// regardless of its shape.
231 ///
232 /// DynamoDB sets this to `false`: it has no equivalent operator and
233 /// emulates the predicates by emitting one `contains(path, vN)` clause
234 /// per rhs element, which requires the rhs to be a concrete list of
235 /// values at filter-construction time. The capability check rejects
236 /// any other rhs shape before the driver is invoked.
237 pub native_array_set_predicates: bool,
238
239 /// Whether the driver supports atomic in-place removal of every element
240 /// equal to a given value from a `Vec<scalar>` field (`stmt::remove`).
241 ///
242 /// - PostgreSQL `text[]`: `true` — `array_remove(col, v)`.
243 /// - MySQL / SQLite JSON: `false` — no value-removal operator.
244 /// - DynamoDB List: `false` — no value-removal on Lists.
245 pub vec_remove: bool,
246
247 /// Whether the driver supports atomic in-place removal of the last
248 /// element of a `Vec<scalar>` field (`stmt::pop`).
249 ///
250 /// - PostgreSQL: `true` — array slicing.
251 /// - MySQL / SQLite: `false`.
252 /// - DynamoDB: `false` — `UpdateExpression` indices must be literal
253 /// integers, so the last index cannot be expressed in one statement.
254 pub vec_pop: bool,
255
256 /// Whether the driver supports atomic in-place removal of an element at a
257 /// given index from a `Vec<scalar>` field (`stmt::remove_at`).
258 ///
259 /// - PostgreSQL: `true` — array slicing.
260 /// - MySQL / SQLite: `false`.
261 /// - DynamoDB: `false`.
262 pub vec_remove_at: bool,
263}
264
265/// Maps application-level types to the concrete database column types used for
266/// storage.
267///
268/// Each database has different native type support. For example, PostgreSQL has
269/// a native `UUID` type while SQLite stores UUIDs as `BLOB`. This struct
270/// captures those mappings so the schema layer can generate correct DDL and the
271/// driver can encode/decode values appropriately.
272///
273/// Pre-built configurations: [`SQLITE`](Self::SQLITE),
274/// [`POSTGRESQL`](Self::POSTGRESQL), [`MYSQL`](Self::MYSQL),
275/// [`DYNAMODB`](Self::DYNAMODB).
276///
277/// # Examples
278///
279/// ```
280/// use toasty_core::driver::StorageTypes;
281///
282/// let st = &StorageTypes::POSTGRESQL;
283/// // PostgreSQL stores UUIDs natively
284/// assert!(matches!(st.default_uuid_type, toasty_core::schema::db::Type::Uuid));
285/// ```
286#[derive(Debug)]
287pub struct StorageTypes {
288 /// The default storage type for a string.
289 pub default_string_type: db::Type,
290
291 /// When `Some` the database supports varchar types with the specified upper
292 /// limit.
293 pub varchar: Option<u64>,
294
295 /// The default storage type for a UUID.
296 pub default_uuid_type: db::Type,
297
298 /// The default storage type for Bytes (Vec<u8>).
299 pub default_bytes_type: db::Type,
300
301 /// The default storage type for a Decimal (fixed-precision decimal).
302 pub default_decimal_type: db::Type,
303
304 /// The default storage type for a BigDecimal (arbitrary-precision decimal).
305 pub default_bigdecimal_type: db::Type,
306
307 /// The default storage type for a Timestamp (instant in time).
308 pub default_timestamp_type: db::Type,
309
310 /// The default storage type for a Zoned (timezone-aware instant).
311 pub default_zoned_type: db::Type,
312
313 /// The default storage type for a Date (civil date).
314 pub default_date_type: db::Type,
315
316 /// The default storage type for a Time (wall clock time).
317 pub default_time_type: db::Type,
318
319 /// The default storage type for a DateTime (civil datetime).
320 pub default_datetime_type: db::Type,
321
322 /// Maximum value for unsigned integers. When `Some`, unsigned integers
323 /// are limited to this value. When `None`, full u64 range is supported.
324 pub max_unsigned_integer: Option<u64>,
325}
326
327/// The database's capabilities to mutate the schema (tables, columns, indices).
328///
329/// Used by the migration generator to decide how to express each
330/// column change. `alter_column_type` gates whether an in-place
331/// `ALTER COLUMN` is possible at all — SQLite has it set to `false`,
332/// and a type change there triggers a full table rebuild (create
333/// new table, copy rows, drop old). `alter_column_properties_atomic`
334/// decides whether several column-property changes (rename, retype,
335/// `NOT NULL`, default) collapse into one statement or emit one per
336/// property. MySQL sets both to `true`; PostgreSQL alters in place
337/// but requires one statement per property.
338///
339/// Pre-built configurations: [`SQLITE`](Self::SQLITE),
340/// [`POSTGRESQL`](Self::POSTGRESQL), [`MYSQL`](Self::MYSQL),
341/// [`DYNAMODB`](Self::DYNAMODB).
342///
343/// # Examples
344///
345/// Access through [`Capability::schema_mutations`]:
346///
347/// ```
348/// use toasty_core::driver::Capability;
349///
350/// let cap = &Capability::POSTGRESQL;
351/// assert!(cap.schema_mutations.alter_column_type);
352/// assert!(!cap.schema_mutations.alter_column_properties_atomic);
353/// ```
354#[derive(Debug)]
355pub struct SchemaMutations {
356 /// Whether the database can change the type of an existing column.
357 pub alter_column_type: bool,
358
359 /// Whether the database can change name, type and constraints of a column all
360 /// withing a single statement.
361 pub alter_column_properties_atomic: bool,
362}
363
364impl Capability {
365 /// Validates the consistency of the capability configuration.
366 ///
367 /// This performs sanity checks to ensure the capability fields are
368 /// internally consistent. For example, if `native_varchar` is true,
369 /// then `storage_types.varchar` must be Some, and vice versa.
370 ///
371 /// Returns an error if any inconsistencies are found.
372 pub fn validate(&self) -> crate::Result<()> {
373 // Validate varchar consistency
374 if self.native_varchar && self.storage_types.varchar.is_none() {
375 return Err(crate::Error::invalid_driver_configuration(
376 "native_varchar is true but storage_types.varchar is None",
377 ));
378 }
379
380 if !self.native_varchar && self.storage_types.varchar.is_some() {
381 return Err(crate::Error::invalid_driver_configuration(
382 "native_varchar is false but storage_types.varchar is Some",
383 ));
384 }
385
386 // ILIKE is a case-insensitive LIKE; a backend cannot offer it without
387 // a native LIKE.
388 if self.native_ilike && !self.native_like {
389 return Err(crate::Error::invalid_driver_configuration(
390 "native_ilike is true but native_like is false",
391 ));
392 }
393
394 Ok(())
395 }
396
397 /// Returns the default string length limit for this database.
398 ///
399 /// This is useful for tests and applications that need to respect
400 /// database-specific string length constraints.
401 pub fn default_string_max_length(&self) -> Option<u64> {
402 match &self.storage_types.default_string_type {
403 db::Type::VarChar(len) => Some(*len),
404 _ => None, // Handle other types gracefully
405 }
406 }
407
408 /// Returns the native database type for an application-level type.
409 ///
410 /// If the database supports the type natively, returns the same type.
411 /// Otherwise, returns the bridge/storage type that the application type
412 /// maps to in this database.
413 ///
414 /// This uses the existing `db::Type::bridge_type()` method to determine
415 /// the appropriate bridge type based on the database's storage capabilities.
416 pub fn native_type_for(&self, ty: &stmt::Type) -> stmt::Type {
417 match ty {
418 stmt::Type::Uuid => self.storage_types.default_uuid_type.bridge_type(ty),
419 #[cfg(feature = "jiff")]
420 stmt::Type::Timestamp => self.storage_types.default_timestamp_type.bridge_type(ty),
421 #[cfg(feature = "jiff")]
422 stmt::Type::Zoned => self.storage_types.default_zoned_type.bridge_type(ty),
423 #[cfg(feature = "jiff")]
424 stmt::Type::Date => self.storage_types.default_date_type.bridge_type(ty),
425 #[cfg(feature = "jiff")]
426 stmt::Type::Time => self.storage_types.default_time_type.bridge_type(ty),
427 #[cfg(feature = "jiff")]
428 stmt::Type::DateTime => self.storage_types.default_datetime_type.bridge_type(ty),
429 _ => ty.clone(),
430 }
431 }
432
433 /// SQLite capabilities.
434 pub const SQLITE: Self = Self {
435 sql: true,
436 storage_types: StorageTypes::SQLITE,
437 schema_mutations: SchemaMutations::SQLITE,
438 cte_with_update: false,
439 select_for_update: false,
440 returning_from_mutation: true,
441 primary_key_ne_predicate: true,
442 auto_increment: true,
443 bigdecimal_implemented: false,
444
445 native_varchar: true,
446
447 // SQLite does not have native enum types; uses TEXT + CHECK
448 native_enum: false,
449 named_enum_types: false,
450
451 // SQLite does not have native date/time types
452 native_timestamp: false,
453 native_date: false,
454 native_time: false,
455 native_datetime: false,
456
457 // SQLite does not have native decimal types
458 native_decimal: false,
459 decimal_arbitrary_precision: false,
460
461 index_or_predicate: true,
462
463 native_starts_with: false,
464 native_like: true,
465
466 // SQLite's `LIKE` is case-insensitive for ASCII only; it has no
467 // `ILIKE` operator, so `.ilike()` is rejected here.
468 native_ilike: false,
469
470 // SQL drivers handle unindexed queries via QuerySql (see field doc).
471 scan: true,
472 scan_supports_sort: true,
473
474 test_connection_pool: false,
475
476 // SQLite exposes `BEGIN DEFERRED|IMMEDIATE|EXCLUSIVE` for
477 // lock-acquisition policy.
478 transaction_lock_mode: true,
479
480 backward_pagination: true,
481
482 // `Vec<scalar>` model fields land in a `TEXT` column holding a JSON
483 // document (JSON1 extension). The driver serializes `Value::List`
484 // to a JSON string at bind time, so the extract pass keeps the list
485 // as one `Value::List` parameter; the `InList` branch in
486 // `extract_params` covers the `IN (...)` case so this flag does
487 // not regress IN-list rendering. The predicate-side `ANY` rewrite
488 // is gated on `predicate_match_any`, which stays `false`, so
489 // `Path::contains` lowers to a `json_each` subquery instead.
490 bind_list_param: true,
491 predicate_match_any: false,
492
493 // SQLite has no native typed-array column type; `Vec<scalar>`
494 // model fields are stored as a JSON document in a `TEXT` column.
495 native_array: false,
496 vec_scalar: true,
497
498 // SQLite renders `IsSuperset` / `Intersects` as `json_each`
499 // subqueries that accept any rhs expression.
500 native_array_set_predicates: true,
501
502 // SQLite JSON1 has no value-removal operator on JSON arrays; pop
503 // and remove_at need a path expression built from
504 // `json_array_length`.
505 vec_remove: false,
506 vec_pop: false,
507 vec_remove_at: false,
508 };
509
510 /// PostgreSQL capabilities
511 pub const POSTGRESQL: Self = Self {
512 cte_with_update: true,
513 storage_types: StorageTypes::POSTGRESQL,
514 schema_mutations: SchemaMutations::POSTGRESQL,
515 select_for_update: true,
516 auto_increment: true,
517 bigdecimal_implemented: false,
518
519 // PostgreSQL has the `^@` prefix-match operator.
520 native_starts_with: true,
521
522 // PostgreSQL is the only backend with a native `ILIKE` operator.
523 native_ilike: true,
524
525 // PostgreSQL has CREATE TYPE ... AS ENUM
526 native_enum: true,
527 named_enum_types: true,
528
529 // PostgreSQL has native date/time types
530 native_timestamp: true,
531 native_date: true,
532 native_time: true,
533 native_datetime: true,
534
535 // PostgreSQL has native NUMERIC type with arbitrary precision
536 native_decimal: true,
537 decimal_arbitrary_precision: true,
538
539 test_connection_pool: true,
540
541 // PostgreSQL has no SQLite-style lock-mode keyword on BEGIN.
542 transaction_lock_mode: false,
543
544 // PostgreSQL accepts a single array-valued bind param and supports
545 // `expr <op> ANY(array)` / `<op> ALL(array)` predicates.
546 bind_list_param: true,
547 predicate_match_any: true,
548
549 // PostgreSQL: native arrays (`text[]`, `int8[]`, …) are the storage
550 // representation for `Vec<scalar>` model fields.
551 native_array: true,
552 vec_scalar: true,
553
554 // PostgreSQL: all three collection removals are atomic via native
555 // array operators / slicing.
556 vec_remove: true,
557 vec_pop: true,
558 vec_remove_at: true,
559
560 ..Self::SQLITE
561 };
562
563 /// MySQL capabilities
564 pub const MYSQL: Self = Self {
565 cte_with_update: false,
566 storage_types: StorageTypes::MYSQL,
567 schema_mutations: SchemaMutations::MYSQL,
568 select_for_update: true,
569 returning_from_mutation: false,
570 auto_increment: true,
571 bigdecimal_implemented: true,
572
573 // MySQL has inline ENUM('a', 'b') column types
574 native_enum: true,
575 named_enum_types: false,
576
577 // MySQL has native date/time types
578 native_timestamp: true,
579 native_date: true,
580 native_time: true,
581 native_datetime: true,
582
583 // MySQL has DECIMAL type but requires fixed precision/scale upfront
584 native_decimal: true,
585 decimal_arbitrary_precision: false,
586
587 test_connection_pool: true,
588
589 // MySQL has no SQLite-style lock-mode keyword on START TRANSACTION.
590 transaction_lock_mode: false,
591
592 // `Vec<scalar>` model fields land in a `JSON` column. The driver
593 // serializes `Value::List` to a JSON string at bind time, so the
594 // extract pass keeps the list as one `Value::List` parameter
595 // instead of expanding it (the `InList` branch in
596 // `extract_params` covers the `IN (...)` case so this flag does
597 // not regress the IN-list rendering).
598 bind_list_param: true,
599 vec_scalar: true,
600
601 ..Self::SQLITE
602 };
603
604 /// DynamoDB capabilities
605 pub const DYNAMODB: Self = Self {
606 sql: false,
607 storage_types: StorageTypes::DYNAMODB,
608 schema_mutations: SchemaMutations::DYNAMODB,
609 cte_with_update: false,
610 select_for_update: false,
611 returning_from_mutation: false,
612 primary_key_ne_predicate: false,
613 auto_increment: false,
614 bigdecimal_implemented: false,
615 native_varchar: false,
616 native_enum: false,
617 named_enum_types: false,
618
619 // DynamoDB does not have native date/time types
620 native_timestamp: false,
621 native_date: false,
622 native_time: false,
623 native_datetime: false,
624
625 // DynamoDB does not have native decimal types
626 native_decimal: false,
627 decimal_arbitrary_precision: false,
628
629 index_or_predicate: false,
630
631 // DynamoDB has `begins_with()` but no LIKE or ILIKE.
632 native_starts_with: true,
633 native_like: false,
634 native_ilike: false,
635
636 scan: true,
637 scan_supports_sort: false,
638
639 test_connection_pool: false,
640
641 // DynamoDB rejects `Operation::Transaction` wholesale.
642 transaction_lock_mode: false,
643
644 backward_pagination: false,
645
646 // DynamoDB: not SQL-based; the array-bind/`ANY`-predicate features do
647 // not apply.
648 bind_list_param: false,
649 predicate_match_any: false,
650
651 // DynamoDB has no SQL-style typed-array column type; the
652 // `db::Type::List(elem)` storage shape doesn't apply. `Vec<scalar>`
653 // model fields land directly on a List `L` attribute via the driver's
654 // `AttributeValue` encoding.
655 native_array: false,
656 vec_scalar: true,
657
658 // DynamoDB emulates `IsSuperset` / `Intersects` by expanding the rhs
659 // into one `contains(path, vN)` clause per element. The expansion
660 // requires the rhs to be a `Value::List` at filter-construction time
661 // — the capability check rejects any other rhs shape.
662 native_array_set_predicates: false,
663
664 // DynamoDB Lists have no atomic value-removal, and pop cannot be
665 // expressed because `UpdateExpression` indices must be literal
666 // integers.
667 vec_remove: false,
668 vec_pop: false,
669 vec_remove_at: false,
670 };
671}
672
673impl StorageTypes {
674 /// SQLite storage types
675 pub const SQLITE: StorageTypes = StorageTypes {
676 default_string_type: db::Type::Text,
677
678 // SQLite doesn't really enforce the "N" in VARCHAR(N) at all – it
679 // treats any type containing "CHAR", "CLOB", or "TEXT" as having TEXT
680 // affinity, and simply ignores the length specifier. In other words,
681 // whether you declare a column as VARCHAR(10), VARCHAR(1000000), or
682 // just TEXT, SQLite won't truncate or complain based on that number.
683 //
684 // Instead, the only hard limit on how big a string (or BLOB) can be is
685 // the SQLITE_MAX_LENGTH parameter, which is set to 1 billion by default.
686 varchar: Some(1_000_000_000),
687
688 // SQLite does not have an inbuilt UUID type. The binary blob type is more
689 // difficult to read than Text but likely has better performance characteristics.
690 default_uuid_type: db::Type::Blob,
691
692 default_bytes_type: db::Type::Blob,
693
694 // SQLite does not have a native decimal type. Store as TEXT.
695 default_decimal_type: db::Type::Text,
696 default_bigdecimal_type: db::Type::Text,
697
698 // SQLite does not have native date/time types. Store as TEXT in ISO 8601 format.
699 default_timestamp_type: db::Type::Text,
700 default_zoned_type: db::Type::Text,
701 default_date_type: db::Type::Text,
702 default_time_type: db::Type::Text,
703 default_datetime_type: db::Type::Text,
704
705 // SQLite INTEGER is a signed 64-bit integer, so unsigned integers
706 // are limited to i64::MAX to prevent overflow
707 max_unsigned_integer: Some(i64::MAX as u64),
708 };
709
710 /// PostgreSQL storage types.
711 pub const POSTGRESQL: StorageTypes = StorageTypes {
712 default_string_type: db::Type::Text,
713
714 // The maximum n you can specify is 10 485 760 characters. Attempts to
715 // declare varchar with a larger typmod will be rejected at
716 // table‐creation time.
717 varchar: Some(10_485_760),
718
719 default_uuid_type: db::Type::Uuid,
720
721 default_bytes_type: db::Type::Blob,
722
723 // PostgreSQL has native NUMERIC type for fixed and arbitrary-precision decimals.
724 default_decimal_type: db::Type::Numeric(None),
725 // TODO: PostgreSQL has native NUMERIC type for arbitrary-precision decimals,
726 // but the encoding is complicated and has to be done separately in the future.
727 default_bigdecimal_type: db::Type::Text,
728
729 // PostgreSQL has native support for temporal types with microsecond precision (6 digits)
730 default_timestamp_type: db::Type::Timestamp(6),
731 default_zoned_type: db::Type::Text,
732 default_date_type: db::Type::Date,
733 default_time_type: db::Type::Time(6),
734 default_datetime_type: db::Type::DateTime(6),
735
736 // PostgreSQL BIGINT is signed 64-bit, so unsigned integers are limited
737 // to i64::MAX. While NUMERIC could theoretically support larger values,
738 // we prefer explicit limits over implicit type switching.
739 max_unsigned_integer: Some(i64::MAX as u64),
740 };
741
742 /// MySQL storage types.
743 pub const MYSQL: StorageTypes = StorageTypes {
744 default_string_type: db::Type::VarChar(191),
745
746 // Values in VARCHAR columns are variable-length strings. The length can
747 // be specified as a value from 0 to 65,535. The effective maximum
748 // length of a VARCHAR is subject to the maximum row size (65,535 bytes,
749 // which is shared among all columns) and the character set used.
750 varchar: Some(65_535),
751
752 // MySQL does not have an inbuilt UUID type. The binary blob type is
753 // more difficult to read than Text but likely has better performance
754 // characteristics. However, limitations in the engine make it easier to
755 // use VarChar for now.
756 default_uuid_type: db::Type::VarChar(36),
757
758 default_bytes_type: db::Type::Blob,
759
760 // MySQL does not have an arbitrary-precision decimal type. The DECIMAL type
761 // requires a fixed precision and scale to be specified upfront. Store as TEXT.
762 default_decimal_type: db::Type::Text,
763 default_bigdecimal_type: db::Type::Text,
764
765 // MySQL has native support for temporal types with microsecond precision (6 digits)
766 // The `TIMESTAMP` time only supports a limited range (1970-2038), so we default to
767 // DATETIME and let Toasty do the UTC conversion.
768 default_timestamp_type: db::Type::DateTime(6),
769 default_zoned_type: db::Type::Text,
770 default_date_type: db::Type::Date,
771 default_time_type: db::Type::Time(6),
772 default_datetime_type: db::Type::DateTime(6),
773
774 // MySQL supports full u64 range via BIGINT UNSIGNED
775 max_unsigned_integer: None,
776 };
777
778 /// DynamoDB storage types.
779 pub const DYNAMODB: StorageTypes = StorageTypes {
780 default_string_type: db::Type::Text,
781
782 // DynamoDB does not support varchar types
783 varchar: None,
784
785 default_uuid_type: db::Type::Text,
786
787 default_bytes_type: db::Type::Blob,
788
789 // DynamoDB does not have a native decimal type. Store as TEXT.
790 default_decimal_type: db::Type::Text,
791 default_bigdecimal_type: db::Type::Text,
792
793 // DynamoDB does not have native date/time types. Store as TEXT (strings).
794 default_timestamp_type: db::Type::Text,
795 default_zoned_type: db::Type::Text,
796 default_date_type: db::Type::Text,
797 default_time_type: db::Type::Text,
798 default_datetime_type: db::Type::Text,
799
800 // DynamoDB supports full u64 range (numbers stored as strings)
801 max_unsigned_integer: None,
802 };
803}
804
805impl SchemaMutations {
806 /// SQLite schema mutation capabilities. SQLite cannot alter column types.
807 pub const SQLITE: Self = Self {
808 alter_column_type: false,
809 alter_column_properties_atomic: false,
810 };
811
812 /// PostgreSQL schema mutation capabilities. Supports altering column types
813 /// but not atomically changing multiple column properties.
814 pub const POSTGRESQL: Self = Self {
815 alter_column_type: true,
816 alter_column_properties_atomic: false,
817 };
818
819 /// MySQL schema mutation capabilities. Supports altering column types and
820 /// atomically changing multiple column properties in a single statement.
821 pub const MYSQL: Self = Self {
822 alter_column_type: true,
823 alter_column_properties_atomic: true,
824 };
825
826 /// DynamoDB schema mutation capabilities. Migrations are not currently supported.
827 pub const DYNAMODB: Self = Self {
828 alter_column_type: false,
829 alter_column_properties_atomic: false,
830 };
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_validate_sqlite_capability() {
839 // SQLite has native_varchar=true and varchar=Some, should pass
840 assert!(Capability::SQLITE.validate().is_ok());
841 }
842
843 #[test]
844 fn test_validate_postgresql_capability() {
845 // PostgreSQL has native_varchar=true and varchar=Some, should pass
846 assert!(Capability::POSTGRESQL.validate().is_ok());
847 }
848
849 #[test]
850 fn test_validate_mysql_capability() {
851 // MySQL has native_varchar=true and varchar=Some, should pass
852 assert!(Capability::MYSQL.validate().is_ok());
853 }
854
855 #[test]
856 fn test_validate_dynamodb_capability() {
857 // DynamoDB has native_varchar=false and varchar=None, should pass
858 assert!(Capability::DYNAMODB.validate().is_ok());
859 }
860
861 #[test]
862 fn test_validate_fails_when_native_varchar_true_but_no_varchar() {
863 let invalid = Capability {
864 native_varchar: true,
865 storage_types: StorageTypes {
866 varchar: None, // Invalid: native_varchar is true but varchar is None
867 ..StorageTypes::SQLITE
868 },
869 ..Capability::SQLITE
870 };
871
872 let result = invalid.validate();
873 assert!(result.is_err());
874 assert!(
875 result
876 .unwrap_err()
877 .to_string()
878 .contains("native_varchar is true but storage_types.varchar is None")
879 );
880 }
881
882 #[test]
883 fn test_validate_fails_when_native_varchar_false_but_has_varchar() {
884 let invalid = Capability {
885 native_varchar: false,
886 storage_types: StorageTypes {
887 varchar: Some(1000), // Invalid: native_varchar is false but varchar is Some
888 ..StorageTypes::DYNAMODB
889 },
890 ..Capability::DYNAMODB
891 };
892
893 let result = invalid.validate();
894 assert!(result.is_err());
895 assert!(
896 result
897 .unwrap_err()
898 .to_string()
899 .contains("native_varchar is false but storage_types.varchar is Some")
900 );
901 }
902}