Skip to content

Commit

Permalink
Add support for tokio-rusqlite
Browse files Browse the repository at this point in the history
  • Loading branch information
czocher committed Feb 7, 2023
1 parent 3ceceba commit bd62538
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 1 deletion.
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ homepage = "https://cj.rs/rusqlite_migration"
repository = "https://github.com/cljoly/rusqlite_migration"
rust-version = "1.61"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
async-tokio-rusqlite = ["dep:tokio-rusqlite", "dep:tokio", "dep:tokio-test"]

[dependencies]
tokio = { version = "1.25", features = ["macros"], optional = true }
tokio-rusqlite = { version = "0.3.0", optional = true }
tokio-test = { version = "0.4.2", optional = true }
log = "0.4"

[dependencies.rusqlite]
Expand All @@ -23,6 +28,7 @@ default-features = false
features = []

[dev-dependencies]
tokio = { version = "1.25", features = ["full"] }
simple-logging = "2.0.2"
env_logger = "0.10"
anyhow = "1"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ conn.execute("INSERT INTO friend (name) VALUES (?1)", params!["John"])
```

Please see the [examples](https://github.com/cljoly/rusqlite_migrate/tree/master/examples) folder for more, in particular:
- `async` migrations in the (quick_start_async.rs)[https://github.com/cljoly/rusqlite_migrate/tree/master/examples/quick_start_async.rs)] file
- migrations with multiple SQL statements (using for instance `r#"…"` or `include_str!(…)`)
- use of lazy_static
- migrations to previous versions (downward migrations)
Expand Down
91 changes: 91 additions & 0 deletions examples/quick_start_async.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use anyhow::Result;
use lazy_static::lazy_static;
use rusqlite::params;
use rusqlite_migration::{asynch::AsyncMigrations, Migrations, M};
use tokio_rusqlite::Connection;

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

#[test]
fn migrations_test() {
assert!(MIGRATIONS.validate().is_ok());
}
}

// Define migrations. These are applied atomically.
lazy_static! {
static ref MIGRATIONS: AsyncMigrations =
AsyncMigrations::new(Migrations::new(vec![
M::up(include_str!("friend_car.sql")),
// PRAGMA are better applied outside of migrations, see below for details.
M::up(r#"
ALTER TABLE friend ADD COLUMN birthday TEXT;
ALTER TABLE friend ADD COLUMN comment TEXT;
"#),

// This migration can be reverted
M::up("CREATE TABLE animal(name TEXT);")
.down("DROP TABLE animal;")

// In the future, if the need to change the schema arises, put
// migrations here, like so:
// M::up("CREATE INDEX UX_friend_email ON friend(email);"),
// M::up("CREATE INDEX UX_friend_name ON friend(name);"),
]));
}

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

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

Ok(async_conn)
}

#[tokio::main]
async fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("trace")).init();

let mut async_conn = init_db().await.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).
async_conn
.call(|conn| conn.pragma_update(None, "journal_mode", "WAL"))
.await
.unwrap();
async_conn
.call(|conn| conn.pragma_update(None, "foreign_keys", "ON"))
.await
.unwrap();

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

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

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

// The table was removed
async_conn
.call(|conn| conn.execute("INSERT INTO animal (name) VALUES (?1)", params!["cat"]))
.await
.unwrap_err();
}
156 changes: 156 additions & 0 deletions src/asynch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use tokio_rusqlite::Connection as AsyncConnection;

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

/// Adapter to make `Migrations` available in an async context.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AsyncMigrations {
migrations: Migrations<'static>,
}

impl AsyncMigrations {
/// Create a set of migrations.
///
/// # Example
///
/// ```
/// use rusqlite_migration::{Migrations, asynch::AsyncMigrations, M};
///
/// let migrations = AsyncMigrations::new(Migrations::new(vec![
/// M::up("CREATE TABLE animals (name TEXT);"),
/// M::up("CREATE TABLE food (name TEXT);"),
/// ]));
/// ```
pub fn new(migrations: Migrations<'static>) -> Self {
Self { migrations }
}

/// Get the current schema version.
///
/// # Example
///
/// ```rust
/// # tokio_test::block_on(async {
/// use rusqlite_migration::{Migrations, asynch::AsyncMigrations, M, SchemaVersion};
/// use std::num::NonZeroUsize;
///
/// let mut conn = tokio_rusqlite::Connection::open_in_memory().await.unwrap();
///
/// let migrations = AsyncMigrations::new(Migrations::new(vec![
/// M::up("CREATE TABLE animals (name TEXT);"),
/// M::up("CREATE TABLE food (name TEXT);"),
/// ]));
///
/// assert_eq!(SchemaVersion::NoneSet, migrations.current_version(&conn).await.unwrap());
///
/// // Go to the latest version
/// migrations.to_latest(&mut conn).await.unwrap();
///
/// assert_eq!(SchemaVersion::Inside(NonZeroUsize::new(2).unwrap()), migrations.current_version(&conn).await.unwrap());
/// # })
/// ```
pub async fn current_version(&self, async_conn: &AsyncConnection) -> Result<SchemaVersion> {
let m = self.migrations.clone();
async_conn.call(move |conn| m.current_version(conn)).await
}

/// Migrate the database to latest schema version. The migrations are applied atomically.
///
/// # Example
///
/// ```rust
/// # tokio_test::block_on(async {
/// use rusqlite_migration::{Migrations, asynch::AsyncMigrations, M};
/// let mut conn = tokio_rusqlite::Connection::open_in_memory().await.unwrap();
///
/// let migrations = AsyncMigrations::new(Migrations::new(vec![
/// M::up("CREATE TABLE animals (name TEXT);"),
/// M::up("CREATE TABLE food (name TEXT);"),
/// ]));
///
/// // Go to the latest version
/// migrations.to_latest(&mut conn).await.unwrap();
///
/// // You can then insert values in the database
/// conn.call(|conn| conn.execute("INSERT INTO animals (name) VALUES (?)", ["dog"])).await.unwrap();
/// conn.call(|conn| conn.execute("INSERT INTO food (name) VALUES (?)", ["carrot"])).await.unwrap();
/// # });
/// ```
pub async fn to_latest(&self, async_conn: &mut AsyncConnection) -> Result<()> {
let m = self.migrations.clone();
async_conn.call(move |conn| m.to_latest(conn)).await
}

/// Migrate the database to a given schema version. The migrations are applied atomically.
///
/// # Specifying versions
///
/// - Empty database (no migrations run yet) has version `0`.
/// - The version increases after each migration, so after the first migration has run, the schema version is `1`. For instance, if there are 3 migrations, version `3` is after all migrations have run.
///
/// *Note*: As a result, the version is the index in the migrations vector *starting from 1*.
///
/// # Example
///
/// ```rust
/// # tokio_test::block_on(async {
/// use rusqlite_migration::{Migrations, asynch::AsyncMigrations, M};
/// let mut conn = tokio_rusqlite::Connection::open_in_memory().await.unwrap();
/// let migrations = AsyncMigrations::new(Migrations::new(vec![
/// // 0: version 0, before having run any migration
/// M::up("CREATE TABLE animals (name TEXT);").down("DROP TABLE animals;"),
/// // 1: version 1, after having created the “animals” table
/// M::up("CREATE TABLE food (name TEXT);").down("DROP TABLE food;"),
/// // 2: version 2, after having created the food table
/// ]));
///
/// migrations.to_latest(&mut conn).await.unwrap(); // Create all tables
///
/// // Go back to version 1, i.e. after running the first migration
/// migrations.to_version(&mut conn, 1).await;
/// conn.call(|conn| conn.execute("INSERT INTO animals (name) VALUES (?)", ["dog"])).await.unwrap();
/// conn.call(|conn| conn.execute("INSERT INTO food (name) VALUES (?)", ["carrot"]).unwrap_err()).await;
///
/// // Go back to an empty database
/// migrations.to_version(&mut conn, 0).await;
/// conn.call(|conn| conn.execute("INSERT INTO animals (name) VALUES (?)", ["cat"]).unwrap_err()).await;
/// conn.call(|conn| conn.execute("INSERT INTO food (name) VALUES (?)", ["milk"]).unwrap_err()).await;
/// # })
/// ```
///
/// # Errors
///
/// Attempts to migrate to a higher version than is supported will result in an error.
///
/// When migrating downwards, all the reversed migrations must have a `.down()` variant,
/// otherwise no migrations are run and the function returns an error.
pub async fn to_version(&self, async_conn: &mut AsyncConnection, version: usize) -> Result<()> {
let m = self.migrations.clone();
async_conn
.call(move |conn| m.to_version(conn, version))
.await
}

/// Run migrations on a temporary in-memory database from first to last, one by one.
/// Convenience method for testing.
///
/// # Example
///
/// ```rust
/// #[cfg(test)]
/// mod tests {
///
/// // … Other tests …
///
/// #[tokio::test]
/// fn migrations_test() {
/// assert!(migrations.validate().await.is_ok());
/// }
/// }
/// ```
pub async fn validate(&self) -> Result<()> {
let mut async_conn = AsyncConnection::open_in_memory().await?;
self.to_latest(&mut async_conn).await
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ use log::{debug, info, trace, warn};
use rusqlite::NO_PARAMS;
use rusqlite::{Connection, OptionalExtension, Transaction};

/// Support for asynchonous migrations using tokio-rusqlite.
#[cfg(feature = "async-tokio-rusqlite")]
pub mod asynch;
mod errors;

#[cfg(test)]
Expand Down

0 comments on commit bd62538

Please sign in to comment.