doido_model/migration/
table.rs1use sea_orm::sea_query::{
6 Alias, ColumnDef, Expr, Table as SqTable, TableAlterStatement, TableCreateStatement,
7 TableDropStatement, TableRenameStatement,
8};
9use sea_orm::{ConnectionTrait, DbErr};
10use sea_orm_migration::SchemaManager;
11
12pub struct TableBuilder {
17 columns: Vec<ColumnDef>,
18}
19
20impl TableBuilder {
21 fn new() -> Self {
22 let mut id = ColumnDef::new(Alias::new("id"));
23 id.big_integer().not_null().auto_increment().primary_key();
24 TableBuilder { columns: vec![id] }
25 }
26
27 pub fn column(&mut self, col: ColumnDef) -> &mut ColumnDef {
30 self.columns.push(col);
31 self.columns.last_mut().expect("just pushed a column")
32 }
33
34 fn typed(&mut self, name: &str, apply: impl FnOnce(&mut ColumnDef)) -> &mut ColumnDef {
35 let mut col = ColumnDef::new(Alias::new(name));
36 apply(&mut col);
37 self.column(col)
38 }
39
40 pub fn string(&mut self, name: &str) -> &mut ColumnDef {
41 self.typed(name, |c| {
42 c.string();
43 })
44 }
45 pub fn text(&mut self, name: &str) -> &mut ColumnDef {
46 self.typed(name, |c| {
47 c.text();
48 })
49 }
50 pub fn integer(&mut self, name: &str) -> &mut ColumnDef {
51 self.typed(name, |c| {
52 c.integer();
53 })
54 }
55 pub fn big_integer(&mut self, name: &str) -> &mut ColumnDef {
56 self.typed(name, |c| {
57 c.big_integer();
58 })
59 }
60 pub fn float(&mut self, name: &str) -> &mut ColumnDef {
61 self.typed(name, |c| {
62 c.float();
63 })
64 }
65 pub fn double(&mut self, name: &str) -> &mut ColumnDef {
66 self.typed(name, |c| {
67 c.double();
68 })
69 }
70 pub fn decimal(&mut self, name: &str) -> &mut ColumnDef {
71 self.typed(name, |c| {
72 c.decimal();
73 })
74 }
75 pub fn boolean(&mut self, name: &str) -> &mut ColumnDef {
76 self.typed(name, |c| {
77 c.boolean();
78 })
79 }
80 pub fn timestamp(&mut self, name: &str) -> &mut ColumnDef {
81 self.typed(name, |c| {
82 c.timestamp();
83 })
84 }
85 pub fn date(&mut self, name: &str) -> &mut ColumnDef {
86 self.typed(name, |c| {
87 c.date();
88 })
89 }
90 pub fn json(&mut self, name: &str) -> &mut ColumnDef {
91 self.typed(name, |c| {
92 c.json();
93 })
94 }
95 pub fn uuid(&mut self, name: &str) -> &mut ColumnDef {
96 self.typed(name, |c| {
97 c.uuid();
98 })
99 }
100 pub fn binary(&mut self, name: &str) -> &mut ColumnDef {
101 self.typed(name, |c| {
102 c.binary();
103 })
104 }
105
106 pub fn references(&mut self, name: &str) -> &mut ColumnDef {
108 self.typed(&format!("{name}_id"), |c| {
109 c.big_integer().not_null();
110 })
111 }
112
113 pub fn timestamps(&mut self) {
116 self.typed("created_at", |c| {
117 c.timestamp().not_null().default(Expr::current_timestamp());
118 });
119 self.typed("updated_at", |c| {
120 c.timestamp().not_null().default(Expr::current_timestamp());
121 });
122 }
123}
124
125enum AlterOp {
127 Add(Box<ColumnDef>),
129 Drop(String),
130 Rename(String, String),
131}
132
133pub struct AlterTableBuilder {
137 ops: Vec<AlterOp>,
138}
139
140impl AlterTableBuilder {
141 fn new() -> Self {
142 AlterTableBuilder { ops: Vec::new() }
143 }
144
145 pub fn add_column(&mut self, name: &str, f: impl FnOnce(&mut ColumnDef)) -> &mut ColumnDef {
148 let mut col = ColumnDef::new(Alias::new(name));
149 f(&mut col);
150 self.ops.push(AlterOp::Add(Box::new(col)));
151 match self.ops.last_mut().expect("just pushed an add op") {
152 AlterOp::Add(col) => col.as_mut(),
153 _ => unreachable!("last op is the add we just pushed"),
154 }
155 }
156
157 pub fn drop_column(&mut self, name: &str) {
159 self.ops.push(AlterOp::Drop(name.to_string()));
160 }
161
162 pub fn rename_column(&mut self, from: &str, to: &str) {
164 self.ops
165 .push(AlterOp::Rename(from.to_string(), to.to_string()));
166 }
167}
168
169fn create_table_statement(name: &str, f: impl FnOnce(&mut TableBuilder)) -> TableCreateStatement {
170 let mut builder = TableBuilder::new();
171 f(&mut builder);
172 let mut stmt = SqTable::create();
173 stmt.table(Alias::new(name)).if_not_exists();
174 for col in builder.columns {
175 stmt.col(col);
176 }
177 stmt
178}
179
180fn drop_table_statement(name: &str) -> TableDropStatement {
181 let mut stmt = SqTable::drop();
182 stmt.table(Alias::new(name)).if_exists();
183 stmt
184}
185
186fn rename_table_statement(from: &str, to: &str) -> TableRenameStatement {
187 let mut stmt = SqTable::rename();
188 stmt.table(Alias::new(from), Alias::new(to));
189 stmt
190}
191
192fn alter_table_statements(name: &str, ops: Vec<AlterOp>) -> Vec<TableAlterStatement> {
193 ops.into_iter()
194 .map(|op| {
195 let mut stmt = SqTable::alter();
196 stmt.table(Alias::new(name));
197 match op {
198 AlterOp::Add(col) => {
199 stmt.add_column(*col);
200 }
201 AlterOp::Drop(name) => {
202 stmt.drop_column(Alias::new(name));
203 }
204 AlterOp::Rename(from, to) => {
205 stmt.rename_column(Alias::new(from), Alias::new(to));
206 }
207 }
208 stmt
209 })
210 .collect()
211}
212
213pub async fn create_table(
216 manager: &SchemaManager<'_>,
217 name: &str,
218 f: impl FnOnce(&mut TableBuilder),
219) -> Result<(), DbErr> {
220 manager
221 .get_connection()
222 .execute(&create_table_statement(name, f))
223 .await
224 .map(|_| ())
225}
226
227pub async fn drop_table(manager: &SchemaManager<'_>, name: &str) -> Result<(), DbErr> {
229 manager
230 .get_connection()
231 .execute(&drop_table_statement(name))
232 .await
233 .map(|_| ())
234}
235
236pub async fn rename_table(manager: &SchemaManager<'_>, from: &str, to: &str) -> Result<(), DbErr> {
238 manager
239 .get_connection()
240 .execute(&rename_table_statement(from, to))
241 .await
242 .map(|_| ())
243}
244
245pub async fn alter_table(
248 manager: &SchemaManager<'_>,
249 name: &str,
250 f: impl FnOnce(&mut AlterTableBuilder),
251) -> Result<(), DbErr> {
252 let mut builder = AlterTableBuilder::new();
253 f(&mut builder);
254 let conn = manager.get_connection();
255 for stmt in alter_table_statements(name, builder.ops) {
256 conn.execute(&stmt).await.map(|_| ())?;
257 }
258 Ok(())
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use sea_orm::sea_query::PostgresQueryBuilder;
265
266 #[test]
267 fn create_table_adds_implicit_id_and_columns() {
268 let sql = create_table_statement("users", |t| {
269 t.string("email").not_null();
270 t.timestamps();
271 })
272 .to_string(PostgresQueryBuilder);
273 assert!(sql.contains("CREATE TABLE IF NOT EXISTS \"users\""));
274 assert!(sql.contains("\"id\""));
275 assert!(sql.contains("\"email\""));
276 assert!(sql.contains("\"created_at\""));
277 assert!(sql.contains("\"updated_at\""));
278 }
279
280 #[test]
281 fn references_adds_id_suffixed_column() {
282 let sql = create_table_statement("comments", |t| {
283 t.references("post");
284 })
285 .to_string(PostgresQueryBuilder);
286 assert!(sql.contains("\"post_id\""));
287 }
288
289 #[test]
290 fn drop_table_is_conditional() {
291 let sql = drop_table_statement("users").to_string(PostgresQueryBuilder);
292 assert!(sql.contains("DROP TABLE IF EXISTS \"users\""));
293 }
294
295 #[test]
296 fn alter_table_emits_one_statement_per_change() {
297 let mut builder = AlterTableBuilder::new();
298 builder.add_column("age", |c| {
299 c.integer();
300 });
301 builder.drop_column("legacy");
302 builder.rename_column("sku", "code");
303
304 let stmts = alter_table_statements("items", builder.ops);
305 assert_eq!(stmts.len(), 3);
306 let add = stmts[0].to_string(PostgresQueryBuilder);
307 assert!(add.contains("ALTER TABLE \"items\""));
308 assert!(add.contains("\"age\""));
309 assert!(stmts[1]
310 .to_string(PostgresQueryBuilder)
311 .contains("\"legacy\""));
312 let rename = stmts[2].to_string(PostgresQueryBuilder);
313 assert!(rename.contains("\"sku\""));
314 assert!(rename.contains("\"code\""));
315 }
316}