Skip to main content

orbit_orm/schema/
mod.rs

1//! A database-neutral schema builder, after Laravel's `Schema::create` and `Blueprint`.
2//!
3//! A migration describes a table once and the same code runs on SQLite, MySQL and MariaDB, and
4//! PostgreSQL:
5//!
6//! ```
7//! use orbit_orm::schema::{self, Action};
8//!
9//! let posts = schema::create("posts", |t| {
10//!     t.id();
11//!     t.string("title");
12//!     t.text("body").nullable();
13//!     t.big_integer("user_id");
14//!     t.foreign("user_id").references("users", "id").on_delete(Action::Restrict);
15//!     t.unique(["title"]);
16//! });
17//! # let _ = posts;
18//! ```
19//!
20//! A [`Statement`] is only a description. [`Statement::to_sql`] renders it for a [`Driver`] as a
21//! list of statements to run in order: PostgreSQL and SQLite create a plain index with a separate
22//! `CREATE INDEX`, and sqlx does not run several statements in one string on every database.
23
24mod blueprint;
25mod column;
26
27use crate::{Driver, Error};
28
29pub use blueprint::{Action, Blueprint, ForeignKey};
30pub use column::Column;
31
32/// The database families that differ in DDL. A [`Driver`] maps to one of these, so the SQL below
33/// matches on a type that has all three variants whatever Cargo features are on.
34#[derive(Clone, Copy, PartialEq, Eq)]
35#[allow(dead_code)]
36pub(crate) enum Dialect {
37    MySql,
38    Postgres,
39    Sqlite,
40}
41
42impl Dialect {
43    pub(crate) fn of(driver: Driver) -> Self {
44        match driver {
45            #[cfg(feature = "mysql")]
46            Driver::MySql => Self::MySql,
47            #[cfg(feature = "postgres")]
48            Driver::Postgres => Self::Postgres,
49            #[cfg(feature = "sqlite")]
50            Driver::Sqlite => Self::Sqlite,
51        }
52    }
53}
54
55/// A schema change, ready to render for any database.
56#[derive(Debug, Clone)]
57pub struct Statement {
58    kind: Kind,
59}
60
61#[derive(Debug, Clone)]
62enum Kind {
63    Create { table: String, blueprint: Blueprint },
64    Drop { table: String, if_exists: bool },
65}
66
67/// Creates a table. The closure describes its columns, indexes and foreign keys.
68pub fn create(table: &str, f: impl FnOnce(&mut Blueprint)) -> Statement {
69    let mut blueprint = Blueprint::new();
70    f(&mut blueprint);
71    Statement {
72        kind: Kind::Create {
73            table: table.to_owned(),
74            blueprint,
75        },
76    }
77}
78
79/// Drops a table. It is an error in the database if the table does not exist.
80pub fn drop(table: &str) -> Statement {
81    Statement {
82        kind: Kind::Drop {
83            table: table.to_owned(),
84            if_exists: false,
85        },
86    }
87}
88
89/// Drops a table if it exists.
90pub fn drop_if_exists(table: &str) -> Statement {
91    Statement {
92        kind: Kind::Drop {
93            table: table.to_owned(),
94            if_exists: true,
95        },
96    }
97}
98
99impl Statement {
100    /// Renders the statement for `driver`: one SQL statement per element, to run in order.
101    ///
102    /// # Panics
103    ///
104    /// Panics when a name is invalid or a default has no SQL literal. Use
105    /// [`try_to_sql`](Self::try_to_sql) to get the error instead.
106    pub fn to_sql(&self, driver: Driver) -> Vec<String> {
107        self.try_to_sql(driver)
108            .unwrap_or_else(|e| panic!("cannot render the schema statement: {e}"))
109    }
110
111    /// Renders the statement for `driver`, or the error for an invalid name. A table, column or
112    /// index column name must not be empty or contain a backtick or a double quote, the rule of
113    /// the query builder.
114    pub fn try_to_sql(&self, driver: Driver) -> Result<Vec<String>, Error> {
115        let dialect = Dialect::of(driver);
116        match &self.kind {
117            Kind::Create { table, blueprint } => blueprint.render(driver, dialect, table),
118            Kind::Drop { table, if_exists } => {
119                crate::query::validate(table)?;
120                let exists = if *if_exists { "IF EXISTS " } else { "" };
121                Ok(vec![format!("DROP TABLE {exists}{}", driver.quote(table))])
122            }
123        }
124    }
125}