tursodatabase/turso · error · InvalidOperationException

Execute requires the command to have a transaction when the

Error message

Execute requires the command to have a transaction when the connection has a pending local transaction.

What it means

If the SqliteConnection has a pending local transaction but the command's Transaction is null (and the text is not transaction control like BEGIN/COMMIT), EnsureExecutable throws InvalidOperationException. The provider will not guess which transaction a statement joins: auto-enlisting could commit your writes into a scope you meant to keep separate, so explicit enlistment is mandatory.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteCommand.cs:599

    private void EnsureExecutable(string method)
    {
        if (_hasOpenReader)
            throw new InvalidOperationException(Properties.Resources.DataReaderOpen);
        if (Connection is null || Connection.State != ConnectionState.Open)
            throw new InvalidOperationException(Properties.Resources.CallRequiresOpenConnection(method));
        if (Transaction is { IsCompleted: true } or { WasRolledBackExternally: true })
            throw new InvalidOperationException(Properties.Resources.TransactionCompleted);
        if (Transaction is not null && !ReferenceEquals(Transaction.Connection, Connection))
            throw new InvalidOperationException(Properties.Resources.TransactionConnectionMismatch);

        var connectionTransaction = Connection.Transaction;
        if (connectionTransaction is null || ReferenceEquals(Transaction, connectionTransaction))
            return;
        if (connectionTransaction.IsCompleted)
            throw new InvalidOperationException(Properties.Resources.TransactionCompleted);
        if (!IsTransactionControlCommand(CommandText))
            throw new InvalidOperationException(Properties.Resources.TransactionRequired);
    }

    private void CloseReader()
    {
        _hasOpenReader = false;
        Connection?.ReaderClosed();
    }

    internal Action OwnBufferedReader()
    {
        _hasOpenReader = true;
        Connection?.ReaderOpened();
        return () =>
        {
            CloseReader();
            Dispose();
        };
    }

View on GitHub (pinned to 6c72522679)

Solutions

  1. Assign cmd.Transaction = tx (the transaction you began) before executing inside a transactional scope.
  2. Or commit/roll back the pending connection transaction before running unenlisted commands.
  3. Centralize command creation so ambient-transaction enlistment is automatic.

Example fix

// before
using var tx = conn.BeginTransaction();
using var cmd = conn.CreateCommand();
cmd.CommandText = "UPDATE files SET seen = 1";
cmd.ExecuteNonQuery(); // throws: connection has a pending local transaction

// after
using var tx = conn.BeginTransaction();
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = "UPDATE files SET seen = 1";
cmd.ExecuteNonQuery();
tx.Commit();
Defensive patterns

Strategy: validation

Validate before calling

// keep the transaction you began; enlist every command created on that connection
SqliteTransaction tx = conn.BeginTransaction();

var cmd = conn.CreateCommand();
cmd.Transaction ??= tx; // never leave a command unenlisted while a tx is pending

Try / catch

catch (InvalidOperationException ex) when (ex.Message.Contains("pending local transaction")) {
    cmd.Transaction = tx; // enlist the tracked transaction, then retry once
    cmd.ExecuteNonQuery();
}

Prevention

When it happens

Trigger: conn.BeginTransaction() followed by creating a new SqliteCommand without setting .Transaction and executing it; commands built by helpers that do not know about the ambient transaction; executing raw SQL while a service-layer transaction wraps the connection.

Common situations: Repository methods managing their own commands while an outer service layer opens the transaction; refactors that added BeginTransaction without enlisting existing commands; ORM/raw-SQL mixes.

Related errors


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