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