Skip to main content

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