feat: add sqlite client
This commit is contained in:
parent
3845cd09d6
commit
97178fcb02
14 changed files with 206 additions and 4 deletions
|
@ -19,12 +19,14 @@ structopt = "0.3"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
dotenv = { version = "0.15", optional = true }
|
||||||
postgres = { version = "0.19", optional = true }
|
postgres = { version = "0.19", optional = true }
|
||||||
mysql = { version = "20.1", optional = true }
|
mysql = { version = "20.1", optional = true }
|
||||||
dotenv = { version = "0.15", optional = true }
|
rusqlite = { version = "0.25", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["postgres"]
|
default = ["postgres"]
|
||||||
|
sqlite = ["rusqlite"]
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
|
@ -25,7 +25,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_command(&self) -> StdResult<()> {
|
pub fn run_command(&self) -> StdResult<()> {
|
||||||
match self.app_opt.command.clone() {
|
match dbg!(self.app_opt.command.clone()) {
|
||||||
Command::Init => {
|
Command::Init => {
|
||||||
commands::initialize_migra_manifest(self)?;
|
commands::initialize_migra_manifest(self)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> {
|
||||||
|
|
||||||
let migration_manager = MigrationManager::from(&config);
|
let migration_manager = MigrationManager::from(&config);
|
||||||
|
|
||||||
|
println!("here");
|
||||||
|
|
||||||
let file_contents = cmd_opts
|
let file_contents = cmd_opts
|
||||||
.file_paths
|
.file_paths
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -33,6 +35,7 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> {
|
||||||
file_contents
|
file_contents
|
||||||
.iter()
|
.iter()
|
||||||
.try_for_each(|content| {
|
.try_for_each(|content| {
|
||||||
|
println!("{}", &content);
|
||||||
maybe_with_transaction(
|
maybe_with_transaction(
|
||||||
!cmd_opts.transaction_opts.single_transaction,
|
!cmd_opts.transaction_opts.single_transaction,
|
||||||
conn,
|
conn,
|
||||||
|
|
|
@ -25,7 +25,11 @@ fn recursive_find_project_root() -> MigraResult<PathBuf> {
|
||||||
search_for_directory_containing_file(¤t_dir, MIGRA_TOML_FILENAME)
|
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 {
|
macro_rules! please_install_with {
|
||||||
(feature $database_name:expr) => {
|
(feature $database_name:expr) => {
|
||||||
panic!(
|
panic!(
|
||||||
|
@ -53,6 +57,8 @@ pub(crate) enum SupportedDatabaseClient {
|
||||||
Postgres,
|
Postgres,
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
Mysql,
|
Mysql,
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "rusqlite"))]
|
||||||
|
Sqlite,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SupportedDatabaseClient {
|
impl Default for SupportedDatabaseClient {
|
||||||
|
@ -62,6 +68,8 @@ impl Default for SupportedDatabaseClient {
|
||||||
SupportedDatabaseClient::Postgres
|
SupportedDatabaseClient::Postgres
|
||||||
} else if #[cfg(feature = "mysql")] {
|
} else if #[cfg(feature = "mysql")] {
|
||||||
SupportedDatabaseClient::Mysql
|
SupportedDatabaseClient::Mysql
|
||||||
|
} else if #[cfg(any(feature = "sqlite", feature = "rusqlite"))] {
|
||||||
|
SupportedDatabaseClient::Sqlite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,6 +114,14 @@ impl DatabaseConfig {
|
||||||
please_install_with!(feature "mysql")
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,10 @@ cfg_if! {
|
||||||
pub use self::mysql::*;
|
pub use self::mysql::*;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(any(feature = "sqlite", feature = "rusqlite"))] {
|
||||||
|
mod sqlite;
|
||||||
|
pub use self::sqlite::*;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
55
migra-cli/src/database/clients/sqlite.rs
Normal file
55
migra-cli/src/database/clients/sqlite.rs
Normal file
|
@ -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<Self> {
|
||||||
|
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<u64> {
|
||||||
|
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<Vec<Vec<String>>> {
|
||||||
|
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::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,10 @@ impl DatabaseConnectionManager {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
SupportedDatabaseClient::Mysql => Box::new(MySqlConnection::open(&connection_string)?),
|
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 })
|
Ok(DatabaseConnectionManager { conn })
|
||||||
|
|
|
@ -80,7 +80,13 @@ pub fn is_migrations_table_not_found<D: std::fmt::Display>(error: D) -> bool {
|
||||||
error_message.contains("ERROR 1146 (42S02)")
|
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 {
|
pub trait ManageMigration {
|
||||||
|
|
|
@ -18,6 +18,11 @@ pub fn database_manifest_path<D: std::fmt::Display>(database_name: D) -> String
|
||||||
pub const DATABASE_URL_DEFAULT_ENV_NAME: &str = "DATABASE_URL";
|
pub const DATABASE_URL_DEFAULT_ENV_NAME: &str = "DATABASE_URL";
|
||||||
pub const POSTGRES_URL: &str = "postgres://postgres:postgres@localhost:6000/migra_tests";
|
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 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 {
|
pub struct Env {
|
||||||
key: &'static str,
|
key: &'static str,
|
||||||
|
@ -156,6 +161,9 @@ Pending migrations:
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
inner(MYSQL_URL)?;
|
inner(MYSQL_URL)?;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "rusqlite"))]
|
||||||
|
remove_sqlite_db().and_then(|_| inner(SQLITE_URL))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +239,9 @@ Pending migrations:
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
inner("mysql")?;
|
inner("mysql")?;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "rusqlite"))]
|
||||||
|
remove_sqlite_db().and_then(|_| inner("sqlite"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +290,9 @@ Pending migrations:
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
inner("mysql")?;
|
inner("mysql")?;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "rusqlite"))]
|
||||||
|
remove_sqlite_db().and_then(|_| inner("sqlite"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,6 +341,9 @@ Pending migrations:
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
inner("mysql")?;
|
inner("mysql")?;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "rusqlite"))]
|
||||||
|
remove_sqlite_db().and_then(|_| inner("sqlite"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -387,6 +404,9 @@ mod make {
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
inner("mysql")?;
|
inner("mysql")?;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "rusqlite"))]
|
||||||
|
remove_sqlite_db().and_then(|_| inner("sqlite"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -451,6 +471,20 @@ mod upgrade {
|
||||||
Ok(())
|
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(())
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
migra-cli/tests/data/Migra_sqlite.toml
Normal file
4
migra-cli/tests/data/Migra_sqlite.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
root = "./sqlite"
|
||||||
|
|
||||||
|
[database]
|
||||||
|
connection = "local.db"
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
DROP TABLE articles;
|
|
@ -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
|
||||||
|
);
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
ALTER TABLE articles
|
||||||
|
DROP COLUMN author_person_id;
|
||||||
|
|
||||||
|
DROP TABLE persons;
|
|
@ -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;
|
Reference in a new issue