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_tx

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Wrap every Insert/Delete block in BeginMutTx ... CommitTx
  2. Model tx-open/tx-closed as states in the generator and only emit data ops in the open state
  3. Remember Replay discards the active transaction — emit BeginMutTx again after it
  4. 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

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


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