1use crate::{schema, ColumnType, Dialect, Entity};
40use sea_query::{Alias, ColumnDef, Index, Table, Value};
41
42pub struct Column {
49 name: String,
50 ty: ColumnType,
51 nullable: bool,
52 default: Option<Value>,
53 keyed: bool,
54}
55
56impl Column {
57 pub fn new(name: impl Into<String>, ty: ColumnType) -> Self {
59 Self { name: name.into(), ty, nullable: true, default: None, keyed: false }
60 }
61
62 pub fn not_null(mut self) -> Self {
65 self.nullable = false;
66 self
67 }
68
69 pub fn null(mut self) -> Self {
71 self.nullable = true;
72 self
73 }
74
75 pub fn default(mut self, value: impl Into<Value>) -> Self {
77 self.default = Some(value.into());
78 self
79 }
80
81 pub fn keyed(mut self) -> Self {
85 self.keyed = true;
86 self
87 }
88
89 fn to_def(&self) -> ColumnDef {
90 let mut def = ColumnDef::new(Alias::new(self.name.clone()));
91 schema::apply_type(&mut def, self.ty, self.keyed);
92 if self.nullable {
93 def.null();
94 } else {
95 def.not_null();
96 }
97 if let Some(v) = &self.default {
98 def.default(v.clone());
99 }
100 def
101 }
102}
103
104pub struct Migration {
112 name: &'static str,
113 ops: Vec<Box<dyn Fn(Dialect) -> Vec<String>>>,
118}
119
120impl Migration {
121 pub fn new(name: &'static str) -> Self {
123 Self { name, ops: Vec::new() }
124 }
125
126 pub fn name(&self) -> &'static str {
128 self.name
129 }
130
131 fn push_stmt<S>(mut self, stmt: S) -> Self
132 where
133 S: sea_query::SchemaStatementBuilder + 'static,
134 {
135 self.ops.push(Box::new(move |d: Dialect| vec![d.build_schema(&stmt)]));
136 self
137 }
138
139 pub fn create_table<E: Entity + 'static>(mut self) -> Self {
145 self.ops.push(Box::new(|d| schema::schema_ddl::<E>(d)));
146 self
147 }
148
149 pub fn statement<S>(self, stmt: S) -> Self
156 where
157 S: sea_query::SchemaStatementBuilder + 'static,
158 {
159 self.push_stmt(stmt)
160 }
161
162 pub fn add_column(self, table: &str, column: Column) -> Self {
164 let mut def = column.to_def();
165 let mut stmt = Table::alter();
166 stmt.table(Alias::new(table)).add_column(&mut def);
167 self.push_stmt(stmt)
168 }
169
170 pub fn drop_column(self, table: &str, column: &str) -> Self {
172 let mut stmt = Table::alter();
173 stmt.table(Alias::new(table)).drop_column(Alias::new(column));
174 self.push_stmt(stmt)
175 }
176
177 pub fn rename_column(self, table: &str, from: &str, to: &str) -> Self {
179 let mut stmt = Table::alter();
180 stmt.table(Alias::new(table)).rename_column(Alias::new(from), Alias::new(to));
181 self.push_stmt(stmt)
182 }
183
184 pub fn rename_table(self, from: &str, to: &str) -> Self {
186 let mut stmt = Table::rename();
187 stmt.table(Alias::new(from), Alias::new(to));
188 self.push_stmt(stmt)
189 }
190
191 pub fn drop_table(self, table: &str) -> Self {
193 let mut stmt = Table::drop();
194 stmt.table(Alias::new(table)).if_exists();
195 self.push_stmt(stmt)
196 }
197
198 pub fn create_index<'c, I>(self, name: &str, table: &str, columns: I) -> Self
200 where
201 I: IntoIterator<Item = &'c str>,
202 {
203 self.index(name, table, columns, false)
204 }
205
206 pub fn create_unique_index<'c, I>(self, name: &str, table: &str, columns: I) -> Self
208 where
209 I: IntoIterator<Item = &'c str>,
210 {
211 self.index(name, table, columns, true)
212 }
213
214 fn index<'c, I>(self, name: &str, table: &str, columns: I, unique: bool) -> Self
215 where
216 I: IntoIterator<Item = &'c str>,
217 {
218 let mut stmt = Index::create();
219 stmt.name(name).table(Alias::new(table)).if_not_exists();
220 if unique {
221 stmt.unique();
222 }
223 for c in columns {
224 stmt.col(Alias::new(c));
225 }
226 self.push_stmt(stmt)
227 }
228
229 pub fn drop_index(self, name: &str, table: &str) -> Self {
231 let mut stmt = Index::drop();
232 stmt.name(name).table(Alias::new(table));
233 self.push_stmt(stmt)
234 }
235
236 pub fn raw(mut self, dialect: Dialect, sql: impl Into<String>) -> Self {
241 let sql = sql.into();
242 self.ops.push(Box::new(move |d| if d == dialect { vec![sql.clone()] } else { Vec::new() }));
243 self
244 }
245
246 pub fn raw_all(mut self, sql: impl Into<String>) -> Self {
250 let sql = sql.into();
251 self.ops.push(Box::new(move |_| vec![sql.clone()]));
252 self
253 }
254
255 pub fn render(&self, dialect: Dialect) -> Vec<String> {
258 self.ops.iter().flat_map(|op| op(dialect)).collect()
259 }
260}
261
262impl<X: crate::Executor> crate::migrate::Migration<X> for Migration {
263 fn name(&self) -> &'static str {
264 self.name
265 }
266
267 fn up<'a>(&'a self, db: &'a X) -> crate::migrate::StepFuture<'a> {
268 Box::pin(async move {
269 for sql in self.render(db.dialect()) {
270 db.execute(&sql, Vec::new()).await?;
271 }
272 Ok(())
273 })
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn add_column_renders_per_dialect() {
283 let m = Migration::new("0002_add_phone")
284 .add_column("users", Column::new("phone", ColumnType::Text).keyed());
285 let sqlite = m.render(Dialect::Sqlite).join("\n");
286 let mysql = m.render(Dialect::MySql).join("\n");
287 assert!(sqlite.contains("ALTER TABLE"), "{sqlite}");
288 assert!(sqlite.contains("phone"));
289 assert!(mysql.contains("varchar") || mysql.contains("VARCHAR"), "{mysql}");
291 assert!(mysql.contains('`'), "mysql quoting");
292 assert!(sqlite.contains('"'), "sqlite quoting");
293 }
294
295 #[test]
296 fn not_null_with_default_backfills() {
297 let m = Migration::new("0003_add_flag").add_column(
298 "users",
299 Column::new("verified", ColumnType::Bool).not_null().default(false),
300 );
301 let sql = m.render(Dialect::Sqlite).join("\n");
302 assert!(sql.to_uppercase().contains("NOT NULL"), "{sql}");
303 assert!(sql.to_uppercase().contains("DEFAULT"), "{sql}");
304 }
305
306 #[test]
307 fn index_ops_render() {
308 let create = Migration::new("a")
309 .create_index("idx_users_phone", "users", ["phone"])
310 .render(Dialect::Sqlite)
311 .join("\n");
312 assert!(create.contains("CREATE INDEX"), "{create}");
313 assert!(create.contains("idx_users_phone"));
314
315 let uniq = Migration::new("b")
316 .create_unique_index("uq_users_phone", "users", ["phone", "region"])
317 .render(Dialect::MySql)
318 .join("\n");
319 assert!(uniq.to_uppercase().contains("UNIQUE"), "{uniq}");
320
321 let drop = Migration::new("c")
322 .drop_index("idx_users_phone", "users")
323 .render(Dialect::Sqlite)
324 .join("\n");
325 assert!(drop.to_uppercase().contains("DROP INDEX"), "{drop}");
326 }
327
328 #[test]
329 fn rename_and_drop_table() {
330 let rename = Migration::new("a")
331 .rename_table("old_users", "users")
332 .render(Dialect::MySql)
333 .join("\n");
334 assert!(rename.to_uppercase().contains("RENAME"), "{rename}");
335
336 let drop = Migration::new("b").drop_table("dead").render(Dialect::Sqlite).join("\n");
337 assert!(drop.to_uppercase().contains("DROP TABLE"), "{drop}");
338 assert!(drop.to_uppercase().contains("IF EXISTS"), "{drop}");
339 }
340
341 #[test]
342 fn statement_bridges_sea_query() {
343 use sea_query::{Alias, ColumnDef, Table};
344 let mut create = Table::create();
345 create
346 .table(Alias::new("widgets"))
347 .if_not_exists()
348 .col(ColumnDef::new(Alias::new("id")).big_integer().primary_key());
349 let m = Migration::new("0001").statement(create);
350 let sqlite = m.render(Dialect::Sqlite).join("\n");
351 assert!(sqlite.to_uppercase().contains("CREATE TABLE"), "{sqlite}");
352 assert!(sqlite.contains("widgets"));
353 assert!(m.render(Dialect::MySql).join("\n").contains('`'));
355 }
356
357 #[test]
358 fn raw_is_dialect_scoped() {
359 let m = Migration::new("a")
360 .raw(Dialect::MySql, "ALTER TABLE t MODIFY c TEXT")
361 .raw(Dialect::Sqlite, "-- noop on sqlite");
362 assert_eq!(m.render(Dialect::MySql), vec!["ALTER TABLE t MODIFY c TEXT"]);
363 assert_eq!(m.render(Dialect::Sqlite), vec!["-- noop on sqlite"]);
364 assert!(m.render(Dialect::Postgres).is_empty());
365 }
366
367 #[test]
368 fn ops_apply_in_call_order() {
369 let m = Migration::new("ordered")
370 .add_column("t", Column::new("a", ColumnType::Int))
371 .add_column("t", Column::new("b", ColumnType::Int));
372 let rendered = m.render(Dialect::Sqlite);
373 assert_eq!(rendered.len(), 2);
374 assert!(rendered[0].contains("\"a\""), "{:?}", rendered);
375 assert!(rendered[1].contains("\"b\""), "{:?}", rendered);
376 }
377}