Skip to content

Commit

Permalink
Load migrations from *.sql files in a directory
Browse files Browse the repository at this point in the history
  • Loading branch information
czocher committed Apr 3, 2023
1 parent 4fa4d60 commit 23d8c19
Show file tree
Hide file tree
Showing 41 changed files with 850 additions and 18 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ jobs:
command: test
args: --features async-tokio-rusqlite

test_from_directory:
name: Test feature from-directory
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --features from-directory

test_examples:
name: Test examples
runs-on: ubuntu-latest
Expand Down
22 changes: 22 additions & 0 deletions examples/from-directory/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "example-from-directory"
version = "0.1.0"
edition = "2018"
publish = false

[dependencies]
rusqlite_migration = { path = "../../rusqlite_migration", features = [
"from-directory",
] }
log = "0.4"
simple-logging = "2.0.2"
env_logger = "0.10"
anyhow = "1"
lazy_static = "1.4.0"
mktemp = "0.5"
include_dir = "0.7.3"

[dependencies.rusqlite]
version = ">=0.23.0"
default-features = false
features = []
13 changes: 13 additions & 0 deletions examples/from-directory/migrations/01-friend_car/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE friend(
friend_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
phone TEXT UNIQUE,
picture BLOB
);

CREATE TABLE car(
registration_plate TEXT PRIMARY KEY,
cost REAL NOT NULL,
bought_on TEXT NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE friend ADD COLUMN birthday TEXT;
ALTER TABLE friend ADD COLUMN comment TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE animal;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE animal(name TEXT);
62 changes: 62 additions & 0 deletions examples/from-directory/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use anyhow::Result;
use include_dir::{include_dir, Dir};
use lazy_static::lazy_static;
use rusqlite::{params, Connection};
use rusqlite_migration::Migrations;

static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");

// Define migrations. These are applied atomically.
lazy_static! {
static ref MIGRATIONS: Migrations<'static> =
Migrations::from_directory(&MIGRATIONS_DIR).unwrap();
}

pub fn init_db() -> Result<Connection> {
let mut conn = Connection::open("./my_db.db3")?;

// Update the database schema, atomically
MIGRATIONS.to_latest(&mut conn)?;

Ok(conn)
}

pub fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("trace")).init();

let mut conn = init_db().unwrap();

// Apply some PRAGMA. These are often better applied outside of migrations, as some needs to be
// executed for each connection (like `foreign_keys`) or to be executed outside transactions
// (`journal_mode` is a noop in a transaction).
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();

// Use the db 🥳
conn.execute(
"INSERT INTO friend (name, birthday) VALUES (?1, ?2)",
params!["John", "1970-01-01"],
)
.unwrap();

conn.execute("INSERT INTO animal (name) VALUES (?1)", params!["dog"])
.unwrap();

// If we want to revert the last migration
MIGRATIONS.to_version(&mut conn, 2).unwrap();

// The table was removed
conn.execute("INSERT INTO animal (name) VALUES (?1)", params!["cat"])
.unwrap_err();
}

// Test that migrations are working
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn migrations_test() {
assert!(MIGRATIONS.validate().is_ok());
}
}
4 changes: 4 additions & 0 deletions rusqlite_migration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ default = []
### Enable support for async migrations with the use of `tokio-rusqlite`
async-tokio-rusqlite = ["dep:tokio-rusqlite", "dep:tokio"]

### Enable loading migrations from *.sql files in a given directory
from-directory = ["dep:include_dir"]

[dependencies]
include_dir = { version = "0.7.3", optional = true }
tokio = { version = "1.25", features = ["macros"], optional = true }
tokio-rusqlite = { version = "0.3.0", optional = true }
log = "0.4"
Expand Down
32 changes: 32 additions & 0 deletions rusqlite_migration/src/asynch.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use std::iter::FromIterator;

use tokio_rusqlite::Connection as AsyncConnection;

use crate::errors::Result;
use crate::{Migrations, SchemaVersion, M};

#[cfg(feature = "from-directory")]
use include_dir::Dir;

/// Adapter to make `Migrations` available in an async context.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AsyncMigrations {
Expand All @@ -29,6 +34,25 @@ impl AsyncMigrations {
}
}

/// Proxy implementation of the same method in the [Migrations](crate::Migrations::from_directory) struct.
///
/// # Example
///
/// ```
/// use rusqlite_migration::AsyncMigrations;
/// use include_dir::{Dir, include_dir};
///
/// static MIGRATION_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../examples/from-directory/migrations");
/// let migrations = AsyncMigrations::from_directory(&MIGRATION_DIR).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
#[cfg(feature = "from-directory")]
pub fn from_directory(dir: &'static Dir<'static>) -> Result<Self> {
Ok(Self {
migrations: Migrations::from_directory(dir)?,
})
}

/// Asynchronous version of the same method in the [Migrations](crate::Migrations::current_version) struct.
///
/// # Example
Expand Down Expand Up @@ -146,3 +170,11 @@ impl AsyncMigrations {
self.to_latest(&mut async_conn).await
}
}

impl FromIterator<M<'static>> for AsyncMigrations {
fn from_iter<T: IntoIterator<Item = M<'static>>>(iter: T) -> Self {
Self {
migrations: Migrations::from_iter(iter),
}
}
}
90 changes: 90 additions & 0 deletions rusqlite_migration/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::iter::FromIterator;

use include_dir::Dir;

use crate::{loader::from_directory, Error, MigrationHook, Result, M};

/// Allows to build a `Vec<M<'u>>` with additional edits.
#[derive(Default, Debug)]
pub struct MigrationsBuilder<'u> {
migrations: Vec<M<'u>>,
}

impl<'u> MigrationsBuilder<'u> {
/// Creates a set of migrations from a given directory by scanning subdirectories with a specified name pattern.
/// The migrations are loaded and stored in the binary.
///
/// See the [`Migrations::from_directory`] method for additional information regarding the directory structure.
///
/// # Example
///
/// ```
/// use rusqlite_migration::{Migrations, MigrationsBuilder};
/// use include_dir::{Dir, include_dir};
///
/// static MIGRATION_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../examples/from-directory/migrations");
/// let migrations: Migrations = MigrationsBuilder::from_directory(&MIGRATION_DIR).unwrap().finalize();
/// ```
///
/// Errors:
///
/// Returns [`Error::FileLoad`] in case the subdirectory names are incorrect,
/// or don't contain at least a valid `up.sql` file.
pub fn from_directory(dir: &'static Dir<'static>) -> Result<Self> {
Ok(Self {
migrations: from_directory(dir)?,
})
}

/// Allows to edit a migration with a given `id`.
///
/// Errors:
///
/// Return [`Error::FileLoad`] in case a migration with a given `id` does not exist.
pub fn edit(mut self, id: usize, f: impl Fn(&mut M)) -> Result<Self> {
if id < 1 {
return Err(Error::FileLoad("id cannot be equal to 0".to_string()));
}
f(self.migrations.get_mut(id - 1).ok_or(Error::FileLoad(
"No migration with the given index".to_string(),
))?);
Ok(self)
}

/// Finalizes the builder and creates a either a [`crate::Migrations`] or [`crate::asynch::AsyncMigration`] instance.
pub fn finalize<T: FromIterator<M<'u>>>(self) -> T {
T::from_iter(self.migrations)
}
}

impl<'u> FromIterator<M<'u>> for MigrationsBuilder<'u> {
fn from_iter<T: IntoIterator<Item = M<'u>>>(iter: T) -> Self {
Self {
migrations: Vec::from_iter(iter),
}
}
}

impl<'u> M<'u> {
/// Replace the `up_hook` in the given migration with the provided one.
///
/// # Warning
///
/// Use [`M::up_with_hook`] instead if you're creating a new migration.
/// This method is meant for editing existing transactions
/// when using the [`MigrationsBuilder`].
pub fn set_up_hook(&mut self, hook: impl MigrationHook + 'static) {
self.up_hook = Some(hook.clone_box());
}

/// Replace the `down_hook` in the given migration with the provided one.
///
/// # Warning
///
/// Use [`M::down_with_hook`] instead if you're creating a new migration.
/// This method is meant for editing existing transactions
/// when using the [`MigrationsBuilder`].
pub fn set_down_hook(&mut self, hook: impl MigrationHook + 'static) {
self.down_hook = Some(hook.clone_box());
}
}
3 changes: 3 additions & 0 deletions rusqlite_migration/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub enum Error {
ForeignKeyCheck(ForeignKeyCheckError),
/// Error returned by the migration hook
Hook(String),
/// Error returned when loading migrations from directory
FileLoad(String),
}

impl Error {
Expand Down Expand Up @@ -55,6 +57,7 @@ impl std::error::Error for Error {
Error::MigrationDefinition(e) => Some(e),
Error::ForeignKeyCheck(e) => Some(e),
Error::Hook(_) => None,
Error::FileLoad(_) => None,
}
}
}
Expand Down
Loading

0 comments on commit 23d8c19

Please sign in to comment.