Zackriya-Solutions/meetily · error · anyhow::Error
DB error: {}
Error message
DB error: {} What it means
sqlx failed to acquire a connection from the SQLite pool. The inner '{}' says why: typically 'database is locked' (SQLITE_BUSY, another connection or process is writing), the pool was closed, or a disk I/O error. Note the pool is created without busy_timeout/WAL options, so lock contention surfaces immediately instead of waiting.
Source
Thrown at frontend/src-tauri/src/audio/retranscription.rs:431
// Check for cancellation
if RETRANSCRIPTION_CANCELLED.load(Ordering::SeqCst) {
return Err(anyhow!("Retranscription cancelled"));
}
emit_progress(&app, &meeting_id, "saving", 80, "Saving transcripts...");
// Create transcript segments with proper timestamps from VAD
let segments = create_transcript_segments(&all_transcripts);
// Save to database
let app_state = app
.try_state::<AppState>()
.ok_or_else(|| anyhow!("App state not available"))?;
// Wrap delete+insert+update in a transaction to prevent data loss
let pool = app_state.db_manager.pool();
let mut conn = pool.acquire().await.map_err(|e| anyhow!("DB error: {}", e))?;
let mut tx = sqlx::Connection::begin(&mut *conn)
.await
.map_err(|e| anyhow!("Failed to start transaction: {}", e))?;
sqlx::query("DELETE FROM transcripts WHERE meeting_id = ?")
.bind(&meeting_id)
.execute(&mut *tx)
.await
.map_err(|e| anyhow!("Failed to delete existing transcripts: {}", e))?;
for segment in &segments {
sqlx::query(
"INSERT INTO transcripts (id, meeting_id, transcript, timestamp, audio_start_time, audio_end_time, duration)
VALUES (?, ?, ?, ?, ?, ?, ?)"
)
.bind(&segment.id)
.bind(&meeting_id)
.bind(&segment.text)View on GitHub (pinned to 0281737d87)
Solutions
- Read the inner sqlx error string to classify: locked vs pool closed vs I/O error.
- Close every other process using the database file, then retry the retranscription.
- Configure the SQLite pool with WAL journal mode and a busy_timeout so writers wait instead of failing.
- If 'pool closed' appears, the app is shutting down - restart and retry.
Example fix
// before (typical pool creation without wait options)
let pool = SqlitePoolOptions::new().connect(&db_url).await?;
// after: wait for locks instead of failing instantly
let opts = SqliteConnectOptions::from_url(&db_url)?
.journal_mode(SqliteJournalMode::Wal)
.busy_timeout(std::time::Duration::from_secs(5));
let pool = SqlitePoolOptions::new().connect_with(opts).await?; Defensive patterns
Strategy: retry
Try / catch
// Retry acquisition with backoff for transient locks
let conn = match pool.acquire().await {
Ok(c) => c,
Err(e) => {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
pool.acquire().await.map_err(|e2| anyhow!("DB error: {e2} (first: {e})"))?
}
}; Prevention
- Run only one app instance per data directory.
- Configure the SQLite pool with WAL + busy_timeout so lock contention waits instead of erroring.
- Keep an eye on disk space and file permissions for the DB.
When it happens
Trigger: A second app instance or another in-app writer (import, live transcript save) holds the SQLite write lock at save time; Pool::close was called; the DB file was deleted or the disk filled while the app runs.
Common situations: Two instances of the app open the same data directory; retranscription save colliding with a background DB write; an external sqlite3 CLI inspecting the file.
Related errors
- DB error: {}
- Failed to start transaction: {}
- Failed to start transaction: {}
- Failed to create meeting: {}
- Failed to insert transcript: {}
AI-assisted analysis of Zackriya-Solutions/meetily@0281737d87 (2026-08-16).
Data as JSON: /api/errors/a791057a16da2a26.
Report an issue: GitHub.