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
- Emit BeginMutTx before any Insert/Delete pair and CommitTx after
- Track active-transaction state in the generator and forbid data ops outside a tx
- After Replay, begin a new transaction before further data ops
- 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
- Emit BeginMutTx before any Delete and CommitTx afterwards
- Remember that Replay discards the active transaction — re-begin before further deletes
- Model tx lifecycle explicitly in generators instead of emitting ops independently
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
- begin mutable transaction while one is already active
- insert without active mutable transaction
- commit without active mutable transaction
- database is not open
- cannot serialize refs without a typespace
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/5d25060c50a70e7a.
Report an issue: GitHub.