Skip to main content

cast_core/
schema.rs

1//! Schema builder. Used in migrations: `Schema::create("users", |t| ...)`.
2//!
3//! Mirrors Laravel's `Schema::create` ergonomics + the full `Blueprint` column
4//! type surface: `t.string("name").not_null().unique()`, `t.decimal("price", 10, 2)`,
5//! `t.morphs("commentable")`, `t.remember_token()`, foreign-key constraint builders, etc.
6//!
7//! ## Dialects
8//!
9//! `Schema::new()` defaults to Postgres. For MySQL / SQLite, use `Schema::for_driver(Driver::*)`.
10//! The MigrationRunner calls `Schema::for_driver(pool.driver())` automatically so user code
11//! rarely needs to think about it.
12
13use 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    /// Create a new table.
48    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    /// Alter an existing table. The closure can call `add_*` / `drop_column` /
58    /// `rename_column` / index / foreign-key methods on `Table`.
59    ///
60    /// Mirrors `Schema::table('users', function (Blueprint $table) { ... })`.
61    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    /// Rename a table. Mirrors `Schema::rename('old', 'new')`.
83    pub fn rename(&mut self, from: &str, to: &str) {
84        self.statements
85            .push(format!("ALTER TABLE {from} RENAME TO {to}"));
86    }
87
88    /// Check if a table exists (executed at apply-time as a SELECT). v0.2 will return bool.
89    pub fn has_table(&mut self, _table: &str) {
90        // Sentinel — meant for runtime use, not migration generation.
91    }
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
104/// A table definition assembled inside the build closure.
105pub 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    // ── identifier ───────────────────────────────────────────────────────────
211
212    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    /// `ULID` placeholder — alias for `uuid_id` (we don't ship a ULID Postgres type).
225    pub fn ulid_id(&mut self) -> &mut ColumnDef {
226        self.uuid_id()
227    }
228
229    // ── numeric ──────────────────────────────────────────────────────────────
230
231    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    /// Postgres has no native unsigned types, so this is a synonym for `big_integer`
252    /// with a `>= 0` check constraint. Provided for Laravel parity.
253    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    // ── string-ish ───────────────────────────────────────────────────────────
282
283    pub fn string(&mut self, name: &str) -> &mut ColumnDef {
284        self.push_column(name, ColumnType::String(sea_query::StringLen::N(255)))
285    }
286
287    /// Variable-length string with a custom max.
288    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    /// Laravel's `remember_token`: nullable VARCHAR(100) used by stay-logged-in cookies.
309    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    // ── enum / binary ────────────────────────────────────────────────────────
319
320    /// CHECK-constrained enum column. Postgres has native ENUM types but they're
321    /// painful for migrations; this models them as `VARCHAR` + CHECK constraint.
322    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    // ── boolean ──────────────────────────────────────────────────────────────
340
341    pub fn boolean(&mut self, name: &str) -> &mut ColumnDef {
342        self.push_column(name, ColumnType::Boolean)
343    }
344
345    // ── time ─────────────────────────────────────────────────────────────────
346
347    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    /// Adds `created_at` + `updated_at`, both `TIMESTAMPTZ NULL DEFAULT CURRENT_TIMESTAMP`.
372    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    // ── json / uuid / network ────────────────────────────────────────────────
387
388    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        // sea-query has no native INET; emit as VARCHAR(45) (max IPv6 len).
402        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    // ── polymorphic / morphs ─────────────────────────────────────────────────
410
411    /// Polymorphic FK columns: `<name>_id BIGINT` + `<name>_type VARCHAR(255)`.
412    /// Mirrors Laravel's `$table->morphs('commentable')`.
413    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    /// UUID polymorphic variant: `<name>_id UUID` + `<name>_type VARCHAR(255)`.
446    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    // ── foreign keys ─────────────────────────────────────────────────────────
463
464    /// Shortcut: add a `bigint` column with a FK to `references.id`. Laravel's
465    /// `$table->foreignId('user_id')->constrained()` is split here into:
466    ///
467    /// - `t.foreign_id_for("user_id", "users")` — most common pattern
468    /// - `t.big_integer("user_id")` + `t.foreign("user_id").references("id").on("users")` — explicit
469    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    /// Begin a fluent foreign-key constraint builder for `column`.
481    /// Mirrors `$table->foreign('user_id')->references('id')->on('users')`.
482    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    // ── indexes ──────────────────────────────────────────────────────────────
494
495    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    /// Postgres trigram / GIN indexes are common — let users emit raw `CREATE INDEX … USING …`.
520    pub fn raw_index(&mut self, sql: impl Into<String>) -> &mut Self {
521        self.indexes.push(sql.into());
522        self
523    }
524
525    // ── alter-table operations ───────────────────────────────────────────────
526
527    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    // ── emit ────────────────────────────────────────────────────────────────
567
568    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                // Inline FK + CHECK constraints inside the CREATE TABLE body.
580                // SQLite has no `ALTER TABLE … ADD CONSTRAINT`, so emitting them
581                // post-create only works on Postgres/MySQL; inline is portable.
582                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                        // Fallback: shouldn't happen for sea_query CREATE TABLE
596                        // output, but if the SQL doesn't end in `)`, fall back to
597                        // post-table ALTER statements (Postgres/MySQL only).
598                        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                // `ALTER TABLE ... ADD COLUMN ...` per column added.
611                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
648/// Fluent builder returned by `Table::foreign(col)`. Drop it (or call `.constrain()`)
649/// to commit the foreign key SQL.
650pub 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    /// The referenced column on the foreign table. Default: `"id"`.
661    pub fn references(mut self, column: &str) -> Self {
662        self.ref_col = column.to_string();
663        self
664    }
665
666    /// The foreign table.
667    pub fn on(mut self, table: &str) -> Self {
668        self.ref_table = table.to_string();
669        self
670    }
671
672    /// `ON DELETE CASCADE` (or `RESTRICT` / `SET NULL` / `SET DEFAULT`).
673    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    /// Commit the constraint to the table. Called explicitly OR implicitly via `Drop`.
696    pub fn commit(self) {
697        // moved out by Drop; nothing else to do here
698        drop(self);
699    }
700}
701
702impl<'a> Drop for ForeignKeyBuilder<'a> {
703    fn drop(&mut self) {
704        if self.ref_table.is_empty() {
705            // No `.on(...)` was called — nothing to emit.
706            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    /// Set the column default. String values that look like SQL string literals
747    /// (no parens, not already quoted, not numeric, not a recognized keyword)
748    /// are auto-quoted to avoid Postgres parsing them as column references.
749    /// Use [`default_raw`](Self::default_raw) to bypass quoting entirely.
750    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    /// Set the default to raw SQL — no quoting, no parsing. Use this when you
762    /// need to pass a specific expression (a cast like `'{}'::jsonb`, a function
763    /// reference like `gen_random_uuid()`, etc.) and don't want the auto-quoting
764    /// in [`default`](Self::default).
765    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    /// Add an inline column comment. Postgres stores this in `COMMENT ON COLUMN`.
779    pub fn comment(&mut self, _text: impl Into<String>) -> &mut Self {
780        // sea-query's API for column comments varies per dialect — emit as a no-op
781        // until v0.2 wires up a post-emit COMMENT ON COLUMN statement.
782        self
783    }
784
785    /// Alias for `default` — Laravel uses `useCurrent()` for timestamps.
786    pub fn use_current(&mut self) -> &mut Self {
787        self.default("CURRENT_TIMESTAMP")
788    }
789}
790
791/// Heuristic: should this default value be passed to SQL verbatim, or quoted
792/// as a string literal? Returns `true` for things that already look like a SQL
793/// expression (quoted strings, numbers, function calls, known keywords).
794fn looks_like_sql_expr(value: &str) -> bool {
795    let v = value.trim();
796    if v.is_empty() {
797        return true;
798    }
799    // Already-quoted string / identifier.
800    if v.starts_with('\'') || v.starts_with('"') || v.starts_with('`') {
801        return true;
802    }
803    // Numeric literal (int or float, signed).
804    if v.parse::<f64>().is_ok() {
805        return true;
806    }
807    // Function call or any sub-expression in parens.
808    if v.contains('(') {
809        return true;
810    }
811    // Recognized keywords / time functions used as bare defaults.
812    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
826// ─── per-driver SQL emission ────────────────────────────────────────────────
827
828fn 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        // Regression: previously emitted `ALTER TABLE … ADD CONSTRAINT …` which
863        // SQLite rejects with `near "CONSTRAINT": syntax error`.
864        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        // SQLite truly doesn't support adding FK to an existing table — we warn
962        // and skip rather than emitting invalid SQL.
963        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        // Should not double-quote.
991        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}