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}