From 97178fcb02c15bad9f8ea2fe1a85d82862b790ca Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Mon, 17 May 2021 10:06:33 +0300 Subject: [PATCH] feat: add sqlite client --- migra-cli/Cargo.toml | 4 +- migra-cli/src/app.rs | 2 +- migra-cli/src/commands/apply.rs | 3 + migra-cli/src/config.rs | 18 ++++- migra-cli/src/database/clients/mod.rs | 7 ++ migra-cli/src/database/clients/sqlite.rs | 55 ++++++++++++++ migra-cli/src/database/connection.rs | 4 + migra-cli/src/database/migration.rs | 8 +- migra-cli/tests/commands.rs | 76 +++++++++++++++++++ migra-cli/tests/data/Migra_sqlite.toml | 4 + .../210218232851_create_articles/down.sql | 3 + .../210218232851_create_articles/up.sql | 8 ++ .../210218233414_create_persons/down.sql | 6 ++ .../210218233414_create_persons/up.sql | 12 +++ 14 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 migra-cli/src/database/clients/sqlite.rs create mode 100644 migra-cli/tests/data/Migra_sqlite.toml create mode 100644 migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql create mode 100644 migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql create mode 100644 migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql create mode 100644 migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql diff --git a/migra-cli/Cargo.toml b/migra-cli/Cargo.toml index 4defe45..d2a0791 100644 --- a/migra-cli/Cargo.toml +++ b/migra-cli/Cargo.toml @@ -19,12 +19,14 @@ structopt = "0.3" serde = { version = "1.0", features = ["derive"] } toml = "0.5" chrono = "0.4" +dotenv = { version = "0.15", optional = true } postgres = { version = "0.19", optional = true } mysql = { version = "20.1", optional = true } -dotenv = { version = "0.15", optional = true } +rusqlite = { version = "0.25", optional = true } [features] default = ["postgres"] +sqlite = ["rusqlite"] [badges] maintenance = { status = "actively-developed" } diff --git a/migra-cli/src/app.rs b/migra-cli/src/app.rs index 4c9cac0..7610ddc 100644 --- a/migra-cli/src/app.rs +++ b/migra-cli/src/app.rs @@ -25,7 +25,7 @@ impl App { } pub fn run_command(&self) -> StdResult<()> { - match self.app_opt.command.clone() { + match dbg!(self.app_opt.command.clone()) { Command::Init => { commands::initialize_migra_manifest(self)?; } diff --git a/migra-cli/src/commands/apply.rs b/migra-cli/src/commands/apply.rs index 0a78448..3c53981 100644 --- a/migra-cli/src/commands/apply.rs +++ b/migra-cli/src/commands/apply.rs @@ -12,6 +12,8 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> { let migration_manager = MigrationManager::from(&config); + println!("here"); + let file_contents = cmd_opts .file_paths .clone() @@ -33,6 +35,7 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> { file_contents .iter() .try_for_each(|content| { + println!("{}", &content); maybe_with_transaction( !cmd_opts.transaction_opts.single_transaction, conn, diff --git a/migra-cli/src/config.rs b/migra-cli/src/config.rs index 6be608d..f8ca862 100644 --- a/migra-cli/src/config.rs +++ b/migra-cli/src/config.rs @@ -25,7 +25,11 @@ fn recursive_find_project_root() -> MigraResult { search_for_directory_containing_file(¤t_dir, MIGRA_TOML_FILENAME) } -#[cfg(any(not(feature = "postgres"), not(feature = "mysql")))] +#[cfg(any( + not(feature = "postgres"), + not(feature = "mysql"), + not(any(feature = "sqlite", feature = "rusqlite")) +))] macro_rules! please_install_with { (feature $database_name:expr) => { panic!( @@ -53,6 +57,8 @@ pub(crate) enum SupportedDatabaseClient { Postgres, #[cfg(feature = "mysql")] Mysql, + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + Sqlite, } impl Default for SupportedDatabaseClient { @@ -62,6 +68,8 @@ impl Default for SupportedDatabaseClient { SupportedDatabaseClient::Postgres } else if #[cfg(feature = "mysql")] { SupportedDatabaseClient::Mysql + } else if #[cfg(any(feature = "sqlite", feature = "rusqlite"))] { + SupportedDatabaseClient::Sqlite } } } @@ -106,6 +114,14 @@ impl DatabaseConfig { please_install_with!(feature "mysql") } } + } else if connection_string.ends_with(".db") { + cfg_if! { + if #[cfg(any(feature = "sqlite", feature = "rusqlite"))] { + Some(SupportedDatabaseClient::Sqlite) + } else { + please_install_with!(feature "sqlite") + } + } } else { None } diff --git a/migra-cli/src/database/clients/mod.rs b/migra-cli/src/database/clients/mod.rs index f745d23..3705af6 100644 --- a/migra-cli/src/database/clients/mod.rs +++ b/migra-cli/src/database/clients/mod.rs @@ -11,3 +11,10 @@ cfg_if! { pub use self::mysql::*; } } + +cfg_if! { + if #[cfg(any(feature = "sqlite", feature = "rusqlite"))] { + mod sqlite; + pub use self::sqlite::*; + } +} diff --git a/migra-cli/src/database/clients/sqlite.rs b/migra-cli/src/database/clients/sqlite.rs new file mode 100644 index 0000000..6b67b3f --- /dev/null +++ b/migra-cli/src/database/clients/sqlite.rs @@ -0,0 +1,55 @@ +use crate::database::builder::merge_query_with_params; +use crate::database::prelude::*; +use crate::error::StdResult; +use rusqlite::Connection; + +pub struct SqliteConnection { + conn: Connection, +} + +impl OpenDatabaseConnection for SqliteConnection { + fn open(connection_string: &str) -> StdResult { + let conn = Connection::open(connection_string)?; + Ok(SqliteConnection { conn }) + } +} + +impl DatabaseStatements for SqliteConnection { + fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String { + format!( + r#"CREATE TABLE IF NOT EXISTS {} ( + id int AUTO_INCREMENT PRIMARY KEY, + name varchar(256) NOT NULL UNIQUE + )"#, + migrations_table_name + ) + } +} + +impl SupportsTransactionalDdl for SqliteConnection {} + +impl DatabaseConnection for SqliteConnection { + fn batch_execute(&mut self, query: &str) -> StdResult<()> { + self.conn.execute_batch(query)?; + Ok(()) + } + + fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult { + let stmt = merge_query_with_params(query, params); + + let res = self.conn.execute(&stmt, [])?; + Ok(res as u64) + } + + fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult>> { + let stmt = merge_query_with_params(query, params); + + let mut stmt = self.conn.prepare(&stmt)?; + + let res = stmt + .query_map([], |row| Ok(vec![row.get(0)?]))? + .collect::>()?; + + Ok(res) + } +} diff --git a/migra-cli/src/database/connection.rs b/migra-cli/src/database/connection.rs index 76e1260..021a257 100644 --- a/migra-cli/src/database/connection.rs +++ b/migra-cli/src/database/connection.rs @@ -44,6 +44,10 @@ impl DatabaseConnectionManager { } #[cfg(feature = "mysql")] SupportedDatabaseClient::Mysql => Box::new(MySqlConnection::open(&connection_string)?), + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + SupportedDatabaseClient::Sqlite => { + Box::new(SqliteConnection::open(&connection_string)?) + } }; Ok(DatabaseConnectionManager { conn }) diff --git a/migra-cli/src/database/migration.rs b/migra-cli/src/database/migration.rs index c5a0a6b..d2ad50d 100644 --- a/migra-cli/src/database/migration.rs +++ b/migra-cli/src/database/migration.rs @@ -80,7 +80,13 @@ pub fn is_migrations_table_not_found(error: D) -> bool { error_message.contains("ERROR 1146 (42S02)") } - is_postgres_error(&error_message) || is_mysql_error(&error_message) + fn is_sqlite_error(error_message: &str) -> bool { + error_message.starts_with("no such table:") + } + + is_postgres_error(&error_message) + || is_mysql_error(&error_message) + || is_sqlite_error(&error_message) } pub trait ManageMigration { diff --git a/migra-cli/tests/commands.rs b/migra-cli/tests/commands.rs index 92ddcab..20742f0 100644 --- a/migra-cli/tests/commands.rs +++ b/migra-cli/tests/commands.rs @@ -18,6 +18,11 @@ pub fn database_manifest_path(database_name: D) -> String pub const DATABASE_URL_DEFAULT_ENV_NAME: &str = "DATABASE_URL"; pub const POSTGRES_URL: &str = "postgres://postgres:postgres@localhost:6000/migra_tests"; pub const MYSQL_URL: &str = "mysql://mysql:mysql@localhost:6001/migra_tests"; +pub const SQLITE_URL: &str = "local.db"; + +pub fn remove_sqlite_db() -> TestResult { + std::fs::remove_file(SQLITE_URL).or(Ok(())) +} pub struct Env { key: &'static str, @@ -156,6 +161,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner(MYSQL_URL)?; + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| inner(SQLITE_URL))?; + Ok(()) } @@ -231,6 +239,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } @@ -279,6 +290,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } @@ -327,6 +341,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } } @@ -387,6 +404,9 @@ mod make { #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } } @@ -451,6 +471,20 @@ mod upgrade { Ok(()) })?; + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| { + inner("sqlite", || { + use rusqlite::Connection; + + let conn = Connection::open_in_memory()?; + let res = + conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?; + assert_eq!(res, ()); + + Ok(()) + }) + })?; + Ok(()) } @@ -646,6 +680,48 @@ mod apply { } } + #[cfg(any(feature = "sqlite", feature = "rusqlite"))] + remove_sqlite_db().and_then(|_| { + println!("upgrade"); + inner( + "sqlite", + vec![ + "migrations/210218232851_create_articles/up", + "migrations/210218233414_create_persons/up", + ], + || { + use rusqlite::Connection; + + let conn = Connection::open_in_memory()?; + println!("upgraded?"); + let res = + conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?; + assert_eq!(res, ()); + + Ok(()) + }, + )?; + + println!("downgrade"); + inner( + "sqlite", + vec![ + "migrations/210218233414_create_persons/down", + "migrations/210218232851_create_articles/down", + ], + || { + use rusqlite::Connection; + + let conn = Connection::open_in_memory()?; + let res = + conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a"); + assert!(res.is_err()); + + Ok(()) + }, + ) + })?; + Ok(()) } } diff --git a/migra-cli/tests/data/Migra_sqlite.toml b/migra-cli/tests/data/Migra_sqlite.toml new file mode 100644 index 0000000..f0ef145 --- /dev/null +++ b/migra-cli/tests/data/Migra_sqlite.toml @@ -0,0 +1,4 @@ +root = "./sqlite" + +[database] +connection = "local.db" diff --git a/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql b/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql new file mode 100644 index 0000000..b625086 --- /dev/null +++ b/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE articles; diff --git a/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql b/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql new file mode 100644 index 0000000..2742046 --- /dev/null +++ b/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here + +CREATE TABLE articles ( + id int AUTO_INCREMENT PRIMARY KEY, + title text NOT NULL CHECK (length(title) > 0), + content text NOT NULL, + created_at timestamp NOT NULL DEFAULT current_timestamp +); diff --git a/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql b/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql new file mode 100644 index 0000000..c03d71a --- /dev/null +++ b/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` + +ALTER TABLE articles + DROP COLUMN author_person_id; + +DROP TABLE persons; diff --git a/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql b/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql new file mode 100644 index 0000000..20bbda9 --- /dev/null +++ b/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here + +CREATE TABLE persons ( + id int AUTO_INCREMENT PRIMARY KEY, + email varchar(256) NOT NULL UNIQUE, + display_name text NOT NULL, + created_at timestamp NOT NULL DEFAULT current_timestamp +); + +ALTER TABLE articles + ADD COLUMN author_person_id int NULL + REFERENCES persons (id) ON UPDATE CASCADE ON DELETE CASCADE;