Merge pull request #10 from pleshevskiy/sqlite

feat: add sqlite client
This commit is contained in:
Dmitriy Pleshevskiy 2021-05-23 13:44:43 +03:00 committed by GitHub
commit c144086cb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 300 additions and 6 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
target/
Cargo.lock
# sqlite databases
*.db

View File

@ -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

View File

@ -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" }

View File

@ -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)?;
}

View File

@ -25,7 +25,11 @@ fn recursive_find_project_root() -> MigraResult<PathBuf> {
search_for_directory_containing_file(&current_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
}

View File

@ -11,3 +11,10 @@ cfg_if! {
pub use self::mysql::*;
}
}
cfg_if! {
if #[cfg(feature = "sqlite")] {
mod sqlite;
pub use self::sqlite::*;
}
}

View File

@ -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<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 {
#[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<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)
}
}

View File

@ -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 })

View File

@ -80,7 +80,13 @@ pub fn is_migrations_table_not_found<D: std::fmt::Display>(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 {

View File

@ -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 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(())
}
}

View File

@ -0,0 +1,4 @@
root = "./sqlite"
[database]
connection = "local.db"

View File

@ -0,0 +1,4 @@
root = "./sqlite_invalid"
[database]
connection = "local.db"

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE articles;

View File

@ -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
);

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE articles;

View File

@ -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
);

View File

@ -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;

View File

@ -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;