Skip to main content

rainier_orm/
blueprint.rs

1//! The schema builder — Laravel's `Blueprint`, rendered per dialect.
2//!
3//! ```ignore
4//! Blueprint::create("posts", |table| {
5//!     table.id();
6//!     table.string("slug").unique();
7//!     table.string("title");
8//!     table.text("body");
9//!     table.boolean("published").default(false);
10//!     table.foreign_id("author_id").constrained_on("users").cascade_on_delete();
11//!     table.timestamps();
12//!
13//!     table.index(["published", "created_at"]);
14//! });
15//! ```
16//!
17//! # Why this exists
18//!
19//! Writing `CREATE TABLE` by hand in a migration means writing it *per engine*:
20//! `AUTOINCREMENT` on SQLite, `AUTO_INCREMENT` on MySQL and `BIGSERIAL` on
21//! Postgres; `BLOB` against `BYTEA`; `CREATE INDEX IF NOT EXISTS` everywhere
22//! except MySQL, which rejects it; `DROP INDEX i` against `DROP INDEX i ON t`.
23//! Translating that is the entire job of a DBAL, and asking the application to
24//! do it is asking it to do the job twice.
25//!
26//! So a table is described **once**, as data, and lowered to the dialect the
27//! executor reports at apply time.
28//!
29//! # It also knows how to undo itself
30//!
31//! Every blueprint can produce the statements that reverse it, which is what
32//! lets a migration satisfy the [`Down`](crate::migrate) half of its contract
33//! without the author writing the same schema backwards — the version of it
34//! that goes stale first.
35
36use sea_query::{
37    Alias, ColumnDef, ForeignKey, ForeignKeyAction, Index, IndexCreateStatement, Table,
38    TableCreateStatement, Value,
39};
40
41use crate::{schema, ColumnType, Dialect};
42
43/// The default length of a `string` column. Laravel's, and the same reason:
44/// it is the largest a MySQL utf8mb4 index can cover in one 767-byte prefix
45/// era, and nobody has had cause to change it since.
46pub const DEFAULT_STRING_LEN: u32 = 255;
47
48/// What a column holds.
49///
50/// The shared subset delegates to the same mapping an
51/// [`Entity`](crate::Entity)'s columns use, so a table built here and a table
52/// derived from a model cannot disagree about what `Text` renders as.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum ColumnKind {
55    /// One of the types an entity field can have.
56    Basic(ColumnType),
57    /// `VARCHAR(n)` — a length an entity cannot express.
58    Varchar(u32),
59    /// A JSON document. `JSONB` on Postgres, `JSON` on MySQL, `TEXT` on SQLite.
60    Json,
61    /// A fixed-point number.
62    ///
63    /// `DECIMAL(p, s)` on MySQL and Postgres. **SQLite has no such type** and
64    /// renders `REAL`, which is a float — so an amount stored there is subject
65    /// to the rounding this type exists to avoid. Store money as integer minor
66    /// units if SQLite is a target you care about.
67    Decimal {
68        /// Total digits.
69        precision: u32,
70        /// Digits after the point.
71        scale: u32,
72    },
73}
74
75impl ColumnKind {
76    fn apply(&self, def: &mut ColumnDef) {
77        match self {
78            ColumnKind::Basic(ty) => schema::apply_type(def, *ty, false),
79            ColumnKind::Varchar(len) => {
80                def.string_len(*len);
81            }
82            ColumnKind::Json => {
83                def.json_binary();
84            }
85            ColumnKind::Decimal { precision, scale } => {
86                def.decimal_len(*precision, *scale);
87            }
88        }
89    }
90}
91
92/// What to do to rows on the other side when a referenced row goes.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Action {
95    /// Delete or update them too.
96    Cascade,
97    /// Refuse, if any exist.
98    Restrict,
99    /// Set the referencing column to `NULL`. Needs a nullable column.
100    SetNull,
101    /// Do nothing, and let the constraint be checked at the end of the
102    /// statement.
103    NoAction,
104}
105
106impl Action {
107    fn to_sea(self) -> ForeignKeyAction {
108        match self {
109            Action::Cascade => ForeignKeyAction::Cascade,
110            Action::Restrict => ForeignKeyAction::Restrict,
111            Action::SetNull => ForeignKeyAction::SetNull,
112            Action::NoAction => ForeignKeyAction::NoAction,
113        }
114    }
115}
116
117/// One column, described.
118///
119/// Returned by every `Blueprint` column method so modifiers chain:
120///
121/// ```ignore
122/// table.string("email").unique();
123/// table.text("bio").nullable();
124/// table.integer("hits").default(0);
125/// ```
126#[derive(Debug, Clone)]
127pub struct Column {
128    name: String,
129    kind: ColumnKind,
130    nullable: bool,
131    default: Option<Value>,
132    auto_increment: bool,
133    primary: bool,
134    unique: bool,
135    indexed: bool,
136    /// Set by `foreign_id().constrained_on(..)`, so the common case is one
137    /// call rather than a column and a matching constraint.
138    references: Option<(String, String, Option<Action>, Option<Action>)>,
139}
140
141impl Column {
142    fn new(name: impl Into<String>, kind: ColumnKind) -> Self {
143        Self {
144            name: name.into(),
145            kind,
146            nullable: false,
147            default: None,
148            auto_increment: false,
149            primary: false,
150            unique: false,
151            indexed: false,
152            references: None,
153        }
154    }
155
156    /// The column's name.
157    pub fn name(&self) -> &str {
158        &self.name
159    }
160
161    /// Allow `NULL`.
162    ///
163    /// Columns are `NOT NULL` by default, which is the opposite of SQL's
164    /// default and the same as Laravel's — nullability should be a decision
165    /// somebody made, not one they got by omission.
166    pub fn nullable(&mut self) -> &mut Self {
167        self.nullable = true;
168        self
169    }
170
171    /// A default value. Also backfills existing rows when a `NOT NULL` column
172    /// is added to a table that already has some.
173    pub fn default(&mut self, value: impl Into<Value>) -> &mut Self {
174        self.default = Some(value.into());
175        self
176    }
177
178    /// Make it the primary key.
179    pub fn primary(&mut self) -> &mut Self {
180        self.primary = true;
181        self
182    }
183
184    /// Let the database assign it. Implies [`primary`](Self::primary).
185    pub fn auto_increment(&mut self) -> &mut Self {
186        self.auto_increment = true;
187        self.primary = true;
188        self
189    }
190
191    /// A single-column `UNIQUE` constraint.
192    pub fn unique(&mut self) -> &mut Self {
193        self.unique = true;
194        self
195    }
196
197    /// A single-column index.
198    pub fn index(&mut self) -> &mut Self {
199        self.indexed = true;
200        self
201    }
202
203    /// Point this column at `table`'s primary key — Laravel's
204    /// `constrained()`.
205    ///
206    /// The referenced column defaults to `id`; say otherwise with
207    /// [`references`](Self::references).
208    pub fn constrained_on(&mut self, table: impl Into<String>) -> &mut Self {
209        self.references = Some((table.into(), "id".to_string(), None, None));
210        self
211    }
212
213    /// Point this column at `table`.`column`.
214    pub fn references(&mut self, table: impl Into<String>, column: impl Into<String>) -> &mut Self {
215        self.references = Some((table.into(), column.into(), None, None));
216        self
217    }
218
219    /// Delete the referencing rows when the referenced row goes.
220    pub fn cascade_on_delete(&mut self) -> &mut Self {
221        self.on_delete(Action::Cascade)
222    }
223
224    /// `NULL` the referencing column when the referenced row goes. The column
225    /// has to be [`nullable`](Self::nullable).
226    pub fn null_on_delete(&mut self) -> &mut Self {
227        self.on_delete(Action::SetNull)
228    }
229
230    /// What happens to this row when the referenced one is deleted.
231    pub fn on_delete(&mut self, action: Action) -> &mut Self {
232        if let Some(reference) = &mut self.references {
233            reference.2 = Some(action);
234        }
235        self
236    }
237
238    /// What happens to this row when the referenced key is updated.
239    pub fn on_update(&mut self, action: Action) -> &mut Self {
240        if let Some(reference) = &mut self.references {
241            reference.3 = Some(action);
242        }
243        self
244    }
245
246    /// The column definition, ready for a `CREATE` or an `ALTER`.
247    fn to_def(&self) -> ColumnDef {
248        let mut def = ColumnDef::new(Alias::new(self.name.clone()));
249
250        // An auto-increment `BIGINT UNSIGNED` is not a thing Postgres will
251        // create, and the key it generates is a positive `i64` on every
252        // backend anyway — so the unsigned types narrow when they are keys.
253        // The same rule the entity path applies, for the same reason.
254        let kind = match (self.auto_increment, self.kind) {
255            (true, ColumnKind::Basic(ColumnType::BigUint)) => ColumnKind::Basic(ColumnType::BigInt),
256            (true, ColumnKind::Basic(ColumnType::Uint)) => ColumnKind::Basic(ColumnType::Int),
257            (_, kind) => kind,
258        };
259        kind.apply(&mut def);
260
261        if self.primary {
262            def.primary_key();
263        }
264        if self.auto_increment {
265            def.auto_increment();
266        }
267        if self.nullable {
268            def.null();
269        } else {
270            def.not_null();
271        }
272        if let Some(value) = &self.default {
273            def.default(value.clone());
274        }
275        if self.unique {
276            def.unique_key();
277        }
278        def
279    }
280}
281
282/// One index, described.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct IndexDef {
285    name: String,
286    columns: Vec<String>,
287    unique: bool,
288}
289
290impl IndexDef {
291    /// The index's name.
292    pub fn name(&self) -> &str {
293        &self.name
294    }
295
296    /// The columns it covers.
297    pub fn columns(&self) -> &[String] {
298        &self.columns
299    }
300
301    /// Whether it enforces uniqueness.
302    pub fn is_unique(&self) -> bool {
303        self.unique
304    }
305
306    /// Name it something other than the convention.
307    pub fn named(&mut self, name: impl Into<String>) -> &mut Self {
308        self.name = name.into();
309        self
310    }
311
312    fn to_stmt(&self, table: &str) -> IndexCreateStatement {
313        let mut stmt = Index::create();
314        stmt.name(&self.name).table(Alias::new(table)).if_not_exists();
315        if self.unique {
316            stmt.unique();
317        }
318        for column in &self.columns {
319            stmt.col(Alias::new(column.clone()));
320        }
321        stmt
322    }
323}
324
325/// Laravel's index naming: `posts_author_id_index`, `users_email_unique`.
326///
327/// Predictable rather than clever, because a `down` has to be able to name the
328/// index it is dropping without being told.
329fn conventional_name(table: &str, columns: &[String], suffix: &str) -> String {
330    format!("{table}_{}_{suffix}", columns.join("_"))
331}
332
333/// A table being created — Laravel's `Blueprint`.
334#[derive(Debug, Clone)]
335pub struct Blueprint {
336    table: String,
337    columns: Vec<Column>,
338    indexes: Vec<IndexDef>,
339    primary: Option<Vec<String>>,
340    if_not_exists: bool,
341}
342
343impl Blueprint {
344    /// A blueprint for `table`.
345    pub fn new(table: impl Into<String>) -> Self {
346        Self {
347            table: table.into(),
348            columns: Vec::new(),
349            indexes: Vec::new(),
350            primary: None,
351            if_not_exists: true,
352        }
353    }
354
355    /// Describe `table` with `build`, and hand back the blueprint.
356    pub fn create(table: impl Into<String>, build: impl FnOnce(&mut Blueprint)) -> Self {
357        let mut blueprint = Self::new(table);
358        build(&mut blueprint);
359        blueprint
360    }
361
362    /// The table's name.
363    pub fn table(&self) -> &str {
364        &self.table
365    }
366
367    /// Emit a plain `CREATE TABLE`, so creating one that exists is an error
368    /// rather than a no-op.
369    ///
370    /// The default is `IF NOT EXISTS`, which makes a migration safe to re-run
371    /// by hand against a database somebody has already touched.
372    pub fn strict(&mut self) -> &mut Self {
373        self.if_not_exists = false;
374        self
375    }
376
377    // --- columns -----------------------------------------------------------
378
379    /// A column of any kind. The typed helpers below all come through here.
380    pub fn column(&mut self, name: impl Into<String>, kind: ColumnKind) -> &mut Column {
381        self.columns.push(Column::new(name, kind));
382        self.columns.last_mut().expect("just pushed")
383    }
384
385    /// An auto-incrementing primary key named `id`.
386    pub fn id(&mut self) -> &mut Column {
387        self.big_increments("id")
388    }
389
390    /// An auto-incrementing 64-bit primary key.
391    pub fn big_increments(&mut self, name: impl Into<String>) -> &mut Column {
392        let column = self.column(name, ColumnKind::Basic(ColumnType::BigUint));
393        column.auto_increment();
394        column
395    }
396
397    /// An auto-incrementing 32-bit primary key.
398    pub fn increments(&mut self, name: impl Into<String>) -> &mut Column {
399        let column = self.column(name, ColumnKind::Basic(ColumnType::Uint));
400        column.auto_increment();
401        column
402    }
403
404    /// `VARCHAR(255)`.
405    pub fn string(&mut self, name: impl Into<String>) -> &mut Column {
406        self.column(name, ColumnKind::Varchar(DEFAULT_STRING_LEN))
407    }
408
409    /// `VARCHAR(len)`.
410    pub fn string_len(&mut self, name: impl Into<String>, len: u32) -> &mut Column {
411        self.column(name, ColumnKind::Varchar(len))
412    }
413
414    /// Unbounded text.
415    ///
416    /// **Not indexable on MySQL** without a prefix length, so reach for
417    /// [`string`](Self::string) for anything you will search or sort by.
418    pub fn text(&mut self, name: impl Into<String>) -> &mut Column {
419        self.column(name, ColumnKind::Basic(ColumnType::Text))
420    }
421
422    /// A 32-bit signed integer.
423    pub fn integer(&mut self, name: impl Into<String>) -> &mut Column {
424        self.column(name, ColumnKind::Basic(ColumnType::Int))
425    }
426
427    /// A 64-bit signed integer.
428    pub fn big_integer(&mut self, name: impl Into<String>) -> &mut Column {
429        self.column(name, ColumnKind::Basic(ColumnType::BigInt))
430    }
431
432    /// A 32-bit unsigned integer.
433    pub fn unsigned_integer(&mut self, name: impl Into<String>) -> &mut Column {
434        self.column(name, ColumnKind::Basic(ColumnType::Uint))
435    }
436
437    /// A 64-bit unsigned integer.
438    pub fn unsigned_big_integer(&mut self, name: impl Into<String>) -> &mut Column {
439        self.column(name, ColumnKind::Basic(ColumnType::BigUint))
440    }
441
442    /// A foreign key column: unsigned 64-bit and indexed, ready for
443    /// [`constrained_on`](Column::constrained_on).
444    ///
445    /// ```ignore
446    /// table.foreign_id("author_id").constrained_on("users").cascade_on_delete();
447    /// ```
448    pub fn foreign_id(&mut self, name: impl Into<String>) -> &mut Column {
449        let column = self.column(name, ColumnKind::Basic(ColumnType::BigUint));
450        column.index();
451        column
452    }
453
454    /// A boolean. An integer on SQLite and MySQL, which have no boolean type.
455    pub fn boolean(&mut self, name: impl Into<String>) -> &mut Column {
456        self.column(name, ColumnKind::Basic(ColumnType::Bool))
457    }
458
459    /// A double-precision float. Not for money — see
460    /// [`decimal`](Self::decimal).
461    pub fn double(&mut self, name: impl Into<String>) -> &mut Column {
462        self.column(name, ColumnKind::Basic(ColumnType::Double))
463    }
464
465    /// A fixed-point number, for money and anything else that must not round.
466    ///
467    /// Read [`ColumnKind::Decimal`] before using it on SQLite, which has no
468    /// fixed-point type and will store a float.
469    pub fn decimal(&mut self, name: impl Into<String>, precision: u32, scale: u32) -> &mut Column {
470        self.column(name, ColumnKind::Decimal { precision, scale })
471    }
472
473    /// A timestamp.
474    pub fn timestamp(&mut self, name: impl Into<String>) -> &mut Column {
475        self.column(name, ColumnKind::Basic(ColumnType::Timestamp))
476    }
477
478    /// A calendar date, with no time.
479    pub fn date(&mut self, name: impl Into<String>) -> &mut Column {
480        self.column(name, ColumnKind::Basic(ColumnType::Date))
481    }
482
483    /// Raw bytes.
484    pub fn binary(&mut self, name: impl Into<String>) -> &mut Column {
485        self.column(name, ColumnKind::Basic(ColumnType::Binary))
486    }
487
488    /// A JSON document.
489    pub fn json(&mut self, name: impl Into<String>) -> &mut Column {
490        self.column(name, ColumnKind::Json)
491    }
492
493    /// `created_at` and `updated_at`, both nullable.
494    ///
495    /// Nullable because nothing here writes them for you — a row inserted by a
496    /// migration or by hand would otherwise fail. A model that always sets
497    /// them can declare its own `NOT NULL` columns instead.
498    pub fn timestamps(&mut self) -> &mut Self {
499        self.timestamp("created_at").nullable();
500        self.timestamp("updated_at").nullable();
501        self
502    }
503
504    /// A nullable `deleted_at`, for soft deletes.
505    pub fn soft_deletes(&mut self) -> &mut Column {
506        let column = self.timestamp("deleted_at");
507        column.nullable();
508        column
509    }
510
511    // --- keys and indexes --------------------------------------------------
512
513    /// A composite primary key.
514    ///
515    /// What a pivot table wants: the pair is the key, so the same link cannot
516    /// be inserted twice.
517    pub fn primary<I, S>(&mut self, columns: I) -> &mut Self
518    where
519        I: IntoIterator<Item = S>,
520        S: Into<String>,
521    {
522        self.primary = Some(columns.into_iter().map(Into::into).collect());
523        self
524    }
525
526    /// A secondary index, named by convention.
527    pub fn index<I, S>(&mut self, columns: I) -> &mut IndexDef
528    where
529        I: IntoIterator<Item = S>,
530        S: Into<String>,
531    {
532        self.push_index(columns, false)
533    }
534
535    /// A `UNIQUE` index over one or more columns.
536    pub fn unique<I, S>(&mut self, columns: I) -> &mut IndexDef
537    where
538        I: IntoIterator<Item = S>,
539        S: Into<String>,
540    {
541        self.push_index(columns, true)
542    }
543
544    fn push_index<I, S>(&mut self, columns: I, unique: bool) -> &mut IndexDef
545    where
546        I: IntoIterator<Item = S>,
547        S: Into<String>,
548    {
549        let columns: Vec<String> = columns.into_iter().map(Into::into).collect();
550        let suffix = if unique { "unique" } else { "index" };
551
552        self.indexes.push(IndexDef {
553            name: conventional_name(&self.table, &columns, suffix),
554            columns,
555            unique,
556        });
557        self.indexes.last_mut().expect("just pushed")
558    }
559
560    // --- rendering ---------------------------------------------------------
561
562    /// The `CREATE TABLE` statement, before rendering.
563    pub fn to_statement(&self) -> TableCreateStatement {
564        let mut stmt = Table::create();
565        stmt.table(Alias::new(self.table.clone()));
566        if self.if_not_exists {
567            stmt.if_not_exists();
568        }
569
570        for column in &self.columns {
571            // An inline `PRIMARY KEY` on a column *and* a composite one is two
572            // primary keys, which no engine accepts. The composite wins,
573            // because it is the more specific thing to have asked for.
574            let mut def = column.to_def();
575            if self.primary.is_some() {
576                strip_inline_primary(&mut def, column);
577            }
578            stmt.col(&mut def);
579        }
580
581        if let Some(columns) = &self.primary {
582            let mut key = Index::create();
583            for column in columns {
584                key.col(Alias::new(column.clone()));
585            }
586            stmt.primary_key(&mut key);
587        }
588
589        for column in &self.columns {
590            let Some((table, referenced, on_delete, on_update)) = &column.references else {
591                continue;
592            };
593
594            let mut constraint = ForeignKey::create();
595            constraint
596                .name(conventional_name(&self.table, std::slice::from_ref(&column.name), "foreign"))
597                .from(Alias::new(self.table.clone()), Alias::new(column.name.clone()))
598                .to(Alias::new(table.clone()), Alias::new(referenced.clone()));
599
600            if let Some(action) = on_delete {
601                constraint.on_delete(action.to_sea());
602            }
603            if let Some(action) = on_update {
604                constraint.on_update(action.to_sea());
605            }
606            stmt.foreign_key(&mut constraint);
607        }
608
609        stmt
610    }
611
612    /// Every statement that creates this table on `dialect`, in order: the
613    /// table, then its indexes.
614    pub fn to_sql(&self, dialect: Dialect) -> Vec<String> {
615        let mut statements = vec![dialect.build_schema(&self.to_statement())];
616
617        // Single-column `index()` modifiers become real indexes here rather
618        // than at declaration time, so `table.string("slug").index()` and
619        // `table.index(["slug"])` produce the same name.
620        for column in &self.columns {
621            if column.indexed {
622                let columns = vec![column.name.clone()];
623                let index = IndexDef {
624                    name: conventional_name(&self.table, &columns, "index"),
625                    columns,
626                    unique: false,
627                };
628                statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
629            }
630        }
631
632        for index in &self.indexes {
633            statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
634        }
635        statements
636    }
637
638    /// The statements that undo it: drop the table.
639    ///
640    /// The indexes go with it — no engine keeps an index on a table that is no
641    /// longer there — so one statement is the whole reversal.
642    pub fn to_reverse_sql(&self, dialect: Dialect) -> Vec<String> {
643        let mut stmt = Table::drop();
644        stmt.table(Alias::new(self.table.clone())).if_exists();
645        vec![dialect.build_schema(&stmt)]
646    }
647}
648
649/// A change to a table that already exists — Laravel's `Schema::table`.
650///
651/// ```ignore
652/// TableChanges::to("posts", |table| {
653///     table.string("subtitle").nullable();
654///     table.index(["author_id", "published"]);
655///     table.rename_column("body", "content");
656/// });
657/// ```
658///
659/// # Reversal
660///
661/// Each change knows its own opposite, so the migration's `down` is derived
662/// rather than written twice: an added column is dropped, a created index is
663/// dropped, a rename is renamed back.
664///
665/// Two are **not** reversible, and say so rather than pretending:
666/// [`drop_column`](Self::drop_column), because the type and the data are gone;
667/// and [`raw`](Self::raw), because nothing here knows what it did.
668#[derive(Debug, Clone)]
669pub struct TableChanges {
670    table: String,
671    changes: Vec<Change>,
672}
673
674#[derive(Debug, Clone)]
675enum Change {
676    AddColumn(Column),
677    DropColumn(String),
678    RenameColumn(String, String),
679    AddIndex(IndexDef),
680    DropIndex(IndexDef),
681    Raw(Option<Dialect>, String, Option<String>),
682}
683
684impl TableChanges {
685    /// Changes to `table`.
686    pub fn new(table: impl Into<String>) -> Self {
687        Self { table: table.into(), changes: Vec::new() }
688    }
689
690    /// Describe changes to `table` with `build`.
691    pub fn to(table: impl Into<String>, build: impl FnOnce(&mut TableChanges)) -> Self {
692        let mut changes = Self::new(table);
693        build(&mut changes);
694        changes
695    }
696
697    /// The table being changed.
698    pub fn table(&self) -> &str {
699        &self.table
700    }
701
702    /// Whether anything was asked for.
703    pub fn is_empty(&self) -> bool {
704        self.changes.is_empty()
705    }
706
707    // --- adding columns ----------------------------------------------------
708
709    /// Add a column of any kind.
710    ///
711    /// A column added to a table that already has rows must either be
712    /// [`nullable`](Column::nullable) or carry a [`default`](Column::default) —
713    /// otherwise the existing rows have no value for it and the engine refuses
714    /// the change.
715    pub fn column(&mut self, name: impl Into<String>, kind: ColumnKind) -> &mut Column {
716        self.changes.push(Change::AddColumn(Column::new(name, kind)));
717        match self.changes.last_mut() {
718            Some(Change::AddColumn(column)) => column,
719            _ => unreachable!("just pushed an AddColumn"),
720        }
721    }
722
723    /// Add a `VARCHAR(255)`.
724    pub fn string(&mut self, name: impl Into<String>) -> &mut Column {
725        self.column(name, ColumnKind::Varchar(DEFAULT_STRING_LEN))
726    }
727
728    /// Add a `VARCHAR(len)`.
729    pub fn string_len(&mut self, name: impl Into<String>, len: u32) -> &mut Column {
730        self.column(name, ColumnKind::Varchar(len))
731    }
732
733    /// Add an unbounded text column.
734    pub fn text(&mut self, name: impl Into<String>) -> &mut Column {
735        self.column(name, ColumnKind::Basic(ColumnType::Text))
736    }
737
738    /// Add a 32-bit signed integer.
739    pub fn integer(&mut self, name: impl Into<String>) -> &mut Column {
740        self.column(name, ColumnKind::Basic(ColumnType::Int))
741    }
742
743    /// Add a 64-bit signed integer.
744    pub fn big_integer(&mut self, name: impl Into<String>) -> &mut Column {
745        self.column(name, ColumnKind::Basic(ColumnType::BigInt))
746    }
747
748    /// Add a 64-bit unsigned integer.
749    pub fn unsigned_big_integer(&mut self, name: impl Into<String>) -> &mut Column {
750        self.column(name, ColumnKind::Basic(ColumnType::BigUint))
751    }
752
753    /// Add a foreign key column.
754    ///
755    /// The **constraint** is not added: no engine adds one to an existing
756    /// table portably (SQLite cannot at all), and a column that silently gained
757    /// no constraint would be worse than one that never claimed to. Declare it
758    /// in the `CREATE TABLE`, or accept the index this gives you.
759    pub fn foreign_id(&mut self, name: impl Into<String>) -> &mut Column {
760        let column = self.column(name, ColumnKind::Basic(ColumnType::BigUint));
761        column.index();
762        column
763    }
764
765    /// Add a boolean.
766    pub fn boolean(&mut self, name: impl Into<String>) -> &mut Column {
767        self.column(name, ColumnKind::Basic(ColumnType::Bool))
768    }
769
770    /// Add a timestamp.
771    pub fn timestamp(&mut self, name: impl Into<String>) -> &mut Column {
772        self.column(name, ColumnKind::Basic(ColumnType::Timestamp))
773    }
774
775    /// Add a JSON document column.
776    pub fn json(&mut self, name: impl Into<String>) -> &mut Column {
777        self.column(name, ColumnKind::Json)
778    }
779
780    // --- everything else ---------------------------------------------------
781
782    /// Drop a column.
783    ///
784    /// Makes the whole step **irreversible**: the type and the data are gone,
785    /// and a `down` that recreated the column empty would report success while
786    /// having restored nothing.
787    pub fn drop_column(&mut self, name: impl Into<String>) -> &mut Self {
788        self.changes.push(Change::DropColumn(name.into()));
789        self
790    }
791
792    /// Rename a column. Reverses by renaming it back.
793    pub fn rename_column(&mut self, from: impl Into<String>, to: impl Into<String>) -> &mut Self {
794        self.changes.push(Change::RenameColumn(from.into(), to.into()));
795        self
796    }
797
798    /// Create an index, named by the same convention a `CREATE TABLE` uses.
799    pub fn index<I, S>(&mut self, columns: I) -> &mut IndexDef
800    where
801        I: IntoIterator<Item = S>,
802        S: Into<String>,
803    {
804        self.push_index(columns, false, true)
805    }
806
807    /// Create a `UNIQUE` index.
808    pub fn unique<I, S>(&mut self, columns: I) -> &mut IndexDef
809    where
810        I: IntoIterator<Item = S>,
811        S: Into<String>,
812    {
813        self.push_index(columns, true, true)
814    }
815
816    /// Drop an index over `columns`.
817    ///
818    /// The columns rather than the name, so the reversal can recreate it —
819    /// and because the name is derivable from them anyway. Use
820    /// [`IndexDef::named`] on the returned value if it was named by hand.
821    pub fn drop_index<I, S>(&mut self, columns: I) -> &mut IndexDef
822    where
823        I: IntoIterator<Item = S>,
824        S: Into<String>,
825    {
826        self.push_index(columns, false, false)
827    }
828
829    /// Drop a `UNIQUE` index over `columns`.
830    pub fn drop_unique<I, S>(&mut self, columns: I) -> &mut IndexDef
831    where
832        I: IntoIterator<Item = S>,
833        S: Into<String>,
834    {
835        self.push_index(columns, true, false)
836    }
837
838    fn push_index<I, S>(&mut self, columns: I, unique: bool, adding: bool) -> &mut IndexDef
839    where
840        I: IntoIterator<Item = S>,
841        S: Into<String>,
842    {
843        let columns: Vec<String> = columns.into_iter().map(Into::into).collect();
844        let suffix = if unique { "unique" } else { "index" };
845        let index =
846            IndexDef { name: conventional_name(&self.table, &columns, suffix), columns, unique };
847
848        self.changes.push(if adding { Change::AddIndex(index) } else { Change::DropIndex(index) });
849        match self.changes.last_mut() {
850            Some(Change::AddIndex(index) | Change::DropIndex(index)) => index,
851            _ => unreachable!("just pushed an index change"),
852        }
853    }
854
855    /// Raw SQL, for one dialect or all of them.
856    ///
857    /// The escape hatch for what no portable form covers — a `MODIFY COLUMN`,
858    /// a generated column, a partial index. Pass `down` when you can undo it;
859    /// `None` makes the step irreversible and says why.
860    ///
861    /// Reach for a typed change first. This one is the reason a schema stops
862    /// being portable.
863    pub fn raw(
864        &mut self,
865        dialect: Option<Dialect>,
866        up: impl Into<String>,
867        down: Option<String>,
868    ) -> &mut Self {
869        self.changes.push(Change::Raw(dialect, up.into(), down));
870        self
871    }
872
873    // --- rendering ---------------------------------------------------------
874
875    /// Every statement that applies these changes on `dialect`, in order.
876    pub fn to_sql(&self, dialect: Dialect) -> Vec<String> {
877        let mut statements = Vec::new();
878
879        for change in &self.changes {
880            match change {
881                Change::AddColumn(column) => {
882                    let mut stmt = Table::alter();
883                    stmt.table(Alias::new(self.table.clone())).add_column(column.to_def());
884                    statements.push(dialect.build_schema(&stmt));
885
886                    // `foreign_id("x").index()` in an alter has to become a
887                    // real index here, the same as it does in a create.
888                    if column.indexed {
889                        statements.push(
890                            dialect.build_schema(&self.column_index(column).to_stmt(&self.table)),
891                        );
892                    }
893                }
894                Change::DropColumn(name) => {
895                    let mut stmt = Table::alter();
896                    stmt.table(Alias::new(self.table.clone()))
897                        .drop_column(Alias::new(name.clone()));
898                    statements.push(dialect.build_schema(&stmt));
899                }
900                Change::RenameColumn(from, to) => {
901                    statements.push(dialect.build_schema(&rename_column_stmt(
902                        &self.table,
903                        from,
904                        to,
905                    )));
906                }
907                Change::AddIndex(index) => {
908                    statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
909                }
910                Change::DropIndex(index) => {
911                    statements.push(dialect.build_schema(&drop_index_stmt(&self.table, index)));
912                }
913                Change::Raw(only, sql, _) => {
914                    if only.is_none() || *only == Some(dialect) {
915                        statements.push(sql.clone());
916                    }
917                }
918            }
919        }
920        statements
921    }
922
923    /// Why these changes cannot be undone, or `None` if they can.
924    ///
925    /// Checked before a rollback runs, so a batch that contains one
926    /// irreversible step refuses up front rather than half-unwinding.
927    pub fn irreversible_because(&self) -> Option<String> {
928        for change in &self.changes {
929            match change {
930                Change::DropColumn(name) => {
931                    return Some(format!(
932                        "`{}`.`{name}` was dropped, and its type and data went with it",
933                        self.table
934                    ))
935                }
936                Change::Raw(_, sql, None) => {
937                    return Some(format!("a raw statement with no reverse: {sql}"))
938                }
939                _ => {}
940            }
941        }
942        None
943    }
944
945    /// The statements that undo these changes on `dialect`, in reverse order.
946    ///
947    /// Empty when [`irreversible_because`](Self::irreversible_because) has an
948    /// answer — ask that first.
949    pub fn to_reverse_sql(&self, dialect: Dialect) -> Vec<String> {
950        let mut statements = Vec::new();
951
952        // Backwards: a column added after an index over it has to go first.
953        for change in self.changes.iter().rev() {
954            match change {
955                Change::AddColumn(column) => {
956                    if column.indexed {
957                        statements.push(dialect.build_schema(&drop_index_stmt(
958                            &self.table,
959                            &self.column_index(column),
960                        )));
961                    }
962                    let mut stmt = Table::alter();
963                    stmt.table(Alias::new(self.table.clone()))
964                        .drop_column(Alias::new(column.name.clone()));
965                    statements.push(dialect.build_schema(&stmt));
966                }
967                Change::RenameColumn(from, to) => {
968                    statements.push(dialect.build_schema(&rename_column_stmt(
969                        &self.table,
970                        to,
971                        from,
972                    )));
973                }
974                Change::AddIndex(index) => {
975                    statements.push(dialect.build_schema(&drop_index_stmt(&self.table, index)));
976                }
977                Change::DropIndex(index) => {
978                    statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
979                }
980                Change::Raw(only, _, Some(down)) => {
981                    if only.is_none() || *only == Some(dialect) {
982                        statements.push(down.clone());
983                    }
984                }
985                // Handled by `irreversible_because`, which the caller asks
986                // first — reaching here means it did not.
987                Change::DropColumn(_) | Change::Raw(_, _, None) => {}
988            }
989        }
990        statements
991    }
992
993    fn column_index(&self, column: &Column) -> IndexDef {
994        let columns = vec![column.name.clone()];
995        IndexDef { name: conventional_name(&self.table, &columns, "index"), columns, unique: false }
996    }
997}
998
999/// `ALTER TABLE t RENAME COLUMN a TO b`, per dialect.
1000fn rename_column_stmt(table: &str, from: &str, to: &str) -> sea_query::TableAlterStatement {
1001    let mut stmt = Table::alter();
1002    stmt.table(Alias::new(table.to_string()))
1003        .rename_column(Alias::new(from.to_string()), Alias::new(to.to_string()));
1004    stmt
1005}
1006
1007/// `DROP INDEX` — with `ON table` where the dialect wants it.
1008fn drop_index_stmt(table: &str, index: &IndexDef) -> sea_query::IndexDropStatement {
1009    let mut stmt = Index::drop();
1010    stmt.name(&index.name).table(Alias::new(table.to_string()));
1011    stmt
1012}
1013
1014/// Remove the inline `PRIMARY KEY` a column asked for, keeping everything else.
1015///
1016/// `ColumnDef` has no "unset" for this, so the definition is rebuilt from the
1017/// column with `primary` cleared.
1018fn strip_inline_primary(def: &mut ColumnDef, column: &Column) {
1019    let mut without = column.clone();
1020    without.primary = false;
1021    without.auto_increment = false;
1022    *def = without.to_def();
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027    use super::*;
1028
1029    fn posts() -> Blueprint {
1030        Blueprint::create("posts", |table| {
1031            table.id();
1032            table.string("slug").unique();
1033            table.text("body");
1034            table.boolean("published").default(false);
1035            table.foreign_id("author_id").constrained_on("users").cascade_on_delete();
1036            table.timestamps();
1037            table.index(["published", "created_at"]);
1038        })
1039    }
1040
1041    #[test]
1042    fn one_description_renders_for_every_engine() {
1043        // The whole point: no `if dialect ==` anywhere in the caller.
1044        for dialect in [Dialect::Sqlite, Dialect::MySql, Dialect::Postgres] {
1045            let sql = posts().to_sql(dialect).join("\n");
1046
1047            assert!(sql.contains("posts"), "{dialect:?}: {sql}");
1048            assert!(sql.to_uppercase().contains("CREATE TABLE"), "{dialect:?}: {sql}");
1049            assert!(sql.contains("author_id"), "{dialect:?}: {sql}");
1050        }
1051    }
1052
1053    #[test]
1054    fn auto_increment_is_spelled_the_way_each_engine_spells_it() {
1055        let sqlite = posts().to_sql(Dialect::Sqlite).join("\n");
1056        let mysql = posts().to_sql(Dialect::MySql).join("\n");
1057        let postgres = posts().to_sql(Dialect::Postgres).join("\n");
1058
1059        assert!(sqlite.contains("AUTOINCREMENT"), "{sqlite}");
1060        assert!(mysql.contains("AUTO_INCREMENT"), "{mysql}");
1061        // Postgres has no keyword — it is a serial type.
1062        assert!(postgres.contains("serial"), "{postgres}");
1063    }
1064
1065    #[test]
1066    fn a_key_never_renders_as_unsigned() {
1067        // Postgres has no unsigned serial, and the key a database generates is
1068        // a positive i64 on every backend anyway.
1069        let postgres = posts().to_sql(Dialect::Postgres).join("\n");
1070        assert!(!postgres.to_lowercase().contains("unsigned"), "{postgres}");
1071    }
1072
1073    #[test]
1074    fn mysql_gets_no_if_not_exists_on_an_index_because_it_rejects_one() {
1075        // The trap this builder exists to close: `CREATE INDEX IF NOT EXISTS`
1076        // is valid on SQLite and Postgres and a syntax error on MySQL.
1077        let mysql = posts().to_sql(Dialect::MySql).join("\n");
1078        let index = mysql.lines().find(|line| line.contains("CREATE INDEX")).expect("an index");
1079
1080        assert!(!index.contains("IF NOT EXISTS"), "{index}");
1081
1082        let sqlite = posts().to_sql(Dialect::Sqlite).join("\n");
1083        assert!(sqlite.contains("CREATE INDEX IF NOT EXISTS"), "{sqlite}");
1084    }
1085
1086    #[test]
1087    fn indexes_are_named_by_convention_so_a_rollback_can_find_them() {
1088        let sql = posts().to_sql(Dialect::Sqlite).join("\n");
1089
1090        assert!(sql.contains("posts_published_created_at_index"), "{sql}");
1091        assert!(sql.contains("posts_author_id_index"), "the foreign id is indexed: {sql}");
1092    }
1093
1094    #[test]
1095    fn a_column_index_and_a_table_index_agree_on_the_name() {
1096        let by_column = Blueprint::create("t", |table| {
1097            table.string("a").index();
1098        });
1099        let by_table = Blueprint::create("t", |table| {
1100            table.string("a");
1101            table.index(["a"]);
1102        });
1103
1104        assert_eq!(by_column.to_sql(Dialect::Sqlite), by_table.to_sql(Dialect::Sqlite));
1105    }
1106
1107    #[test]
1108    fn columns_are_not_null_unless_they_say_otherwise() {
1109        let sql = Blueprint::create("t", |table| {
1110            table.string("required");
1111            table.string("optional").nullable();
1112        })
1113        .to_sql(Dialect::Sqlite)
1114        .join("\n");
1115
1116        assert!(sql.contains("\"required\" varchar(255) NOT NULL"), "{sql}");
1117        assert!(sql.contains("\"optional\" varchar(255) NULL"), "{sql}");
1118    }
1119
1120    #[test]
1121    fn a_composite_primary_key_replaces_the_inline_one() {
1122        // Two primary keys is not valid anywhere, and a pivot declares its
1123        // pair after the columns.
1124        let sql = Blueprint::create("post_tag", |table| {
1125            table.foreign_id("post_id").constrained_on("posts").cascade_on_delete();
1126            table.foreign_id("tag_id").constrained_on("tags").cascade_on_delete();
1127            table.primary(["post_id", "tag_id"]);
1128        })
1129        .to_sql(Dialect::Sqlite)
1130        .join("\n");
1131
1132        assert_eq!(sql.matches("PRIMARY KEY").count(), 1, "{sql}");
1133        assert!(sql.contains("PRIMARY KEY (\"post_id\", \"tag_id\")"), "{sql}");
1134    }
1135
1136    #[test]
1137    fn foreign_keys_carry_their_action() {
1138        let sql = posts().to_sql(Dialect::Sqlite).join("\n");
1139        assert!(sql.contains("ON DELETE CASCADE"), "{sql}");
1140        assert!(sql.contains("REFERENCES \"users\" (\"id\")"), "{sql}");
1141    }
1142
1143    #[test]
1144    fn the_reverse_of_a_create_is_a_drop() {
1145        let sql = posts().to_reverse_sql(Dialect::Sqlite).join("\n");
1146        assert_eq!(sql, "DROP TABLE IF EXISTS \"posts\"");
1147    }
1148
1149    #[test]
1150    fn json_becomes_each_engines_own_json_type() {
1151        let blueprint = Blueprint::create("t", |table| {
1152            table.json("payload");
1153        });
1154        let rendered = |d| blueprint.to_sql(d).join("\n").to_lowercase();
1155
1156        assert!(rendered(Dialect::Postgres).contains("jsonb"));
1157        assert!(rendered(Dialect::MySql).contains("json"));
1158        // SQLite has no JSON type; text is what it has.
1159        assert!(rendered(Dialect::Sqlite).contains("jsonb_text"));
1160    }
1161
1162    #[test]
1163    fn decimal_is_fixed_point_where_that_exists_and_a_float_where_it_does_not() {
1164        // Documented rather than papered over: SQLite has no DECIMAL, so an
1165        // amount stored there rounds. Silently swapping in TEXT instead would
1166        // break every read of it.
1167        let blueprint = Blueprint::create("t", |table| {
1168            table.decimal("amount", 10, 2);
1169        });
1170        let rendered = |d| blueprint.to_sql(d).join("\n").to_lowercase();
1171
1172        assert!(rendered(Dialect::Postgres).contains("decimal(10, 2)"));
1173        assert!(rendered(Dialect::MySql).contains("decimal(10, 2)"));
1174        assert!(rendered(Dialect::Sqlite).contains("real(10, 2)"));
1175    }
1176
1177    #[test]
1178    fn binary_is_bytea_on_postgres_and_blob_elsewhere() {
1179        let blueprint = Blueprint::create("t", |table| {
1180            table.binary("bytes");
1181        });
1182
1183        assert!(blueprint.to_sql(Dialect::Postgres).join("").contains("bytea"));
1184        assert!(blueprint.to_sql(Dialect::MySql).join("").contains("blob"));
1185    }
1186}