Skip to main content

cast_core/
schema.rs

1//! Schema builder. Used in migrations: `Schema::create("users", |t| ...)`.
2//!
3//! Mirrors Laravel's `Schema::create` ergonomics: `t.string("name").not_null().unique()`.
4
5use sea_query::{ColumnDef as SeaColumnDef, ColumnType, PostgresQueryBuilder, Table as SeaTable};
6
7#[derive(Default)]
8pub struct Schema {
9    pub statements: Vec<String>,
10}
11
12impl Schema {
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    pub fn create<F>(&mut self, table: &str, build: F)
18    where
19        F: FnOnce(&mut Table),
20    {
21        let mut t = Table::new(table);
22        build(&mut t);
23        let (create_sql, post_sqls) = t.into_sql();
24        self.statements.push(create_sql);
25        self.statements.extend(post_sqls);
26    }
27
28    pub fn drop(&mut self, table: &str) {
29        self.statements
30            .push(format!("DROP TABLE IF EXISTS {} CASCADE", table));
31    }
32
33    pub fn drop_if_exists(&mut self, table: &str) {
34        self.drop(table);
35    }
36
37    pub fn raw(&mut self, sql: impl Into<String>) {
38        self.statements.push(sql.into());
39    }
40}
41
42/// A table definition assembled inside `Schema::create`'s closure.
43pub struct Table {
44    name: String,
45    columns: Vec<Box<ColumnDef>>,
46    indexes: Vec<String>,
47    foreign_keys: Vec<String>,
48}
49
50impl Table {
51    pub fn new(name: impl Into<String>) -> Self {
52        Self {
53            name: name.into(),
54            columns: Vec::new(),
55            indexes: Vec::new(),
56            foreign_keys: Vec::new(),
57        }
58    }
59
60    fn push_column(&mut self, name: &str, ty: ColumnType) -> &mut ColumnDef {
61        let sea_def = SeaColumnDef::new_with_type(sea_query::Alias::new(name), ty);
62        // Box gives the SeaColumnDef a stable address even if `columns` reallocates,
63        // so the `&mut ColumnDef` returned to the caller stays valid across other
64        // method calls in the building closure.
65        let boxed = Box::new(ColumnDef {
66            sea_def,
67            name: name.to_string(),
68        });
69        self.columns.push(boxed);
70        self.columns.last_mut().unwrap().as_mut()
71    }
72
73    pub fn id(&mut self) -> &mut ColumnDef {
74        let cd = self.push_column("id", ColumnType::BigInteger);
75        cd.sea_def.not_null().primary_key().auto_increment();
76        cd
77    }
78
79    pub fn uuid_id(&mut self) -> &mut ColumnDef {
80        let cd = self.push_column("id", ColumnType::Uuid);
81        cd.sea_def.not_null().primary_key();
82        cd
83    }
84
85    pub fn string(&mut self, name: &str) -> &mut ColumnDef {
86        self.push_column(name, ColumnType::String(sea_query::StringLen::N(255)))
87    }
88
89    pub fn text(&mut self, name: &str) -> &mut ColumnDef {
90        self.push_column(name, ColumnType::Text)
91    }
92
93    pub fn integer(&mut self, name: &str) -> &mut ColumnDef {
94        self.push_column(name, ColumnType::Integer)
95    }
96
97    pub fn big_integer(&mut self, name: &str) -> &mut ColumnDef {
98        self.push_column(name, ColumnType::BigInteger)
99    }
100
101    pub fn boolean(&mut self, name: &str) -> &mut ColumnDef {
102        self.push_column(name, ColumnType::Boolean)
103    }
104
105    pub fn timestamp(&mut self, name: &str) -> &mut ColumnDef {
106        self.push_column(name, ColumnType::Timestamp)
107    }
108
109    pub fn timestamp_tz(&mut self, name: &str) -> &mut ColumnDef {
110        self.push_column(name, ColumnType::TimestampWithTimeZone)
111    }
112
113    pub fn timestamps(&mut self) {
114        self.push_column("created_at", ColumnType::TimestampWithTimeZone)
115            .nullable()
116            .default("CURRENT_TIMESTAMP");
117        self.push_column("updated_at", ColumnType::TimestampWithTimeZone)
118            .nullable()
119            .default("CURRENT_TIMESTAMP");
120    }
121
122    pub fn soft_deletes(&mut self) {
123        self.push_column("deleted_at", ColumnType::TimestampWithTimeZone)
124            .nullable();
125    }
126
127    pub fn json(&mut self, name: &str) -> &mut ColumnDef {
128        self.push_column(name, ColumnType::Json)
129    }
130
131    pub fn uuid(&mut self, name: &str) -> &mut ColumnDef {
132        self.push_column(name, ColumnType::Uuid)
133    }
134
135    /// Add a `bigint` foreign key referencing `references.id`.
136    pub fn foreign_id_for(&mut self, name: &str, references: &str) -> &mut ColumnDef {
137        let fk_sql = format!(
138            "ALTER TABLE {} ADD CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {} (id) ON DELETE CASCADE",
139            self.name, self.name, name, name, references
140        );
141        self.foreign_keys.push(fk_sql);
142        self.push_column(name, ColumnType::BigInteger)
143    }
144
145    pub fn index(&mut self, columns: &[&str]) -> &mut Self {
146        let idx_name = format!("idx_{}_{}", self.name, columns.join("_"));
147        let sql = format!(
148            "CREATE INDEX {} ON {} ({})",
149            idx_name,
150            self.name,
151            columns.join(", ")
152        );
153        self.indexes.push(sql);
154        self
155    }
156
157    pub fn unique_index(&mut self, columns: &[&str]) -> &mut Self {
158        let idx_name = format!("uq_{}_{}", self.name, columns.join("_"));
159        let sql = format!(
160            "CREATE UNIQUE INDEX {} ON {} ({})",
161            idx_name,
162            self.name,
163            columns.join(", ")
164        );
165        self.indexes.push(sql);
166        self
167    }
168
169    fn into_sql(self) -> (String, Vec<String>) {
170        let mut t = SeaTable::create();
171        t.table(sea_query::Alias::new(&self.name)).if_not_exists();
172        for col in &self.columns {
173            t.col(&mut col.sea_def.clone());
174        }
175        let create_sql = t.build(PostgresQueryBuilder);
176        let mut post = self.indexes;
177        post.extend(self.foreign_keys);
178        (create_sql, post)
179    }
180}
181
182pub struct ColumnDef {
183    sea_def: SeaColumnDef,
184    pub name: String,
185}
186
187impl ColumnDef {
188    pub fn not_null(&mut self) -> &mut Self {
189        self.sea_def.not_null();
190        self
191    }
192
193    pub fn nullable(&mut self) -> &mut Self {
194        self.sea_def.null();
195        self
196    }
197
198    pub fn unique(&mut self) -> &mut Self {
199        self.sea_def.unique_key();
200        self
201    }
202
203    pub fn primary_key(&mut self) -> &mut Self {
204        self.sea_def.primary_key();
205        self
206    }
207
208    pub fn default(&mut self, value: impl Into<String>) -> &mut Self {
209        self.sea_def.default(sea_query::Expr::cust(value.into()));
210        self
211    }
212
213    pub fn default_value<T>(&mut self, value: T) -> &mut Self
214    where
215        T: Into<sea_query::Value>,
216    {
217        self.sea_def.default(value);
218        self
219    }
220}