Skip to content

bug: ensure_workspace_schema_and_backfill silently ignores ALTER TABLE failures that abort DuckDB transactions #345

Description

@evan-zhang11

Problem

The ensure_workspace_schema_and_backfill function in backend/src/db.rs uses let _ = conn.execute(...) to ignore errors from schema migration statements:

fn ensure_workspace_schema_and_backfill(conn: &duckdb::Connection) {
    let _ = conn.execute("ALTER TABLE users ADD COLUMN current_workspace_id VARCHAR", []);
    let _ = conn.execute("ALTER TABLE files ADD COLUMN workspace_id VARCHAR", []);
    let _ = conn.execute("ALTER TABLE workspaces ADD COLUMN slug VARCHAR", []);
    // ... more ALTER TABLE statements ...
    let _ = conn.execute("DROP INDEX IF EXISTS idx_fonts_workspace_fontstack", []);

    recover_detached_workspace_members(conn).expect("Failed to recover detached workspace members");
    backfill_workspace_data(conn).expect("Failed to backfill workspace data");
}

Impact

In DuckDB 1.5.4+, a failed DDL statement (e.g., adding a column that already exists) can cause the transaction to abort. When this happens, ALL subsequent statements fail with:

TransactionContext Error: Current transaction is aborted (please ROLLBACK)

This is the root cause of the PR #344 CI failures — all 158 backend tests fail because the database cannot initialize. The same pattern affects the PostGIS integration tests (6 failures) and E2E tests (50 failures).

The recover_detached_workspace_members call panics with .expect(), crashing the server on startup.

Reproduction

  1. Initialize a DuckDB database with DuckDB 1.5.4 (spatial extension 1.5.4)
  2. Call init_database() on a database that already has some of the schema columns
  3. The ALTER TABLE statements fail silently, aborting the transaction
  4. recover_detached_workspace_members panics

Fix Suggestion

  1. Replace let _ = conn.execute(...) with error-aware logic that checks if the error is "column already exists" (expected) vs. a real failure
  2. If the transaction is aborted, call ROLLBACK before continuing
  3. Consider wrapping each migration step in its own transaction
  4. Replace .expect() with proper error handling that logs the failure and exits gracefully
fn run_migration_step(conn: &duckdb::Connection, sql: &str) {
    if let Err(e) = conn.execute(sql, []) {
        let msg = e.to_string();
        // These errors are expected when the column/index already exists
        if !msg.contains("already exists") && !msg.contains("duplicate") {
            tracing::warn!(sql = %sql, error = %msg, "Unexpected migration error");
            // Try to recover transaction state
            let _ = conn.execute("ROLLBACK", []);
        }
    }
}

Severity

Critical — Blocks all CI, prevents server startup after DuckDB 1.5.4 upgrade.

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions