tursodatabase/turso · error · InvalidOperationException

Parallel transactions are not supported.

Error message

Parallel transactions are not supported.

What it means

BeginTransaction throws InvalidOperationException when the connection already exposes a non-null Transaction, because SQLite permits only one active transaction per connection. There is no nested-transaction support and no automatic commit of the previous one; the second BeginTransaction is rejected outright. Note that disposing a SqliteTransaction rolls it back, so a transaction whose Dispose was skipped also keeps this guard tripped.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.cs:346

    {
        ArgumentNullException.ThrowIfNull(connection);
    }

    public new virtual SqliteTransaction BeginTransaction()
        => BeginTransaction(IsolationLevel.Unspecified);

    public virtual SqliteTransaction BeginTransaction(bool deferred)
        => BeginTransaction(IsolationLevel.Unspecified, deferred);

    public new virtual SqliteTransaction BeginTransaction(IsolationLevel isolationLevel)
        => BeginTransaction(isolationLevel, deferred: isolationLevel == IsolationLevel.ReadUncommitted);

    public virtual SqliteTransaction BeginTransaction(IsolationLevel isolationLevel, bool deferred)
    {
        if (State != ConnectionState.Open)
            throw new InvalidOperationException(Properties.Resources.CallRequiresOpenConnection(nameof(BeginTransaction)));
        if (Transaction is not null)
            throw new InvalidOperationException(Properties.Resources.ParallelTransactionsNotSupported);

        Transaction = new SqliteTransaction(this, isolationLevel, deferred);
        return Transaction;
    }

    public virtual void CreateCollation(string name, Comparison<string>? comparison)
    {
        RegisterCollation(name, comparison is null ? null : (left, right) => comparison(left, right));
    }

    public virtual void CreateCollation<T>(string name, T state, Func<T, string, string, int>? comparison)
    {
        RegisterCollation(name, comparison is null ? null : (left, right) => comparison(state, left, right));
    }

    public virtual void CreateFunction<TResult>(string name, Func<TResult>? function, bool isDeterministic = false)
    {
        RegisterScalarFunction(name, 0, isDeterministic, function is null ? null : _ => function());

View on GitHub (pinned to 6c72522679)

Solutions

  1. Commit, roll back, or dispose the existing transaction before starting a new one ('await using' / 'using' on the SqliteTransaction guarantees this).
  2. Pass the existing transaction down (set cmd.Transaction = conn.Transaction) instead of starting a second one; or use savepoints ('SAVEPOINT sp1') for nested semantics on SQLite 3.31+.
  3. Restructure so exactly one layer owns transaction boundaries.

Example fix

// before
using var conn = new SqliteConnection(cs);
conn.Open();
var tx1 = conn.BeginTransaction();
var tx2 = conn.BeginTransaction(); // throws: Parallel transactions are not supported

// after
using var conn = new SqliteConnection(cs);
conn.Open();
using (var tx1 = conn.BeginTransaction())
{
    // do work; reuse tx1 on all commands, or nest with savepoints:
    conn.ExecuteNonQuery("SAVEPOINT sp1");
    // ...
    conn.ExecuteNonQuery("RELEASE sp1");
}
Defensive patterns

Strategy: validation

Validate before calling

static SqliteTransaction? ExistingOrNew(SqliteConnection conn)
    => conn.Transaction ?? conn.BeginTransaction();

// always scope transactions so Dispose rolls back on error:
// using var tx = conn.BeginTransaction();

Try / catch

null

Prevention

When it happens

Trigger: Calling BeginTransaction twice without Commit/Rollback/Dispose in between; wrapping code that starts its own transaction inside an outer transactional block; helper methods that 'ensure a transaction' unconditionally; forgetting 'using' on the first transaction so it stays active after an exception.

Common situations: Unit-of-work patterns layered over repository methods that also transact, re-entrant service code, and error paths that skip transaction cleanup leaving Transaction set on a pooled/reused connection instance.

Related errors


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20). Data as JSON: /api/errors/259ce80413d1797b. Report an issue: GitHub.