Skip to main content

toasty_core/driver/
capability.rs

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