Skip to main content

rainier_orm/
blueprint.rs

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