From 34fa8680922949952b175999980720e816046fc8 Mon Sep 17 00:00:00 2001 From: Alex Kasko Date: Wed, 26 Nov 2025 17:00:13 +0000 Subject: [PATCH] Start transaction before prepare call Previously, when auto-commit if off, the implicit transaction was started automatically when `PreparedStatement#execute(void)` is called. This was found to cause the problems with `Statement#executeQuery(String)`, that calls `prepare(String)` and `execute()` internally. As transaction was starting only after `prepare` call - the `prepare` itself was running with effective auto-commit mode (in a separate implicit transaction). It is harmless in most cases, but was found to break transaction-wide caching in `duckdb-iceberg`. The problem was only happening when the `execute(String)` is running outside of active transaction, for example as a first call in freshly opened connection. Still this was causing non-negligible overhead. This change makes the implicit transaction to be started before `prepare` call, so the same transaction is active during `execute` call. There are no changes in logic when auto-commit is on (default per JDBC spec), so the Iceberg caching is still broken in this mode. This is intended to be fixed in future, for now to have effective caching with `duckdb-iceberg` it is necessary to disable auto-commit, it can be done by one of the following methods: In connection config: ```java Properties config = new Properties(); config.put(DuckDBDriver.JDBC_AUTO_COMMIT, false); Connection conn = DriverManager.getConnection(url, config); ``` In connection URL: ```java Connection conn = DriverManager.getConnection("jdbc:duckdb:path/to/my.db;jdbc_auto_commit=false;"); ``` On a connection manually: ```java conn.setAutoCommit(false); ``` Testing: there are no easily observable effects from this change, the transactional logic is covered by existing tests, Iceberg caching is tested manually. --- .../org/duckdb/DuckDBPreparedStatement.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 2368c7149..166db15e4 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -101,7 +101,7 @@ private boolean startTransaction() throws SQLException { this.conn.transactionRunning = true; // Start transaction via Statement try (Statement s = conn.createStatement()) { - s.execute("BEGIN TRANSACTION;"); + s.execute("BEGIN TRANSACTION"); return true; } } catch (NullPointerException e) { @@ -138,6 +138,11 @@ private void prepare(String sql) throws SQLException { conn.connRefLock.lock(); try { conn.checkOpen(); + + if (!isConnAutoCommit()) { + startTransaction(); + } + stmtRef = DuckDBNative.duckdb_jdbc_prepare(conn.connRef, sql.getBytes(UTF_8)); // Track prepared statement inside the parent connection conn.preparedStatements.add(this); @@ -156,10 +161,6 @@ private void prepare(String sql) throws SQLException { @Override public boolean execute() throws SQLException { - return execute(true); - } - - private boolean execute(boolean startTransaction) throws SQLException { checkOpen(); checkPrepared(); @@ -180,7 +181,7 @@ private boolean execute(boolean startTransaction) throws SQLException { } selectResult = null; - if (startTransaction && !isConnAutoCommit()) { + if (!isConnAutoCommit()) { startTransaction(); } @@ -657,7 +658,7 @@ private long[] executeBatchedPreparedStatement() throws SQLException { long[] updateCounts = new long[this.batchedParams.size()]; for (int i = 0; i < this.batchedParams.size(); i++) { params = this.batchedParams.get(i); - execute(false); + execute(); updateCounts[i] = getUpdateCountInternal(); } clearBatch(); @@ -690,7 +691,7 @@ private long[] executeBatchedStatements() throws SQLException { long[] updateCounts = new long[this.batchedStatements.size()]; for (int i = 0; i < this.batchedStatements.size(); i++) { prepare(this.batchedStatements.get(i)); - execute(false); + execute(); updateCounts[i] = getUpdateCountInternal(); } clearBatch();