Embed migrations into Rust binary

This commit is contained in:
Gregory Schier
2025-06-07 19:25:36 -07:00
parent d0fde99b1c
commit 1abe01aa5a
51 changed files with 44 additions and 31 deletions

View File

@@ -27,7 +27,7 @@ async function createMigration() {
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, '../src-tauri/migrations');
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {

20
src-tauri/Cargo.lock generated
View File

@@ -2475,6 +2475,25 @@ dependencies = [
"tiff",
]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -7654,6 +7673,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"hex",
"include_dir",
"log",
"nanoid",
"r2d2",

View File

@@ -57,7 +57,6 @@
],
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
"resources": [
"migrations",
"vendored/protoc/include",
"vendored/plugins",
"vendored/plugin-runtime"

View File

@@ -7,6 +7,8 @@ publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
hex = "0.4.3"
include_dir = "0.7"
log = "0.4.22"
nanoid = "0.4.0"
r2d2 = "0.8.10"
@@ -22,7 +24,6 @@ tauri-plugin-dialog = { workspace = true }
thiserror = "2.0.11"
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
hex = "0.4.3"
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -59,7 +59,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.build(manager)
.unwrap();
if let Err(e) = migrate_db(app_handle.app_handle(), &pool) {
if let Err(e) = migrate_db(&pool) {
error!("Failed to run database migration {e:?}");
app_handle
.dialog()

View File

@@ -1,26 +1,16 @@
use crate::error::Error::MigrationError;
use crate::error::Result;
use log::info;
use include_dir::{include_dir, Dir, DirEntry};
use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, TransactionBehavior, params};
use rusqlite::{params, OptionalExtension, TransactionBehavior};
use sha2::{Digest, Sha384};
use std::fs;
use std::path::Path;
use std::result::Result as StdResult;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime};
pub(crate) fn migrate_db<R: Runtime>(
app_handle: &AppHandle<R>,
pool: &Pool<SqliteConnectionManager>,
) -> Result<()> {
let migrations_dir = app_handle
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource");
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
info!("Running database migrations from: {:?}", migrations_dir);
pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
info!("Running database migrations");
// Ensure the table exists
// NOTE: Yaak used to use sqlx for migrations, so we need to mirror that table structure. We
@@ -39,20 +29,22 @@ pub(crate) fn migrate_db<R: Runtime>(
)?;
// Read and sort all .sql files
let mut entries = fs::read_dir(migrations_dir)
.expect("Failed to find migrations directory")
.filter_map(StdResult::ok)
let mut entries = MIGRATIONS_DIR
.entries()
.into_iter()
.filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false))
.collect::<Vec<_>>();
// Ensure they're in the correct order
entries.sort_by_key(|e| e.file_name());
entries.sort_by_key(|e| e.path());
// Run each migration in a transaction
let mut num_migrations = 0;
for entry in entries {
num_migrations += 1;
let mut conn = pool.get()?;
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
match run_migration(entry.path().as_path(), &mut tx) {
match run_migration(entry, &mut tx) {
Ok(_) => tx.commit()?,
Err(e) => {
let msg = format!(
@@ -66,16 +58,15 @@ pub(crate) fn migrate_db<R: Runtime>(
};
}
info!("Finished running migrations");
info!("Finished running {} migrations", num_migrations);
Ok(())
}
fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Result<bool> {
fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
let start = std::time::Instant::now();
let (version, description) =
split_migration_filename(migration_path.file_name().unwrap().to_str().unwrap())
.expect("Failed to parse migration filename");
let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap())
.expect("Failed to parse migration filename");
// Skip if already applied
let row: Option<i64> = tx
@@ -85,11 +76,13 @@ fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Resul
.optional()?;
if row.is_some() {
debug!("Skipping migration {description}");
// Migration was already run
return Ok(false);
}
let sql = fs::read_to_string(migration_path).expect("Failed to read migration file");
let sql =
migration_path.as_file().unwrap().contents_utf8().expect("Failed to read migration file");
info!("Applying migration {description}");
// Split on `;`? → optional depending on how your SQL is structured