A Refinery driver for SurrealDB, enabling database schema migrations using SurrealQL.
The API is not yet stable. Expect breaking changes before 1.0.0.
[dependencies]
surrealdb-refinery = "0.2"
surrealdb = "3.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }refinery's own types (Runner, Migration, Report, Target) are re-exported,
so there is no need to depend on refinery or refinery-core directly.
The minimum supported Rust version is 1.92.
protocol-ws and protocol-http are enabled by default, which is what
connect("ws://…") and connect("http://…") need. Embedded storage engines are
off by default, because a migration driver only needs the client API and
kv-rocksdb in particular forces a C++ RocksDB build on every consumer:
# for an embedded database, or for mem:// in tests
surrealdb-refinery = { version = "0.2", features = ["kv-mem"] }Available: protocol-ws, protocol-http, kv-mem, kv-rocksdb, kv-surrealkv.
Create migration files in a migrations/ directory:
migrations/V1__create_users.surql
DEFINE TABLE users SCHEMAFULL;
DEFINE FIELD name ON users TYPE string;
DEFINE FIELD email ON users TYPE string;
DEFINE INDEX email_idx ON users FIELDS email UNIQUE;Run them from your application:
use surrealdb_refinery::{MigrationConnection, Runner, load_migrations};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Discover `V{version}__{name}.surql` files on disk, ordered by version.
let migrations = load_migrations("migrations")?;
let db = surrealdb::engine::any::connect("mem://").await?;
// A namespace and database must be selected before migrating.
db.use_ns("myapp").await?;
db.use_db("myapp").await?;
let mut connection = MigrationConnection(&db);
let report = Runner::new(&migrations).run_async(&mut connection).await?;
for migration in report.applied_migrations() {
println!("applied V{} {}", migration.version(), migration.name());
}
Ok(())
}MigrationConnection borrows the Surreal handle, so pass &db. It is generic
over the connection type, so a local Surreal<Db> or a remote
Surreal<Client> works as well as Surreal<Any>. Do not change the session's
namespace or database while a run is in progress — the run holds no session of
its own and would continue against the new target.
Migration files follow the pattern V{version}__{description}.surql:
V1__initial_schema.surqlV2__add_users_table.surqlV3__add_indexes.surql
The .sql extension is also accepted. A file carrying either extension whose
name does not match the pattern is an error, not a warning — a silently skipped
migration is how a database ends up half-migrated.
Migrations must not wrap themselves in BEGIN / COMMIT. The driver runs
each migration in a transaction of its own, and SurrealDB refuses a nested
BEGIN with "Cannot BEGIN a transaction within a transaction". The batch is
cancelled, so nothing is applied — but the migration will not run until the
BEGIN and COMMIT lines are removed.
Each migration body and the history row recording it are executed in a single SurrealDB transaction. A migration therefore cannot be applied without being recorded, and if any statement fails the transaction is cancelled and nothing from that migration persists — including DDL, which SurrealDB rolls back too.
Applied migrations are recorded in refinery_schema_history, which can be
renamed with Runner::set_migration_table_name. A UNIQUE index on version
makes double-application an error rather than a duplicate row, which is what
stops two concurrent runners from both applying the same migration.
The table name must be a bare SurrealDB identifier: ASCII letters, digits and
underscores, not starting with a digit. refinery interpolates it into its own
INSERT unquoted, so a name needing quotes is rejected rather than silently
mishandled.
- Async only. There is no blocking
Migrateimplementation, justAsyncMigrate. - Migrations are read at runtime, not embedded at compile time. The
directory must exist wherever the binary runs. refinery's
embed_migrations!only discovers.sqland.rsfiles, which is why this crate does its own discovery. - The
surrealdbclient crate is BUSL-1.1 licensed, so that applies to your dependency graph even though this driver is Apache-2.0.
Licensed under the Apache License, Version 2.0.