clockworklabs/SpacetimeDB · error · anyhow::Error

delete without active mutable transaction

Error message

delete without active mutable transaction

What it means

Delete interactions in the dst engine require an open mutable transaction: self.active_mut_tx was None because no BeginMutTx preceded the Delete, or the previous transaction already ended via CommitTx or was discarded by a Replay interaction.

Source

Thrown at crates/dst/src/engine.rs:236

                    Ok((_generated_columns, row, _flags)) => InsertOutcome::Accepted(row.to_product_value()),
                    // Generated rows can intentionally hit unique constraints; the oracle validates that rejection.
                    Err(error) if Self::is_unique_constraint_violation(&error) => {
                        InsertOutcome::UniqueConstraintViolation
                    }
                    Err(error) => return Err(error.into()),
                };
                Ok(Observation::Inserted { outcome })
            }
            Interaction::Delete { table, row } => {
                let table_id = self.table_ids[*table];
                let db = self
                    .db
                    .as_ref()
                    .ok_or_else(|| anyhow::anyhow!("database is not open"))?;
                let tx = self
                    .active_mut_tx
                    .as_mut()
                    .ok_or_else(|| anyhow::anyhow!("delete without active mutable transaction"))?;
                db.delete_by_rel(tx, table_id, [row.clone()]);
                Ok(Observation::Deleted)
            }
            Interaction::CommitTx => {
                let tx = self
                    .active_mut_tx
                    .take()
                    .ok_or_else(|| anyhow::anyhow!("commit without active mutable transaction"))?;
                let db = self
                    .db
                    .as_ref()
                    .ok_or_else(|| anyhow::anyhow!("database is not open"))?;
                let Some((_tx_offset, tx_data, _tx_metrics, _reducer)) = db.commit_tx(tx)? else {
                    anyhow::bail!("commit produced no transaction data");
                };
                Ok(Observation::Committed {
                    delta: self.commit_delta_from_tx_data(&tx_data),
                })

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Emit BeginMutTx before any Insert/Delete pair and CommitTx after
  2. Track active-transaction state in the generator and forbid data ops outside a tx
  3. After Replay, begin a new transaction before further data ops
  4. Assert in the driver that Delete only follows a successful BeganMutTx

Example fix

// before: delete outside any transaction
vec![Delete { .. }, CommitTx]

// after: open, delete, commit
vec![BeginMutTx, Delete { .. }, CommitTx]
Defensive patterns

Strategy: validation

Validate before calling

// Sequence-level guard before emitting a Delete
fn sequence_allows_delete(tx_active: bool) -> bool {
    tx_active
}

Prevention

When it happens

Trigger: Sequence beginning with Delete; Delete after CommitTx without a new BeginMutTx; Delete after Replay (which implicitly takes and drops the active transaction).

Common situations: Workload-generator state-machine bugs; hand-written interaction lists in DST tests; misunderstandings of Replay's implicit transaction discard.

Related errors


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