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<Box<ColumnDef>>,
110    indexes: Vec<String>,
111    foreign_keys: Vec<String>,
112    drops: Vec<String>,
113    renames: Vec<(String, String)>,
114    alters: Vec<String>,
115}
116
117impl Table {
118    fn new(name: impl Into<String>, mode: TableMode, driver: Driver) -> Self {
119        Self {
120            name: name.into(),
121            mode,
122            driver,
123            columns: Vec::new(),
124            indexes: Vec::new(),
125            foreign_keys: Vec::new(),
126            drops: Vec::new(),
127            renames: Vec::new(),
128            alters: Vec::new(),
129        }
130    }
131
132    fn push_column(&mut self, name: &str, ty: ColumnType) -> &mut ColumnDef {
133        let sea_def = SeaColumnDef::new_with_type(sea_query::Alias::new(name), ty);
134        let boxed = Box::new(ColumnDef {
135            sea_def,
136            name: name.to_string(),
137            mode: self.mode,
138        });
139        self.columns.push(boxed);
140        self.columns.last_mut().unwrap().as_mut()
141    }
142
143    // ── identifier ───────────────────────────────────────────────────────────
144
145    pub fn id(&mut self) -> &mut ColumnDef {
146        let cd = self.push_column("id", ColumnType::BigInteger);
147        cd.sea_def.not_null().primary_key().auto_increment();
148        cd
149    }
150
151    pub fn uuid_id(&mut self) -> &mut ColumnDef {
152        let cd = self.push_column("id", ColumnType::Uuid);
153        cd.sea_def.not_null().primary_key();
154        cd
155    }
156
157    /// `ULID` placeholder — alias for `uuid_id` (we don't ship a ULID Postgres type).
158    pub fn ulid_id(&mut self) -> &mut ColumnDef {
159        self.uuid_id()
160    }
161
162    // ── numeric ──────────────────────────────────────────────────────────────
163
164    pub fn tiny_integer(&mut self, name: &str) -> &mut ColumnDef {
165        self.push_column(name, ColumnType::TinyInteger)
166    }
167
168    pub fn small_integer(&mut self, name: &str) -> &mut ColumnDef {
169        self.push_column(name, ColumnType::SmallInteger)
170    }
171
172    pub fn medium_integer(&mut self, name: &str) -> &mut ColumnDef {
173        self.push_column(name, ColumnType::Integer)
174    }
175
176    pub fn integer(&mut self, name: &str) -> &mut ColumnDef {
177        self.push_column(name, ColumnType::Integer)
178    }
179
180    pub fn big_integer(&mut self, name: &str) -> &mut ColumnDef {
181        self.push_column(name, ColumnType::BigInteger)
182    }
183
184    /// Postgres has no native unsigned types, so this is a synonym for `big_integer`
185    /// with a `>= 0` check constraint. Provided for Laravel parity.
186    pub fn unsigned_big_integer(&mut self, name: &str) -> &mut ColumnDef {
187        let check = format!(
188            "ALTER TABLE {} ADD CONSTRAINT {}_{}_unsigned CHECK ({} >= 0)",
189            self.name, self.name, name, name
190        );
191        self.alters.push(check);
192        self.push_column(name, ColumnType::BigInteger)
193    }
194
195    pub fn unsigned_integer(&mut self, name: &str) -> &mut ColumnDef {
196        let check = format!(
197            "ALTER TABLE {} ADD CONSTRAINT {}_{}_unsigned CHECK ({} >= 0)",
198            self.name, self.name, name, name
199        );
200        self.alters.push(check);
201        self.push_column(name, ColumnType::Integer)
202    }
203
204    pub fn decimal(&mut self, name: &str, precision: u32, scale: u32) -> &mut ColumnDef {
205        self.push_column(name, ColumnType::Decimal(Some((precision, scale))))
206    }
207
208    pub fn float(&mut self, name: &str) -> &mut ColumnDef {
209        self.push_column(name, ColumnType::Float)
210    }
211
212    pub fn double(&mut self, name: &str) -> &mut ColumnDef {
213        self.push_column(name, ColumnType::Double)
214    }
215
216    // ── string-ish ───────────────────────────────────────────────────────────
217
218    pub fn string(&mut self, name: &str) -> &mut ColumnDef {
219        self.push_column(name, ColumnType::String(sea_query::StringLen::N(255)))
220    }
221
222    /// Variable-length string with a custom max.
223    pub fn string_with_length(&mut self, name: &str, length: u32) -> &mut ColumnDef {
224        self.push_column(name, ColumnType::String(sea_query::StringLen::N(length)))
225    }
226
227    pub fn text(&mut self, name: &str) -> &mut ColumnDef {
228        self.push_column(name, ColumnType::Text)
229    }
230
231    pub fn long_text(&mut self, name: &str) -> &mut ColumnDef {
232        self.push_column(name, ColumnType::Text)
233    }
234
235    pub fn medium_text(&mut self, name: &str) -> &mut ColumnDef {
236        self.push_column(name, ColumnType::Text)
237    }
238
239    pub fn char(&mut self, name: &str, length: u32) -> &mut ColumnDef {
240        self.push_column(name, ColumnType::Char(Some(length)))
241    }
242
243    /// Laravel's `remember_token`: nullable VARCHAR(100) used by stay-logged-in cookies.
244    pub fn remember_token(&mut self) -> &mut ColumnDef {
245        let cd = self.push_column(
246            "remember_token",
247            ColumnType::String(sea_query::StringLen::N(100)),
248        );
249        cd.sea_def.null();
250        cd
251    }
252
253    // ── enum / binary ────────────────────────────────────────────────────────
254
255    /// CHECK-constrained enum column. Postgres has native ENUM types but they're
256    /// painful for migrations; this models them as `VARCHAR` + CHECK constraint.
257    pub fn enum_col(&mut self, name: &str, variants: &[&str]) -> &mut ColumnDef {
258        let list = variants
259            .iter()
260            .map(|v| format!("'{}'", v.replace('\'', "''")))
261            .collect::<Vec<_>>()
262            .join(", ");
263        let check = format!(
264            "ALTER TABLE {} ADD CONSTRAINT {}_{}_enum CHECK ({} IN ({}))",
265            self.name, self.name, name, name, list
266        );
267        self.alters.push(check);
268        self.push_column(name, ColumnType::String(sea_query::StringLen::N(64)))
269    }
270
271    pub fn binary(&mut self, name: &str) -> &mut ColumnDef {
272        self.push_column(name, ColumnType::VarBinary(sea_query::StringLen::None))
273    }
274
275    // ── boolean ──────────────────────────────────────────────────────────────
276
277    pub fn boolean(&mut self, name: &str) -> &mut ColumnDef {
278        self.push_column(name, ColumnType::Boolean)
279    }
280
281    // ── time ─────────────────────────────────────────────────────────────────
282
283    pub fn timestamp(&mut self, name: &str) -> &mut ColumnDef {
284        self.push_column(name, ColumnType::Timestamp)
285    }
286
287    pub fn timestamp_tz(&mut self, name: &str) -> &mut ColumnDef {
288        self.push_column(name, ColumnType::TimestampWithTimeZone)
289    }
290
291    pub fn date(&mut self, name: &str) -> &mut ColumnDef {
292        self.push_column(name, ColumnType::Date)
293    }
294
295    pub fn time(&mut self, name: &str) -> &mut ColumnDef {
296        self.push_column(name, ColumnType::Time)
297    }
298
299    pub fn date_time(&mut self, name: &str) -> &mut ColumnDef {
300        self.push_column(name, ColumnType::DateTime)
301    }
302
303    pub fn year(&mut self, name: &str) -> &mut ColumnDef {
304        self.push_column(name, ColumnType::Year)
305    }
306
307    /// Adds `created_at` + `updated_at`, both `TIMESTAMPTZ NULL DEFAULT CURRENT_TIMESTAMP`.
308    pub fn timestamps(&mut self) {
309        self.push_column("created_at", ColumnType::TimestampWithTimeZone)
310            .nullable()
311            .default("CURRENT_TIMESTAMP");
312        self.push_column("updated_at", ColumnType::TimestampWithTimeZone)
313            .nullable()
314            .default("CURRENT_TIMESTAMP");
315    }
316
317    pub fn soft_deletes(&mut self) {
318        self.push_column("deleted_at", ColumnType::TimestampWithTimeZone)
319            .nullable();
320    }
321
322    // ── json / uuid / network ────────────────────────────────────────────────
323
324    pub fn json(&mut self, name: &str) -> &mut ColumnDef {
325        self.push_column(name, ColumnType::Json)
326    }
327
328    pub fn jsonb(&mut self, name: &str) -> &mut ColumnDef {
329        self.push_column(name, ColumnType::JsonBinary)
330    }
331
332    pub fn uuid(&mut self, name: &str) -> &mut ColumnDef {
333        self.push_column(name, ColumnType::Uuid)
334    }
335
336    pub fn ip_address(&mut self, name: &str) -> &mut ColumnDef {
337        // sea-query has no native INET; emit as VARCHAR(45) (max IPv6 len).
338        self.push_column(name, ColumnType::String(sea_query::StringLen::N(45)))
339    }
340
341    pub fn mac_address(&mut self, name: &str) -> &mut ColumnDef {
342        self.push_column(name, ColumnType::String(sea_query::StringLen::N(17)))
343    }
344
345    // ── polymorphic / morphs ─────────────────────────────────────────────────
346
347    /// Polymorphic FK columns: `<name>_id BIGINT` + `<name>_type VARCHAR(255)`.
348    /// Mirrors Laravel's `$table->morphs('commentable')`.
349    pub fn morphs(&mut self, name: &str) {
350        self.push_column(&format!("{name}_id"), ColumnType::BigInteger)
351            .not_null();
352        self.push_column(
353            &format!("{name}_type"),
354            ColumnType::String(sea_query::StringLen::N(255)),
355        )
356        .not_null();
357        let idx_name = format!("idx_{}_{}_type_id", self.name, name);
358        let sql = format!(
359            "CREATE INDEX {} ON {} ({}_type, {}_id)",
360            idx_name, self.name, name, name
361        );
362        self.indexes.push(sql);
363    }
364
365    pub fn nullable_morphs(&mut self, name: &str) {
366        self.push_column(&format!("{name}_id"), ColumnType::BigInteger)
367            .nullable();
368        self.push_column(
369            &format!("{name}_type"),
370            ColumnType::String(sea_query::StringLen::N(255)),
371        )
372        .nullable();
373        let idx_name = format!("idx_{}_{}_type_id", self.name, name);
374        let sql = format!(
375            "CREATE INDEX {} ON {} ({}_type, {}_id)",
376            idx_name, self.name, name, name
377        );
378        self.indexes.push(sql);
379    }
380
381    /// UUID polymorphic variant: `<name>_id UUID` + `<name>_type VARCHAR(255)`.
382    pub fn uuid_morphs(&mut self, name: &str) {
383        self.push_column(&format!("{name}_id"), ColumnType::Uuid)
384            .not_null();
385        self.push_column(
386            &format!("{name}_type"),
387            ColumnType::String(sea_query::StringLen::N(255)),
388        )
389        .not_null();
390        let idx_name = format!("idx_{}_{}_type_id", self.name, name);
391        let sql = format!(
392            "CREATE INDEX {} ON {} ({}_type, {}_id)",
393            idx_name, self.name, name, name
394        );
395        self.indexes.push(sql);
396    }
397
398    // ── foreign keys ─────────────────────────────────────────────────────────
399
400    /// Shortcut: add a `bigint` column with a FK to `references.id`. Laravel's
401    /// `$table->foreignId('user_id')->constrained()` is split here into:
402    ///
403    /// - `t.foreign_id_for("user_id", "users")` — most common pattern
404    /// - `t.big_integer("user_id")` + `t.foreign("user_id").references("id").on("users")` — explicit
405    pub fn foreign_id_for(&mut self, name: &str, references: &str) -> &mut ColumnDef {
406        let fk_sql = format!(
407            "ALTER TABLE {} ADD CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {} (id) ON DELETE CASCADE",
408            self.name, self.name, name, name, references
409        );
410        self.foreign_keys.push(fk_sql);
411        self.push_column(name, ColumnType::BigInteger)
412    }
413
414    /// Begin a fluent foreign-key constraint builder for `column`.
415    /// Mirrors `$table->foreign('user_id')->references('id')->on('users')`.
416    pub fn foreign(&mut self, column: &str) -> ForeignKeyBuilder<'_> {
417        ForeignKeyBuilder {
418            table: &mut self.foreign_keys,
419            table_name: self.name.clone(),
420            column: column.to_string(),
421            ref_col: "id".to_string(),
422            ref_table: String::new(),
423            on_delete: None,
424            on_update: None,
425        }
426    }
427
428    // ── indexes ──────────────────────────────────────────────────────────────
429
430    pub fn index(&mut self, columns: &[&str]) -> &mut Self {
431        let idx_name = format!("idx_{}_{}", self.name, columns.join("_"));
432        let sql = format!(
433            "CREATE INDEX {} ON {} ({})",
434            idx_name,
435            self.name,
436            columns.join(", ")
437        );
438        self.indexes.push(sql);
439        self
440    }
441
442    pub fn unique_index(&mut self, columns: &[&str]) -> &mut Self {
443        let idx_name = format!("uq_{}_{}", self.name, columns.join("_"));
444        let sql = format!(
445            "CREATE UNIQUE INDEX {} ON {} ({})",
446            idx_name,
447            self.name,
448            columns.join(", ")
449        );
450        self.indexes.push(sql);
451        self
452    }
453
454    /// Postgres trigram / GIN indexes are common — let users emit raw `CREATE INDEX … USING …`.
455    pub fn raw_index(&mut self, sql: impl Into<String>) -> &mut Self {
456        self.indexes.push(sql.into());
457        self
458    }
459
460    // ── alter-table operations ───────────────────────────────────────────────
461
462    pub fn drop_column(&mut self, name: &str) -> &mut Self {
463        self.drops.push(format!(
464            "ALTER TABLE {} DROP COLUMN IF EXISTS {}",
465            self.name, name
466        ));
467        self
468    }
469
470    pub fn rename_column(&mut self, from: &str, to: &str) -> &mut Self {
471        self.renames.push((from.to_string(), to.to_string()));
472        self
473    }
474
475    pub fn drop_index(&mut self, name: &str) -> &mut Self {
476        self.drops.push(format!("DROP INDEX IF EXISTS {}", name));
477        self
478    }
479
480    pub fn drop_foreign(&mut self, constraint: &str) -> &mut Self {
481        self.drops.push(format!(
482            "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}",
483            self.name, constraint
484        ));
485        self
486    }
487
488    pub fn drop_unique(&mut self, name: &str) -> &mut Self {
489        self.drops
490            .push(format!("DROP INDEX IF EXISTS {}", name));
491        self
492    }
493
494    pub fn drop_timestamps(&mut self) -> &mut Self {
495        self.drop_column("created_at").drop_column("updated_at")
496    }
497
498    pub fn drop_soft_deletes(&mut self) -> &mut Self {
499        self.drop_column("deleted_at")
500    }
501
502    // ── emit ────────────────────────────────────────────────────────────────
503
504    fn into_statements(self) -> Vec<String> {
505        let mut out = Vec::new();
506        match self.mode {
507            TableMode::Create => {
508                let mut t = SeaTable::create();
509                t.table(sea_query::Alias::new(&self.name)).if_not_exists();
510                for col in &self.columns {
511                    t.col(&mut col.sea_def.clone());
512                }
513                out.push(build_per_driver(&t, self.driver));
514            }
515            TableMode::Alter => {
516                // `ALTER TABLE ... ADD COLUMN ...` per column added.
517                for col in &self.columns {
518                    let mut t = SeaTable::alter();
519                    t.table(sea_query::Alias::new(&self.name));
520                    t.add_column(&mut col.sea_def.clone());
521                    out.push(build_alter_per_driver(&t, self.driver));
522                }
523            }
524        }
525        for (from, to) in &self.renames {
526            out.push(format!(
527                "ALTER TABLE {} RENAME COLUMN {} TO {}",
528                self.name, from, to
529            ));
530        }
531        out.extend(self.drops);
532        out.extend(self.indexes);
533        out.extend(self.foreign_keys);
534        out.extend(self.alters);
535        out
536    }
537}
538
539/// Fluent builder returned by `Table::foreign(col)`. Drop it (or call `.constrain()`)
540/// to commit the foreign key SQL.
541pub struct ForeignKeyBuilder<'a> {
542    table: &'a mut Vec<String>,
543    table_name: String,
544    column: String,
545    ref_col: String,
546    ref_table: String,
547    on_delete: Option<String>,
548    on_update: Option<String>,
549}
550
551impl<'a> ForeignKeyBuilder<'a> {
552    /// The referenced column on the foreign table. Default: `"id"`.
553    pub fn references(mut self, column: &str) -> Self {
554        self.ref_col = column.to_string();
555        self
556    }
557
558    /// The foreign table.
559    pub fn on(mut self, table: &str) -> Self {
560        self.ref_table = table.to_string();
561        self
562    }
563
564    /// `ON DELETE CASCADE` (or `RESTRICT` / `SET NULL` / `SET DEFAULT`).
565    pub fn on_delete(mut self, action: &str) -> Self {
566        self.on_delete = Some(action.to_string());
567        self
568    }
569
570    pub fn on_update(mut self, action: &str) -> Self {
571        self.on_update = Some(action.to_string());
572        self
573    }
574
575    pub fn cascade(self) -> Self {
576        self.on_delete("CASCADE")
577    }
578
579    pub fn set_null(self) -> Self {
580        self.on_delete("SET NULL")
581    }
582
583    pub fn restrict(self) -> Self {
584        self.on_delete("RESTRICT")
585    }
586
587    /// Commit the constraint to the table. Called explicitly OR implicitly via `Drop`.
588    pub fn commit(self) {
589        // moved out by Drop; nothing else to do here
590        drop(self);
591    }
592}
593
594impl<'a> Drop for ForeignKeyBuilder<'a> {
595    fn drop(&mut self) {
596        if self.ref_table.is_empty() {
597            // No `.on(...)` was called — nothing to emit.
598            return;
599        }
600        let constraint = format!("fk_{}_{}", self.table_name, self.column);
601        let mut sql = format!(
602            "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
603            self.table_name, constraint, self.column, self.ref_table, self.ref_col
604        );
605        if let Some(action) = &self.on_delete {
606            sql.push_str(&format!(" ON DELETE {action}"));
607        }
608        if let Some(action) = &self.on_update {
609            sql.push_str(&format!(" ON UPDATE {action}"));
610        }
611        self.table.push(sql);
612    }
613}
614
615pub struct ColumnDef {
616    sea_def: SeaColumnDef,
617    pub name: String,
618    #[allow(dead_code)]
619    mode: TableMode,
620}
621
622impl ColumnDef {
623    pub fn not_null(&mut self) -> &mut Self {
624        self.sea_def.not_null();
625        self
626    }
627
628    pub fn nullable(&mut self) -> &mut Self {
629        self.sea_def.null();
630        self
631    }
632
633    pub fn unique(&mut self) -> &mut Self {
634        self.sea_def.unique_key();
635        self
636    }
637
638    pub fn primary_key(&mut self) -> &mut Self {
639        self.sea_def.primary_key();
640        self
641    }
642
643    pub fn default(&mut self, value: impl Into<String>) -> &mut Self {
644        self.sea_def.default(sea_query::Expr::cust(value.into()));
645        self
646    }
647
648    pub fn default_value<T>(&mut self, value: T) -> &mut Self
649    where
650        T: Into<sea_query::Value>,
651    {
652        self.sea_def.default(value);
653        self
654    }
655
656    /// Add an inline column comment. Postgres stores this in `COMMENT ON COLUMN`.
657    pub fn comment(&mut self, _text: impl Into<String>) -> &mut Self {
658        // sea-query's API for column comments varies per dialect — emit as a no-op
659        // until v0.2 wires up a post-emit COMMENT ON COLUMN statement.
660        self
661    }
662
663    /// Alias for `default` — Laravel uses `useCurrent()` for timestamps.
664    pub fn use_current(&mut self) -> &mut Self {
665        self.default("CURRENT_TIMESTAMP")
666    }
667}
668
669// ─── per-driver SQL emission ────────────────────────────────────────────────
670
671fn build_per_driver(t: &sea_query::TableCreateStatement, driver: Driver) -> String {
672    match driver {
673        Driver::Postgres => t.build(PostgresQueryBuilder),
674        Driver::MySql => t.build(MysqlQueryBuilder),
675        Driver::Sqlite => t.build(SqliteQueryBuilder),
676    }
677}
678
679fn build_alter_per_driver(t: &sea_query::TableAlterStatement, driver: Driver) -> String {
680    match driver {
681        Driver::Postgres => t.build(PostgresQueryBuilder),
682        Driver::MySql => t.build(MysqlQueryBuilder),
683        Driver::Sqlite => t.build(SqliteQueryBuilder),
684    }
685}