mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 09:38:29 +02:00
Embed migrations into Rust binary
This commit is contained in:
@@ -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
20
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user