3 releases (stable)

2.0.0 Jan 13, 2025
1.0.1 Jan 9, 2025
1.0.0 Jan 8, 2025
0.1.0 Jan 8, 2025

#43 in Database implementations

Download history 273/week @ 2025-01-06 92/week @ 2025-01-13 34/week @ 2025-01-20

399 downloads per month

MIT/Apache

72KB
1K SLoC

tern


A database migration library and CLI supporting embedded migrations written in SQL or Rust.

It aims to support static SQL migration sets, but expands to work with migration queries written in Rust that are either statically determined or that need to be dynamically built at the time of being applied, while being agnostic to the particular choice of crate for database interaction.

Executors

The abstract Executor is the thing ultimately responsible for actually connecting to a database and issuing queries. Right now, this project supports all of the sqlx pool types via the generic Pool, which includes PostgreSQL, MySQL, and SQLite. These can be enabled via feature flag.

Supporting more third-party crates is definitely desired! If yours is not available here, please feel free to contribute, either with a PR or feature request. Adding a new executor seems like it should not be hard.

Usage

Embedded migrations are prepared, built, and ran off a directory living in a Rust project's source. This directory can contain .rs and .sql files having names matching the regex ^V(\d+)__(\w+)\.(sql|rs)$, e.g., V13__create_a_table.sql or V5__create_a_different_table.rs.

The stages of a migration are handled by a few different traits, but implementing any of them manually is generally not necessary; tern exposes derive macros that do this.

  • MigrationSource: Prepares the migrations for use in some operation by parsing the directory into a sorted, uniform collection, and exposing methods to return subsets for a given operation.
  • MigrationContext: A type providing a context to perform the operation on the migrations provided by MigrationSource.

Put together, it looks like this.

use tern::executor::SqlxPgExecutor;
use tern::{MigrationSource, MigrationContext, Runner};

/// `$CARGO_MANIFEST_DIR/src/migrations` is a collection of migration files.
/// The optional `table` attribute permits a custom location for a migration
/// history table in the target database.
#[derive(MigrationSource, MigrationContext)]
#[tern(source = "src/migrations", table = "example")]
struct Example {
   // `Example` itself needs to be an executor without this annotation.
   #[tern(executor_via)]
    executor: SqlxPgExecutor,
}

let executor = SqlxPgExecutor::new("postgres://user@localhost").await.unwrap();
let context = Example { executor };
let mut runner = Runner::new(context);
let report: tern::Report = runner.apply_all().await.unwrap();
println!("{report:#?}");

For more in-depth examples, see the examples.

SQL migrations

Since migrations are embedded in the final executable, and static SQL migrations are not Rust source, any change to a SQL migration won't force a recompilation. The proc macro that parses these files will then not be up-to-date, and this can cause confusing issues. To remedy, a build.rs file can be put in the project directory with these contents:

fn main() -> {
    println!("cargo:rerun-if-changed=src/migrations/")
}

Rust migrations

Migrations can be expressed in Rust, and these can take advantage of the arbitrary migration context to flexibly build the query at runtime. For this to work, the derive macros get us nearly there, but the user needs to follow a couple rules and write an implementation of a trait to complete the requirements.

The first rule is that the type deriving MigrationSource be declared in super of the migrations. So if source = "src/migrations", a perfect place to put a MigrationContext/MigrationSource-deriving type is in the module src/migrations.rs, which would have to exist in any case. For now this is required because it's the easiest way to know for sure how to reference the module containing the migration when expanding the syntax coming from macros that need it.

The other requirement is that there be a struct called TernMigration in that migration source, and that it derives Migration. This is also required for now by an implementation detail of the macros: we need a way for the Migration macro to share data with the MigrationSource macro, or else not use Migration and parse the entire Rust source file within MigrationSource instead, which is clearly the least appealing option.

This TernMigration is what is needed to apply the migration when combined with the last thing required from the user: the actual query that should be ran and how it runs in the custom context. This is represented by the QueryBuilder trait:

use tern::error::TernResult;
use tern::migration::{Query, QueryBuilder};
use tern::Migration;

use super::Example;

/// Use the optional macro attribute `#[tern(no_transaction)]` to avoid
/// running this in a database transaction.
#[derive(Migration)]
pub struct TernMigration;

impl QueryBuilder for TernMigration {
    /// The custom-defined migration context.
    type Ctx = Example;

    /// When `await`ed, this should produce a valid SQL query wrapped by
    /// `Query`.  This is what will run against the database.
    async fn build(&self, ctx: &mut Self::Ctx) -> TernResult<Query> {
        // Really anything can happen here.  It just depends on what
        // `Self::Ctx` can do.
        let sql = "SELECT 1;";
        let query = Query::new(sql);
        Ok(query)
    }
}

Reversible migrations

As of now, the official stance is to not support an up-down style of migration set, the philosophy being that down migrations are not that useful in practice. The "Important Notes" section in this flyway documentation summarizes our feelings well.

Database transactions

By default, a migration and its accompanying schema history table update are ran in a database transaction. Sometimes this is not desirable and other times it is not allowed. For instance, in postgres you cannot create an index CONCURRENTLY in a transaction. To give the user the option, tern understands certain annotations and will not run that migration in a database transaction if they are present.

For a SQL migration:

-- tern:noTransaction is the annotation for SQL.  It needs to be found
-- somewhere on the first line of the file.
CREATE INDEX CONCURRENTLY IF NOT EXISTS blah ON whatever;

For a Rust migration:

use tern::Migration;

/// Don't run this in a migration.
#[derive(Migration)]
#[tern(no_transaction)]
pub struct TernMigration;

CLI

With the feature flag "cli" enabled the type [App] is exported, which is a CLI wrapping Runner methods that can be imported into your own migration project to turn it into a CLI.

> $ my-migration-project --help
Usage: my-migration-project <COMMAND>

Commands:
  migrate  Operations on the set of migration files
  history  Operations on the table storing the history of these migrations
  help     Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

Minimum supported Rust version

tern's MSRV is 1.81.0.

Licence

This project is licensed under either of:

Dependencies

~3–19MB
~282K SLoC