clockworklabs/SpacetimeDB · error · anyhow::Error
insert without active mutable transaction
Error message
insert without active mutable transaction
What it means
Insert interactions in the dst engine require an open mutable transaction: self.active_mut_tx was None because no BeginMutTx was issued, or the previous transaction already ended via CommitTx (or was discarded by Replay).
Source
Thrown at crates/dst/src/engine.rs:216
);
let db = self
.db
.as_ref()
.ok_or_else(|| anyhow::anyhow!("database is not open"))?;
self.active_mut_tx = Some(db.begin_mut_tx(IsolationLevel::Serializable, Workload::Internal));
Ok(Observation::BeganMutTx)
}
Interaction::Insert { table, row } => {
let table_id = self.table_ids[*table];
let bytes = row_to_bytes(row);
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!("insert without active mutable transaction"))?;
let outcome = match db.insert(tx, table_id, &bytes) {
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_txView on GitHub (pinned to 6dee26c6ef)
Solutions
- Wrap every Insert/Delete block in BeginMutTx ... CommitTx
- Model tx-open/tx-closed as states in the generator and only emit data ops in the open state
- Remember Replay discards the active transaction — emit BeginMutTx again after it
- Add an assertion in the driver that data ops only follow a successful BeganMutTx observation
Example fix
// before: insert with no transaction open
vec![Insert { .. }, Delete { .. }, CommitTx]
// after: open a transaction first, then operate, then commit
vec![BeginMutTx, Insert { .. }, Delete { .. }, CommitTx] Defensive patterns
Strategy: validation
Validate before calling
// Sequence-level guard before emitting an Insert
fn sequence_allows_insert(tx_active: bool) -> bool {
tx_active
} Prevention
- Generate interactions from a tx state machine so data ops only appear inside an open transaction
- Begin a fresh transaction after every CommitTx or Replay before any data op
- Assert that Insert/Delete always follows a BeganMutTx observation in test properties
When it happens
Trigger: An interaction sequence starting with Insert instead of BeginMutTx; Insert after CommitTx without beginning a new transaction; Insert after a Replay interaction (which drops the active tx) without re-beginning.
Common situations: Workload-generator state machine bugs that skip the begin edge; hand-written interaction scripts in DST tests; refactors of the sequence builder.
Related errors
- begin mutable transaction while one is already active
- delete 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/30956b8da61a09b9.
Report an issue: GitHub.