SeaQL/sea-orm · error

There is uncommitted nested transaction

Error message

There is uncommitted nested transaction

What it means

`into_transaction()` converts the accumulated mock statement log into a `Transaction`, but only when `transaction_depth` is 0 — i.e. all nested (SAVEPOINT) transactions have been committed or rolled back. If nested transactions are still open, flushing the log would lose/ambiguate statements, so it panics. Typically reached when a `MockExecResult`/statement collector is finalized (e.g. at drop or end of test) with unbalanced nesting.

Source

Thrown at src/database/mock.rs:434

            true
        } else {
            self.push(Statement::from_string(
                db_backend,
                format!("ROLLBACK TO SAVEPOINT savepoint_{}", self.transaction_depth),
            ));
            self.transaction_depth -= 1;
            false
        }
    }

    fn push(&mut self, stmt: Statement) {
        self.stmts.push(stmt);
    }

    fn into_transaction(self) -> Transaction {
        match self.transaction_depth {
            0 => Transaction { stmts: self.stmts },
            _ => panic!("There is uncommitted nested transaction"),
        }
    }
}

#[cfg(test)]
#[cfg(feature = "mock")]
mod tests {
    #[cfg(feature = "sync")]
    use crate::util::StreamShim;
    use crate::{
        DbBackend, DbErr, IntoMockRow, MockDatabase, Statement, Transaction, TransactionError,
        TransactionTrait, entity::*, error::*, tests_cfg::*,
    };
    // In the sync variant `StreamShim` provides `try_next`; `futures_util` isn't a dependency there.
    #[cfg(not(feature = "sync"))]
    use futures_util::TryStreamExt;
    use pretty_assertions::assert_eq;

View on GitHub (pinned to e29bcd1b41)

Solutions

  1. Commit or rollback every nested transaction before calling `into_transaction()`.
  2. Count begins vs commits/rollbacks in the test script; they must balance to depth 0.
  3. Fix early-return paths in the code under test so inner transactions always close (RAII/guard pattern).
  4. If inspecting the log mid-test, do it after all nesting has unwound.

Example fix

// before
begin(stmt), begin(stmt), commit(stmt)
into_transaction() // panics: depth still 1

// after
begin(stmt), begin(stmt), commit(stmt), commit(stmt)
into_transaction() // ok: depth 0
Defensive patterns

Strategy: validation

Validate before calling

assert_eq!(depth, 0, "cannot finalize with uncommitted nested transactions");
let tx = collector.into_transaction();

Type guard

fn can_finalize(c: &StatementCollector) -> bool {
    c.transaction_depth == 0
}

Try / catch

std::panic::catch_unwind(|| collector.into_transaction()) // prefer balancing nesting first

Prevention

When it happens

Trigger: Calling `into_transaction()` (directly or via drain/finalize paths) while `transaction_depth > 0`: a `begin` was issued but its matching `commit`/`rollback` never ran.

Common situations: Tests simulating nested transactions where one nesting level's commit was forgotten; early-return in code under test skipping the inner commit; asserting on the transaction log before closing all savepoints.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of SeaQL/sea-orm@e29bcd1b41 (2026-09-10). Data as JSON: /api/errors/552d7c10b97b7ae9. Report an issue: GitHub.