1use sea_query::{
14 ColumnDef as SeaColumnDef, ColumnType, MysqlQueryBuilder, PostgresQueryBuilder,
15 SqliteQueryBuilder, Table as SeaTable,
16};
17
18use crate::pool::Driver;
19
20pub struct Schema {
21 pub statements: Vec<String>,
22 driver: Driver,
23}
24
25impl Default for Schema {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl Schema {
32 pub fn new() -> Self {
33 Self::for_driver(Driver::Postgres)
34 }
35
36 pub fn for_driver(driver: Driver) -> Self {
37 Self {
38 statements: Vec::new(),
39 driver,
40 }
41 }
42
43 pub fn driver(&self) -> Driver {
44 self.driver
45 }
46
47 pub fn create<F>(&mut self, table: &str, build: F)
49 where
50 F: FnOnce(&mut Table),
51 {
52 let mut t = Table::new(table, TableMode::Create, self.driver);
53 build(&mut t);
54 self.statements.extend(t.into_statements());
55 }
56
57 pub fn table<F>(&mut self, table: &str, build: F)
62 where
63 F: FnOnce(&mut Table),
64 {
65 let mut t = Table::new(table, TableMode::Alter, self.driver);
66 build(&mut t);
67 self.statements.extend(t.into_statements());
68 }
69
70 pub fn drop(&mut self, table: &str) {
71 let sql = match self.driver {
72 Driver::Postgres => format!("DROP TABLE IF EXISTS {} CASCADE", table),
73 Driver::MySql | Driver::Sqlite => format!("DROP TABLE IF EXISTS {}", table),
74 };
75 self.statements.push(sql);
76 }
77
78 pub fn drop_if_exists(&mut self, table: &str) {
79 self.drop(table);
80 }
81
82 pub fn rename(&mut self, from: &str, to: &str) {
84 self.statements
85 .push(format!("ALTER TABLE {from} RENAME TO {to}"));
86 }
87
88 pub fn has_table(&mut self, _table: &str) {
90 }
92
93 pub fn raw(&mut self, sql: impl Into<String>) {
94 self.statements.push(sql.into());
95 }
96}
97
98#[derive(Clone, Copy, PartialEq, Eq)]
99enum TableMode {
100 Create,
101 Alter,
102}
103
104pub struct Table {
106 name: String,
107 mode: TableMode,
108 driver: Driver,
109 columns: Vec<ColumnDef>,
110 indexes: Vec<String>,
111 foreign_keys: Vec<PendingFk>,
112 drops: Vec<String>,
113 renames: Vec<(String, String)>,
114 checks: Vec<PendingCheck>,
115 primary_keys: Vec<String>,
119}
120
121#[derive(Clone)]
122struct PendingFk {
123 column: String,
124 ref_table: String,
125 ref_col: String,
126 on_delete: Option<String>,
127 on_update: Option<String>,
128}
129
130impl PendingFk {
131 fn constraint_name(&self, table: &str) -> String {
132 format!("fk_{}_{}", table, self.column)
133 }
134
135 fn actions(&self) -> String {
136 let mut s = String::new();
137 if let Some(action) = &self.on_delete {
138 s.push_str(&format!(" ON DELETE {action}"));
139 }
140 if let Some(action) = &self.on_update {
141 s.push_str(&format!(" ON UPDATE {action}"));
142 }
143 s
144 }
145
146 fn inline_clause(&self, table: &str) -> String {
147 format!(
148 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}",
149 self.constraint_name(table),
150 self.column,
151 self.ref_table,
152 self.ref_col,
153 self.actions(),
154 )
155 }
156
157 fn alter_sql(&self, table: &str) -> String {
158 format!(
159 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}",
160 table,
161 self.constraint_name(table),
162 self.column,
163 self.ref_table,
164 self.ref_col,
165 self.actions(),
166 )
167 }
168}
169
170#[derive(Clone)]
171struct PendingCheck {
172 name: String,
173 expr: String,
174}
175
176impl PendingCheck {
177 fn inline_clause(&self) -> String {
178 format!("CONSTRAINT {} CHECK ({})", self.name, self.expr)
179 }
180
181 fn alter_sql(&self, table: &str) -> String {
182 format!(
183 "ALTER TABLE {} ADD CONSTRAINT {} CHECK ({})",
184 table, self.name, self.expr
185 )
186 }
187}
188
189impl Table {
190 fn new(name: impl Into<String>, mode: TableMode, driver: Driver) -> Self {
191 Self {
192 name: name.into(),
193 mode,
194 driver,
195 columns: Vec::new(),
196 indexes: Vec::new(),
197 foreign_keys: Vec::new(),
198 drops: Vec::new(),
199 renames: Vec::new(),
200 checks: Vec::new(),
201 primary_keys: Vec::new(),
202 }
203 }
204
205 fn push_column(&mut self, name: &str, ty: ColumnType) -> &mut ColumnDef {
206 let sea_def = SeaColumnDef::new_with_type(sea_query::Alias::new(name), ty);
207 self.columns.push(ColumnDef {
208 sea_def,
209 name: name.to_string(),
210 mode: self.mode,
211 });
212 self.columns.last_mut().unwrap()
213 }
214
215 pub fn primary(&mut self, columns: &[&str]) -> &mut Self {
231 if columns.is_empty() {
232 return self;
233 }
234 self.primary_keys = columns.iter().map(|c| (*c).to_string()).collect();
235 self
236 }
237
238 pub fn id(&mut self) -> &mut ColumnDef {
239 let cd = self.push_column("id", ColumnType::BigInteger);
240 cd.sea_def.not_null().primary_key().auto_increment();
241 cd
242 }
243
244 pub fn uuid_id(&mut self) -> &mut ColumnDef {
245 let cd = self.push_column("id", ColumnType::Uuid);
246 cd.sea_def.not_null().primary_key();
247 cd
248 }
249
250 pub fn ulid_id(&mut self) -> &mut ColumnDef {
252 self.uuid_id()
253 }
254
255 pub fn tiny_integer(&mut self, name: &str) -> &mut ColumnDef {
258 self.push_column(name, ColumnType::TinyInteger)
259 }
260
261 pub fn small_integer(&mut self, name: &str) -> &mut ColumnDef {
262 self.push_column(name, ColumnType::SmallInteger)
263 }
264
265 pub fn medium_integer(&mut self, name: &str) -> &mut ColumnDef {
266 self.push_column(name, ColumnType::Integer)
267 }
268
269 pub fn integer(&mut self, name: &str) -> &mut ColumnDef {
270 self.push_column(name, ColumnType::Integer)
271 }
272
273 pub fn big_integer(&mut self, name: &str) -> &mut ColumnDef {
274 self.push_column(name, ColumnType::BigInteger)
275 }
276
277 pub fn unsigned_big_integer(&mut self, name: &str) -> &mut ColumnDef {
280 self.checks.push(PendingCheck {
281 name: format!("{}_{}_unsigned", self.name, name),
282 expr: format!("{} >= 0", name),
283 });
284 self.push_column(name, ColumnType::BigInteger)
285 }
286
287 pub fn unsigned_integer(&mut self, name: &str) -> &mut ColumnDef {
288 self.checks.push(PendingCheck {
289 name: format!("{}_{}_unsigned", self.name, name),
290 expr: format!("{} >= 0", name),
291 });
292 self.push_column(name, ColumnType::Integer)
293 }
294
295 pub fn decimal(&mut self, name: &str, precision: u32, scale: u32) -> &mut ColumnDef {
296 self.push_column(name, ColumnType::Decimal(Some((precision, scale))))
297 }
298
299 pub fn float(&mut self, name: &str) -> &mut ColumnDef {
300 self.push_column(name, ColumnType::Float)
301 }
302
303 pub fn double(&mut self, name: &str) -> &mut ColumnDef {
304 self.push_column(name, ColumnType::Double)
305 }
306
307 pub fn string(&mut self, name: &str) -> &mut ColumnDef {
310 self.push_column(name, ColumnType::String(sea_query::StringLen::N(255)))
311 }
312
313 pub fn string_with_length(&mut self, name: &str, length: u32) -> &mut ColumnDef {
315 self.push_column(name, ColumnType::String(sea_query::StringLen::N(length)))
316 }
317
318 pub fn text(&mut self, name: &str) -> &mut ColumnDef {
319 self.push_column(name, ColumnType::Text)
320 }
321
322 pub fn long_text(&mut self, name: &str) -> &mut ColumnDef {
323 self.push_column(name, ColumnType::Text)
324 }
325
326 pub fn medium_text(&mut self, name: &str) -> &mut ColumnDef {
327 self.push_column(name, ColumnType::Text)
328 }
329
330 pub fn char(&mut self, name: &str, length: u32) -> &mut ColumnDef {
331 self.push_column(name, ColumnType::Char(Some(length)))
332 }
333
334 pub fn remember_token(&mut self) -> &mut ColumnDef {
336 let cd = self.push_column(
337 "remember_token",
338 ColumnType::String(sea_query::StringLen::N(100)),
339 );
340 cd.sea_def.null();
341 cd
342 }
343
344 pub fn enum_col(&mut self, name: &str, variants: &[&str]) -> &mut ColumnDef {
349 let list = variants
350 .iter()
351 .map(|v| format!("'{}'", v.replace('\'', "''")))
352 .collect::<Vec<_>>()
353 .join(", ");
354 self.checks.push(PendingCheck {
355 name: format!("{}_{}_enum", self.name, name),
356 expr: format!("{} IN ({})", name, list),
357 });
358 self.push_column(name, ColumnType::String(sea_query::StringLen::N(64)))
359 }
360
361 pub fn binary(&mut self, name: &str) -> &mut ColumnDef {
362 self.push_column(name, ColumnType::VarBinary(sea_query::StringLen::None))
363 }
364
365 pub fn boolean(&mut self, name: &str) -> &mut ColumnDef {
368 self.push_column(name, ColumnType::Boolean)
369 }
370
371 pub fn timestamp(&mut self, name: &str) -> &mut ColumnDef {
374 self.push_column(name, ColumnType::Timestamp)
375 }
376
377 pub fn timestamp_tz(&mut self, name: &str) -> &mut ColumnDef {
378 self.push_column(name, ColumnType::TimestampWithTimeZone)
379 }
380
381 pub fn date(&mut self, name: &str) -> &mut ColumnDef {
382 self.push_column(name, ColumnType::Date)
383 }
384
385 pub fn time(&mut self, name: &str) -> &mut ColumnDef {
386 self.push_column(name, ColumnType::Time)
387 }
388
389 pub fn date_time(&mut self, name: &str) -> &mut ColumnDef {
390 self.push_column(name, ColumnType::DateTime)
391 }
392
393 pub fn year(&mut self, name: &str) -> &mut ColumnDef {
394 self.push_column(name, ColumnType::Year)
395 }
396
397 pub fn timestamps(&mut self) {
399 self.push_column("created_at", ColumnType::TimestampWithTimeZone)
400 .nullable()
401 .default("CURRENT_TIMESTAMP");
402 self.push_column("updated_at", ColumnType::TimestampWithTimeZone)
403 .nullable()
404 .default("CURRENT_TIMESTAMP");
405 }
406
407 pub fn soft_deletes(&mut self) {
408 self.push_column("deleted_at", ColumnType::TimestampWithTimeZone)
409 .nullable();
410 }
411
412 pub fn json(&mut self, name: &str) -> &mut ColumnDef {
415 self.push_column(name, ColumnType::Json)
416 }
417
418 pub fn jsonb(&mut self, name: &str) -> &mut ColumnDef {
419 self.push_column(name, ColumnType::JsonBinary)
420 }
421
422 pub fn uuid(&mut self, name: &str) -> &mut ColumnDef {
423 self.push_column(name, ColumnType::Uuid)
424 }
425
426 pub fn ip_address(&mut self, name: &str) -> &mut ColumnDef {
427 self.push_column(name, ColumnType::String(sea_query::StringLen::N(45)))
429 }
430
431 pub fn mac_address(&mut self, name: &str) -> &mut ColumnDef {
432 self.push_column(name, ColumnType::String(sea_query::StringLen::N(17)))
433 }
434
435 pub fn morphs(&mut self, name: &str) {
440 self.push_column(&format!("{name}_id"), ColumnType::BigInteger)
441 .not_null();
442 self.push_column(
443 &format!("{name}_type"),
444 ColumnType::String(sea_query::StringLen::N(255)),
445 )
446 .not_null();
447 let idx_name = format!("idx_{}_{}_type_id", self.name, name);
448 let sql = format!(
449 "CREATE INDEX {} ON {} ({}_type, {}_id)",
450 idx_name, self.name, name, name
451 );
452 self.indexes.push(sql);
453 }
454
455 pub fn nullable_morphs(&mut self, name: &str) {
456 self.push_column(&format!("{name}_id"), ColumnType::BigInteger)
457 .nullable();
458 self.push_column(
459 &format!("{name}_type"),
460 ColumnType::String(sea_query::StringLen::N(255)),
461 )
462 .nullable();
463 let idx_name = format!("idx_{}_{}_type_id", self.name, name);
464 let sql = format!(
465 "CREATE INDEX {} ON {} ({}_type, {}_id)",
466 idx_name, self.name, name, name
467 );
468 self.indexes.push(sql);
469 }
470
471 pub fn uuid_morphs(&mut self, name: &str) {
473 self.push_column(&format!("{name}_id"), ColumnType::Uuid)
474 .not_null();
475 self.push_column(
476 &format!("{name}_type"),
477 ColumnType::String(sea_query::StringLen::N(255)),
478 )
479 .not_null();
480 let idx_name = format!("idx_{}_{}_type_id", self.name, name);
481 let sql = format!(
482 "CREATE INDEX {} ON {} ({}_type, {}_id)",
483 idx_name, self.name, name, name
484 );
485 self.indexes.push(sql);
486 }
487
488 pub fn foreign_id_for(&mut self, name: &str, references: &str) -> &mut ColumnDef {
496 self.foreign_id_for_with_action(name, references, "CASCADE", false)
497 }
498
499 pub fn foreign_id_for_nullable(&mut self, name: &str, references: &str) -> &mut ColumnDef {
503 self.foreign_id_for_with_action(name, references, "SET NULL", true)
504 }
505
506 pub fn foreign_id_for_restrict(&mut self, name: &str, references: &str) -> &mut ColumnDef {
509 self.foreign_id_for_with_action(name, references, "RESTRICT", false)
510 }
511
512 pub fn foreign_id_for_no_action(&mut self, name: &str, references: &str) -> &mut ColumnDef {
516 let cd = {
517 self.foreign_keys.push(PendingFk {
518 column: name.to_string(),
519 ref_table: references.to_string(),
520 ref_col: "id".to_string(),
521 on_delete: None,
522 on_update: None,
523 });
524 self.push_column(name, ColumnType::BigInteger)
525 };
526 cd
527 }
528
529 fn foreign_id_for_with_action(
530 &mut self,
531 name: &str,
532 references: &str,
533 on_delete: &str,
534 nullable: bool,
535 ) -> &mut ColumnDef {
536 self.foreign_keys.push(PendingFk {
537 column: name.to_string(),
538 ref_table: references.to_string(),
539 ref_col: "id".to_string(),
540 on_delete: Some(on_delete.to_string()),
541 on_update: None,
542 });
543 let cd = self.push_column(name, ColumnType::BigInteger);
544 if nullable {
545 cd.nullable();
546 }
547 cd
548 }
549
550 pub fn foreign(&mut self, column: &str) -> ForeignKeyBuilder<'_> {
553 ForeignKeyBuilder {
554 table: &mut self.foreign_keys,
555 column: column.to_string(),
556 ref_col: "id".to_string(),
557 ref_table: String::new(),
558 on_delete: None,
559 on_update: None,
560 }
561 }
562
563 pub fn index(&mut self, columns: &[&str]) -> &mut Self {
566 let idx_name = format!("idx_{}_{}", self.name, columns.join("_"));
567 let sql = format!(
568 "CREATE INDEX {} ON {} ({})",
569 idx_name,
570 self.name,
571 columns.join(", ")
572 );
573 self.indexes.push(sql);
574 self
575 }
576
577 pub fn unique_index(&mut self, columns: &[&str]) -> &mut Self {
578 let idx_name = format!("uq_{}_{}", self.name, columns.join("_"));
579 let sql = format!(
580 "CREATE UNIQUE INDEX {} ON {} ({})",
581 idx_name,
582 self.name,
583 columns.join(", ")
584 );
585 self.indexes.push(sql);
586 self
587 }
588
589 pub fn raw_index(&mut self, sql: impl Into<String>) -> &mut Self {
591 self.indexes.push(sql.into());
592 self
593 }
594
595 pub fn drop_column(&mut self, name: &str) -> &mut Self {
598 self.drops.push(format!(
599 "ALTER TABLE {} DROP COLUMN IF EXISTS {}",
600 self.name, name
601 ));
602 self
603 }
604
605 pub fn rename_column(&mut self, from: &str, to: &str) -> &mut Self {
606 self.renames.push((from.to_string(), to.to_string()));
607 self
608 }
609
610 pub fn drop_index(&mut self, name: &str) -> &mut Self {
611 self.drops.push(format!("DROP INDEX IF EXISTS {}", name));
612 self
613 }
614
615 pub fn drop_foreign(&mut self, constraint: &str) -> &mut Self {
616 self.drops.push(format!(
617 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}",
618 self.name, constraint
619 ));
620 self
621 }
622
623 pub fn drop_unique(&mut self, name: &str) -> &mut Self {
624 self.drops.push(format!("DROP INDEX IF EXISTS {}", name));
625 self
626 }
627
628 pub fn drop_timestamps(&mut self) -> &mut Self {
629 self.drop_column("created_at").drop_column("updated_at")
630 }
631
632 pub fn drop_soft_deletes(&mut self) -> &mut Self {
633 self.drop_column("deleted_at")
634 }
635
636 fn into_statements(self) -> Vec<String> {
639 let mut out = Vec::new();
640 match self.mode {
641 TableMode::Create => {
642 let mut t = SeaTable::create();
643 t.table(sea_query::Alias::new(&self.name)).if_not_exists();
644 for col in &self.columns {
645 t.col(col.sea_def.clone());
646 }
647 let mut sql = build_per_driver(&t, self.driver);
648
649 let mut inline = Vec::new();
654 if !self.primary_keys.is_empty() {
655 inline.push(format!("PRIMARY KEY ({})", self.primary_keys.join(", ")));
656 }
657 for fk in &self.foreign_keys {
658 inline.push(fk.inline_clause(&self.name));
659 }
660 for chk in &self.checks {
661 inline.push(chk.inline_clause());
662 }
663 if !inline.is_empty() {
664 let trimmed_len = sql.trim_end().len();
665 if trimmed_len > 0 && sql.as_bytes()[trimmed_len - 1] == b')' {
666 let injection = format!(", {}", inline.join(", "));
667 sql.insert_str(trimmed_len - 1, &injection);
668 } else {
669 for fk in &self.foreign_keys {
673 out.push(fk.alter_sql(&self.name));
674 }
675 for chk in &self.checks {
676 out.push(chk.alter_sql(&self.name));
677 }
678 }
679 }
680
681 out.push(sql);
682 }
683 TableMode::Alter => {
684 for col in &self.columns {
686 let mut t = SeaTable::alter();
687 t.table(sea_query::Alias::new(&self.name));
688 t.add_column(col.sea_def.clone());
689 out.push(build_alter_per_driver(&t, self.driver));
690 }
691
692 let has_constraints = !self.foreign_keys.is_empty() || !self.checks.is_empty();
693 if self.driver == Driver::Sqlite && has_constraints {
694 tracing::warn!(
695 table = %self.name,
696 fks = self.foreign_keys.len(),
697 checks = self.checks.len(),
698 "SQLite does not support ALTER TABLE ADD CONSTRAINT; FK/CHECK additions on existing tables are skipped. Recreate the table with the constraint inline.",
699 );
700 } else {
701 for fk in &self.foreign_keys {
702 out.push(fk.alter_sql(&self.name));
703 }
704 for chk in &self.checks {
705 out.push(chk.alter_sql(&self.name));
706 }
707 }
708 }
709 }
710 for (from, to) in &self.renames {
711 out.push(format!(
712 "ALTER TABLE {} RENAME COLUMN {} TO {}",
713 self.name, from, to
714 ));
715 }
716 out.extend(self.drops);
717 out.extend(self.indexes);
718 out
719 }
720}
721
722pub struct ForeignKeyBuilder<'a> {
725 table: &'a mut Vec<PendingFk>,
726 column: String,
727 ref_col: String,
728 ref_table: String,
729 on_delete: Option<String>,
730 on_update: Option<String>,
731}
732
733impl<'a> ForeignKeyBuilder<'a> {
734 pub fn references(mut self, column: &str) -> Self {
736 self.ref_col = column.to_string();
737 self
738 }
739
740 pub fn on(mut self, table: &str) -> Self {
742 self.ref_table = table.to_string();
743 self
744 }
745
746 pub fn on_delete(mut self, action: &str) -> Self {
748 self.on_delete = Some(action.to_string());
749 self
750 }
751
752 pub fn on_update(mut self, action: &str) -> Self {
753 self.on_update = Some(action.to_string());
754 self
755 }
756
757 pub fn cascade(self) -> Self {
758 self.on_delete("CASCADE")
759 }
760
761 pub fn set_null(self) -> Self {
762 self.on_delete("SET NULL")
763 }
764
765 pub fn restrict(self) -> Self {
766 self.on_delete("RESTRICT")
767 }
768
769 pub fn commit(self) {
771 drop(self);
773 }
774}
775
776impl<'a> Drop for ForeignKeyBuilder<'a> {
777 fn drop(&mut self) {
778 if self.ref_table.is_empty() {
779 return;
781 }
782 self.table.push(PendingFk {
783 column: std::mem::take(&mut self.column),
784 ref_table: std::mem::take(&mut self.ref_table),
785 ref_col: std::mem::take(&mut self.ref_col),
786 on_delete: self.on_delete.take(),
787 on_update: self.on_update.take(),
788 });
789 }
790}
791
792pub struct ColumnDef {
793 sea_def: SeaColumnDef,
794 pub name: String,
795 #[allow(dead_code)]
796 mode: TableMode,
797}
798
799impl ColumnDef {
800 pub fn not_null(&mut self) -> &mut Self {
801 self.sea_def.not_null();
802 self
803 }
804
805 pub fn nullable(&mut self) -> &mut Self {
806 self.sea_def.null();
807 self
808 }
809
810 pub fn unique(&mut self) -> &mut Self {
811 self.sea_def.unique_key();
812 self
813 }
814
815 pub fn primary_key(&mut self) -> &mut Self {
816 self.sea_def.primary_key();
817 self
818 }
819
820 pub fn default(&mut self, value: impl Into<String>) -> &mut Self {
825 let v = value.into();
826 let expr = if looks_like_sql_expr(&v) {
827 v
828 } else {
829 format!("'{}'", v.replace('\'', "''"))
830 };
831 self.sea_def.default(sea_query::Expr::cust(expr));
832 self
833 }
834
835 pub fn default_raw(&mut self, sql: impl Into<String>) -> &mut Self {
840 self.sea_def.default(sea_query::Expr::cust(sql.into()));
841 self
842 }
843
844 pub fn default_value<T>(&mut self, value: T) -> &mut Self
845 where
846 T: Into<sea_query::Value>,
847 {
848 self.sea_def.default(value);
849 self
850 }
851
852 pub fn comment(&mut self, _text: impl Into<String>) -> &mut Self {
854 self
857 }
858
859 pub fn use_current(&mut self) -> &mut Self {
861 self.default("CURRENT_TIMESTAMP")
862 }
863}
864
865fn looks_like_sql_expr(value: &str) -> bool {
869 let v = value.trim();
870 if v.is_empty() {
871 return true;
872 }
873 if v.starts_with('\'') || v.starts_with('"') || v.starts_with('`') {
875 return true;
876 }
877 if v.parse::<f64>().is_ok() {
879 return true;
880 }
881 if v.contains('(') {
883 return true;
884 }
885 matches!(
887 v.to_ascii_uppercase().as_str(),
888 "TRUE"
889 | "FALSE"
890 | "NULL"
891 | "CURRENT_TIMESTAMP"
892 | "CURRENT_DATE"
893 | "CURRENT_TIME"
894 | "NOW"
895 | "LOCALTIMESTAMP"
896 | "LOCALTIME"
897 )
898}
899
900fn build_per_driver(t: &sea_query::TableCreateStatement, driver: Driver) -> String {
903 match driver {
904 Driver::Postgres => t.build(PostgresQueryBuilder),
905 Driver::MySql => t.build(MysqlQueryBuilder),
906 Driver::Sqlite => t.build(SqliteQueryBuilder),
907 }
908}
909
910fn build_alter_per_driver(t: &sea_query::TableAlterStatement, driver: Driver) -> String {
911 match driver {
912 Driver::Postgres => t.build(PostgresQueryBuilder),
913 Driver::MySql => t.build(MysqlQueryBuilder),
914 Driver::Sqlite => t.build(SqliteQueryBuilder),
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 fn create_stmts(driver: Driver, f: impl FnOnce(&mut Table)) -> Vec<String> {
923 let mut t = Table::new("posts", TableMode::Create, driver);
924 f(&mut t);
925 t.into_statements()
926 }
927
928 fn alter_stmts(driver: Driver, f: impl FnOnce(&mut Table)) -> Vec<String> {
929 let mut t = Table::new("posts", TableMode::Alter, driver);
930 f(&mut t);
931 t.into_statements()
932 }
933
934 #[test]
935 fn foreign_id_for_inlines_fk_in_create_on_sqlite() {
936 let stmts = create_stmts(Driver::Sqlite, |t| {
939 t.id();
940 t.foreign_id_for("user_id", "users");
941 });
942 let create = stmts
943 .iter()
944 .find(|s| s.starts_with("CREATE TABLE"))
945 .unwrap();
946 assert!(
947 create.contains("FOREIGN KEY"),
948 "FK should be inline in CREATE TABLE, got: {create}"
949 );
950 assert!(
951 create.contains("REFERENCES users (id)"),
952 "FK target should be inline, got: {create}"
953 );
954 assert!(
955 create.contains("ON DELETE CASCADE"),
956 "FK action should be inline, got: {create}"
957 );
958 assert!(
959 !stmts.iter().any(|s| s.starts_with("ALTER TABLE")),
960 "no ALTER TABLE should be emitted on SQLite, got: {stmts:?}"
961 );
962 }
963
964 #[test]
965 fn foreign_id_for_inlines_fk_in_create_on_postgres() {
966 let stmts = create_stmts(Driver::Postgres, |t| {
967 t.id();
968 t.foreign_id_for("user_id", "users");
969 });
970 let create = stmts
971 .iter()
972 .find(|s| s.starts_with("CREATE TABLE"))
973 .unwrap();
974 assert!(create.contains("FOREIGN KEY"));
975 assert!(create.contains("REFERENCES users (id)"));
976 assert!(!stmts.iter().any(|s| s.starts_with("ALTER TABLE")));
977 }
978
979 #[test]
980 fn explicit_foreign_builder_inlines_in_create() {
981 let stmts = create_stmts(Driver::Sqlite, |t| {
982 t.id();
983 t.big_integer("user_id").not_null();
984 t.foreign("user_id").references("id").on("users").cascade();
985 });
986 let create = stmts
987 .iter()
988 .find(|s| s.starts_with("CREATE TABLE"))
989 .unwrap();
990 assert!(create.contains("FOREIGN KEY (user_id)"));
991 assert!(create.contains("ON DELETE CASCADE"));
992 }
993
994 #[test]
995 fn unsigned_inlines_check_constraint() {
996 let stmts = create_stmts(Driver::Sqlite, |t| {
997 t.unsigned_big_integer("balance");
998 });
999 let create = stmts
1000 .iter()
1001 .find(|s| s.starts_with("CREATE TABLE"))
1002 .unwrap();
1003 assert!(
1004 create.contains("CHECK (balance >= 0)"),
1005 "CHECK should be inline, got: {create}"
1006 );
1007 assert!(!stmts.iter().any(|s| s.starts_with("ALTER TABLE")));
1008 }
1009
1010 #[test]
1011 fn enum_col_inlines_check_constraint() {
1012 let stmts = create_stmts(Driver::Sqlite, |t| {
1013 t.enum_col("status", &["draft", "published"]);
1014 });
1015 let create = stmts
1016 .iter()
1017 .find(|s| s.starts_with("CREATE TABLE"))
1018 .unwrap();
1019 assert!(create.contains("CHECK (status IN ('draft', 'published'))"));
1020 }
1021
1022 #[test]
1023 fn alter_mode_emits_alter_table_on_postgres() {
1024 let stmts = alter_stmts(Driver::Postgres, |t| {
1025 t.foreign("user_id").references("id").on("users").cascade();
1026 });
1027 assert!(stmts
1028 .iter()
1029 .any(|s| s.contains("ALTER TABLE posts ADD CONSTRAINT")
1030 && s.contains("FOREIGN KEY (user_id)")));
1031 }
1032
1033 #[test]
1034 fn alter_mode_skips_fk_on_sqlite() {
1035 let stmts = alter_stmts(Driver::Sqlite, |t| {
1038 t.foreign("user_id").references("id").on("users").cascade();
1039 });
1040 assert!(
1041 !stmts.iter().any(|s| s.contains("ADD CONSTRAINT")),
1042 "no ADD CONSTRAINT on SQLite alter, got: {stmts:?}"
1043 );
1044 }
1045
1046 #[test]
1047 fn default_quotes_string_literals() {
1048 let stmts = create_stmts(Driver::Postgres, |t| {
1049 t.string("status").not_null().default("pending");
1050 });
1051 let create = &stmts[0];
1052 assert!(
1053 create.contains("DEFAULT 'pending'"),
1054 "string default should be auto-quoted, got: {create}"
1055 );
1056 }
1057
1058 #[test]
1059 fn default_preserves_already_quoted() {
1060 let stmts = create_stmts(Driver::Postgres, |t| {
1061 t.string("status").default("'pending'");
1062 });
1063 assert!(stmts[0].contains("DEFAULT 'pending'"));
1064 assert!(!stmts[0].contains("'''"));
1066 }
1067
1068 #[test]
1069 fn default_preserves_numeric_literal() {
1070 let stmts = create_stmts(Driver::Postgres, |t| {
1071 t.integer("attempts").default("0");
1072 t.integer("max").default("3");
1073 t.float("ratio").default("1.5");
1074 });
1075 assert!(stmts[0].contains("DEFAULT 0"));
1076 assert!(stmts[0].contains("DEFAULT 3"));
1077 assert!(stmts[0].contains("DEFAULT 1.5"));
1078 }
1079
1080 #[test]
1081 fn default_preserves_boolean_keywords() {
1082 let stmts = create_stmts(Driver::Postgres, |t| {
1083 t.boolean("active").default("true");
1084 t.boolean("paid").default("false");
1085 });
1086 assert!(stmts[0].contains("DEFAULT TRUE") || stmts[0].contains("DEFAULT true"));
1087 assert!(stmts[0].contains("DEFAULT FALSE") || stmts[0].contains("DEFAULT false"));
1088 }
1089
1090 #[test]
1091 fn default_preserves_current_timestamp() {
1092 let stmts = create_stmts(Driver::Postgres, |t| {
1093 t.timestamp("ts").default("CURRENT_TIMESTAMP");
1094 });
1095 assert!(stmts[0].contains("DEFAULT CURRENT_TIMESTAMP"));
1096 }
1097
1098 #[test]
1099 fn default_preserves_function_call() {
1100 let stmts = create_stmts(Driver::Postgres, |t| {
1101 t.uuid("id").default("gen_random_uuid()");
1102 });
1103 assert!(stmts[0].contains("DEFAULT gen_random_uuid()"));
1104 }
1105
1106 #[test]
1107 fn default_escapes_embedded_quotes() {
1108 let stmts = create_stmts(Driver::Postgres, |t| {
1109 t.string("note").default("O'Reilly");
1110 });
1111 assert!(
1112 stmts[0].contains("DEFAULT 'O''Reilly'"),
1113 "embedded quote should be escaped, got: {}",
1114 stmts[0]
1115 );
1116 }
1117
1118 #[test]
1119 fn default_raw_bypasses_quoting() {
1120 let stmts = create_stmts(Driver::Postgres, |t| {
1121 t.jsonb("meta").default_raw("'{}'::jsonb");
1122 });
1123 assert!(stmts[0].contains("DEFAULT '{}'::jsonb"));
1124 }
1125}