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}