Skip to main content

orbit_orm/schema/
blueprint.rs

1use super::Dialect;
2use super::column::{Column, ColumnType};
3use crate::query::validate;
4use crate::{Driver, Error};
5
6/// What a foreign key does to this table's rows when the referenced row is deleted or updated.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Action {
9    Cascade,
10    Restrict,
11    SetNull,
12    SetDefault,
13    NoAction,
14}
15
16impl Action {
17    fn sql(self) -> &'static str {
18        match self {
19            Self::Cascade => "CASCADE",
20            Self::Restrict => "RESTRICT",
21            Self::SetNull => "SET NULL",
22            Self::SetDefault => "SET DEFAULT",
23            Self::NoAction => "NO ACTION",
24        }
25    }
26}
27
28/// A foreign key under construction. Chain `references`, `on_delete`, `on_update` and `name`.
29#[derive(Debug, Clone)]
30pub struct ForeignKey {
31    column: String,
32    name: Option<String>,
33    target: Option<(String, String)>,
34    on_delete: Option<Action>,
35    on_update: Option<Action>,
36}
37
38impl ForeignKey {
39    /// The table and column that this column points at.
40    pub fn references(&mut self, table: &str, column: &str) -> &mut Self {
41        self.target = Some((table.to_owned(), column.to_owned()));
42        self
43    }
44
45    /// What happens when the referenced row is deleted.
46    pub fn on_delete(&mut self, action: Action) -> &mut Self {
47        self.on_delete = Some(action);
48        self
49    }
50
51    /// What happens when the referenced key is updated.
52    pub fn on_update(&mut self, action: Action) -> &mut Self {
53        self.on_update = Some(action);
54        self
55    }
56
57    /// Names the constraint, as the second argument of Laravel's `$table->foreign('col', 'name')`.
58    /// Without it the name is `{table}_{column}_foreign`.
59    pub fn name(&mut self, name: &str) -> &mut Self {
60        self.name = Some(name.to_owned());
61        self
62    }
63}
64
65/// A plain or unique index under construction. Chain `name` to choose its name.
66#[derive(Debug, Clone)]
67pub struct Index {
68    columns: Vec<String>,
69    unique: bool,
70    name: Option<String>,
71}
72
73impl Index {
74    /// Names the index, as the second argument of Laravel's `$table->index(['a'], 'name')`.
75    /// Without it the name is `{table}_{columns}_index`, or `{table}_{columns}_unique` for a
76    /// unique index.
77    pub fn name(&mut self, name: &str) -> &mut Self {
78        self.name = Some(name.to_owned());
79        self
80    }
81
82    /// The index's name, checked: the custom one, or the default for `table`.
83    fn name_for(&self, table: &str) -> Result<String, Error> {
84        match &self.name {
85            Some(name) => {
86                validate(name)?;
87                Ok(name.clone())
88            }
89            None => Ok(name_of(
90                table,
91                &self.columns,
92                if self.unique { "unique" } else { "index" },
93            )),
94        }
95    }
96}
97
98/// Every operation of a blueprint, in call order. A column is recorded as `Column`, its index in
99/// the column list, at the point it is declared, as Laravel 11+ records each column as a command.
100/// `schema::create` renders the columns, then the indexes, primary keys and foreign keys inside
101/// `CREATE TABLE`; `schema::table` renders every command as its own statement in call order.
102#[derive(Debug, Clone)]
103enum Command {
104    Column(usize),
105    Index(Index),
106    Primary { columns: Vec<String> },
107    Foreign(ForeignKey),
108    RenameColumn { from: String, to: String },
109    DropColumn { name: String },
110    DropIndex { name: String },
111    DropForeign { name: String },
112}
113
114/// The columns and commands (indexes, keys, renames, drops) of a table, in the order they were
115/// added.
116#[derive(Debug, Clone, Default)]
117pub struct Blueprint {
118    columns: Vec<Column>,
119    commands: Vec<Command>,
120}
121
122impl Blueprint {
123    pub(crate) fn new() -> Self {
124        Self::default()
125    }
126
127    fn add(&mut self, name: &str, ty: ColumnType) -> &mut Column {
128        self.commands.push(Command::Column(self.columns.len()));
129        self.columns.push(Column::new(name, ty));
130        self.columns.last_mut().expect("just pushed")
131    }
132
133    /// An auto-incrementing 64-bit primary key named `id`. In [`schema::table`](super::table)
134    /// SQLite cannot add it, as SQLite cannot add a primary key column to an existing table.
135    pub fn id(&mut self) -> &mut Column {
136        self.add("id", ColumnType::Id)
137    }
138
139    /// A `VARCHAR(255)`; `TEXT` on SQLite. Change the length with `.length(n)`.
140    pub fn string(&mut self, name: &str) -> &mut Column {
141        self.add(name, ColumnType::String(255))
142    }
143
144    /// A `TEXT` column.
145    pub fn text(&mut self, name: &str) -> &mut Column {
146        self.add(name, ColumnType::Text)
147    }
148
149    /// A `MEDIUMTEXT` column (up to 16 MB) on MySQL and MariaDB, as Laravel's `mediumText`. It is
150    /// `TEXT` on PostgreSQL, which has no size limit, and on SQLite, where text is limited only by
151    /// the database's size limits.
152    pub fn medium_text(&mut self, name: &str) -> &mut Column {
153        self.add(name, ColumnType::MediumText)
154    }
155
156    /// A `LONGTEXT` column (up to 4 GB) on MySQL and MariaDB, as Laravel's `longText`. It is `TEXT`
157    /// on PostgreSQL, which has no size limit, and on SQLite, where text is limited only by the
158    /// database's size limits. WordPress uses `LONGTEXT` for post content and for option and meta
159    /// values.
160    pub fn long_text(&mut self, name: &str) -> &mut Column {
161        self.add(name, ColumnType::LongText)
162    }
163
164    /// An `INTEGER` column.
165    pub fn integer(&mut self, name: &str) -> &mut Column {
166        self.add(name, ColumnType::Integer)
167    }
168
169    /// A `BIGINT` column; `INTEGER` on SQLite, whose integers are 64-bit already.
170    pub fn big_integer(&mut self, name: &str) -> &mut Column {
171        self.add(name, ColumnType::BigInteger)
172    }
173
174    /// A boolean: `BOOLEAN` on PostgreSQL, `TINYINT(1)` elsewhere.
175    pub fn boolean(&mut self, name: &str) -> &mut Column {
176        self.add(name, ColumnType::Boolean)
177    }
178
179    /// A date and time without a time zone: `DATETIME`, or `TIMESTAMP(0)` on PostgreSQL. Set
180    /// fractional seconds with `.precision(p)` and a default of the current time with
181    /// `.use_current()`.
182    pub fn date_time(&mut self, name: &str) -> &mut Column {
183        self.add(name, ColumnType::DateTime(0))
184    }
185
186    /// A timestamp, as Laravel's `timestamp`: `TIMESTAMP` on MySQL, `TIMESTAMP(0) WITHOUT TIME
187    /// ZONE` on PostgreSQL, `DATETIME` on SQLite. Set fractional seconds with `.precision(p)` and
188    /// a default of the current time with `.use_current()`.
189    ///
190    /// On MySQL and MariaDB with `explicit_defaults_for_timestamp` off (the MariaDB default
191    /// before 10.10), the first `TIMESTAMP` column of a table that is `NOT NULL` and has no
192    /// default gets `DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` from the server.
193    /// Laravel's columns get the same. Add `.nullable()` or a default to avoid it.
194    pub fn timestamp(&mut self, name: &str) -> &mut Column {
195        self.add(name, ColumnType::Timestamp(0))
196    }
197
198    /// A column of raw bytes, as Laravel's `binary`: `BLOB`, or `BYTEA` on PostgreSQL.
199    pub fn binary(&mut self, name: &str) -> &mut Column {
200        self.add(name, ColumnType::Binary)
201    }
202
203    /// Nullable `created_at` and `updated_at` columns.
204    pub fn timestamps(&mut self) {
205        self.date_time("created_at").nullable();
206        self.date_time("updated_at").nullable();
207    }
208
209    /// A unique constraint over `columns`, named `{table}_{columns}_unique` unless `.name()`
210    /// gives another name.
211    pub fn unique<I, S>(&mut self, columns: I) -> &mut Index
212    where
213        I: IntoIterator<Item = S>,
214        S: AsRef<str>,
215    {
216        self.add_index(columns, true)
217    }
218
219    /// A plain index over `columns`, named `{table}_{columns}_index` unless `.name()` gives
220    /// another name.
221    pub fn index<I, S>(&mut self, columns: I) -> &mut Index
222    where
223        I: IntoIterator<Item = S>,
224        S: AsRef<str>,
225    {
226        self.add_index(columns, false)
227    }
228
229    fn add_index<I, S>(&mut self, columns: I, unique: bool) -> &mut Index
230    where
231        I: IntoIterator<Item = S>,
232        S: AsRef<str>,
233    {
234        self.commands.push(Command::Index(Index {
235            columns: collect(columns),
236            unique,
237            name: None,
238        }));
239        match self.commands.last_mut() {
240            Some(Command::Index(index)) => index,
241            _ => unreachable!("just pushed an index"),
242        }
243    }
244
245    /// A primary key over `columns`, as Laravel's `$table->primary(['a', 'b'])`: the table
246    /// constraint `PRIMARY KEY (a, b)`. For a key of one column, `.primary()` on the column does
247    /// the same. In [`schema::table`](super::table) SQLite cannot add it: it is
248    /// [`Error::Unsupported`] there.
249    pub fn primary<I, S>(&mut self, columns: I)
250    where
251        I: IntoIterator<Item = S>,
252        S: AsRef<str>,
253    {
254        self.commands.push(Command::Primary {
255            columns: collect(columns),
256        });
257    }
258
259    /// A foreign key on `column`, named `{table}_{column}_foreign` unless `.name()` gives another
260    /// name. In [`schema::table`](super::table) SQLite renders nothing, as SQLite cannot add a
261    /// constraint to an existing table.
262    pub fn foreign(&mut self, column: &str) -> &mut ForeignKey {
263        self.commands.push(Command::Foreign(ForeignKey {
264            column: column.to_owned(),
265            name: None,
266            target: None,
267            on_delete: None,
268            on_update: None,
269        }));
270        match self.commands.last_mut() {
271            Some(Command::Foreign(fk)) => fk,
272            _ => unreachable!("just pushed a foreign key"),
273        }
274    }
275
276    /// Renames the column `from` to `to`: `ALTER TABLE ... RENAME COLUMN`. Only in
277    /// [`schema::table`](super::table).
278    ///
279    /// `RENAME COLUMN` needs MySQL 8.0.3 or later, or MariaDB 10.5.2 or later. On older servers
280    /// Laravel falls back to `CHANGE old new <definition>`; Orbit does not, and the server
281    /// reports a syntax error.
282    pub fn rename_column(&mut self, from: &str, to: &str) {
283        self.commands.push(Command::RenameColumn {
284            from: from.to_owned(),
285            to: to.to_owned(),
286        });
287    }
288
289    /// Drops the column `name`: `ALTER TABLE ... DROP COLUMN`. Only in
290    /// [`schema::table`](super::table). SQLite needs version 3.35 or later.
291    pub fn drop_column(&mut self, name: &str) {
292        self.commands.push(Command::DropColumn {
293            name: name.to_owned(),
294        });
295    }
296
297    /// Drops the index `name`, as Laravel's `dropIndex('name')`. Only in
298    /// [`schema::table`](super::table).
299    ///
300    /// On MySQL it drops a unique index too. On SQLite it drops a unique only when `unique` added
301    /// it in `schema::table`: a unique declared in [`schema::create`](super::create) is an inline
302    /// constraint there and cannot be dropped this way. On PostgreSQL `unique` always adds a
303    /// constraint, and `DROP INDEX` cannot drop the index of a constraint. Orbit has no
304    /// `drop_unique` (Laravel's `dropUnique`).
305    pub fn drop_index(&mut self, name: &str) {
306        self.commands.push(Command::DropIndex {
307            name: name.to_owned(),
308        });
309    }
310
311    /// Drops the foreign key `name`, as Laravel's `dropForeign('name')`. Only in
312    /// [`schema::table`](super::table). SQLite renders nothing, as it cannot drop a constraint
313    /// from an existing table and `foreign` added none there.
314    pub fn drop_foreign(&mut self, name: &str) {
315        self.commands.push(Command::DropForeign {
316            name: name.to_owned(),
317        });
318    }
319
320    /// Renders `CREATE TABLE`, and the separate `CREATE INDEX` statements of PostgreSQL and
321    /// SQLite. With `if_not_exists`, both take `IF NOT EXISTS`.
322    pub(crate) fn render_create(
323        &self,
324        driver: Driver,
325        dialect: Dialect,
326        table: &str,
327        if_not_exists: bool,
328    ) -> Result<Vec<String>, Error> {
329        validate(table)?;
330        let unsupported = |operation| Error::Unsupported {
331            operation,
332            driver: driver.name(),
333        };
334        let q = |name: &str| driver.quote(name);
335        let exists = if if_not_exists { "IF NOT EXISTS " } else { "" };
336
337        let mut parts = Vec::new();
338        for column in &self.columns {
339            if column.change {
340                return Err(unsupported("changing a column in create"));
341            }
342            parts.push(column.render(driver, dialect)?);
343        }
344        let mut after = Vec::new();
345        for command in &self.commands {
346            match command {
347                Command::Column(_) => {}
348                Command::Index(index) => {
349                    let columns = column_list(driver, &index.columns)?;
350                    let name = index.name_for(table)?;
351                    if index.unique {
352                        parts.push(format!("CONSTRAINT {} UNIQUE ({columns})", q(&name)));
353                    } else {
354                        match dialect {
355                            Dialect::MySql => parts.push(format!("INDEX {} ({columns})", q(&name))),
356                            _ => after.push(format!(
357                                "CREATE INDEX {exists}{} ON {} ({columns})",
358                                q(&name),
359                                q(table)
360                            )),
361                        }
362                    }
363                }
364                Command::Primary { columns } => {
365                    parts.push(format!("PRIMARY KEY ({})", column_list(driver, columns)?));
366                }
367                Command::Foreign(_) => {}
368                Command::RenameColumn { .. } => {
369                    return Err(unsupported("renaming a column in create"));
370                }
371                Command::DropColumn { .. } => {
372                    return Err(unsupported("dropping a column in create"));
373                }
374                Command::DropIndex { .. } => {
375                    return Err(unsupported("dropping an index in create"));
376                }
377                Command::DropForeign { .. } => {
378                    return Err(unsupported("dropping a foreign key in create"));
379                }
380            }
381        }
382        // Foreign keys follow the indexes inside CREATE TABLE.
383        for command in &self.commands {
384            if let Command::Foreign(fk) = command {
385                parts.push(fk.render(driver, table)?);
386            }
387        }
388        let mut out = vec![format!(
389            "CREATE TABLE {exists}{} ({})",
390            q(table),
391            parts.join(", ")
392        )];
393        out.extend(after);
394        Ok(out)
395    }
396
397    /// Renders the changes to an existing table, one statement per operation, in call order as
398    /// Laravel 11+ does: an added or changed column renders where it was declared, among the
399    /// other commands. A foreign key and `drop_foreign` render nothing on SQLite.
400    pub(crate) fn render_alter(
401        &self,
402        driver: Driver,
403        dialect: Dialect,
404        table: &str,
405    ) -> Result<Vec<String>, Error> {
406        validate(table)?;
407        let unsupported = |operation| Error::Unsupported {
408            operation,
409            driver: driver.name(),
410        };
411        let q = |name: &str| driver.quote(name);
412        let alter = format!("ALTER TABLE {}", q(table));
413
414        let mut out = Vec::new();
415        for command in &self.commands {
416            match command {
417                Command::Column(i) => {
418                    let column = &self.columns[*i];
419                    if column.change {
420                        if dialect == Dialect::Sqlite {
421                            return Err(unsupported("changing a column"));
422                        }
423                        if column.ty == ColumnType::Id {
424                            return Err(unsupported("changing an id column"));
425                        }
426                        if column.primary {
427                            return Err(unsupported("changing a column into a primary key"));
428                        }
429                        out.push(match dialect {
430                            Dialect::Postgres => {
431                                format!("{alter} {}", column.render_change_postgres(driver)?)
432                            }
433                            _ => {
434                                format!("{alter} MODIFY COLUMN {}", column.render(driver, dialect)?)
435                            }
436                        });
437                    } else {
438                        if (column.ty == ColumnType::Id || column.primary)
439                            && dialect == Dialect::Sqlite
440                        {
441                            return Err(unsupported("adding a primary key column"));
442                        }
443                        out.push(format!(
444                            "{alter} ADD COLUMN {}",
445                            column.render(driver, dialect)?
446                        ));
447                    }
448                }
449                Command::Index(index) => {
450                    let columns = column_list(driver, &index.columns)?;
451                    let name = q(&index.name_for(table)?);
452                    out.push(match (dialect, index.unique) {
453                        (Dialect::MySql, false) => format!("{alter} ADD INDEX {name} ({columns})"),
454                        (Dialect::MySql, true) => format!("{alter} ADD UNIQUE {name} ({columns})"),
455                        (Dialect::Postgres, true) => {
456                            format!("{alter} ADD CONSTRAINT {name} UNIQUE ({columns})")
457                        }
458                        (Dialect::Sqlite, true) => {
459                            format!("CREATE UNIQUE INDEX {name} ON {} ({columns})", q(table))
460                        }
461                        (_, false) => format!("CREATE INDEX {name} ON {} ({columns})", q(table)),
462                    });
463                }
464                Command::Primary { columns } => {
465                    let columns = column_list(driver, columns)?;
466                    if dialect == Dialect::Sqlite {
467                        return Err(unsupported("adding a primary key"));
468                    }
469                    out.push(format!("{alter} ADD PRIMARY KEY ({columns})"));
470                }
471                Command::Foreign(fk) => {
472                    // Rendered on SQLite too, so a missing target or a bad name is still an error.
473                    let constraint = fk.render(driver, table)?;
474                    if dialect != Dialect::Sqlite {
475                        out.push(format!("{alter} ADD {constraint}"));
476                    }
477                }
478                Command::RenameColumn { from, to } => {
479                    validate(from)?;
480                    validate(to)?;
481                    out.push(format!("{alter} RENAME COLUMN {} TO {}", q(from), q(to)));
482                }
483                Command::DropColumn { name } => {
484                    validate(name)?;
485                    out.push(format!("{alter} DROP COLUMN {}", q(name)));
486                }
487                Command::DropIndex { name } => {
488                    validate(name)?;
489                    out.push(match dialect {
490                        Dialect::MySql => format!("{alter} DROP INDEX {}", q(name)),
491                        _ => format!("DROP INDEX {}", q(name)),
492                    });
493                }
494                Command::DropForeign { name } => {
495                    validate(name)?;
496                    match dialect {
497                        Dialect::MySql => out.push(format!("{alter} DROP FOREIGN KEY {}", q(name))),
498                        Dialect::Postgres => {
499                            out.push(format!("{alter} DROP CONSTRAINT {}", q(name)))
500                        }
501                        Dialect::Sqlite => {}
502                    }
503                }
504            }
505        }
506        Ok(out)
507    }
508}
509
510impl ForeignKey {
511    /// The `CONSTRAINT ... FOREIGN KEY ... REFERENCES ...` clause, with its custom name or the
512    /// default one for `table`.
513    fn render(&self, driver: Driver, table: &str) -> Result<String, Error> {
514        let q = |name: &str| driver.quote(name);
515        validate(&self.column)?;
516        let Some((ref_table, ref_column)) = &self.target else {
517            return Err(Error::Unsupported {
518                operation: "a foreign key without references()",
519                driver: driver.name(),
520            });
521        };
522        validate(ref_table)?;
523        validate(ref_column)?;
524        let name = match &self.name {
525            Some(name) => {
526                validate(name)?;
527                name.clone()
528            }
529            None => name_of(table, std::slice::from_ref(&self.column), "foreign"),
530        };
531        let mut sql = format!(
532            "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
533            q(&name),
534            q(&self.column),
535            q(ref_table),
536            q(ref_column)
537        );
538        if let Some(a) = self.on_delete {
539            sql.push_str(&format!(" ON DELETE {}", a.sql()));
540        }
541        if let Some(a) = self.on_update {
542            sql.push_str(&format!(" ON UPDATE {}", a.sql()));
543        }
544        Ok(sql)
545    }
546}
547
548/// The default name of an index or constraint: `{table}_{columns}_{suffix}`, as Laravel names it.
549fn name_of(table: &str, columns: &[String], suffix: &str) -> String {
550    format!("{table}_{}_{suffix}", columns.join("_"))
551}
552
553/// The quoted, comma-separated column list of an index, each name checked.
554fn column_list(driver: Driver, columns: &[String]) -> Result<String, Error> {
555    for c in columns {
556        validate(c)?;
557    }
558    Ok(columns
559        .iter()
560        .map(|c| driver.quote(c))
561        .collect::<Vec<_>>()
562        .join(", "))
563}
564
565fn collect<I, S>(columns: I) -> Vec<String>
566where
567    I: IntoIterator<Item = S>,
568    S: AsRef<str>,
569{
570    columns.into_iter().map(|c| c.as_ref().to_owned()).collect()
571}