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

  1. Read the inner sqlx error string to classify: locked vs pool closed vs I/O error.
  2. Close every other process using the database file, then retry the retranscription.
  3. Configure the SQLite pool with WAL journal mode and a busy_timeout so writers wait instead of failing.
  4. 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

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


AI-assisted analysis of Zackriya-Solutions/meetily@0281737d87 (2026-08-16). Data as JSON: /api/errors/a791057a16da2a26. Report an issue: GitHub.