diff --git a/.gitignore b/.gitignore index 2c96eb1..6b1d017 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ target/ Cargo.lock + +# sqlite databases +*.db diff --git a/README.md b/README.md index d71507a..1e83b91 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,11 @@ For more information about the commands, simply run `migra help` ### Supported databases -- [x] Postgres -- [x] MySQL +| Database | Feature | Default | +|----------|--------------|:------------------:| +| Postgres | postgres | :heavy_check_mark: | +| MySQL | mysql | :x: | +| Sqlite | sqlite | :x: | ## License 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/config.rs b/migra-cli/src/config.rs index 6be608d..7a18243 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(feature = "sqlite") +))] macro_rules! please_install_with { (feature $database_name:expr) => { panic!( @@ -53,6 +57,8 @@ pub(crate) enum SupportedDatabaseClient { Postgres, #[cfg(feature = "mysql")] Mysql, + #[cfg(feature = "sqlite")] + 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(feature = "sqlite")] { + SupportedDatabaseClient::Sqlite } } } @@ -106,6 +114,14 @@ impl DatabaseConfig { please_install_with!(feature "mysql") } } + } else if connection_string.ends_with(".db") { + cfg_if! { + if #[cfg(feature = "sqlite")] { + 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..f2fed90 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(feature = "sqlite")] { + 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..8a0eafb --- /dev/null +++ b/migra-cli/src/database/clients/sqlite.rs @@ -0,0 +1,60 @@ +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 { + #[inline] + fn supports_transactional_ddl(&self) -> bool { + true + } +} + +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..d1acdb8 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(feature = "sqlite")] + 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..d43dcef 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(feature = "sqlite")] + remove_sqlite_db().and_then(|_| inner(SQLITE_URL))?; + Ok(()) } @@ -231,6 +239,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } @@ -279,6 +290,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } @@ -327,6 +341,9 @@ Pending migrations: #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } } @@ -387,6 +404,9 @@ mod make { #[cfg(feature = "mysql")] inner("mysql")?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| inner("sqlite"))?; + Ok(()) } } @@ -451,6 +471,20 @@ mod upgrade { Ok(()) })?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| { + inner("sqlite", || { + use rusqlite::Connection; + + let conn = Connection::open(SQLITE_URL)?; + let res = + conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?; + assert_eq!(res, ()); + + Ok(()) + }) + })?; + Ok(()) } @@ -493,6 +527,22 @@ mod upgrade { Ok(()) })?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| { + inner("sqlite_invalid", || { + use rusqlite::Connection; + + let conn = Connection::open(SQLITE_URL)?; + let articles_res = conn.execute_batch("SELECT a.id FROM articles AS a"); + let persons_res = conn.execute_batch("SELECT p.id FROM persons AS p"); + + assert!(articles_res.is_ok()); + assert!(persons_res.is_err()); + + Ok(()) + }) + })?; + Ok(()) } @@ -529,6 +579,22 @@ mod upgrade { Ok(()) })?; + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| { + inner("sqlite_invalid", || { + use rusqlite::Connection; + + let conn = Connection::open(SQLITE_URL)?; + let articles_res = conn.execute_batch("SELECT a.id FROM articles AS a"); + let persons_res = conn.execute_batch("SELECT p.id FROM persons AS p"); + + assert!(articles_res.is_err()); + assert!(persons_res.is_err()); + + Ok(()) + }) + })?; + Ok(()) } } @@ -646,6 +712,45 @@ mod apply { } } + #[cfg(feature = "sqlite")] + remove_sqlite_db().and_then(|_| { + inner( + "sqlite", + vec![ + "migrations/210218232851_create_articles/up", + "migrations/210218233414_create_persons/up", + ], + || { + use rusqlite::Connection; + + let conn = Connection::open(SQLITE_URL)?; + let res = + conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?; + assert_eq!(res, ()); + + Ok(()) + }, + )?; + + inner( + "sqlite", + vec![ + "migrations/210218233414_create_persons/down", + "migrations/210218232851_create_articles/down", + ], + || { + use rusqlite::Connection; + + let conn = Connection::open(SQLITE_URL)?; + 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/Migra_sqlite_invalid.toml b/migra-cli/tests/data/Migra_sqlite_invalid.toml new file mode 100644 index 0000000..9b9c973 --- /dev/null +++ b/migra-cli/tests/data/Migra_sqlite_invalid.toml @@ -0,0 +1,4 @@ +root = "./sqlite_invalid" + +[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..ea50e2f --- /dev/null +++ b/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql @@ -0,0 +1,16 @@ +-- This file should undo anything in `up.sql` + +CREATE TABLE tmp_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 +); + +INSERT INTO tmp_articles (id, title, content, created_at) +SELECT id, title, content, created_at FROM articles; + +DROP TABLE articles; +ALTER TABLE tmp_articles RENAME TO articles; + +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; diff --git a/migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql b/migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql new file mode 100644 index 0000000..b625086 --- /dev/null +++ b/migra-cli/tests/data/sqlite_invalid/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_invalid/migrations/210218232851_create_articles/up.sql b/migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/up.sql new file mode 100644 index 0000000..2742046 --- /dev/null +++ b/migra-cli/tests/data/sqlite_invalid/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_invalid/migrations/210218233414_create_persons/down.sql b/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql new file mode 100644 index 0000000..ea50e2f --- /dev/null +++ b/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql @@ -0,0 +1,16 @@ +-- This file should undo anything in `up.sql` + +CREATE TABLE tmp_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 +); + +INSERT INTO tmp_articles (id, title, content, created_at) +SELECT id, title, content, created_at FROM articles; + +DROP TABLE articles; +ALTER TABLE tmp_articles RENAME TO articles; + +DROP TABLE persons; diff --git a/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql b/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql new file mode 100644 index 0000000..c01a919 --- /dev/null +++ b/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql @@ -0,0 +1,14 @@ +-- 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 +); + +/* This table doesn't exist + ↓↓↓↓↓↓↓ */ +ALTER TABLE recipes + ADD COLUMN author_person_id int NULL + REFERENCES persons (id) ON UPDATE CASCADE ON DELETE CASCADE;