clockworklabs/SpacetimeDB · critical

Deletion for non-existent table {table_id:?}... huh?

Error message

Deletion for non-existent table {table_id:?}... huh?

What it means

While applying a committed transaction's deletions, the datastore looks up each table targeted by delete operations (get_table_and_blob_store_mut). The Err arm with an empty row_ptrs set is tolerated as a no-op (deleting zero rows from a missing table), but finding actual row pointers to delete in a table absent from the committed state means transaction state is inconsistent, so the code panics ('... huh?'). This is an internal locking-datastore invariant violation, not an API misuse by callers.

Source

Thrown at crates/datastore/src/locking_tx_datastore/committed_state.rs:662

                let truncated = table.row_count == 0;
                if truncated {
                    truncates.insert(table_id);
                }
            }
        }

        for (table_id, row_ptrs) in delete_tables {
            match self.get_table_and_blob_store_mut(table_id) {
                Ok((table, blob_store, ..)) => delete_rows(
                    tx_data,
                    table_id,
                    table,
                    blob_store,
                    row_ptrs.len(),
                    row_ptrs.iter(),
                    truncates,
                ),
                Err(_) if !row_ptrs.is_empty() => panic!("Deletion for non-existent table {table_id:?}... huh?"),
                Err(_) => {}
            }
        }

        // Delete all tables marked for deletion.
        // The order here does not matter as once a `table_id` has been dropped
        // it will never be re-created.
        for change in pending_schema_changes {
            if let PendingSchemaChange::TableRemoved(table_id, mut table) = change {
                let row_ptrs = table.scan_all_row_ptrs();
                truncates.insert(table_id);
                delete_rows(
                    tx_data,
                    table_id,
                    &mut table,
                    &mut self.blob_store,
                    row_ptrs.len(),
                    row_ptrs.into_iter(),

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Reproduce with a minimal reducer sequence (create/insert/delete/drop on one table) and report upstream with the panic backtrace.
  2. Upgrade to the latest spacetimedb patch release, where invariant fixes land.
  3. Work around by separating schema changes from data mutations: stop writers, apply the drop, then resume.
Defensive patterns

Strategy: try-catch

Try / catch

// isolate engine panics at the task boundary so one bad commit does not kill the worker
let outcome = tokio::task::spawn_blocking(move || run_reducers(db, tx)).await;
match outcome {
    Ok(r) => r,
    Err(join_err) if join_err.is_panic() => {
        // datastore invariant violation: snapshot workload, report upstream, restart worker
        report_invariant(join_err.into_panic());
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Committing a MutTx whose delete set references rows in a table that no longer exists in committed state, e.g. interleaved table-drop schema changes with pending row deletions in the same epoch (the code right below also processes pending_schema_changes TableRemoved).

Common situations: Almost always a spacetimedb engine bug; can surface under concurrent reducers mixing DDL (drop/truncate) with row deletion on the same table, or after an upgrade changed schema-change bookkeeping.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/fbebe1bfd9e304ff. Report an issue: GitHub.