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