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}
116
117#[derive(Clone)]
118struct PendingFk {
119 column: String,
120 ref_table: String,
121 ref_col: String,
122 on_delete: Option<String>,
123 on_update: Option<String>,
124}
125
126impl PendingFk {
127 fn constraint_name(&self, table: &str) -> String {
128 format!("fk_{}_{}", table, self.column)
129 }
130
131 fn actions(&self) -> String {
132 let mut s = String::new();
133 if let Some(action) = &self.on_delete {
134 s.push_str(&format!(" ON DELETE {action}"));
135 }
136 if let Some(action) = &self.on_update {
137 s.push_str(&format!(" ON UPDATE {action}"));
138 }
139 s
140 }
141
142 fn inline_clause(&self, table: &str) -> String {
143 format!(
144 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}",
145 self.constraint_name(table),
146 self.column,
147 self.ref_table,
148 self.ref_col,
149 self.actions(),
150 )
151 }
152
153 fn alter_sql(&self, table: &str) -> String {
154 format!(
155 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}",
156 table,
157 self.constraint_name(table),
158 self.column,
159 self.ref_table,
160 self.ref_col,
161 self.actions(),
162 )
163 }
164}
165
166#[derive(Clone)]
167struct PendingCheck {
168 name: String,
169 expr: String,
170}
171
172impl PendingCheck {
173 fn inline_clause(&self) -> String {
174 format!("CONSTRAINT {} CHECK ({})", self.name, self.expr)
175 }
176
177 fn alter_sql(&self, table: &str) -> String {
178 format!(
179 "ALTER TABLE {} ADD CONSTRAINT {} CHECK ({})",
180 table, self.name, self.expr
181 )
182 }
183}
184
185impl Table {
186 fn new(name: impl Into<String>, mode: TableMode, driver: Driver) -> Self {
187 Self {
188 name: name.into(),
189 mode,
190 driver,
191 columns: Vec::new(),
192 indexes: Vec::new(),
193 foreign_keys: Vec::new(),
194 drops: Vec::new(),
195 renames: Vec::new(),
196 checks: Vec::new(),
197 }
198 }
199
200 fn push_column(&mut self, name: &str, ty: ColumnType) -> &mut ColumnDef {
201 let sea_def = SeaColumnDef::new_with_type(sea_query::Alias::new(name), ty);
202 self.columns.push(ColumnDef {
203 sea_def,
204 name: name.to_string(),
205 mode: self.mode,
206 });
207 self.columns.last_mut().unwrap()
208 }
209
210 pub fn id(&mut self) -> &mut ColumnDef {
213 let cd = self.push_column("id", ColumnType::BigInteger);
214 cd.sea_def.not_null().primary_key().auto_increment();
215 cd
216 }
217
218 pub fn uuid_id(&mut self) -> &mut ColumnDef {
219 let cd = self.push_column("id", ColumnType::Uuid);
220 cd.sea_def.not_null().primary_key();
221 cd
222 }
223
224 pub fn ulid_id(&mut self) -> &mut ColumnDef {
226 self.uuid_id()
227 }
228
229 pub fn tiny_integer(&mut self, name: &str) -> &mut ColumnDef {
232 self.push_column(name, ColumnType::TinyInteger)
233 }
234
235 pub fn small_integer(&mut self, name: &str) -> &mut ColumnDef {
236 self.push_column(name, ColumnType::SmallInteger)
237 }
238
239 pub fn medium_integer(&mut self, name: &str) -> &mut ColumnDef {
240 self.push_column(name, ColumnType::Integer)
241 }
242
243 pub fn integer(&mut self, name: &str) -> &mut ColumnDef {
244 self.push_column(name, ColumnType::Integer)
245 }
246
247 pub fn big_integer(&mut self, name: &str) -> &mut ColumnDef {
248 self.push_column(name, ColumnType::BigInteger)
249 }
250
251 pub fn unsigned_big_integer(&mut self, name: &str) -> &mut ColumnDef {
254 self.checks.push(PendingCheck {
255 name: format!("{}_{}_unsigned", self.name, name),
256 expr: format!("{} >= 0", name),
257 });
258 self.push_column(name, ColumnType::BigInteger)
259 }
260
261 pub fn unsigned_integer(&mut self, name: &str) -> &mut ColumnDef {
262 self.checks.push(PendingCheck {
263 name: format!("{}_{}_unsigned", self.name, name),
264 expr: format!("{} >= 0", name),
265 });
266 self.push_column(name, ColumnType::Integer)
267 }
268
269 pub fn decimal(&mut self, name: &str, precision: u32, scale: u32) -> &mut ColumnDef {
270 self.push_column(name, ColumnType::Decimal(Some((precision, scale))))
271 }
272
273 pub fn float(&mut self, name: &str) -> &mut ColumnDef {
274 self.push_column(name, ColumnType::Float)
275 }
276
277 pub fn double(&mut self, name: &str) -> &mut ColumnDef {
278 self.push_column(name, ColumnType::Double)
279 }
280
281 pub fn string(&mut self, name: &str) -> &mut ColumnDef {
284 self.push_column(name, ColumnType::String(sea_query::StringLen::N(255)))
285 }
286
287 pub fn string_with_length(&mut self, name: &str, length: u32) -> &mut ColumnDef {
289 self.push_column(name, ColumnType::String(sea_query::StringLen::N(length)))
290 }
291
292 pub fn text(&mut self, name: &str) -> &mut ColumnDef {
293 self.push_column(name, ColumnType::Text)
294 }
295
296 pub fn long_text(&mut self, name: &str) -> &mut ColumnDef {
297 self.push_column(name, ColumnType::Text)
298 }
299
300 pub fn medium_text(&mut self, name: &str) -> &mut ColumnDef {
301 self.push_column(name, ColumnType::Text)
302 }
303
304 pub fn char(&mut self, name: &str, length: u32) -> &mut ColumnDef {
305 self.push_column(name, ColumnType::Char(Some(length)))
306 }
307
308 pub fn remember_token(&mut self) -> &mut ColumnDef {
310 let cd = self.push_column(
311 "remember_token",
312 ColumnType::String(sea_query::StringLen::N(100)),
313 );
314 cd.sea_def.null();
315 cd
316 }
317
318 pub fn enum_col(&mut self, name: &str, variants: &[&str]) -> &mut ColumnDef {
323 let list = variants
324 .iter()
325 .map(|v| format!("'{}'", v.replace('\'', "''")))
326 .collect::<Vec<_>>()
327 .join(", ");
328 self.checks.push(PendingCheck {
329 name: format!("{}_{}_enum", self.name, name),
330 expr: format!("{} IN ({})", name, list),
331 });
332 self.push_column(name, ColumnType::String(sea_query::StringLen::N(64)))
333 }
334
335 pub fn binary(&mut self, name: &str) -> &mut ColumnDef {
336 self.push_column(name, ColumnType::VarBinary(sea_query::StringLen::None))
337 }
338
339 pub fn boolean(&mut self, name: &str) -> &mut ColumnDef {
342 self.push_column(name, ColumnType::Boolean)
343 }
344
345 pub fn timestamp(&mut self, name: &str) -> &mut ColumnDef {
348 self.push_column(name, ColumnType::Timestamp)
349 }
350
351 pub fn timestamp_tz(&mut self, name: &str) -> &mut ColumnDef {
352 self.push_column(name, ColumnType::TimestampWithTimeZone)
353 }
354
355 pub fn date(&mut self, name: &str) -> &mut ColumnDef {
356 self.push_column(name, ColumnType::Date)
357 }
358
359 pub fn time(&mut self, name: &str) -> &mut ColumnDef {
360 self.push_column(name, ColumnType::Time)
361 }
362
363 pub fn date_time(&mut self, name: &str) -> &mut ColumnDef {
364 self.push_column(name, ColumnType::DateTime)
365 }
366
367 pub fn year(&mut self, name: &str) -> &mut ColumnDef {
368 self.push_column(name, ColumnType::Year)
369 }
370
371 pub fn timestamps(&mut self) {
373 self.push_column("created_at", ColumnType::TimestampWithTimeZone)
374 .nullable()
375 .default("CURRENT_TIMESTAMP");
376 self.push_column("updated_at", ColumnType::TimestampWithTimeZone)
377 .nullable()
378 .default("CURRENT_TIMESTAMP");
379 }
380
381 pub fn soft_deletes(&mut self) {
382 self.push_column("deleted_at", ColumnType::TimestampWithTimeZone)
383 .nullable();
384 }
385
386 pub fn json(&mut self, name: &str) -> &mut ColumnDef {
389 self.push_column(name, ColumnType::Json)
390 }
391
392 pub fn jsonb(&mut self, name: &str) -> &mut ColumnDef {
393 self.push_column(name, ColumnType::JsonBinary)
394 }
395
396 pub fn uuid(&mut self, name: &str) -> &mut ColumnDef {
397 self.push_column(name, ColumnType::Uuid)
398 }
399
400 pub fn ip_address(&mut self, name: &str) -> &mut ColumnDef {
401 self.push_column(name, ColumnType::String(sea_query::StringLen::N(45)))
403 }
404
405 pub fn mac_address(&mut self, name: &str) -> &mut ColumnDef {
406 self.push_column(name, ColumnType::String(sea_query::StringLen::N(17)))
407 }
408
409 pub fn morphs(&mut self, name: &str) {
414 self.push_column(&format!("{name}_id"), ColumnType::BigInteger)
415 .not_null();
416 self.push_column(
417 &format!("{name}_type"),
418 ColumnType::String(sea_query::StringLen::N(255)),
419 )
420 .not_null();
421 let idx_name = format!("idx_{}_{}_type_id", self.name, name);
422 let sql = format!(
423 "CREATE INDEX {} ON {} ({}_type, {}_id)",
424 idx_name, self.name, name, name
425 );
426 self.indexes.push(sql);
427 }
428
429 pub fn nullable_morphs(&mut self, name: &str) {
430 self.push_column(&format!("{name}_id"), ColumnType::BigInteger)
431 .nullable();
432 self.push_column(
433 &format!("{name}_type"),
434 ColumnType::String(sea_query::StringLen::N(255)),
435 )
436 .nullable();
437 let idx_name = format!("idx_{}_{}_type_id", self.name, name);
438 let sql = format!(
439 "CREATE INDEX {} ON {} ({}_type, {}_id)",
440 idx_name, self.name, name, name
441 );
442 self.indexes.push(sql);
443 }
444
445 pub fn uuid_morphs(&mut self, name: &str) {
447 self.push_column(&format!("{name}_id"), ColumnType::Uuid)
448 .not_null();
449 self.push_column(
450 &format!("{name}_type"),
451 ColumnType::String(sea_query::StringLen::N(255)),
452 )
453 .not_null();
454 let idx_name = format!("idx_{}_{}_type_id", self.name, name);
455 let sql = format!(
456 "CREATE INDEX {} ON {} ({}_type, {}_id)",
457 idx_name, self.name, name, name
458 );
459 self.indexes.push(sql);
460 }
461
462 pub fn foreign_id_for(&mut self, name: &str, references: &str) -> &mut ColumnDef {
470 self.foreign_keys.push(PendingFk {
471 column: name.to_string(),
472 ref_table: references.to_string(),
473 ref_col: "id".to_string(),
474 on_delete: Some("CASCADE".to_string()),
475 on_update: None,
476 });
477 self.push_column(name, ColumnType::BigInteger)
478 }
479
480 pub fn foreign(&mut self, column: &str) -> ForeignKeyBuilder<'_> {
483 ForeignKeyBuilder {
484 table: &mut self.foreign_keys,
485 column: column.to_string(),
486 ref_col: "id".to_string(),
487 ref_table: String::new(),
488 on_delete: None,
489 on_update: None,
490 }
491 }
492
493 pub fn index(&mut self, columns: &[&str]) -> &mut Self {
496 let idx_name = format!("idx_{}_{}", self.name, columns.join("_"));
497 let sql = format!(
498 "CREATE INDEX {} ON {} ({})",
499 idx_name,
500 self.name,
501 columns.join(", ")
502 );
503 self.indexes.push(sql);
504 self
505 }
506
507 pub fn unique_index(&mut self, columns: &[&str]) -> &mut Self {
508 let idx_name = format!("uq_{}_{}", self.name, columns.join("_"));
509 let sql = format!(
510 "CREATE UNIQUE INDEX {} ON {} ({})",
511 idx_name,
512 self.name,
513 columns.join(", ")
514 );
515 self.indexes.push(sql);
516 self
517 }
518
519 pub fn raw_index(&mut self, sql: impl Into<String>) -> &mut Self {
521 self.indexes.push(sql.into());
522 self
523 }
524
525 pub fn drop_column(&mut self, name: &str) -> &mut Self {
528 self.drops.push(format!(
529 "ALTER TABLE {} DROP COLUMN IF EXISTS {}",
530 self.name, name
531 ));
532 self
533 }
534
535 pub fn rename_column(&mut self, from: &str, to: &str) -> &mut Self {
536 self.renames.push((from.to_string(), to.to_string()));
537 self
538 }
539
540 pub fn drop_index(&mut self, name: &str) -> &mut Self {
541 self.drops.push(format!("DROP INDEX IF EXISTS {}", name));
542 self
543 }
544
545 pub fn drop_foreign(&mut self, constraint: &str) -> &mut Self {
546 self.drops.push(format!(
547 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}",
548 self.name, constraint
549 ));
550 self
551 }
552
553 pub fn drop_unique(&mut self, name: &str) -> &mut Self {
554 self.drops.push(format!("DROP INDEX IF EXISTS {}", name));
555 self
556 }
557
558 pub fn drop_timestamps(&mut self) -> &mut Self {
559 self.drop_column("created_at").drop_column("updated_at")
560 }
561
562 pub fn drop_soft_deletes(&mut self) -> &mut Self {
563 self.drop_column("deleted_at")
564 }
565
566 fn into_statements(self) -> Vec<String> {
569 let mut out = Vec::new();
570 match self.mode {
571 TableMode::Create => {
572 let mut t = SeaTable::create();
573 t.table(sea_query::Alias::new(&self.name)).if_not_exists();
574 for col in &self.columns {
575 t.col(col.sea_def.clone());
576 }
577 let mut sql = build_per_driver(&t, self.driver);
578
579 let mut inline = Vec::new();
583 for fk in &self.foreign_keys {
584 inline.push(fk.inline_clause(&self.name));
585 }
586 for chk in &self.checks {
587 inline.push(chk.inline_clause());
588 }
589 if !inline.is_empty() {
590 let trimmed_len = sql.trim_end().len();
591 if trimmed_len > 0 && sql.as_bytes()[trimmed_len - 1] == b')' {
592 let injection = format!(", {}", inline.join(", "));
593 sql.insert_str(trimmed_len - 1, &injection);
594 } else {
595 for fk in &self.foreign_keys {
599 out.push(fk.alter_sql(&self.name));
600 }
601 for chk in &self.checks {
602 out.push(chk.alter_sql(&self.name));
603 }
604 }
605 }
606
607 out.push(sql);
608 }
609 TableMode::Alter => {
610 for col in &self.columns {
612 let mut t = SeaTable::alter();
613 t.table(sea_query::Alias::new(&self.name));
614 t.add_column(col.sea_def.clone());
615 out.push(build_alter_per_driver(&t, self.driver));
616 }
617
618 let has_constraints = !self.foreign_keys.is_empty() || !self.checks.is_empty();
619 if self.driver == Driver::Sqlite && has_constraints {
620 tracing::warn!(
621 table = %self.name,
622 fks = self.foreign_keys.len(),
623 checks = self.checks.len(),
624 "SQLite does not support ALTER TABLE ADD CONSTRAINT; FK/CHECK additions on existing tables are skipped. Recreate the table with the constraint inline.",
625 );
626 } else {
627 for fk in &self.foreign_keys {
628 out.push(fk.alter_sql(&self.name));
629 }
630 for chk in &self.checks {
631 out.push(chk.alter_sql(&self.name));
632 }
633 }
634 }
635 }
636 for (from, to) in &self.renames {
637 out.push(format!(
638 "ALTER TABLE {} RENAME COLUMN {} TO {}",
639 self.name, from, to
640 ));
641 }
642 out.extend(self.drops);
643 out.extend(self.indexes);
644 out
645 }
646}
647
648pub struct ForeignKeyBuilder<'a> {
651 table: &'a mut Vec<PendingFk>,
652 column: String,
653 ref_col: String,
654 ref_table: String,
655 on_delete: Option<String>,
656 on_update: Option<String>,
657}
658
659impl<'a> ForeignKeyBuilder<'a> {
660 pub fn references(mut self, column: &str) -> Self {
662 self.ref_col = column.to_string();
663 self
664 }
665
666 pub fn on(mut self, table: &str) -> Self {
668 self.ref_table = table.to_string();
669 self
670 }
671
672 pub fn on_delete(mut self, action: &str) -> Self {
674 self.on_delete = Some(action.to_string());
675 self
676 }
677
678 pub fn on_update(mut self, action: &str) -> Self {
679 self.on_update = Some(action.to_string());
680 self
681 }
682
683 pub fn cascade(self) -> Self {
684 self.on_delete("CASCADE")
685 }
686
687 pub fn set_null(self) -> Self {
688 self.on_delete("SET NULL")
689 }
690
691 pub fn restrict(self) -> Self {
692 self.on_delete("RESTRICT")
693 }
694
695 pub fn commit(self) {
697 drop(self);
699 }
700}
701
702impl<'a> Drop for ForeignKeyBuilder<'a> {
703 fn drop(&mut self) {
704 if self.ref_table.is_empty() {
705 return;
707 }
708 self.table.push(PendingFk {
709 column: std::mem::take(&mut self.column),
710 ref_table: std::mem::take(&mut self.ref_table),
711 ref_col: std::mem::take(&mut self.ref_col),
712 on_delete: self.on_delete.take(),
713 on_update: self.on_update.take(),
714 });
715 }
716}
717
718pub struct ColumnDef {
719 sea_def: SeaColumnDef,
720 pub name: String,
721 #[allow(dead_code)]
722 mode: TableMode,
723}
724
725impl ColumnDef {
726 pub fn not_null(&mut self) -> &mut Self {
727 self.sea_def.not_null();
728 self
729 }
730
731 pub fn nullable(&mut self) -> &mut Self {
732 self.sea_def.null();
733 self
734 }
735
736 pub fn unique(&mut self) -> &mut Self {
737 self.sea_def.unique_key();
738 self
739 }
740
741 pub fn primary_key(&mut self) -> &mut Self {
742 self.sea_def.primary_key();
743 self
744 }
745
746 pub fn default(&mut self, value: impl Into<String>) -> &mut Self {
751 let v = value.into();
752 let expr = if looks_like_sql_expr(&v) {
753 v
754 } else {
755 format!("'{}'", v.replace('\'', "''"))
756 };
757 self.sea_def.default(sea_query::Expr::cust(expr));
758 self
759 }
760
761 pub fn default_raw(&mut self, sql: impl Into<String>) -> &mut Self {
766 self.sea_def.default(sea_query::Expr::cust(sql.into()));
767 self
768 }
769
770 pub fn default_value<T>(&mut self, value: T) -> &mut Self
771 where
772 T: Into<sea_query::Value>,
773 {
774 self.sea_def.default(value);
775 self
776 }
777
778 pub fn comment(&mut self, _text: impl Into<String>) -> &mut Self {
780 self
783 }
784
785 pub fn use_current(&mut self) -> &mut Self {
787 self.default("CURRENT_TIMESTAMP")
788 }
789}
790
791fn looks_like_sql_expr(value: &str) -> bool {
795 let v = value.trim();
796 if v.is_empty() {
797 return true;
798 }
799 if v.starts_with('\'') || v.starts_with('"') || v.starts_with('`') {
801 return true;
802 }
803 if v.parse::<f64>().is_ok() {
805 return true;
806 }
807 if v.contains('(') {
809 return true;
810 }
811 matches!(
813 v.to_ascii_uppercase().as_str(),
814 "TRUE"
815 | "FALSE"
816 | "NULL"
817 | "CURRENT_TIMESTAMP"
818 | "CURRENT_DATE"
819 | "CURRENT_TIME"
820 | "NOW"
821 | "LOCALTIMESTAMP"
822 | "LOCALTIME"
823 )
824}
825
826fn build_per_driver(t: &sea_query::TableCreateStatement, driver: Driver) -> String {
829 match driver {
830 Driver::Postgres => t.build(PostgresQueryBuilder),
831 Driver::MySql => t.build(MysqlQueryBuilder),
832 Driver::Sqlite => t.build(SqliteQueryBuilder),
833 }
834}
835
836fn build_alter_per_driver(t: &sea_query::TableAlterStatement, driver: Driver) -> String {
837 match driver {
838 Driver::Postgres => t.build(PostgresQueryBuilder),
839 Driver::MySql => t.build(MysqlQueryBuilder),
840 Driver::Sqlite => t.build(SqliteQueryBuilder),
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 fn create_stmts(driver: Driver, f: impl FnOnce(&mut Table)) -> Vec<String> {
849 let mut t = Table::new("posts", TableMode::Create, driver);
850 f(&mut t);
851 t.into_statements()
852 }
853
854 fn alter_stmts(driver: Driver, f: impl FnOnce(&mut Table)) -> Vec<String> {
855 let mut t = Table::new("posts", TableMode::Alter, driver);
856 f(&mut t);
857 t.into_statements()
858 }
859
860 #[test]
861 fn foreign_id_for_inlines_fk_in_create_on_sqlite() {
862 let stmts = create_stmts(Driver::Sqlite, |t| {
865 t.id();
866 t.foreign_id_for("user_id", "users");
867 });
868 let create = stmts
869 .iter()
870 .find(|s| s.starts_with("CREATE TABLE"))
871 .unwrap();
872 assert!(
873 create.contains("FOREIGN KEY"),
874 "FK should be inline in CREATE TABLE, got: {create}"
875 );
876 assert!(
877 create.contains("REFERENCES users (id)"),
878 "FK target should be inline, got: {create}"
879 );
880 assert!(
881 create.contains("ON DELETE CASCADE"),
882 "FK action should be inline, got: {create}"
883 );
884 assert!(
885 !stmts.iter().any(|s| s.starts_with("ALTER TABLE")),
886 "no ALTER TABLE should be emitted on SQLite, got: {stmts:?}"
887 );
888 }
889
890 #[test]
891 fn foreign_id_for_inlines_fk_in_create_on_postgres() {
892 let stmts = create_stmts(Driver::Postgres, |t| {
893 t.id();
894 t.foreign_id_for("user_id", "users");
895 });
896 let create = stmts
897 .iter()
898 .find(|s| s.starts_with("CREATE TABLE"))
899 .unwrap();
900 assert!(create.contains("FOREIGN KEY"));
901 assert!(create.contains("REFERENCES users (id)"));
902 assert!(!stmts.iter().any(|s| s.starts_with("ALTER TABLE")));
903 }
904
905 #[test]
906 fn explicit_foreign_builder_inlines_in_create() {
907 let stmts = create_stmts(Driver::Sqlite, |t| {
908 t.id();
909 t.big_integer("user_id").not_null();
910 t.foreign("user_id").references("id").on("users").cascade();
911 });
912 let create = stmts
913 .iter()
914 .find(|s| s.starts_with("CREATE TABLE"))
915 .unwrap();
916 assert!(create.contains("FOREIGN KEY (user_id)"));
917 assert!(create.contains("ON DELETE CASCADE"));
918 }
919
920 #[test]
921 fn unsigned_inlines_check_constraint() {
922 let stmts = create_stmts(Driver::Sqlite, |t| {
923 t.unsigned_big_integer("balance");
924 });
925 let create = stmts
926 .iter()
927 .find(|s| s.starts_with("CREATE TABLE"))
928 .unwrap();
929 assert!(
930 create.contains("CHECK (balance >= 0)"),
931 "CHECK should be inline, got: {create}"
932 );
933 assert!(!stmts.iter().any(|s| s.starts_with("ALTER TABLE")));
934 }
935
936 #[test]
937 fn enum_col_inlines_check_constraint() {
938 let stmts = create_stmts(Driver::Sqlite, |t| {
939 t.enum_col("status", &["draft", "published"]);
940 });
941 let create = stmts
942 .iter()
943 .find(|s| s.starts_with("CREATE TABLE"))
944 .unwrap();
945 assert!(create.contains("CHECK (status IN ('draft', 'published'))"));
946 }
947
948 #[test]
949 fn alter_mode_emits_alter_table_on_postgres() {
950 let stmts = alter_stmts(Driver::Postgres, |t| {
951 t.foreign("user_id").references("id").on("users").cascade();
952 });
953 assert!(stmts
954 .iter()
955 .any(|s| s.contains("ALTER TABLE posts ADD CONSTRAINT")
956 && s.contains("FOREIGN KEY (user_id)")));
957 }
958
959 #[test]
960 fn alter_mode_skips_fk_on_sqlite() {
961 let stmts = alter_stmts(Driver::Sqlite, |t| {
964 t.foreign("user_id").references("id").on("users").cascade();
965 });
966 assert!(
967 !stmts.iter().any(|s| s.contains("ADD CONSTRAINT")),
968 "no ADD CONSTRAINT on SQLite alter, got: {stmts:?}"
969 );
970 }
971
972 #[test]
973 fn default_quotes_string_literals() {
974 let stmts = create_stmts(Driver::Postgres, |t| {
975 t.string("status").not_null().default("pending");
976 });
977 let create = &stmts[0];
978 assert!(
979 create.contains("DEFAULT 'pending'"),
980 "string default should be auto-quoted, got: {create}"
981 );
982 }
983
984 #[test]
985 fn default_preserves_already_quoted() {
986 let stmts = create_stmts(Driver::Postgres, |t| {
987 t.string("status").default("'pending'");
988 });
989 assert!(stmts[0].contains("DEFAULT 'pending'"));
990 assert!(!stmts[0].contains("'''"));
992 }
993
994 #[test]
995 fn default_preserves_numeric_literal() {
996 let stmts = create_stmts(Driver::Postgres, |t| {
997 t.integer("attempts").default("0");
998 t.integer("max").default("3");
999 t.float("ratio").default("1.5");
1000 });
1001 assert!(stmts[0].contains("DEFAULT 0"));
1002 assert!(stmts[0].contains("DEFAULT 3"));
1003 assert!(stmts[0].contains("DEFAULT 1.5"));
1004 }
1005
1006 #[test]
1007 fn default_preserves_boolean_keywords() {
1008 let stmts = create_stmts(Driver::Postgres, |t| {
1009 t.boolean("active").default("true");
1010 t.boolean("paid").default("false");
1011 });
1012 assert!(stmts[0].contains("DEFAULT TRUE") || stmts[0].contains("DEFAULT true"));
1013 assert!(stmts[0].contains("DEFAULT FALSE") || stmts[0].contains("DEFAULT false"));
1014 }
1015
1016 #[test]
1017 fn default_preserves_current_timestamp() {
1018 let stmts = create_stmts(Driver::Postgres, |t| {
1019 t.timestamp("ts").default("CURRENT_TIMESTAMP");
1020 });
1021 assert!(stmts[0].contains("DEFAULT CURRENT_TIMESTAMP"));
1022 }
1023
1024 #[test]
1025 fn default_preserves_function_call() {
1026 let stmts = create_stmts(Driver::Postgres, |t| {
1027 t.uuid("id").default("gen_random_uuid()");
1028 });
1029 assert!(stmts[0].contains("DEFAULT gen_random_uuid()"));
1030 }
1031
1032 #[test]
1033 fn default_escapes_embedded_quotes() {
1034 let stmts = create_stmts(Driver::Postgres, |t| {
1035 t.string("note").default("O'Reilly");
1036 });
1037 assert!(
1038 stmts[0].contains("DEFAULT 'O''Reilly'"),
1039 "embedded quote should be escaped, got: {}",
1040 stmts[0]
1041 );
1042 }
1043
1044 #[test]
1045 fn default_raw_bypasses_quoting() {
1046 let stmts = create_stmts(Driver::Postgres, |t| {
1047 t.jsonb("meta").default_raw("'{}'::jsonb");
1048 });
1049 assert!(stmts[0].contains("DEFAULT '{}'::jsonb"));
1050 }
1051}