toasty_core/driver/
capability.rs

1use crate::{schema::db, stmt};
2
3#[derive(Debug)]
4pub struct Capability {
5    /// When true, the database uses a SQL-based query language.
6    pub sql: bool,
7
8    /// Column storage types supported by the database.
9    pub storage_types: StorageTypes,
10
11    /// Schema mutation capabilities supported by the datbase.
12    pub schema_mutations: SchemaMutations,
13
14    /// SQL: supports update statements in CTE queries.
15    pub cte_with_update: bool,
16
17    /// SQL: Supports row-level locking. If false, then the driver is expected
18    /// to serializable transaction-level isolation.
19    pub select_for_update: bool,
20
21    /// SQL: Mysql doesn't support returning clauses from insert / update queries
22    pub returning_from_mutation: bool,
23
24    /// DynamoDB does not support != predicates on the primary key.
25    pub primary_key_ne_predicate: bool,
26
27    /// Whether the database has an auto increment modifier for integer columns.
28    pub auto_increment: bool,
29
30    pub native_varchar: bool,
31
32    /// Whether the database has native support for Timestamp types.
33    pub native_timestamp: bool,
34
35    /// Whether the database has native support for Date types.
36    pub native_date: bool,
37
38    /// Whether the database has native support for Time types.
39    pub native_time: bool,
40
41    /// Whether the database has native support for DateTime types.
42    pub native_datetime: bool,
43
44    /// Whether the database has native support for Decimal types.
45    pub native_decimal: bool,
46
47    /// Whether BigDecimal driver support is implemented.
48    /// TODO: Remove this flag when PostgreSQL BigDecimal support is implemented.
49    /// Currently only MySQL has implemented BigDecimal driver support.
50    pub bigdecimal_implemented: bool,
51
52    /// Whether the database's decimal type supports arbitrary precision.
53    /// When false, the decimal type requires fixed precision and scale to be specified upfront.
54    /// - PostgreSQL: true (NUMERIC supports arbitrary precision)
55    /// - MySQL: false (DECIMAL requires fixed precision/scale)
56    /// - SQLite/DynamoDB: false (no native decimal support, stored as TEXT)
57    pub decimal_arbitrary_precision: bool,
58
59    /// Whether OR is supported in index key conditions (e.g. DynamoDB KeyConditionExpression).
60    /// DynamoDB: false. All other backends: true (SQL backends never use index key conditions).
61    pub index_or_predicate: bool,
62
63    /// Whether to test connection pool behavior.
64    /// TODO: We only need this for the `connection_per_clone.rs` test, come up with a better way.
65    pub test_connection_pool: bool,
66}
67
68#[derive(Debug)]
69pub struct StorageTypes {
70    /// The default storage type for a string.
71    pub default_string_type: db::Type,
72
73    /// When `Some` the database supports varchar types with the specified upper
74    /// limit.
75    pub varchar: Option<u64>,
76
77    /// The default storage type for a UUID.
78    pub default_uuid_type: db::Type,
79
80    /// The default storage type for Bytes (Vec<u8>).
81    pub default_bytes_type: db::Type,
82
83    /// The default storage type for a Decimal (fixed-precision decimal).
84    pub default_decimal_type: db::Type,
85
86    /// The default storage type for a BigDecimal (arbitrary-precision decimal).
87    pub default_bigdecimal_type: db::Type,
88
89    /// The default storage type for a Timestamp (instant in time).
90    pub default_timestamp_type: db::Type,
91
92    /// The default storage type for a Zoned (timezone-aware instant).
93    pub default_zoned_type: db::Type,
94
95    /// The default storage type for a Date (civil date).
96    pub default_date_type: db::Type,
97
98    /// The default storage type for a Time (wall clock time).
99    pub default_time_type: db::Type,
100
101    /// The default storage type for a DateTime (civil datetime).
102    pub default_datetime_type: db::Type,
103
104    /// Maximum value for unsigned integers. When `Some`, unsigned integers
105    /// are limited to this value. When `None`, full u64 range is supported.
106    pub max_unsigned_integer: Option<u64>,
107}
108
109/// The database's capabilities to mutate the schema (tables, columns, indices).
110#[derive(Debug)]
111pub struct SchemaMutations {
112    /// Whether the database can change the type of an existing column.
113    pub alter_column_type: bool,
114
115    /// Whether the database can change name, type and constraints of a column all
116    /// withing a single statement.
117    pub alter_column_properties_atomic: bool,
118}
119
120impl Capability {
121    /// Validates the consistency of the capability configuration.
122    ///
123    /// This performs sanity checks to ensure the capability fields are
124    /// internally consistent. For example, if `native_varchar` is true,
125    /// then `storage_types.varchar` must be Some, and vice versa.
126    ///
127    /// Returns an error if any inconsistencies are found.
128    pub fn validate(&self) -> crate::Result<()> {
129        // Validate varchar consistency
130        if self.native_varchar && self.storage_types.varchar.is_none() {
131            return Err(crate::Error::invalid_driver_configuration(
132                "native_varchar is true but storage_types.varchar is None",
133            ));
134        }
135
136        if !self.native_varchar && self.storage_types.varchar.is_some() {
137            return Err(crate::Error::invalid_driver_configuration(
138                "native_varchar is false but storage_types.varchar is Some",
139            ));
140        }
141
142        Ok(())
143    }
144
145    /// Returns the default string length limit for this database.
146    ///
147    /// This is useful for tests and applications that need to respect
148    /// database-specific string length constraints.
149    pub fn default_string_max_length(&self) -> Option<u64> {
150        match &self.storage_types.default_string_type {
151            db::Type::VarChar(len) => Some(*len),
152            _ => None, // Handle other types gracefully
153        }
154    }
155
156    /// Returns the native database type for an application-level type.
157    ///
158    /// If the database supports the type natively, returns the same type.
159    /// Otherwise, returns the bridge/storage type that the application type
160    /// maps to in this database.
161    ///
162    /// This uses the existing `db::Type::bridge_type()` method to determine
163    /// the appropriate bridge type based on the database's storage capabilities.
164    pub fn native_type_for(&self, ty: &stmt::Type) -> stmt::Type {
165        match ty {
166            stmt::Type::Uuid => self.storage_types.default_uuid_type.bridge_type(ty),
167            #[cfg(feature = "jiff")]
168            stmt::Type::Timestamp => self.storage_types.default_timestamp_type.bridge_type(ty),
169            #[cfg(feature = "jiff")]
170            stmt::Type::Zoned => self.storage_types.default_zoned_type.bridge_type(ty),
171            #[cfg(feature = "jiff")]
172            stmt::Type::Date => self.storage_types.default_date_type.bridge_type(ty),
173            #[cfg(feature = "jiff")]
174            stmt::Type::Time => self.storage_types.default_time_type.bridge_type(ty),
175            #[cfg(feature = "jiff")]
176            stmt::Type::DateTime => self.storage_types.default_datetime_type.bridge_type(ty),
177            _ => ty.clone(),
178        }
179    }
180
181    /// SQLite capabilities.
182    pub const SQLITE: Self = Self {
183        sql: true,
184        storage_types: StorageTypes::SQLITE,
185        schema_mutations: SchemaMutations::SQLITE,
186        cte_with_update: false,
187        select_for_update: false,
188        returning_from_mutation: true,
189        primary_key_ne_predicate: true,
190        auto_increment: true,
191        bigdecimal_implemented: false,
192
193        native_varchar: true,
194
195        // SQLite does not have native date/time types
196        native_timestamp: false,
197        native_date: false,
198        native_time: false,
199        native_datetime: false,
200
201        // SQLite does not have native decimal types
202        native_decimal: false,
203        decimal_arbitrary_precision: false,
204
205        index_or_predicate: true,
206
207        test_connection_pool: false,
208    };
209
210    /// PostgreSQL capabilities
211    pub const POSTGRESQL: Self = Self {
212        cte_with_update: true,
213        storage_types: StorageTypes::POSTGRESQL,
214        schema_mutations: SchemaMutations::POSTGRESQL,
215        select_for_update: true,
216        auto_increment: true,
217        bigdecimal_implemented: false,
218
219        // PostgreSQL has native date/time types
220        native_timestamp: true,
221        native_date: true,
222        native_time: true,
223        native_datetime: true,
224
225        // PostgreSQL has native NUMERIC type with arbitrary precision
226        native_decimal: true,
227        decimal_arbitrary_precision: true,
228
229        test_connection_pool: true,
230
231        ..Self::SQLITE
232    };
233
234    /// MySQL capabilities
235    pub const MYSQL: Self = Self {
236        cte_with_update: false,
237        storage_types: StorageTypes::MYSQL,
238        schema_mutations: SchemaMutations::MYSQL,
239        select_for_update: true,
240        returning_from_mutation: false,
241        auto_increment: true,
242        bigdecimal_implemented: true,
243
244        // MySQL has native date/time types
245        native_timestamp: true,
246        native_date: true,
247        native_time: true,
248        native_datetime: true,
249
250        // MySQL has DECIMAL type but requires fixed precision/scale upfront
251        native_decimal: true,
252        decimal_arbitrary_precision: false,
253
254        test_connection_pool: true,
255
256        ..Self::SQLITE
257    };
258
259    /// DynamoDB capabilities
260    pub const DYNAMODB: Self = Self {
261        sql: false,
262        storage_types: StorageTypes::DYNAMODB,
263        schema_mutations: SchemaMutations::DYNAMODB,
264        cte_with_update: false,
265        select_for_update: false,
266        returning_from_mutation: false,
267        primary_key_ne_predicate: false,
268        auto_increment: false,
269        bigdecimal_implemented: false,
270        native_varchar: false,
271
272        // DynamoDB does not have native date/time types
273        native_timestamp: false,
274        native_date: false,
275        native_time: false,
276        native_datetime: false,
277
278        // DynamoDB does not have native decimal types
279        native_decimal: false,
280        decimal_arbitrary_precision: false,
281
282        index_or_predicate: false,
283
284        test_connection_pool: false,
285    };
286}
287
288impl StorageTypes {
289    /// SQLite storage types
290    pub const SQLITE: StorageTypes = StorageTypes {
291        default_string_type: db::Type::Text,
292
293        // SQLite doesn't really enforce the "N" in VARCHAR(N) at all – it
294        // treats any type containing "CHAR", "CLOB", or "TEXT" as having TEXT
295        // affinity, and simply ignores the length specifier. In other words,
296        // whether you declare a column as VARCHAR(10), VARCHAR(1000000), or
297        // just TEXT, SQLite won't truncate or complain based on that number.
298        //
299        // Instead, the only hard limit on how big a string (or BLOB) can be is
300        // the SQLITE_MAX_LENGTH parameter, which is set to 1 billion by default.
301        varchar: Some(1_000_000_000),
302
303        // SQLite does not have an inbuilt UUID type. The binary blob type is more
304        // difficult to read than Text but likely has better performance characteristics.
305        default_uuid_type: db::Type::Blob,
306
307        default_bytes_type: db::Type::Blob,
308
309        // SQLite does not have a native decimal type. Store as TEXT.
310        default_decimal_type: db::Type::Text,
311        default_bigdecimal_type: db::Type::Text,
312
313        // SQLite does not have native date/time types. Store as TEXT in ISO 8601 format.
314        default_timestamp_type: db::Type::Text,
315        default_zoned_type: db::Type::Text,
316        default_date_type: db::Type::Text,
317        default_time_type: db::Type::Text,
318        default_datetime_type: db::Type::Text,
319
320        // SQLite INTEGER is a signed 64-bit integer, so unsigned integers
321        // are limited to i64::MAX to prevent overflow
322        max_unsigned_integer: Some(i64::MAX as u64),
323    };
324
325    pub const POSTGRESQL: StorageTypes = StorageTypes {
326        default_string_type: db::Type::Text,
327
328        // The maximum n you can specify is 10 485 760 characters. Attempts to
329        // declare varchar with a larger typmod will be rejected at
330        // table‐creation time.
331        varchar: Some(10_485_760),
332
333        default_uuid_type: db::Type::Uuid,
334
335        default_bytes_type: db::Type::Blob,
336
337        // PostgreSQL has native NUMERIC type for fixed and arbitrary-precision decimals.
338        default_decimal_type: db::Type::Numeric(None),
339        // TODO: PostgreSQL has native NUMERIC type for arbitrary-precision decimals,
340        // but the encoding is complicated and has to be done separately in the future.
341        default_bigdecimal_type: db::Type::Text,
342
343        // PostgreSQL has native support for temporal types with microsecond precision (6 digits)
344        default_timestamp_type: db::Type::Timestamp(6),
345        default_zoned_type: db::Type::Text,
346        default_date_type: db::Type::Date,
347        default_time_type: db::Type::Time(6),
348        default_datetime_type: db::Type::DateTime(6),
349
350        // PostgreSQL BIGINT is signed 64-bit, so unsigned integers are limited
351        // to i64::MAX. While NUMERIC could theoretically support larger values,
352        // we prefer explicit limits over implicit type switching.
353        max_unsigned_integer: Some(i64::MAX as u64),
354    };
355
356    pub const MYSQL: StorageTypes = StorageTypes {
357        default_string_type: db::Type::VarChar(191),
358
359        // Values in VARCHAR columns are variable-length strings. The length can
360        // be specified as a value from 0 to 65,535. The effective maximum
361        // length of a VARCHAR is subject to the maximum row size (65,535 bytes,
362        // which is shared among all columns) and the character set used.
363        varchar: Some(65_535),
364
365        // MySQL does not have an inbuilt UUID type. The binary blob type is
366        // more difficult to read than Text but likely has better performance
367        // characteristics. However, limitations in the engine make it easier to
368        // use VarChar for now.
369        default_uuid_type: db::Type::VarChar(36),
370
371        default_bytes_type: db::Type::Blob,
372
373        // MySQL does not have an arbitrary-precision decimal type. The DECIMAL type
374        // requires a fixed precision and scale to be specified upfront. Store as TEXT.
375        default_decimal_type: db::Type::Text,
376        default_bigdecimal_type: db::Type::Text,
377
378        // MySQL has native support for temporal types with microsecond precision (6 digits)
379        // The `TIMESTAMP` time only supports a limited range (1970-2038), so we default to
380        // DATETIME and let Toasty do the UTC conversion.
381        default_timestamp_type: db::Type::DateTime(6),
382        default_zoned_type: db::Type::Text,
383        default_date_type: db::Type::Date,
384        default_time_type: db::Type::Time(6),
385        default_datetime_type: db::Type::DateTime(6),
386
387        // MySQL supports full u64 range via BIGINT UNSIGNED
388        max_unsigned_integer: None,
389    };
390
391    pub const DYNAMODB: StorageTypes = StorageTypes {
392        default_string_type: db::Type::Text,
393
394        // DynamoDB does not support varchar types
395        varchar: None,
396
397        default_uuid_type: db::Type::Text,
398
399        default_bytes_type: db::Type::Blob,
400
401        // DynamoDB does not have a native decimal type. Store as TEXT.
402        default_decimal_type: db::Type::Text,
403        default_bigdecimal_type: db::Type::Text,
404
405        // DynamoDB does not have native date/time types. Store as TEXT (strings).
406        default_timestamp_type: db::Type::Text,
407        default_zoned_type: db::Type::Text,
408        default_date_type: db::Type::Text,
409        default_time_type: db::Type::Text,
410        default_datetime_type: db::Type::Text,
411
412        // DynamoDB supports full u64 range (numbers stored as strings)
413        max_unsigned_integer: None,
414    };
415}
416
417impl SchemaMutations {
418    pub const SQLITE: Self = Self {
419        alter_column_type: false,
420        alter_column_properties_atomic: false,
421    };
422
423    pub const POSTGRESQL: Self = Self {
424        alter_column_type: true,
425        alter_column_properties_atomic: false,
426    };
427
428    pub const MYSQL: Self = Self {
429        alter_column_type: true,
430        alter_column_properties_atomic: true,
431    };
432
433    // DynamoDB migrations are currently not supported.
434    pub const DYNAMODB: Self = Self {
435        alter_column_type: false,
436        alter_column_properties_atomic: false,
437    };
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_validate_sqlite_capability() {
446        // SQLite has native_varchar=true and varchar=Some, should pass
447        assert!(Capability::SQLITE.validate().is_ok());
448    }
449
450    #[test]
451    fn test_validate_postgresql_capability() {
452        // PostgreSQL has native_varchar=true and varchar=Some, should pass
453        assert!(Capability::POSTGRESQL.validate().is_ok());
454    }
455
456    #[test]
457    fn test_validate_mysql_capability() {
458        // MySQL has native_varchar=true and varchar=Some, should pass
459        assert!(Capability::MYSQL.validate().is_ok());
460    }
461
462    #[test]
463    fn test_validate_dynamodb_capability() {
464        // DynamoDB has native_varchar=false and varchar=None, should pass
465        assert!(Capability::DYNAMODB.validate().is_ok());
466    }
467
468    #[test]
469    fn test_validate_fails_when_native_varchar_true_but_no_varchar() {
470        let invalid = Capability {
471            native_varchar: true,
472            storage_types: StorageTypes {
473                varchar: None, // Invalid: native_varchar is true but varchar is None
474                ..StorageTypes::SQLITE
475            },
476            ..Capability::SQLITE
477        };
478
479        let result = invalid.validate();
480        assert!(result.is_err());
481        assert!(result
482            .unwrap_err()
483            .to_string()
484            .contains("native_varchar is true but storage_types.varchar is None"));
485    }
486
487    #[test]
488    fn test_validate_fails_when_native_varchar_false_but_has_varchar() {
489        let invalid = Capability {
490            native_varchar: false,
491            storage_types: StorageTypes {
492                varchar: Some(1000), // Invalid: native_varchar is false but varchar is Some
493                ..StorageTypes::DYNAMODB
494            },
495            ..Capability::DYNAMODB
496        };
497
498        let result = invalid.validate();
499        assert!(result.is_err());
500        assert!(result
501            .unwrap_err()
502            .to_string()
503            .contains("native_varchar is false but storage_types.varchar is Some"));
504    }
505}