Database.load. Two
engines, one model: backend-core uses an inline, tauri-plugin-sql-style engine; backend-diesel
uses Diesel’s embed_migrations! + MigrationHarness. Both run under the same crash-safe backup
protocol on disk.
Core backend (backend-core)
Define Migration values and register them on the builder, keyed by connection string:
The Migration struct
i64
The ordering and identity key.
Up migrations run in ascending order; a Down is matched to
its Up by sharing the same version.String
Human-readable label, recorded alongside the applied version.
String
The SQL to run. May contain multiple
;-separated statements (run via execute_batch).MigrationKind
MigrationKind::Up (schema-forward) or MigrationKind::Down (revert).add_migrations(conn_str, Vec<Migration>) is repeatable per connection string — later calls extend
the list.
How it runs
Onload, pending Up migrations run in ascending version order, each inside its own
transaction. A failing migration rolls that one back and surfaces the error; already-committed
earlier migrations stay (the backup protocol is the whole-batch
safety net). Applied versions are tracked in a __better_duck_migrations table:
Revert
db.revert() (the revert command) runs the Down SQL of the highest
applied version, in one transaction, deletes its bookkeeping row, and returns the reverted version
(or null if nothing was applied).
Diesel backend (backend-diesel)
Register Diesel embedded migrations instead of a Vec:
load, pending migrations run through Diesel’s MigrationHarness
(run_pending_migrations), wrapped in the same crash-safe backup skeleton as the core engine.
db.revert() calls the harness’s revert_last_migration and best-effort parses the Diesel version
string (a numeric timestamp) to an i64. In-memory migrations run on the shared pool handle;
read_only skips them.
See better-duck-diesel setup for authoring the embedded migration
directory and the schema/DSL side.
Crash-safe backup protocol
On-disk migrations go through a backup-in-place protocol, because the per-migration transactions mean a mid-batch failure could otherwise leave earlier migrations committed with no clean rollback. It also guards against file corruption or an interrupted process. The sequence runs only when something is pending:1
Recover an interrupted run
If a
<db>.migrate-journal exists from a prior interrupted run, restore from <db>.bak first
(a journal with no backup is treated as stale and removed).2
Checkpoint
CHECKPOINT to fold the WAL into the main file so a file-level copy is self-contained.3
Back up + journal
Copy the file to
<db>.bak (fsynced), then write the journal — its mere presence records “a
migration is in flight and did not confirm success”.4
Migrate, then clear
Run the migrations. On success, delete the journal first, then the backup (so a crash in
between leaves only a harmless stale backup). On failure, restore from the backup and surface
the error.
<db>.bak and <db>.migrate-journal (and <db>.wal /
<db>.tmp are cleaned on restore). The whole protocol runs before the long-lived connection
handle is opened, so migration has exclusive file access.
Special cases:
- In-memory databases (
duckdb::memory:) have no file — migrations run directly on the registered handle, with no backup protocol. read_only(true)skips migrations entirely (they are writes).
See also
- Maintenance — the
revertcommand in context. - Registration — where
add_migrationssits on the builder. - Backends — the one API difference between the two builders.