t8y2/dbx · error · SQLException

A manual transaction is already active

Error message

A manual transaction is already active

What it means

beginManualTransaction enforces a single active manual transaction via the static manualTransactionActive flag. If begin is called while a previous manual transaction was begun but not committed/rolled back, it throws this SQLException instead of nesting or silently reusing the existing transaction.

Source

Thrown at plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java:1044

                        }
                        rows.add(row);
                    }
                }
            }

            result.set("columns", columns);
            result.set("rows", rows);
            result.put("affected_rows", columns.isEmpty() ? Math.max(executed.updateCount(), 0) : 0);
            result.put("execution_time_ms", (System.nanoTime() - start) / 1_000_000);
            result.put("truncated", truncated);
            return result;
        }
    }

    private static ObjectNode beginManualTransaction(JsonNode connection, String database, String schema)
        throws SQLException {
        if (manualTransactionActive) {
            throw new SQLException("A manual transaction is already active");
        }
        Connection conn = openConnection(connection);
        DatabaseMetaData metadata = readMetadata(conn::getMetaData);
        Boolean supportsTransactions = metadata == null ? null : readMetadata(metadata::supportsTransactions);
        if (Boolean.FALSE.equals(supportsTransactions)) {
            throw new SQLFeatureNotSupportedException("This JDBC driver does not support transactions");
        }
        applyExecutionContext(connection, conn, database, schema);
        conn.setAutoCommit(false);
        manualTransactionActive = true;
        return okResult();
    }

    private static JsonNode executeInManualTransaction(
        JsonNode connection,
        String sql,
        String database,
        String schema,

View on GitHub (pinned to c0390bff16)

Solutions

  1. Commit or roll back the active manual transaction before starting a new one.
  2. Wrap transactional work in try/finally so commit/rollback always runs, resetting the flag even on failure.
  3. Check for an earlier operation that failed mid-transaction and left manualTransactionActive true; restart the session/plugin if stuck.
  4. Serialize transaction usage — the plugin's transaction state is process-wide static, so avoid concurrent manual transactions.

Example fix

// before
beginTx(conn);
runStatements();        // throws, no rollback
beginTx(conn);          // -> "A manual transaction is already active"
// after
beginTx(conn);
try {
    runStatements();
    commitTx(conn);
} catch (Exception e) {
    rollbackTx(conn);
    throw e;
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    plugin.beginManualTransaction(connCfg);
    // ... statements ...
    plugin.commit();
} catch (java.sql.SQLException e) {
    if (e.getMessage().contains("already active")) {
        plugin.rollback();               // clear the stuck transaction
        throw new IllegalStateException("Previous manual transaction was not finalized", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling the begin-transaction operation twice without an intervening commit or rollback on the same plugin instance — e.g. an earlier script path errored between begin and commit, or concurrent requests both hit beginManualTransaction.

Common situations: Exception in the middle of a manual transaction leaving the flag set, forgetting to call commit/rollback in an error handler, or two threads/tasks sharing the plugin's static state and racing on begin.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/8d8aebe3aba5112f. Report an issue: GitHub.