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