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