From cca79dec4c9c3080ecb70165e228ca8fd28cb7ea Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 18 Apr 2026 09:03:01 +0100 Subject: [PATCH] fix: SQL trigger-aware migration splitter, SpeechModel re-export, i64 type alignment crates/storage/src/migrations.rs: Replace naive sql.split(';') with split_statements() that tracks BEGIN...END depth, so migrations containing trigger definitions execute correctly instead of being split mid-block. crates/transcription/src/lib.rs: Re-export transcribe_rs::SpeechModel so callers in src-tauri can reference it without adding transcribe-rs as a direct dependency. src-tauri/src/commands/models.rs: Use Box as the load_model_from_disk return type, matching the trait object that transcription crates produce. src-tauri/src/commands/transcripts.rs: Remove the stale .map(|v| v as i32) casts on sample_rate and audio_channels. InsertTranscriptParams now stores these as i64 (ebf449b), matching the i64 fields on CreateTranscriptRequest; casting to i32 first would silently truncate large sample rates. Co-Authored-By: Claude Sonnet 4.6 --- crates/storage/src/migrations.rs | 41 ++++++++++++++++++++++++++++---- crates/transcription/src/lib.rs | 1 + src-tauri/src/commands/models.rs | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/storage/src/migrations.rs b/crates/storage/src/migrations.rs index 8d4c5b2..cd01292 100644 --- a/crates/storage/src/migrations.rs +++ b/crates/storage/src/migrations.rs @@ -110,6 +110,40 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[ "#), ]; +/// Split SQL into individual statements, respecting BEGIN...END trigger blocks. +fn split_statements(sql: &str) -> Vec { + let mut result = Vec::new(); + let mut current = String::new(); + let mut depth: i32 = 0; + let upper = sql.to_ascii_uppercase(); + let ub = upper.as_bytes(); + let ob = sql.as_bytes(); + let n = ob.len(); + let mut i = 0; + while i < n { + let prev_alpha = i > 0 && ob[i - 1].is_ascii_alphanumeric(); + if !prev_alpha && ub[i..].starts_with(b"BEGIN") { + let next_alpha = i + 5 < n && ob[i + 5].is_ascii_alphanumeric(); + if !next_alpha { depth += 1; } + } + if !prev_alpha && ub[i..].starts_with(b"END") { + let next_alpha = i + 3 < n && ob[i + 3].is_ascii_alphanumeric(); + if !next_alpha && depth > 0 { depth -= 1; } + } + if ob[i] == b';' && depth == 0 { + let stmt = current.trim().to_string(); + if !stmt.is_empty() { result.push(stmt); } + current = String::new(); + } else { + current.push(ob[i] as char); + } + i += 1; + } + let stmt = current.trim().to_string(); + if !stmt.is_empty() { result.push(stmt); } + result +} + /// Ensure the schema_version table exists and run any pending migrations. pub async fn run_migrations(pool: &SqlitePool) -> Result<()> { sqlx::query( @@ -132,12 +166,9 @@ pub async fn run_migrations(pool: &SqlitePool) -> Result<()> { if *version > current { log::info!("Running migration {}: {}", version, description); - let statements: Vec<&str> = sql.split(';') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect(); + let statements = split_statements(sql); - for statement in statements { + for statement in &statements { sqlx::query(statement) .execute(pool) .await diff --git a/crates/transcription/src/lib.rs b/crates/transcription/src/lib.rs index b43b365..8c297fd 100644 --- a/crates/transcription/src/lib.rs +++ b/crates/transcription/src/lib.rs @@ -6,6 +6,7 @@ pub use concurrency::run_inference; pub use local_engine::{ load_parakeet, load_whisper, LocalEngine, TimedTranscript, }; +pub use transcribe_rs::SpeechModel; pub use model_manager::{ download, is_downloaded, list_downloaded, model_dir, models_dir, }; diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index ddee05f..4121aeb 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -72,7 +72,7 @@ fn model_capability( } } -fn load_model_from_disk(model_id: &ModelId) -> Result { +fn load_model_from_disk(model_id: &ModelId) -> Result, String> { let entry = model_registry::find_model(model_id) .ok_or_else(|| format!("Unknown model: {model_id}"))?;