From 36508424e947fc047ba731d85da15db8f3479d11 Mon Sep 17 00:00:00 2001 From: thijs-s Date: Fri, 30 Jan 2026 22:15:19 +0100 Subject: [PATCH 1/2] Fix optimizer crash in JDBC prepare by initializing active_query Initialize `active_query` in `ClientContext::PrepareInternal` to ensure the optimizer has a valid context during statement preparation. This fixes a null pointer dereference encountered by optimizer extensions when accessing `GetCurrentQuery()` via the JDBC driver. The implementation uses a new `ActiveQueryGuard` RAII helper to ensure strict exception safety and proper resource cleanup. Added regression tests in `TestOptimizerCrash.java`. --- src/duckdb/src/main/client_context.cpp | 26 +++++++- .../java/org/duckdb/TestOptimizerCrash.java | 61 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/duckdb/TestOptimizerCrash.java diff --git a/src/duckdb/src/main/client_context.cpp b/src/duckdb/src/main/client_context.cpp index 998cb977..ce8471f9 100644 --- a/src/duckdb/src/main/client_context.cpp +++ b/src/duckdb/src/main/client_context.cpp @@ -82,6 +82,26 @@ struct ActiveQueryContext { BaseQueryResult *open_result = nullptr; }; +//! RAII wrapper that ensures the active query is reset if an exception occurs during preparation +struct ActiveQueryGuard { + unique_ptr &active_query; + bool set_active_query; + + ActiveQueryGuard(unique_ptr &active_query_p, const string &query) + : active_query(active_query_p), set_active_query(false) { + if (!active_query) { + active_query = make_uniq(); + set_active_query = true; + active_query->query = query; + } + } + ~ActiveQueryGuard() { + if (set_active_query) { + active_query.reset(); + } + } +}; + #ifdef DEBUG struct DebugClientContextState : public ClientContextState { ~DebugClientContextState() override { @@ -711,7 +731,11 @@ unique_ptr ClientContext::PrepareInternal(ClientContextLock & shared_ptr prepared_data; auto unbound_statement = statement->Copy(); RunFunctionInTransactionInternal( - lock, [&]() { prepared_data = CreatePreparedStatement(lock, statement_query, std::move(statement), {}); }, + lock, + [&]() { + ActiveQueryGuard guard(active_query, statement_query); + prepared_data = CreatePreparedStatement(lock, statement_query, std::move(statement), {}); + }, false); prepared_data->unbound_statement = std::move(unbound_statement); return make_uniq(shared_from_this(), std::move(prepared_data), std::move(statement_query), diff --git a/src/test/java/org/duckdb/TestOptimizerCrash.java b/src/test/java/org/duckdb/TestOptimizerCrash.java new file mode 100644 index 00000000..686cc740 --- /dev/null +++ b/src/test/java/org/duckdb/TestOptimizerCrash.java @@ -0,0 +1,61 @@ +package org.duckdb; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +public class TestOptimizerCrash { + public static void test_optimizer_access() throws Exception { + Class.forName("org.duckdb.DuckDBDriver"); + try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { + // preparing a statement triggers the optimizer + try (PreparedStatement stmt = conn.prepareStatement("SELECT 42")) { + stmt.execute(); + } + } + } + + public static void test_optimizer_crash_on_exception() throws Exception { + Class.forName("org.duckdb.DuckDBDriver"); + try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { + try { + // This should fail during prepare/plan + conn.prepareStatement("SELECT * FROM non_existent_table"); + } catch (SQLException e) { + // Expected + } + + // This should succeed if active_query was reset correctly + try (PreparedStatement stmt = conn.prepareStatement("SELECT 42")) { + stmt.execute(); + } + } + } + + public static void test_optimizer_simple_statement() throws Exception { + Class.forName("org.duckdb.DuckDBDriver"); + try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { + try (Statement stmt = conn.createStatement()) { + stmt.execute("SELECT 42"); + } + } + } + + public static void test_optimizer_crash_on_prepare_fail_repeated() throws Exception { + Class.forName("org.duckdb.DuckDBDriver"); + try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { + for (int i = 0; i < 10; i++) { + try { + conn.prepareStatement("SELECT * FROM non_existent_table"); + } catch (SQLException e) { + // Expected + } + } + try (PreparedStatement stmt = conn.prepareStatement("SELECT 42")) { + stmt.execute(); + } + } + } +} From e75a9d7441480fa97b061071b69f012bdd05eeed Mon Sep 17 00:00:00 2001 From: thijs-s Date: Fri, 30 Jan 2026 22:28:54 +0100 Subject: [PATCH 2/2] remove temporary test file --- .../java/org/duckdb/TestOptimizerCrash.java | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/test/java/org/duckdb/TestOptimizerCrash.java diff --git a/src/test/java/org/duckdb/TestOptimizerCrash.java b/src/test/java/org/duckdb/TestOptimizerCrash.java deleted file mode 100644 index 686cc740..00000000 --- a/src/test/java/org/duckdb/TestOptimizerCrash.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.duckdb; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Statement; - -public class TestOptimizerCrash { - public static void test_optimizer_access() throws Exception { - Class.forName("org.duckdb.DuckDBDriver"); - try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { - // preparing a statement triggers the optimizer - try (PreparedStatement stmt = conn.prepareStatement("SELECT 42")) { - stmt.execute(); - } - } - } - - public static void test_optimizer_crash_on_exception() throws Exception { - Class.forName("org.duckdb.DuckDBDriver"); - try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { - try { - // This should fail during prepare/plan - conn.prepareStatement("SELECT * FROM non_existent_table"); - } catch (SQLException e) { - // Expected - } - - // This should succeed if active_query was reset correctly - try (PreparedStatement stmt = conn.prepareStatement("SELECT 42")) { - stmt.execute(); - } - } - } - - public static void test_optimizer_simple_statement() throws Exception { - Class.forName("org.duckdb.DuckDBDriver"); - try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { - try (Statement stmt = conn.createStatement()) { - stmt.execute("SELECT 42"); - } - } - } - - public static void test_optimizer_crash_on_prepare_fail_repeated() throws Exception { - Class.forName("org.duckdb.DuckDBDriver"); - try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) { - for (int i = 0; i < 10; i++) { - try { - conn.prepareStatement("SELECT * FROM non_existent_table"); - } catch (SQLException e) { - // Expected - } - } - try (PreparedStatement stmt = conn.prepareStatement("SELECT 42")) { - stmt.execute(); - } - } - } -}