1use async_trait::async_trait;
17use sea_query::{
18 ColumnDef, Expr, Iden, Index, MysqlQueryBuilder, PostgresQueryBuilder, Query,
19 QueryStatementWriter, SchemaStatementBuilder, SqliteQueryBuilder, Table,
20};
21use sqlx::{AnyConnection, AnyPool};
22
23use crate::error::{CoreError, CoreResult};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DbBackend {
29 Postgres,
30 Mysql,
31 Sqlite,
32}
33
34impl DbBackend {
35 pub fn from_url(url: &str) -> CoreResult<Self> {
38 if url.starts_with("postgres") {
39 Ok(Self::Postgres)
40 } else if url.starts_with("mysql") {
41 Ok(Self::Mysql)
42 } else if url.starts_with("sqlite") {
43 Ok(Self::Sqlite)
44 } else {
45 Err(CoreError::Config(format!(
46 "unrecognised database URL scheme: {url}"
47 )))
48 }
49 }
50}
51
52pub fn bool_col<T: sea_query::IntoIden>(name: T) -> ColumnDef {
58 ColumnDef::new(name).integer().to_owned()
59}
60
61pub const KEY_LEN: u32 = 255;
64
65pub fn key_col<T: sea_query::IntoIden>(name: T) -> ColumnDef {
73 ColumnDef::new(name).string_len(KEY_LEN).to_owned()
74}
75
76fn schema_sql<S: SchemaStatementBuilder>(backend: DbBackend, stmt: &S) -> String {
77 match backend {
78 DbBackend::Postgres => stmt.build(PostgresQueryBuilder),
79 DbBackend::Mysql => stmt.build(MysqlQueryBuilder),
80 DbBackend::Sqlite => stmt.build(SqliteQueryBuilder),
81 }
82}
83
84fn query_sql<Q: QueryStatementWriter>(backend: DbBackend, stmt: &Q) -> String {
85 match backend {
86 DbBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
87 DbBackend::Mysql => stmt.to_string(MysqlQueryBuilder),
88 DbBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
89 }
90}
91
92pub struct Schema<'c> {
95 conn: &'c mut AnyConnection,
96 backend: DbBackend,
97}
98
99impl Schema<'_> {
100 pub fn backend(&self) -> DbBackend {
101 self.backend
102 }
103
104 pub async fn exec<S: SchemaStatementBuilder>(&mut self, stmt: S) -> CoreResult<()> {
109 let sql = schema_sql(self.backend, &stmt);
110 drop(stmt);
111 sqlx::query(&sql).execute(&mut *self.conn).await?;
112 Ok(())
113 }
114
115 pub async fn raw(&mut self, sql: &str) -> CoreResult<()> {
118 sqlx::query(sql).execute(&mut *self.conn).await?;
119 Ok(())
120 }
121}
122
123#[async_trait(?Send)]
125pub trait Migration: Send + Sync {
126 fn name(&self) -> &str;
129
130 async fn up(&self, schema: &mut Schema<'_>) -> CoreResult<()>;
132
133 async fn down(&self, _schema: &mut Schema<'_>) -> CoreResult<()> {
136 Err(CoreError::Irreversible {
137 module: String::new(),
138 name: self.name().to_string(),
139 })
140 }
141}
142
143pub struct SqlMigration {
146 name: String,
147 up: String,
148 down: Option<String>,
149}
150
151impl SqlMigration {
152 pub fn new(name: impl Into<String>, up: impl Into<String>) -> Self {
153 Self {
154 name: name.into(),
155 up: up.into(),
156 down: None,
157 }
158 }
159
160 pub fn reversible(mut self, down: impl Into<String>) -> Self {
161 self.down = Some(down.into());
162 self
163 }
164}
165
166#[async_trait(?Send)]
167impl Migration for SqlMigration {
168 fn name(&self) -> &str {
169 &self.name
170 }
171
172 async fn up(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
173 schema.raw(&self.up).await
174 }
175
176 async fn down(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
177 match &self.down {
178 Some(sql) => schema.raw(sql).await,
179 None => Err(CoreError::Irreversible {
180 module: String::new(),
181 name: self.name.clone(),
182 }),
183 }
184 }
185}
186
187pub struct MigrationSet {
189 pub module_id: &'static str,
190 pub migrations: Vec<Box<dyn Migration>>,
191}
192
193impl MigrationSet {
194 pub fn new(module_id: &'static str, migrations: Vec<Box<dyn Migration>>) -> Self {
195 Self {
196 module_id,
197 migrations,
198 }
199 }
200}
201
202#[derive(Iden)]
203enum LateriteMigrations {
204 Table,
205 ModuleId,
206 Name,
207}
208
209async fn ensure_tracking_table(pool: &AnyPool, backend: DbBackend) -> CoreResult<()> {
210 let stmt = Table::create()
211 .table(LateriteMigrations::Table)
212 .if_not_exists()
213 .col(
214 ColumnDef::new(LateriteMigrations::ModuleId)
215 .string_len(255)
216 .not_null(),
217 )
218 .col(
219 ColumnDef::new(LateriteMigrations::Name)
220 .string_len(255)
221 .not_null(),
222 )
223 .primary_key(
224 Index::create()
225 .col(LateriteMigrations::ModuleId)
226 .col(LateriteMigrations::Name),
227 )
228 .to_owned();
229 sqlx::query(&schema_sql(backend, &stmt))
230 .execute(pool)
231 .await?;
232 Ok(())
233}
234
235async fn is_applied(
236 pool: &AnyPool,
237 backend: DbBackend,
238 module_id: &str,
239 name: &str,
240) -> CoreResult<bool> {
241 let stmt = Query::select()
242 .column(LateriteMigrations::Name)
243 .from(LateriteMigrations::Table)
244 .and_where(Expr::col(LateriteMigrations::ModuleId).eq(module_id))
245 .and_where(Expr::col(LateriteMigrations::Name).eq(name))
246 .limit(1)
247 .to_owned();
248 let found: Option<String> = sqlx::query_scalar(&query_sql(backend, &stmt))
249 .fetch_optional(pool)
250 .await?;
251 Ok(found.is_some())
252}
253
254pub async fn applied(
256 pool: &AnyPool,
257 backend: DbBackend,
258 module_id: &str,
259) -> CoreResult<Vec<String>> {
260 ensure_tracking_table(pool, backend).await?;
261 let stmt = Query::select()
262 .column(LateriteMigrations::Name)
263 .from(LateriteMigrations::Table)
264 .and_where(Expr::col(LateriteMigrations::ModuleId).eq(module_id))
265 .order_by(LateriteMigrations::Name, sea_query::Order::Asc)
266 .to_owned();
267 let names: Vec<String> = sqlx::query_scalar(&query_sql(backend, &stmt))
268 .fetch_all(pool)
269 .await?;
270 Ok(names)
271}
272
273pub async fn run(pool: &AnyPool, backend: DbBackend, sets: &[MigrationSet]) -> CoreResult<()> {
276 ensure_tracking_table(pool, backend).await?;
277 for set in sets {
278 for migration in &set.migrations {
279 if is_applied(pool, backend, set.module_id, migration.name()).await? {
280 continue;
281 }
282 let mut tx = pool.begin().await?;
283 {
284 let mut schema = Schema {
285 conn: &mut tx,
286 backend,
287 };
288 migration.up(&mut schema).await?;
289 }
290 let insert = Query::insert()
291 .into_table(LateriteMigrations::Table)
292 .columns([LateriteMigrations::ModuleId, LateriteMigrations::Name])
293 .values_panic([set.module_id.into(), migration.name().into()])
294 .to_owned();
295 sqlx::query(&query_sql(backend, &insert))
296 .execute(&mut *tx)
297 .await?;
298 tx.commit().await?;
299 }
300 }
301 Ok(())
302}
303
304pub async fn rollback(
307 pool: &AnyPool,
308 backend: DbBackend,
309 set: &MigrationSet,
310 steps: usize,
311) -> CoreResult<()> {
312 ensure_tracking_table(pool, backend).await?;
313 let mut done = 0;
314 for migration in set.migrations.iter().rev() {
315 if done >= steps {
316 break;
317 }
318 if !is_applied(pool, backend, set.module_id, migration.name()).await? {
319 continue;
320 }
321 let mut tx = pool.begin().await?;
322 {
323 let mut schema = Schema {
324 conn: &mut tx,
325 backend,
326 };
327 migration.down(&mut schema).await.map_err(|e| match e {
328 CoreError::Irreversible { name, .. } => CoreError::Irreversible {
329 module: set.module_id.to_string(),
330 name,
331 },
332 other => other,
333 })?;
334 }
335 let delete = Query::delete()
336 .from_table(LateriteMigrations::Table)
337 .and_where(Expr::col(LateriteMigrations::ModuleId).eq(set.module_id))
338 .and_where(Expr::col(LateriteMigrations::Name).eq(migration.name()))
339 .to_owned();
340 sqlx::query(&query_sql(backend, &delete))
341 .execute(&mut *tx)
342 .await?;
343 tx.commit().await?;
344 done += 1;
345 }
346 Ok(())
347}
348
349pub async fn reset(pool: &AnyPool, backend: DbBackend, set: &MigrationSet) -> CoreResult<()> {
351 rollback(pool, backend, set, set.migrations.len()).await
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[derive(Iden)]
359 enum Demo {
360 Table,
361 Id,
362 }
363
364 struct CreateDemo;
365
366 #[async_trait(?Send)]
367 impl Migration for CreateDemo {
368 fn name(&self) -> &str {
369 "0001_create_demo"
370 }
371 async fn up(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
372 schema
373 .exec(
374 Table::create()
375 .table(Demo::Table)
376 .if_not_exists()
377 .col(ColumnDef::new(Demo::Id).integer().not_null())
378 .to_owned(),
379 )
380 .await
381 }
382 async fn down(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
383 schema
384 .exec(Table::drop().table(Demo::Table).to_owned())
385 .await
386 }
387 }
388
389 async fn sqlite_pool() -> AnyPool {
390 sqlx::any::install_default_drivers();
391 sqlx::any::AnyPoolOptions::new()
392 .max_connections(1)
393 .connect("sqlite::memory:")
394 .await
395 .unwrap()
396 }
397
398 #[tokio::test]
399 async fn applies_and_rolls_back_on_sqlite() {
400 let pool = sqlite_pool().await;
401 let backend = DbBackend::Sqlite;
402 let set = MigrationSet::new("test.demo", vec![Box::new(CreateDemo)]);
403
404 run(&pool, backend, std::slice::from_ref(&set))
405 .await
406 .unwrap();
407 run(&pool, backend, std::slice::from_ref(&set))
409 .await
410 .unwrap();
411 sqlx::query("insert into demo (id) values (1)")
412 .execute(&pool)
413 .await
414 .unwrap();
415 assert_eq!(applied(&pool, backend, "test.demo").await.unwrap().len(), 1);
416
417 reset(&pool, backend, &set).await.unwrap();
418 assert!(sqlx::query("select count(*) from demo")
420 .fetch_one(&pool)
421 .await
422 .is_err());
423 assert!(applied(&pool, backend, "test.demo")
424 .await
425 .unwrap()
426 .is_empty());
427 }
428
429 #[tokio::test]
430 async fn irreversible_migration_reports_module_and_name() {
431 let pool = sqlite_pool().await;
432 let backend = DbBackend::Sqlite;
433 let set = MigrationSet::new(
434 "test.oneway",
435 vec![Box::new(SqlMigration::new(
436 "0001_make_t",
437 "create table t (id integer not null)",
438 ))],
439 );
440 run(&pool, backend, std::slice::from_ref(&set))
441 .await
442 .unwrap();
443 let err = rollback(&pool, backend, &set, 1).await.unwrap_err();
444 match err {
445 CoreError::Irreversible { module, name } => {
446 assert_eq!(module, "test.oneway");
447 assert_eq!(name, "0001_make_t");
448 }
449 other => panic!("expected Irreversible, got {other:?}"),
450 }
451 }
452}