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}