1use rustlavel_core::{Error, Result};
12
13#[derive(Debug, Clone, PartialEq)]
20pub enum ColumnType {
21 Id,
23 UuidId,
25 SmallInteger,
26 Integer,
27 BigInteger,
28 Float,
30 Decimal { precision: u32, scale: u32 },
31 Boolean,
32 String { length: u32 },
33 Text,
34 Json,
35 Uuid,
36 Date,
37 Time,
38 Timestamp,
39 Binary,
40 Raw(String),
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum ReturningStyle {
47 Suffix,
49 OutputClause,
52 SeparateQuery(&'static str),
54}
55
56pub trait Dialect: Send + Sync + std::fmt::Debug + 'static {
58 fn name(&self) -> &'static str;
60
61 fn quote(&self, identifier: &str) -> String;
65
66 fn placeholder(&self, position: usize) -> String;
68
69 fn column_type(&self, kind: &ColumnType) -> String;
71
72 fn now(&self) -> &'static str;
74
75 fn uuid_default(&self) -> Option<&'static str>;
77
78 fn returning(&self) -> ReturningStyle;
80
81 fn limit_offset(&self, limit: Option<i64>, offset: Option<i64>, ordered: bool) -> String;
86
87 fn supports_if_not_exists_table(&self) -> bool {
89 true
90 }
91
92 fn supports_if_not_exists_index(&self) -> bool {
98 false
99 }
100
101 fn booleans_are_integers(&self) -> bool {
107 false
108 }
109
110 fn max_identifier_length(&self) -> usize {
112 63
113 }
114
115 fn migrations_table_sql(&self, table: &str) -> String {
121 format!(
122 "create table if not exists {} (\n \
123 id {} primary key,\n \
124 name {} not null unique,\n \
125 batch {} not null,\n \
126 ran_at {} not null default {}\n)",
127 self.quote(table),
128 self.column_type(&ColumnType::Id),
129 self.column_type(&ColumnType::String { length: 255 }),
130 self.column_type(&ColumnType::Integer),
131 self.column_type(&ColumnType::Timestamp),
132 self.now()
133 )
134 }
135
136 fn add_column_clause(&self) -> &'static str {
142 "add column"
143 }
144
145 fn begin_sql(&self) -> &'static str {
151 "begin"
152 }
153
154 fn commit_sql(&self) -> &'static str {
155 "commit"
156 }
157
158 fn rollback_sql(&self) -> &'static str {
159 "rollback"
160 }
161
162 fn savepoint_sql(&self, name: &str) -> String {
163 format!("savepoint {name}")
164 }
165
166 fn rollback_to_savepoint_sql(&self, name: &str) -> String {
167 format!("rollback to savepoint {name}")
168 }
169
170 fn skip_locked(&self) -> (&'static str, &'static str) {
187 ("", "")
188 }
189
190 fn current_schema_expression(&self) -> &'static str;
195
196 fn list_tables_sql(&self) -> &'static str;
203
204 fn disable_foreign_keys_sql(&self) -> Option<&'static str> {
207 None
208 }
209
210 fn enable_foreign_keys_sql(&self) -> Option<&'static str> {
211 None
212 }
213
214 fn drop_table_sql(&self, table: &str) -> String {
216 format!("drop table if exists {}", self.quote(table))
217 }
218}
219
220pub fn quote_qualified(dialect: &dyn Dialect, name: &str) -> Result<String> {
222 let parts: Result<Vec<String>> = name
223 .split('.')
224 .map(|part| {
225 validate_identifier(part, dialect.max_identifier_length())
226 .map(|_| dialect.quote(part))
227 })
228 .collect();
229 Ok(parts?.join("."))
230}
231
232pub fn validate_identifier(name: &str, max_length: usize) -> Result<()> {
237 let valid = !name.is_empty()
238 && name.len() <= max_length
239 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
240 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
241
242 if valid {
243 Ok(())
244 } else {
245 Err(Error::msg(format!(
246 "`{name}` is not a valid SQL identifier. Identifiers may contain letters, digits and \
247 underscores, must not start with a digit, and must be at most {max_length} characters."
248 )))
249 }
250}
251
252#[derive(Debug, Default, Clone, Copy)]
255pub struct Postgres;
256
257impl Dialect for Postgres {
258 fn name(&self) -> &'static str {
259 "postgres"
260 }
261
262 fn quote(&self, identifier: &str) -> String {
263 format!("\"{identifier}\"")
264 }
265
266 fn placeholder(&self, position: usize) -> String {
267 format!("${position}")
268 }
269
270 fn column_type(&self, kind: &ColumnType) -> String {
271 match kind {
272 ColumnType::Id => "bigserial".into(),
273 ColumnType::UuidId | ColumnType::Uuid => "uuid".into(),
274 ColumnType::SmallInteger => "smallint".into(),
275 ColumnType::Integer => "integer".into(),
276 ColumnType::BigInteger => "bigint".into(),
277 ColumnType::Float => "double precision".into(),
278 ColumnType::Decimal { precision, scale } => format!("numeric({precision}, {scale})"),
279 ColumnType::Boolean => "boolean".into(),
280 ColumnType::String { length } => format!("varchar({length})"),
281 ColumnType::Text => "text".into(),
282 ColumnType::Json => "jsonb".into(),
283 ColumnType::Date => "date".into(),
284 ColumnType::Time => "time".into(),
285 ColumnType::Timestamp => "timestamptz".into(),
286 ColumnType::Binary => "bytea".into(),
287 ColumnType::Raw(sql) => sql.clone(),
288 }
289 }
290
291 fn now(&self) -> &'static str {
292 "now()"
293 }
294
295 fn uuid_default(&self) -> Option<&'static str> {
296 Some("gen_random_uuid()")
297 }
298
299 fn returning(&self) -> ReturningStyle {
300 ReturningStyle::Suffix
301 }
302
303 fn limit_offset(&self, limit: Option<i64>, offset: Option<i64>, _ordered: bool) -> String {
304 let mut out = String::new();
305 if let Some(limit) = limit {
306 out.push_str(&format!(" limit {}", limit.max(0)));
307 }
308 if let Some(offset) = offset {
309 out.push_str(&format!(" offset {}", offset.max(0)));
310 }
311 out
312 }
313
314 fn supports_if_not_exists_index(&self) -> bool {
315 true
316 }
317
318 fn skip_locked(&self) -> (&'static str, &'static str) {
319 ("", " for update skip locked")
320 }
321
322 fn current_schema_expression(&self) -> &'static str {
323 "current_schema()"
324 }
325
326 fn list_tables_sql(&self) -> &'static str {
327 "select tablename from pg_tables where schemaname = current_schema()"
328 }
329
330 fn drop_table_sql(&self, table: &str) -> String {
331 format!("drop table if exists {} cascade", self.quote(table))
334 }
335}
336
337#[derive(Debug, Default, Clone, Copy)]
361pub struct Sqlite;
362
363impl Dialect for Sqlite {
364 fn name(&self) -> &'static str {
365 "sqlite"
366 }
367
368 fn quote(&self, identifier: &str) -> String {
369 format!("\"{identifier}\"")
370 }
371
372 fn placeholder(&self, _position: usize) -> String {
373 "?".into()
374 }
375
376 fn column_type(&self, kind: &ColumnType) -> String {
377 match kind {
378 ColumnType::Id => "integer".into(),
383 ColumnType::SmallInteger | ColumnType::Integer | ColumnType::BigInteger => {
384 "integer".into()
385 }
386 ColumnType::Float => "real".into(),
387 ColumnType::Decimal { precision, scale } => format!("numeric({precision}, {scale})"),
392 ColumnType::Boolean => "integer".into(),
395 ColumnType::String { length } => format!("varchar({length})"),
396 ColumnType::Text | ColumnType::Json => "text".into(),
397 ColumnType::UuidId | ColumnType::Uuid => "text".into(),
398 ColumnType::Date | ColumnType::Time | ColumnType::Timestamp => "text".into(),
399 ColumnType::Binary => "blob".into(),
400 ColumnType::Raw(sql) => sql.clone(),
401 }
402 }
403
404 fn now(&self) -> &'static str {
405 "current_timestamp"
406 }
407
408 fn uuid_default(&self) -> Option<&'static str> {
409 None
413 }
414
415 fn returning(&self) -> ReturningStyle {
416 ReturningStyle::SeparateQuery("select last_insert_rowid()")
420 }
421
422 fn limit_offset(&self, limit: Option<i64>, offset: Option<i64>, _ordered: bool) -> String {
423 let mut out = String::new();
424 if let Some(limit) = limit {
425 out.push_str(&format!(" limit {}", limit.max(0)));
426 }
427 if let Some(offset) = offset {
428 if limit.is_none() {
431 out.push_str(" limit -1");
432 }
433 out.push_str(&format!(" offset {}", offset.max(0)));
434 }
435 out
436 }
437
438 fn supports_if_not_exists_index(&self) -> bool {
439 true
440 }
441
442 fn booleans_are_integers(&self) -> bool {
443 true
444 }
445
446 fn max_identifier_length(&self) -> usize {
447 63
451 }
452
453 fn current_schema_expression(&self) -> &'static str {
454 "'main'"
455 }
456
457 fn skip_locked(&self) -> (&'static str, &'static str) {
462 ("", "")
463 }
464
465 fn begin_sql(&self) -> &'static str {
477 "begin immediate"
478 }
479
480 fn list_tables_sql(&self) -> &'static str {
481 "select name from sqlite_master where type = 'table' and name not like 'sqlite_%'"
484 }
485
486 fn disable_foreign_keys_sql(&self) -> Option<&'static str> {
487 Some("pragma foreign_keys = off")
491 }
492
493 fn enable_foreign_keys_sql(&self) -> Option<&'static str> {
494 Some("pragma foreign_keys = on")
495 }
496}
497
498#[derive(Debug, Default, Clone, Copy)]
501pub struct MySql;
502
503impl Dialect for MySql {
504 fn name(&self) -> &'static str {
505 "mysql"
506 }
507
508 fn quote(&self, identifier: &str) -> String {
509 format!("`{identifier}`")
510 }
511
512 fn placeholder(&self, _position: usize) -> String {
513 "?".into()
515 }
516
517 fn column_type(&self, kind: &ColumnType) -> String {
518 match kind {
519 ColumnType::Id => "bigint not null auto_increment".into(),
524 ColumnType::UuidId | ColumnType::Uuid => "char(36)".into(),
526 ColumnType::SmallInteger => "smallint".into(),
527 ColumnType::Integer => "int".into(),
528 ColumnType::BigInteger => "bigint".into(),
529 ColumnType::Float => "double".into(),
530 ColumnType::Decimal { precision, scale } => format!("decimal({precision}, {scale})"),
531 ColumnType::Boolean => "tinyint(1)".into(),
534 ColumnType::String { length } => format!("varchar({length})"),
535 ColumnType::Text => "text".into(),
536 ColumnType::Json => "json".into(),
537 ColumnType::Date => "date".into(),
538 ColumnType::Time => "time".into(),
539 ColumnType::Timestamp => "datetime(6)".into(),
542 ColumnType::Binary => "longblob".into(),
543 ColumnType::Raw(sql) => sql.clone(),
544 }
545 }
546
547 fn now(&self) -> &'static str {
548 "current_timestamp(6)"
549 }
550
551 fn uuid_default(&self) -> Option<&'static str> {
552 None
555 }
556
557 fn returning(&self) -> ReturningStyle {
558 ReturningStyle::SeparateQuery("select last_insert_id()")
559 }
560
561 fn limit_offset(&self, limit: Option<i64>, offset: Option<i64>, _ordered: bool) -> String {
562 let mut out = String::new();
563 match (limit, offset) {
564 (None, Some(offset)) => {
567 out.push_str(&format!(" limit 18446744073709551615 offset {}", offset.max(0)));
568 }
569 (Some(limit), offset) => {
570 out.push_str(&format!(" limit {}", limit.max(0)));
571 if let Some(offset) = offset {
572 out.push_str(&format!(" offset {}", offset.max(0)));
573 }
574 }
575 (None, None) => {}
576 }
577 out
578 }
579
580 fn booleans_are_integers(&self) -> bool {
581 true
582 }
583
584 fn max_identifier_length(&self) -> usize {
585 64
586 }
587
588 fn current_schema_expression(&self) -> &'static str {
589 "database()"
592 }
593
594 fn skip_locked(&self) -> (&'static str, &'static str) {
599 ("", " for update skip locked")
600 }
601
602 fn list_tables_sql(&self) -> &'static str {
603 "select table_name from information_schema.tables \
604 where table_schema = database() and table_type = 'BASE TABLE'"
605 }
606
607 fn disable_foreign_keys_sql(&self) -> Option<&'static str> {
608 Some("set foreign_key_checks = 0")
609 }
610
611 fn enable_foreign_keys_sql(&self) -> Option<&'static str> {
612 Some("set foreign_key_checks = 1")
613 }
614}
615
616#[derive(Debug, Default, Clone, Copy)]
619pub struct SqlServer;
620
621impl Dialect for SqlServer {
622 fn name(&self) -> &'static str {
623 "sqlserver"
624 }
625
626 fn quote(&self, identifier: &str) -> String {
627 format!("[{identifier}]")
628 }
629
630 fn placeholder(&self, position: usize) -> String {
631 format!("@P{position}")
632 }
633
634 fn column_type(&self, kind: &ColumnType) -> String {
635 match kind {
636 ColumnType::Id => "bigint identity(1,1)".into(),
637 ColumnType::UuidId | ColumnType::Uuid => "uniqueidentifier".into(),
638 ColumnType::SmallInteger => "smallint".into(),
639 ColumnType::Integer => "int".into(),
640 ColumnType::BigInteger => "bigint".into(),
641 ColumnType::Float => "float".into(),
642 ColumnType::Decimal { precision, scale } => format!("decimal({precision}, {scale})"),
643 ColumnType::Boolean => "bit".into(),
644 ColumnType::String { length } => format!("nvarchar({length})"),
647 ColumnType::Text | ColumnType::Json => "nvarchar(max)".into(),
648 ColumnType::Date => "date".into(),
649 ColumnType::Time => "time".into(),
650 ColumnType::Timestamp => "datetime2".into(),
651 ColumnType::Binary => "varbinary(max)".into(),
652 ColumnType::Raw(sql) => sql.clone(),
653 }
654 }
655
656 fn now(&self) -> &'static str {
657 "sysutcdatetime()"
658 }
659
660 fn uuid_default(&self) -> Option<&'static str> {
661 Some("newid()")
662 }
663
664 fn returning(&self) -> ReturningStyle {
665 ReturningStyle::OutputClause
666 }
667
668 fn limit_offset(&self, limit: Option<i64>, offset: Option<i64>, ordered: bool) -> String {
669 if limit.is_none() && offset.is_none() {
670 return String::new();
671 }
672
673 let mut out = String::new();
677 if !ordered {
678 out.push_str(" order by (select null)");
679 }
680 out.push_str(&format!(" offset {} rows", offset.unwrap_or(0).max(0)));
681 if let Some(limit) = limit {
682 out.push_str(&format!(" fetch next {} rows only", limit.max(0)));
683 }
684 out
685 }
686
687 fn supports_if_not_exists_table(&self) -> bool {
688 false
689 }
690
691 fn booleans_are_integers(&self) -> bool {
692 true
693 }
694
695 fn max_identifier_length(&self) -> usize {
696 128
697 }
698
699 fn migrations_table_sql(&self, table: &str) -> String {
700 format!(
702 "if object_id('{table}', 'U') is null create table {} (\n \
703 [id] bigint identity(1,1) primary key,\n \
704 [name] nvarchar(255) not null unique,\n \
705 [batch] int not null,\n \
706 [ran_at] datetime2 not null default sysutcdatetime()\n)",
707 self.quote(table)
708 )
709 }
710
711 fn add_column_clause(&self) -> &'static str {
712 "add"
713 }
714
715 fn begin_sql(&self) -> &'static str {
716 "begin transaction"
717 }
718
719 fn commit_sql(&self) -> &'static str {
720 "commit transaction"
721 }
722
723 fn rollback_sql(&self) -> &'static str {
724 "rollback transaction"
725 }
726
727 fn savepoint_sql(&self, name: &str) -> String {
728 format!("save transaction {name}")
731 }
732
733 fn rollback_to_savepoint_sql(&self, name: &str) -> String {
734 format!("rollback transaction {name}")
735 }
736
737 fn current_schema_expression(&self) -> &'static str {
738 "schema_name()"
739 }
740
741 fn skip_locked(&self) -> (&'static str, &'static str) {
746 (" with (updlock, readpast, rowlock)", "")
747 }
748
749 fn list_tables_sql(&self) -> &'static str {
750 "select t.name from sys.tables t \
754 where t.is_ms_shipped = 0 and schema_name(t.schema_id) = schema_name()"
755 }
756
757 fn disable_foreign_keys_sql(&self) -> Option<&'static str> {
758 Some("exec sp_MSforeachtable 'alter table ? nocheck constraint all'")
760 }
761
762 fn enable_foreign_keys_sql(&self) -> Option<&'static str> {
763 Some("exec sp_MSforeachtable 'alter table ? with check check constraint all'")
764 }
765}
766
767pub fn by_name(name: &str) -> Result<Box<dyn Dialect>> {
769 match name.to_ascii_lowercase().as_str() {
770 "postgres" | "postgresql" | "pgsql" => Ok(Box::new(Postgres)),
771 "mysql" | "mariadb" => Ok(Box::new(MySql)),
772 "sqlserver" | "mssql" => Ok(Box::new(SqlServer)),
773 other => Err(Error::msg(format!(
774 "`{other}` is not a database this framework speaks. Available: postgres, mysql, sqlserver."
775 ))),
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782
783 fn all() -> Vec<Box<dyn Dialect>> {
784 vec![Box::new(Postgres), Box::new(MySql), Box::new(SqlServer)]
785 }
786
787 #[test]
788 fn each_dialect_quotes_the_way_its_database_expects() {
789 assert_eq!(Postgres.quote("users"), "\"users\"");
790 assert_eq!(MySql.quote("users"), "`users`");
791 assert_eq!(SqlServer.quote("users"), "[users]");
792 }
793
794 #[test]
795 fn placeholders_differ_in_kind_not_just_spelling() {
796 assert_eq!(Postgres.placeholder(1), "$1");
797 assert_eq!(Postgres.placeholder(3), "$3");
798
799 assert_eq!(MySql.placeholder(1), "?");
801 assert_eq!(MySql.placeholder(3), "?");
802
803 assert_eq!(SqlServer.placeholder(3), "@P3");
804 }
805
806 #[test]
807 fn a_qualified_name_is_quoted_one_part_at_a_time() {
808 assert_eq!(
809 quote_qualified(&Postgres, "public.users").unwrap(),
810 "\"public\".\"users\""
811 );
812 assert_eq!(quote_qualified(&MySql, "shop.orders").unwrap(), "`shop`.`orders`");
813 assert_eq!(quote_qualified(&SqlServer, "dbo.users").unwrap(), "[dbo].[users]");
814 }
815
816 #[test]
817 fn an_injected_identifier_is_rejected_by_every_dialect() {
818 for dialect in all() {
819 for hostile in ["users; drop table users", "a b", "1abc", "", "us\"er"] {
820 assert!(
821 quote_qualified(dialect.as_ref(), hostile).is_err(),
822 "{} accepted {hostile:?}",
823 dialect.name()
824 );
825 }
826 }
827 }
828
829 #[test]
830 fn identifier_length_limits_follow_the_database() {
831 let long = "a".repeat(100);
832
833 assert!(validate_identifier(&long, Postgres.max_identifier_length()).is_err());
834 assert!(validate_identifier(&long, MySql.max_identifier_length()).is_err());
835 assert!(validate_identifier(&long, SqlServer.max_identifier_length()).is_ok());
836 }
837
838 #[test]
839 fn the_key_column_is_auto_incrementing_everywhere() {
840 assert_eq!(Postgres.column_type(&ColumnType::Id), "bigserial");
841 assert_eq!(MySql.column_type(&ColumnType::Id), "bigint not null auto_increment");
842 assert_eq!(SqlServer.column_type(&ColumnType::Id), "bigint identity(1,1)");
843 }
844
845 #[test]
846 fn text_and_json_map_to_what_each_database_actually_has() {
847 assert_eq!(Postgres.column_type(&ColumnType::Json), "jsonb");
848 assert_eq!(MySql.column_type(&ColumnType::Json), "json");
849 assert_eq!(SqlServer.column_type(&ColumnType::Json), "nvarchar(max)");
851 }
852
853 #[test]
854 fn a_string_column_carries_its_length_everywhere() {
855 let kind = ColumnType::String { length: 120 };
856
857 assert_eq!(Postgres.column_type(&kind), "varchar(120)");
858 assert_eq!(MySql.column_type(&kind), "varchar(120)");
859 assert_eq!(SqlServer.column_type(&kind), "nvarchar(120)");
860 }
861
862 #[test]
863 fn paging_uses_each_databases_own_syntax() {
864 assert_eq!(Postgres.limit_offset(Some(10), Some(20), true), " limit 10 offset 20");
865 assert_eq!(MySql.limit_offset(Some(10), Some(20), true), " limit 10 offset 20");
866 assert_eq!(
867 SqlServer.limit_offset(Some(10), Some(20), true),
868 " offset 20 rows fetch next 10 rows only"
869 );
870 }
871
872 #[test]
873 fn sql_server_supplies_an_ordering_when_paging_has_none() {
874 let paged = SqlServer.limit_offset(Some(10), None, false);
877 assert!(paged.starts_with(" order by (select null)"), "{paged}");
878
879 assert!(!SqlServer.limit_offset(Some(10), None, true).contains("order by"));
881 }
882
883 #[test]
884 fn mysql_cannot_offset_without_a_limit() {
885 let offset_only = MySql.limit_offset(None, Some(20), true);
886
887 assert!(offset_only.contains("limit 18446744073709551615"), "{offset_only}");
888 assert!(offset_only.ends_with("offset 20"));
889 }
890
891 #[test]
892 fn no_paging_produces_no_clause() {
893 for dialect in all() {
894 assert_eq!(dialect.limit_offset(None, None, true), "", "{}", dialect.name());
895 }
896 }
897
898 #[test]
899 fn generated_keys_come_back_differently() {
900 assert_eq!(Postgres.returning(), ReturningStyle::Suffix);
901 assert_eq!(SqlServer.returning(), ReturningStyle::OutputClause);
902 assert_eq!(
903 MySql.returning(),
904 ReturningStyle::SeparateQuery("select last_insert_id()")
905 );
906 }
907
908 #[test]
909 fn the_migration_table_is_valid_for_each_database() {
910 let postgres = Postgres.migrations_table_sql("rustlavel_migrations");
911 assert!(postgres.contains("create table if not exists \"rustlavel_migrations\""));
912 assert!(postgres.contains("bigserial primary key"));
913
914 let mysql = MySql.migrations_table_sql("rustlavel_migrations");
915 assert!(mysql.contains("`rustlavel_migrations`"));
916 assert!(mysql.contains("auto_increment primary key"), "{mysql}");
918
919 let sqlserver = SqlServer.migrations_table_sql("rustlavel_migrations");
921 assert!(sqlserver.starts_with("if object_id("));
922 assert!(sqlserver.contains("identity(1,1)"));
923 }
924
925 #[test]
926 fn transaction_control_uses_each_databases_own_words() {
927 assert_eq!(Postgres.begin_sql(), "begin");
930 assert_eq!(MySql.begin_sql(), "begin");
931 assert_eq!(SqlServer.begin_sql(), "begin transaction");
932
933 assert_eq!(SqlServer.commit_sql(), "commit transaction");
934 assert_eq!(SqlServer.rollback_sql(), "rollback transaction");
935 assert_eq!(SqlServer.savepoint_sql("sp1"), "save transaction sp1");
936 assert_eq!(SqlServer.rollback_to_savepoint_sql("sp1"), "rollback transaction sp1");
937
938 assert_eq!(Postgres.savepoint_sql("sp1"), "savepoint sp1");
939 assert_eq!(Postgres.rollback_to_savepoint_sql("sp1"), "rollback to savepoint sp1");
940 }
941
942 #[test]
949 fn each_database_claims_a_row_in_its_own_way() {
950 assert_eq!(Postgres.skip_locked(), ("", " for update skip locked"));
951 assert_eq!(MySql.skip_locked(), ("", " for update skip locked"));
952 assert_eq!(SqlServer.skip_locked(), (" with (updlock, readpast, rowlock)", ""));
953 assert_eq!(Sqlite.skip_locked(), ("", ""));
956 assert_eq!(Sqlite.begin_sql(), "begin immediate");
957
958 for dialect in [&Postgres as &dyn Dialect, &MySql, &SqlServer, &Sqlite] {
962 let (hint, clause) = dialect.skip_locked();
963 assert!(
964 hint.is_empty() || clause.is_empty(),
965 "{} sets both a hint and a clause",
966 dialect.name()
967 );
968 }
969 }
970
971 #[test]
972 fn schema_expressions_are_what_each_database_calls_them() {
973 assert_eq!(Postgres.current_schema_expression(), "current_schema()");
974 assert_eq!(MySql.current_schema_expression(), "database()");
975 assert_eq!(SqlServer.current_schema_expression(), "schema_name()");
976 }
977
978 #[test]
979 fn every_dialect_can_enumerate_its_own_tables() {
980 for dialect in all() {
981 let sql = dialect.list_tables_sql();
982
983 assert!(sql.starts_with("select "), "{}: {sql}", dialect.name());
984 assert!(
987 sql.contains("current_schema()")
988 || sql.contains("database()")
989 || sql.contains("schema_name()"),
990 "{} does not scope its table list: {sql}",
991 dialect.name()
992 );
993 }
994 }
995
996 #[test]
997 fn sql_server_adds_a_column_without_saying_column() {
998 assert_eq!(Postgres.add_column_clause(), "add column");
1001 assert_eq!(MySql.add_column_clause(), "add column");
1002 assert_eq!(SqlServer.add_column_clause(), "add");
1003 }
1004
1005 #[test]
1006 fn sql_server_never_lists_microsofts_own_tables() {
1007 assert!(SqlServer.list_tables_sql().contains("is_ms_shipped = 0"));
1010 }
1011
1012 #[test]
1013 fn dropping_a_table_takes_its_dependants_with_it() {
1014 assert!(Postgres.drop_table_sql("users").ends_with("cascade"));
1017 assert!(Postgres.disable_foreign_keys_sql().is_none());
1018
1019 assert_eq!(MySql.drop_table_sql("users"), "drop table if exists `users`");
1020 assert!(MySql.disable_foreign_keys_sql().is_some());
1021 assert!(MySql.enable_foreign_keys_sql().is_some());
1022
1023 assert_eq!(SqlServer.drop_table_sql("users"), "drop table if exists [users]");
1024 assert!(SqlServer.disable_foreign_keys_sql().is_some());
1025 }
1026
1027 #[test]
1028 fn dialects_are_found_by_the_names_people_use() {
1029 for (name, expected) in [
1030 ("postgres", "postgres"),
1031 ("postgresql", "postgres"),
1032 ("mysql", "mysql"),
1033 ("mariadb", "mysql"),
1034 ("sqlserver", "sqlserver"),
1035 ("mssql", "sqlserver"),
1036 ("MySQL", "mysql"),
1037 ] {
1038 assert_eq!(by_name(name).unwrap().name(), expected, "for {name}");
1039 }
1040 }
1041
1042 #[test]
1043 fn an_unknown_database_lists_the_ones_that_exist() {
1044 let error = by_name("oracle").unwrap_err().to_string();
1045
1046 assert!(error.contains("postgres, mysql, sqlserver"), "{error}");
1047 }
1048}