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 timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`; 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); const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) { if (!fs.existsSync(migrationsDir)) {

20
src-tauri/Cargo.lock generated
View File

@@ -2475,6 +2475,25 @@ dependencies = [
"tiff", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@@ -7654,6 +7673,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"hex", "hex",
"include_dir",
"log", "log",
"nanoid", "nanoid",
"r2d2", "r2d2",

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.build(manager) .build(manager)
.unwrap(); .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:?}"); error!("Failed to run database migration {e:?}");
app_handle app_handle
.dialog() .dialog()

View File

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