From 996fd3d0043aadc886c0d9e87b043e01f1158eee Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 6 Jun 2024 13:36:13 +0200 Subject: [PATCH 1/2] Add refinery driver for libsql. Disclaimer: I'm not overly familiar with the code base, so I'll quickly share my approach: * Adapted the tokio-postgres implementation for AsyncMigrate * Adapted the sqlite tests from rusqlite: * Made tests async * And removed all tests for run_iter(). --- README.md | 2 +- refinery/Cargo.toml | 1 + refinery/tests/libsql.rs | 726 ++++++++++++++++++++++++++ refinery_core/Cargo.toml | 2 + refinery_core/src/drivers/libsql.rs | 67 +++ refinery_core/src/drivers/mod.rs | 3 + refinery_core/src/drivers/rusqlite.rs | 1 + refinery_core/src/lib.rs | 3 + 8 files changed, 804 insertions(+), 1 deletion(-) create mode 100644 refinery/tests/libsql.rs create mode 100644 refinery_core/src/drivers/libsql.rs diff --git a/README.md b/README.md index bac63cdd..fb7d3cc3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Powerful SQL migration toolkit for Rust. Refinery strives to make running migrations for different databases as easy as possible. It works by running your migrations on a provided database connection, either by embedding them on your Rust code, or via the [refinery_cli]. -Currently [`postgres`](https://crates.io/crates/postgres), [`tokio-postgres`](https://crates.io/crates/tokio-postgres) , [`mysql`](https://crates.io/crates/mysql), [`mysql_async`](https://crates.io/crates/mysql_async), [`rusqlite`](https://crates.io/crates/rusqlite) and [`tiberius`](https://github.com/prisma/tiberius) are supported. +Currently [`postgres`](https://crates.io/crates/postgres), [`tokio-postgres`](https://crates.io/crates/tokio-postgres) , [`mysql`](https://crates.io/crates/mysql), [`mysql_async`](https://crates.io/crates/mysql_async), [`rusqlite`](https://crates.io/crates/rusqlite), [`libsql`](https://crates.io/crates/libsql) and [`tiberius`](https://github.com/prisma/tiberius) are supported. If you are using a driver that is not yet supported, namely [`SQLx`](https://github.com/launchbadge/sqlx) you can run migrations providing a [`Config`](https://docs.rs/refinery/latest/refinery/config/struct.Config.html) instead of the connection type, as `Config` impl's `Migrate`. You will still need to provide the `postgres`/`mysql`/`rusqlite`/`tiberius` driver as a feature for [`Runner::run`](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.run) and `tokio-postgres`/`mysql_async` for [`Runner::run_async`](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.run_async). `refinery` works best with [`Barrel`](https://crates.io/crates/barrel) but you can also have your migrations in `.sql` files or use any other Rust crate for schema generation. diff --git a/refinery/Cargo.toml b/refinery/Cargo.toml index fce329ec..75831677 100644 --- a/refinery/Cargo.toml +++ b/refinery/Cargo.toml @@ -16,6 +16,7 @@ edition = "2018" default = ["toml"] rusqlite-bundled = ["refinery-core/rusqlite-bundled"] rusqlite = ["refinery-core/rusqlite"] +libsql = ["refinery-core/libsql"] postgres = ["refinery-core/postgres"] mysql = ["refinery-core/mysql"] tokio-postgres = ["refinery-core/tokio-postgres"] diff --git a/refinery/tests/libsql.rs b/refinery/tests/libsql.rs new file mode 100644 index 00000000..b00c98e9 --- /dev/null +++ b/refinery/tests/libsql.rs @@ -0,0 +1,726 @@ +use barrel::backend::Sqlite as Sql; + +#[cfg(feature = "libsql")] +mod libsql { + use refinery::{ + config::{Config, ConfigDbType}, + embed_migrations, + error::Kind, + AsyncMigrate, Migration, Runner, Target, + }; + use refinery_core::libsql::{params, Builder, Connection, Row}; + use time::OffsetDateTime; + + const DEFAULT_TABLE_NAME: &str = "refinery_schema_history"; + + mod embedded { + use refinery::embed_migrations; + embed_migrations!("./tests/migrations"); + } + + mod broken { + use refinery::embed_migrations; + embed_migrations!("./tests/migrations_broken"); + } + + mod missing { + use refinery::embed_migrations; + embed_migrations!("./tests/migrations_missing"); + } + + fn get_migrations() -> Vec { + embed_migrations!("./tests/migrations"); + + let migration1 = + Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + + let migration2 = Migration::unapplied( + "V2__add_cars_and_motos_table.sql", + include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + ) + .unwrap(); + + let migration3 = Migration::unapplied( + "V3__add_brand_to_cars_table", + include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + ) + .unwrap(); + + let migration4 = Migration::unapplied( + "V4__add_year_to_motos_table.rs", + &migrations::V4__add_year_to_motos_table::migration(), + ) + .unwrap(); + + let migration5 = Migration::unapplied( + "V5__add_year_field_to_cars", + "ALTER TABLE cars ADD year INTEGER;", + ) + .unwrap(); + + vec![migration1, migration2, migration3, migration4, migration5] + } + + async fn in_memory_conn() -> Connection { + let db = Builder::new_local(":memory:").build().await.unwrap(); + db.connect().unwrap() + } + + async fn query_one(conn: &mut Connection, sql: &str) -> Option { + let mut rows = conn.query(&sql, params![]).await.unwrap(); + rows.next().await.unwrap() + } + + #[tokio::test] + async fn report_contains_applied_migrations() { + let mut conn = in_memory_conn().await; + let report = embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let migrations = get_migrations(); + let applied_migrations = report.applied_migrations(); + + assert_eq!(4, applied_migrations.len()); + + assert_eq!(migrations[0].version(), applied_migrations[0].version()); + assert_eq!(migrations[1].version(), applied_migrations[1].version()); + assert_eq!(migrations[2].version(), applied_migrations[2].version()); + assert_eq!(migrations[3].version(), applied_migrations[3].version()); + + assert_eq!(migrations[0].name(), migrations[0].name()); + assert_eq!(migrations[1].name(), applied_migrations[1].name()); + assert_eq!(migrations[2].name(), applied_migrations[2].name()); + assert_eq!(migrations[3].name(), applied_migrations[3].name()); + + assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); + assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); + assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); + assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); + } + + #[tokio::test] + async fn creates_migration_table() { + let mut conn = in_memory_conn().await; + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + let table_name: String = query_one( + &mut conn, + &format!( + "SELECT name FROM sqlite_master WHERE type='table' AND name='{}'", + DEFAULT_TABLE_NAME + ), + ) + .await + .unwrap() + .get(0) + .unwrap(); + assert_eq!(DEFAULT_TABLE_NAME, table_name); + } + + #[tokio::test] + async fn creates_migration_table_grouped_transaction() { + let mut conn = in_memory_conn().await; + embedded::migrations::runner() + .set_grouped(true) + .run_async(&mut conn) + .await + .unwrap(); + + let row = query_one( + &mut conn, + &format!( + "SELECT name FROM sqlite_master WHERE type='table' AND name='{}'", + DEFAULT_TABLE_NAME + ), + ) + .await + .unwrap(); + + let table_name: String = row.get(0).unwrap(); + assert_eq!(DEFAULT_TABLE_NAME, table_name); + } + + #[tokio::test] + async fn applies_migration() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + conn.execute( + "INSERT INTO persons (name, city) VALUES (?, ?)", + ["John Legend", "New York"], + ) + .await + .unwrap(); + let row = query_one(&mut conn, "SELECT name, city FROM persons") + .await + .unwrap(); + let (name, city): (String, String) = (row.get(0).unwrap(), row.get(1).unwrap()); + assert_eq!("John Legend", name); + assert_eq!("New York", city); + } + + #[tokio::test] + async fn applies_migration_grouped_transaction() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .set_grouped(true) + .run_async(&mut conn) + .await + .unwrap(); + + conn.execute( + "INSERT INTO persons (name, city) VALUES (?, ?)", + ["John Legend", "New York"], + ) + .await + .unwrap(); + let row = query_one(&mut conn, "SELECT name, city FROM persons") + .await + .unwrap(); + let (name, city): (String, String) = (row.get(0).unwrap(), row.get(1).unwrap()); + assert_eq!("John Legend", name); + assert_eq!("New York", city); + } + + #[tokio::test] + async fn updates_schema_history() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + + assert_eq!(4, current.version()); + + assert_eq!( + OffsetDateTime::now_utc().date(), + current.applied_on().unwrap().date() + ); + } + + #[tokio::test] + async fn updates_schema_history_grouped_transaction() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .set_grouped(true) + .run_async(&mut conn) + .await + .unwrap(); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + + assert_eq!(4, current.version()); + + assert_eq!( + OffsetDateTime::now_utc().date(), + current.applied_on().unwrap().date() + ); + } + + #[tokio::test] + async fn updates_to_last_working_if_not_grouped() { + let mut conn = in_memory_conn().await; + + let result = broken::migrations::runner().run_async(&mut conn).await; + + assert!(result.is_err()); + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + + let err = result.unwrap_err(); + let migrations = get_migrations(); + let applied_migrations = err.report().unwrap().applied_migrations(); + + assert_eq!( + OffsetDateTime::now_utc().date(), + current.applied_on().unwrap().date() + ); + assert_eq!(2, current.version()); + assert_eq!(2, applied_migrations.len()); + + assert_eq!(1, applied_migrations[0].version()); + assert_eq!(2, applied_migrations[1].version()); + + assert_eq!("initial", migrations[0].name()); + assert_eq!("add_cars_table", applied_migrations[1].name()); + + assert_eq!(2959965718684201605, applied_migrations[0].checksum()); + assert_eq!(8238603820526370208, applied_migrations[1].checksum()); + } + + #[tokio::test] + async fn doesnt_update_to_last_working_if_grouped() { + let mut conn = in_memory_conn().await; + + let result = broken::migrations::runner() + .set_grouped(true) + .run_async(&mut conn) + .await; + + assert!(result.is_err()); + let row = query_one(&mut conn, "SELECT version FROM refinery_schema_history").await; + assert!(row.is_none()); + } + + #[tokio::test] + async fn gets_applied_migrations() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let migrations = get_migrations(); + let applied_migrations = conn + .get_applied_migrations(DEFAULT_TABLE_NAME) + .await + .unwrap(); + assert_eq!(4, applied_migrations.len()); + + assert_eq!(migrations[0].version(), applied_migrations[0].version()); + assert_eq!(migrations[1].version(), applied_migrations[1].version()); + assert_eq!(migrations[2].version(), applied_migrations[2].version()); + assert_eq!(migrations[3].version(), applied_migrations[3].version()); + + assert_eq!(migrations[0].name(), migrations[0].name()); + assert_eq!(migrations[1].name(), applied_migrations[1].name()); + assert_eq!(migrations[2].name(), applied_migrations[2].name()); + assert_eq!(migrations[3].name(), applied_migrations[3].name()); + + assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); + assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); + assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); + assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); + } + + #[tokio::test] + async fn applies_new_migration() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let migrations = get_migrations(); + + let mchecksum = migrations[4].checksum(); + conn.migrate( + &migrations, + true, + true, + false, + Target::Latest, + DEFAULT_TABLE_NAME, + ) + .await + .unwrap(); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + + assert_eq!(5, current.version()); + assert_eq!(mchecksum, current.checksum()); + } + + #[tokio::test] + async fn migrates_to_target_migration() { + let mut conn = in_memory_conn().await; + + let report = embedded::migrations::runner() + .set_target(Target::Version(3)) + .run_async(&mut conn) + .await + .unwrap(); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + + let applied_migrations = report.applied_migrations(); + let migrations = get_migrations(); + + assert_eq!(3, current.version()); + + assert_eq!(3, applied_migrations.len()); + + assert_eq!(migrations[0].version(), applied_migrations[0].version()); + assert_eq!(migrations[1].version(), applied_migrations[1].version()); + assert_eq!(migrations[2].version(), applied_migrations[2].version()); + + assert_eq!(migrations[0].name(), migrations[0].name()); + assert_eq!(migrations[1].name(), applied_migrations[1].name()); + assert_eq!(migrations[2].name(), applied_migrations[2].name()); + + assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); + assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); + assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); + } + + #[tokio::test] + async fn migrates_to_target_migration_grouped() { + let mut conn = in_memory_conn().await; + + let report = embedded::migrations::runner() + .set_target(Target::Version(3)) + .set_grouped(true) + .run_async(&mut conn) + .await + .unwrap(); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + + let applied_migrations = report.applied_migrations(); + let migrations = get_migrations(); + + assert_eq!(3, current.version()); + + assert_eq!(3, applied_migrations.len()); + + assert_eq!(migrations[0].version(), applied_migrations[0].version()); + assert_eq!(migrations[1].version(), applied_migrations[1].version()); + assert_eq!(migrations[2].version(), applied_migrations[2].version()); + + assert_eq!(migrations[0].name(), migrations[0].name()); + assert_eq!(migrations[1].name(), applied_migrations[1].name()); + assert_eq!(migrations[2].name(), applied_migrations[2].name()); + + assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); + assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); + assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); + } + + #[tokio::test] + async fn aborts_on_missing_migration_on_filesystem() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let migration = Migration::unapplied( + "V4__add_year_field_to_cars", + "ALTER TABLE cars ADD year INTEGER;", + ) + .unwrap(); + let err = conn + .migrate( + &[migration], + true, + true, + false, + Target::Latest, + DEFAULT_TABLE_NAME, + ) + .await + .unwrap_err(); + + match err.kind() { + Kind::MissingVersion(missing) => { + assert_eq!(1, missing.version()); + assert_eq!("initial", missing.name()); + } + _ => panic!("failed test"), + } + } + + #[tokio::test] + async fn aborts_on_divergent_migration() { + let mut conn = in_memory_conn().await; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let migration = Migration::unapplied( + "V2__add_year_field_to_cars", + "ALTER TABLE cars ADD year INTEGER;", + ) + .unwrap(); + let err = conn + .migrate( + &[migration.clone()], + true, + false, + false, + Target::Latest, + DEFAULT_TABLE_NAME, + ) + .await + .unwrap_err(); + + match err.kind() { + Kind::DivergentVersion(applied, divergent) => { + assert_eq!(&migration, divergent); + assert_eq!(2, applied.version()); + assert_eq!("add_cars_and_motos_table", applied.name()); + } + _ => panic!("failed test"), + } + } + + #[tokio::test] + async fn aborts_on_missing_migration_on_database() { + let mut conn = in_memory_conn().await; + + missing::migrations::runner() + .run_async(&mut conn) + .await + .unwrap(); + + let migration1 = Migration::unapplied( + "V1__initial", + concat!( + "CREATE TABLE persons (", + "id int,", + "name varchar(255),", + "city varchar(255)", + ");" + ), + ) + .unwrap(); + + let migration2 = Migration::unapplied( + "V2__add_cars_table", + include_str!("./migrations_missing/V2__add_cars_table.sql"), + ) + .unwrap(); + let err = conn + .migrate( + &[migration1, migration2], + true, + true, + false, + Target::Latest, + DEFAULT_TABLE_NAME, + ) + .await + .unwrap_err(); + match err.kind() { + Kind::MissingVersion(missing) => { + assert_eq!(1, missing.version()); + assert_eq!("initial", missing.name()); + } + _ => panic!("failed test"), + } + } + + // NOTE: Doesn't seem to have anything to do with the driver. Adapted from rusqlite's tests. + #[test] + fn migrates_from_config() { + let db = tempfile::NamedTempFile::new_in(".").unwrap(); + let mut config = Config::new(ConfigDbType::Sqlite).set_db_path(db.path().to_str().unwrap()); + + let migrations = get_migrations(); + let runner = Runner::new(&migrations) + .set_grouped(false) + .set_abort_divergent(true) + .set_abort_missing(true); + + runner.run(&mut config).unwrap(); + + let applied_migrations = runner.get_applied_migrations(&mut config).unwrap(); + assert_eq!(5, applied_migrations.len()); + + assert_eq!(migrations[0].version(), applied_migrations[0].version()); + assert_eq!(migrations[1].version(), applied_migrations[1].version()); + assert_eq!(migrations[2].version(), applied_migrations[2].version()); + assert_eq!(migrations[3].version(), applied_migrations[3].version()); + assert_eq!(migrations[4].version(), applied_migrations[4].version()); + + assert_eq!(migrations[0].name(), migrations[0].name()); + assert_eq!(migrations[1].name(), applied_migrations[1].name()); + assert_eq!(migrations[2].name(), applied_migrations[2].name()); + assert_eq!(migrations[3].name(), applied_migrations[3].name()); + assert_eq!(migrations[4].name(), applied_migrations[4].name()); + + assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); + assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); + assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); + assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); + assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + } + + // NOTE: Doesn't seem to have anything to do with the driver. Adapted from rusqlite's tests. + #[test] + fn migrate_from_config_report_contains_migrations() { + let db = tempfile::NamedTempFile::new_in(".").unwrap(); + let mut config = Config::new(ConfigDbType::Sqlite).set_db_path(db.path().to_str().unwrap()); + + let migrations = get_migrations(); + let runner = Runner::new(&migrations) + .set_grouped(false) + .set_abort_divergent(true) + .set_abort_missing(true); + + let report = runner.run(&mut config).unwrap(); + + let applied_migrations = report.applied_migrations(); + assert_eq!(5, applied_migrations.len()); + + assert_eq!(migrations[0].version(), applied_migrations[0].version()); + assert_eq!(migrations[1].version(), applied_migrations[1].version()); + assert_eq!(migrations[2].version(), applied_migrations[2].version()); + assert_eq!(migrations[3].version(), applied_migrations[3].version()); + assert_eq!(migrations[4].version(), applied_migrations[4].version()); + + assert_eq!(migrations[0].name(), migrations[0].name()); + assert_eq!(migrations[1].name(), applied_migrations[1].name()); + assert_eq!(migrations[2].name(), applied_migrations[2].name()); + assert_eq!(migrations[3].name(), applied_migrations[3].name()); + assert_eq!(migrations[4].name(), applied_migrations[4].name()); + + assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); + assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); + assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); + assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); + assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + } + + // NOTE: Doesn't seem to have anything to do with the driver. Adapted from rusqlite's tests. + #[test] + fn migrate_from_config_report_returns_last_applied_migration() { + let db = tempfile::NamedTempFile::new_in(".").unwrap(); + let mut config = Config::new(ConfigDbType::Sqlite).set_db_path(db.path().to_str().unwrap()); + + let migrations = get_migrations(); + let runner = Runner::new(&migrations) + .set_grouped(false) + .set_abort_divergent(true) + .set_abort_missing(true); + + runner.run(&mut config).unwrap(); + + let applied_migration = runner + .get_last_applied_migration(&mut config) + .unwrap() + .unwrap(); + assert_eq!(5, applied_migration.version()); + + assert_eq!(migrations[4].version(), applied_migration.version()); + assert_eq!(migrations[4].name(), applied_migration.name()); + assert_eq!(migrations[4].checksum(), applied_migration.checksum()); + } + + #[tokio::test] + async fn doesnt_run_migrations_if_fake_version() { + let mut conn = in_memory_conn().await; + + let report = embedded::migrations::runner() + .set_target(Target::FakeVersion(2)) + .run_async(&mut conn) + .await + .unwrap(); + + let applied_migrations = report.applied_migrations(); + + assert!(applied_migrations.is_empty()); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + let migrations = get_migrations(); + let mchecksum = migrations[1].checksum(); + + assert_eq!(2, current.version()); + assert_eq!(mchecksum, current.checksum()); + + let row: Option = query_one( + &mut conn, + "SELECT name FROM sqlite_master WHERE type='table' AND name='persons'", + ) + .await; + + assert!(matches!(row, None)); + } + + #[tokio::test] + async fn doesnt_run_migrations_if_fake() { + let mut conn = in_memory_conn().await; + + let report = embedded::migrations::runner() + .set_target(Target::Fake) + .run_async(&mut conn) + .await + .unwrap(); + + let applied_migrations = report.applied_migrations(); + + assert!(applied_migrations.is_empty()); + + let current = conn + .get_last_applied_migration(DEFAULT_TABLE_NAME) + .await + .unwrap() + .unwrap(); + let migrations = get_migrations(); + let mchecksum = migrations[3].checksum(); + + assert_eq!(4, current.version()); + assert_eq!(mchecksum, current.checksum()); + + let row: Option = query_one( + &mut conn, + "SELECT name FROM sqlite_master WHERE type='table' AND name='persons'", + ) + .await; + + assert!(matches!(row, None)); + } + + // #[tokio::test] + // fn migrates_from_cli() { + // run_test(|| { + // Command::new("refinery") + // .args([ + // "migrate", + // "-c", + // "tests/sqlite_refinery.toml", + // "-p", + // "tests/migrations", + // ]) + // .unwrap() + // .assert() + // .stdout(contains("applying migration: V2__add_cars_and_motos_table")) + // .stdout(contains("applying migration: V3__add_brand_to_cars_table")); + // }) + // } +} diff --git a/refinery_core/Cargo.toml b/refinery_core/Cargo.toml index c8cb46f0..d623bfa8 100644 --- a/refinery_core/Cargo.toml +++ b/refinery_core/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" [features] default = [] +libsql = ["dep:libsql"] rusqlite-bundled = ["rusqlite", "rusqlite/bundled"] tiberius = ["dep:tiberius", "futures", "tokio", "tokio/net"] tiberius-config = ["tiberius", "tokio", "tokio-util", "serde"] @@ -29,6 +30,7 @@ url = "2.0" walkdir = "2.3.1" # allow multiple versions of the same dependency if API is similar +libsql = { version = ">=0.3.5", optional = true } rusqlite = { version = ">= 0.23, <= 0.31", optional = true } postgres = { version = ">=0.17, <= 0.19", optional = true } tokio-postgres = { version = ">= 0.5, <= 0.7", optional = true } diff --git a/refinery_core/src/drivers/libsql.rs b/refinery_core/src/drivers/libsql.rs new file mode 100644 index 00000000..7e951aef --- /dev/null +++ b/refinery_core/src/drivers/libsql.rs @@ -0,0 +1,67 @@ +use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction}; +use crate::Migration; +use async_trait::async_trait; +use libsql::{params, Connection, Error as LibsqlError, Transaction as LibsqlTransaction}; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +async fn query_applied_migrations( + transaction: &LibsqlTransaction, + query: &str, +) -> Result, LibsqlError> { + let mut rows = transaction.query(query, params![]).await?; + let mut applied = Vec::new(); + loop { + // for row in rows.into_iter() + let Some(row) = rows.next().await? else { + break; + }; + + let version = row.get(0)?; + let applied_on: String = row.get(2)?; + // Safe to call unwrap, as we stored it in RFC3339 format on the database + let applied_on = OffsetDateTime::parse(&applied_on, &Rfc3339).unwrap(); + let checksum: String = row.get(3)?; + + applied.push(Migration::applied( + version, + row.get(1)?, + applied_on, + checksum + .parse::() + .expect("checksum must be a valid u64"), + )); + } + Ok(applied) +} + +#[async_trait] +impl AsyncTransaction for Connection { + type Error = LibsqlError; + + async fn execute(&mut self, queries: &[&str]) -> Result { + let transaction = self.transaction().await?; + let mut count = 0; + for query in queries { + transaction.execute_batch(query).await?; + count += 1; + } + transaction.commit().await?; + Ok(count as usize) + } +} + +#[async_trait] +impl AsyncQuery> for Connection { + async fn query( + &mut self, + query: &str, + ) -> Result, ::Error> { + let transaction = self.transaction().await?; + let applied = query_applied_migrations(&transaction, query).await?; + transaction.commit().await?; + Ok(applied) + } +} + +impl AsyncMigrate for Connection {} diff --git a/refinery_core/src/drivers/mod.rs b/refinery_core/src/drivers/mod.rs index 867d4c4d..d6184151 100644 --- a/refinery_core/src/drivers/mod.rs +++ b/refinery_core/src/drivers/mod.rs @@ -16,4 +16,7 @@ pub mod mysql; #[cfg(feature = "tiberius")] pub mod tiberius; +#[cfg(feature = "libsql")] +pub mod libsql; + mod config; diff --git a/refinery_core/src/drivers/rusqlite.rs b/refinery_core/src/drivers/rusqlite.rs index 9547ba48..8c101ec3 100644 --- a/refinery_core/src/drivers/rusqlite.rs +++ b/refinery_core/src/drivers/rusqlite.rs @@ -1,5 +1,6 @@ use crate::traits::sync::{Migrate, Query, Transaction}; use crate::Migration; + use rusqlite::{Connection as RqlConnection, Error as RqlError}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; diff --git a/refinery_core/src/lib.rs b/refinery_core/src/lib.rs index b4d85b77..e66a5438 100644 --- a/refinery_core/src/lib.rs +++ b/refinery_core/src/lib.rs @@ -30,3 +30,6 @@ pub use mysql_async; #[cfg(feature = "tiberius")] pub use tiberius; + +#[cfg(feature = "libsql")] +pub use libsql; From 152a7da0f06d117ffa7a4e34fd9ec22645a0399e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 14 Jun 2024 10:13:11 +0200 Subject: [PATCH 2/2] Pull only "core" dependencies for libsql. --- refinery_core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refinery_core/Cargo.toml b/refinery_core/Cargo.toml index d623bfa8..a4b2e425 100644 --- a/refinery_core/Cargo.toml +++ b/refinery_core/Cargo.toml @@ -30,7 +30,7 @@ url = "2.0" walkdir = "2.3.1" # allow multiple versions of the same dependency if API is similar -libsql = { version = ">=0.3.5", optional = true } +libsql = { version = ">=0.3.5", optional = true, default-features = false, features = ["core"] } rusqlite = { version = ">= 0.23, <= 0.31", optional = true } postgres = { version = ">=0.17, <= 0.19", optional = true } tokio-postgres = { version = ">= 0.5, <= 0.7", optional = true }