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 pub fn name(&self) -> &'static str {
53 match self {
54 Self::Postgres => "postgres",
55 Self::Mysql => "mysql",
56 Self::Sqlite => "sqlite",
57 }
58 }
59}
60
61pub fn bool_col<T: sea_query::IntoIden>(name: T) -> ColumnDef {
67 ColumnDef::new(name).integer().to_owned()
68}
69
70pub const KEY_LEN: u32 = 255;
73
74pub fn key_col<T: sea_query::IntoIden>(name: T) -> ColumnDef {
82 ColumnDef::new(name).string_len(KEY_LEN).to_owned()
83}
84
85fn schema_sql<S: SchemaStatementBuilder>(backend: DbBackend, stmt: &S) -> String {
86 match backend {
87 DbBackend::Postgres => stmt.build(PostgresQueryBuilder),
88 DbBackend::Mysql => stmt.build(MysqlQueryBuilder),
89 DbBackend::Sqlite => stmt.build(SqliteQueryBuilder),
90 }
91}
92
93fn query_sql<Q: QueryStatementWriter>(backend: DbBackend, stmt: &Q) -> String {
94 match backend {
95 DbBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
96 DbBackend::Mysql => stmt.to_string(MysqlQueryBuilder),
97 DbBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
98 }
99}
100
101pub struct Schema<'c> {
104 conn: &'c mut AnyConnection,
105 backend: DbBackend,
106}
107
108impl Schema<'_> {
109 pub fn backend(&self) -> DbBackend {
110 self.backend
111 }
112
113 pub async fn exec<S: SchemaStatementBuilder>(&mut self, stmt: S) -> CoreResult<()> {
118 let sql = schema_sql(self.backend, &stmt);
119 drop(stmt);
120 sqlx::query(&sql).execute(&mut *self.conn).await?;
121 Ok(())
122 }
123
124 pub async fn raw(&mut self, sql: &str) -> CoreResult<()> {
127 sqlx::query(sql).execute(&mut *self.conn).await?;
128 Ok(())
129 }
130}
131
132#[async_trait(?Send)]
134pub trait Migration: Send + Sync {
135 fn name(&self) -> &str;
138
139 async fn up(&self, schema: &mut Schema<'_>) -> CoreResult<()>;
141
142 async fn down(&self, _schema: &mut Schema<'_>) -> CoreResult<()> {
145 Err(CoreError::Irreversible {
146 module: String::new(),
147 name: self.name().to_string(),
148 })
149 }
150}
151
152pub struct SqlMigration {
155 name: String,
156 up: String,
157 down: Option<String>,
158}
159
160impl SqlMigration {
161 pub fn new(name: impl Into<String>, up: impl Into<String>) -> Self {
162 Self {
163 name: name.into(),
164 up: up.into(),
165 down: None,
166 }
167 }
168
169 pub fn reversible(mut self, down: impl Into<String>) -> Self {
170 self.down = Some(down.into());
171 self
172 }
173}
174
175#[async_trait(?Send)]
176impl Migration for SqlMigration {
177 fn name(&self) -> &str {
178 &self.name
179 }
180
181 async fn up(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
182 schema.raw(&self.up).await
183 }
184
185 async fn down(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
186 match &self.down {
187 Some(sql) => schema.raw(sql).await,
188 None => Err(CoreError::Irreversible {
189 module: String::new(),
190 name: self.name.clone(),
191 }),
192 }
193 }
194}
195
196pub struct MigrationSet {
198 pub module_id: &'static str,
199 pub migrations: Vec<Box<dyn Migration>>,
200}
201
202impl MigrationSet {
203 pub fn new(module_id: &'static str, migrations: Vec<Box<dyn Migration>>) -> Self {
204 Self {
205 module_id,
206 migrations,
207 }
208 }
209}
210
211#[macro_export]
238macro_rules! migration_set {
239 (module_id: $id:literal, $($m:ident,)*) => {
240 $( mod $m; )*
241
242 pub const MODULE_ID: &str = $id;
245
246 pub fn migrations() -> $crate::MigrationSet {
248 $crate::MigrationSet::new(
249 MODULE_ID,
250 ::std::vec![ $( ::std::boxed::Box::new($m::Migration), )* ],
251 )
252 }
253 };
254}
255
256#[derive(Iden)]
257enum LateriteMigrations {
258 Table,
259 ModuleId,
260 Name,
261}
262
263async fn ensure_tracking_table(pool: &AnyPool, backend: DbBackend) -> CoreResult<()> {
264 let stmt = Table::create()
265 .table(LateriteMigrations::Table)
266 .if_not_exists()
267 .col(
268 ColumnDef::new(LateriteMigrations::ModuleId)
269 .string_len(255)
270 .not_null(),
271 )
272 .col(
273 ColumnDef::new(LateriteMigrations::Name)
274 .string_len(255)
275 .not_null(),
276 )
277 .primary_key(
278 Index::create()
279 .col(LateriteMigrations::ModuleId)
280 .col(LateriteMigrations::Name),
281 )
282 .to_owned();
283 sqlx::query(&schema_sql(backend, &stmt))
284 .execute(pool)
285 .await?;
286 Ok(())
287}
288
289async fn is_applied(
290 pool: &AnyPool,
291 backend: DbBackend,
292 module_id: &str,
293 name: &str,
294) -> CoreResult<bool> {
295 let stmt = Query::select()
296 .column(LateriteMigrations::Name)
297 .from(LateriteMigrations::Table)
298 .and_where(Expr::col(LateriteMigrations::ModuleId).eq(module_id))
299 .and_where(Expr::col(LateriteMigrations::Name).eq(name))
300 .limit(1)
301 .to_owned();
302 let found: Option<String> = sqlx::query_scalar(&query_sql(backend, &stmt))
303 .fetch_optional(pool)
304 .await?;
305 Ok(found.is_some())
306}
307
308pub async fn applied(
310 pool: &AnyPool,
311 backend: DbBackend,
312 module_id: &str,
313) -> CoreResult<Vec<String>> {
314 ensure_tracking_table(pool, backend).await?;
315 let stmt = Query::select()
316 .column(LateriteMigrations::Name)
317 .from(LateriteMigrations::Table)
318 .and_where(Expr::col(LateriteMigrations::ModuleId).eq(module_id))
319 .order_by(LateriteMigrations::Name, sea_query::Order::Asc)
320 .to_owned();
321 let names: Vec<String> = sqlx::query_scalar(&query_sql(backend, &stmt))
322 .fetch_all(pool)
323 .await?;
324 Ok(names)
325}
326
327pub async fn run(pool: &AnyPool, backend: DbBackend, sets: &[MigrationSet]) -> CoreResult<()> {
330 ensure_tracking_table(pool, backend).await?;
331 for set in sets {
332 for migration in &set.migrations {
333 if is_applied(pool, backend, set.module_id, migration.name()).await? {
334 continue;
335 }
336 let mut tx = pool.begin().await?;
337 {
338 let mut schema = Schema {
339 conn: &mut tx,
340 backend,
341 };
342 migration.up(&mut schema).await?;
343 }
344 let insert = Query::insert()
345 .into_table(LateriteMigrations::Table)
346 .columns([LateriteMigrations::ModuleId, LateriteMigrations::Name])
347 .values_panic([set.module_id.into(), migration.name().into()])
348 .to_owned();
349 sqlx::query(&query_sql(backend, &insert))
350 .execute(&mut *tx)
351 .await?;
352 tx.commit().await?;
353 }
354 }
355 Ok(())
356}
357
358pub async fn rollback(
361 pool: &AnyPool,
362 backend: DbBackend,
363 set: &MigrationSet,
364 steps: usize,
365) -> CoreResult<()> {
366 ensure_tracking_table(pool, backend).await?;
367 let mut done = 0;
368 for migration in set.migrations.iter().rev() {
369 if done >= steps {
370 break;
371 }
372 if !is_applied(pool, backend, set.module_id, migration.name()).await? {
373 continue;
374 }
375 let mut tx = pool.begin().await?;
376 {
377 let mut schema = Schema {
378 conn: &mut tx,
379 backend,
380 };
381 migration.down(&mut schema).await.map_err(|e| match e {
382 CoreError::Irreversible { name, .. } => CoreError::Irreversible {
383 module: set.module_id.to_string(),
384 name,
385 },
386 other => other,
387 })?;
388 }
389 let delete = Query::delete()
390 .from_table(LateriteMigrations::Table)
391 .and_where(Expr::col(LateriteMigrations::ModuleId).eq(set.module_id))
392 .and_where(Expr::col(LateriteMigrations::Name).eq(migration.name()))
393 .to_owned();
394 sqlx::query(&query_sql(backend, &delete))
395 .execute(&mut *tx)
396 .await?;
397 tx.commit().await?;
398 done += 1;
399 }
400 Ok(())
401}
402
403pub async fn reset(pool: &AnyPool, backend: DbBackend, set: &MigrationSet) -> CoreResult<()> {
405 rollback(pool, backend, set, set.migrations.len()).await
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[derive(Iden)]
413 enum Demo {
414 Table,
415 Id,
416 }
417
418 struct CreateDemo;
419
420 #[async_trait(?Send)]
421 impl Migration for CreateDemo {
422 fn name(&self) -> &str {
423 "0001_create_demo"
424 }
425 async fn up(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
426 schema
427 .exec(
428 Table::create()
429 .table(Demo::Table)
430 .if_not_exists()
431 .col(ColumnDef::new(Demo::Id).integer().not_null())
432 .to_owned(),
433 )
434 .await
435 }
436 async fn down(&self, schema: &mut Schema<'_>) -> CoreResult<()> {
437 schema
438 .exec(Table::drop().table(Demo::Table).to_owned())
439 .await
440 }
441 }
442
443 async fn sqlite_pool() -> AnyPool {
444 sqlx::any::install_default_drivers();
445 sqlx::any::AnyPoolOptions::new()
446 .max_connections(1)
447 .connect("sqlite::memory:")
448 .await
449 .unwrap()
450 }
451
452 #[tokio::test]
453 async fn applies_and_rolls_back_on_sqlite() {
454 let pool = sqlite_pool().await;
455 let backend = DbBackend::Sqlite;
456 let set = MigrationSet::new("test.demo", vec![Box::new(CreateDemo)]);
457
458 run(&pool, backend, std::slice::from_ref(&set))
459 .await
460 .unwrap();
461 run(&pool, backend, std::slice::from_ref(&set))
463 .await
464 .unwrap();
465 sqlx::query("insert into demo (id) values (1)")
466 .execute(&pool)
467 .await
468 .unwrap();
469 assert_eq!(applied(&pool, backend, "test.demo").await.unwrap().len(), 1);
470
471 reset(&pool, backend, &set).await.unwrap();
472 assert!(sqlx::query("select count(*) from demo")
474 .fetch_one(&pool)
475 .await
476 .is_err());
477 assert!(applied(&pool, backend, "test.demo")
478 .await
479 .unwrap()
480 .is_empty());
481 }
482
483 #[tokio::test]
484 async fn irreversible_migration_reports_module_and_name() {
485 let pool = sqlite_pool().await;
486 let backend = DbBackend::Sqlite;
487 let set = MigrationSet::new(
488 "test.oneway",
489 vec![Box::new(SqlMigration::new(
490 "0001_make_t",
491 "create table t (id integer not null)",
492 ))],
493 );
494 run(&pool, backend, std::slice::from_ref(&set))
495 .await
496 .unwrap();
497 let err = rollback(&pool, backend, &set, 1).await.unwrap_err();
498 match err {
499 CoreError::Irreversible { module, name } => {
500 assert_eq!(module, "test.oneway");
501 assert_eq!(name, "0001_make_t");
502 }
503 other => panic!("expected Irreversible, got {other:?}"),
504 }
505 }
506}