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