From 5490b1284adef4340ffceba12fdf3bfb1ed86249 Mon Sep 17 00:00:00 2001 From: Alex Kasko Date: Mon, 3 Aug 2026 20:42:19 +0100 Subject: [PATCH] Auto-close result returned from getResultSet (1.5) This is a backport of the PR #781 to `v1.5-variegata` stable branch. `ResultSet` objects returned from `Statement#executeQuery()` is closed automatically, when its parent statement is closed. Per JDBC specification: > When a Statement object is closed, its current ResultSet object, if > one exists, is also closed. Though, when a `ResultSet` is obtained from `Statement#execute()` + `Statement.getResultSet()`, such `ResultSet` was not considered to be a "current ResultSet object" because, according to the spec: > This method [`Statement.getResultSet()`] should be called only once per > result. Thus such result set was not closed automatically. Such code is used in practice and it has started to cause a memory leak (of the full materialized result set) after #533 was added. This PR fixes the leak by retaining the `ResultSet`, returned from `Statement.getResultSet()`, as a "current ResultSet object" and closing it automatically on `Statement` close. Testing: new tests added to `TestClosure` Fixes: #780 --- .../org/duckdb/DuckDBPreparedStatement.java | 10 ++++---- src/test/java/org/duckdb/TestClosure.java | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 0d056057b..c62f12973 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -53,6 +53,7 @@ public class DuckDBPreparedStatement implements PreparedStatement { volatile boolean closeOnCompletion = false; private DuckDBResultSet selectResult = null; + private boolean selectResultReturned = false; private long updateResult = 0; private DuckDBChunkedResult chunkedResult = null; @@ -231,6 +232,7 @@ public boolean execute() throws SQLException { cleanupCancelQueryTask(); DuckDBResultSetMetaData resultMeta = DuckDBNative.duckdb_jdbc_query_result_meta(resultRef); selectResult = new DuckDBResultSet(conn, this, resultMeta, resultRef); + selectResultReturned = false; returnsResultSet = resultMeta.return_type.equals(QUERY_RESULT); returnsChangedRows = resultMeta.return_type.equals(CHANGED_ROWS); returnsNothing = resultMeta.return_type.equals(NOTHING); @@ -607,14 +609,13 @@ public ResultSet getResultSet() throws SQLException { throw new SQLException("Statement was closed"); } - if (!returnsResultSet) { + if (!returnsResultSet || selectResultReturned) { return null; } // getResultSet can only be called once per result - ResultSet to_return = selectResult; - this.selectResult = null; - return to_return; + this.selectResultReturned = true; + return selectResult; } private long getUpdateCountInternal() throws SQLException { @@ -1374,6 +1375,7 @@ private void clearResults() throws SQLException { if (selectResult != null) { selectResult.close(); selectResult = null; + selectResultReturned = false; } if (chunkedResult != null) { chunkedResult.close(); diff --git a/src/test/java/org/duckdb/TestClosure.java b/src/test/java/org/duckdb/TestClosure.java index 8f8787e2a..6c4f549e6 100644 --- a/src/test/java/org/duckdb/TestClosure.java +++ b/src/test/java/org/duckdb/TestClosure.java @@ -110,6 +110,18 @@ public static void test_results_auto_closed_on_conn_close() throws Exception { assertTrue(stmt.isClosed()); } + public static void test_results_execute_auto_closed_on_conn_close() throws Exception { + Connection conn = DriverManager.getConnection(JDBC_URL); + Statement stmt = conn.createStatement(); + stmt.execute("select 42"); + ResultSet rs = stmt.getResultSet(); + assertNull(stmt.getResultSet()); + rs.next(); + conn.close(); + assertTrue(rs.isClosed()); + assertTrue(stmt.isClosed()); + } + public static void test_results_auto_closed_on_conn_close_prepared() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); PreparedStatement ps = conn.prepareStatement("select 42"); @@ -120,6 +132,18 @@ public static void test_results_auto_closed_on_conn_close_prepared() throws Exce assertTrue(ps.isClosed()); } + public static void test_results_execute_auto_closed_on_conn_close_prepared() throws Exception { + Connection conn = DriverManager.getConnection(JDBC_URL); + PreparedStatement ps = conn.prepareStatement("select 42"); + ps.execute(); + ResultSet rs = ps.getResultSet(); + assertNull(ps.getResultSet()); + rs.next(); + conn.close(); + assertTrue(rs.isClosed()); + assertTrue(ps.isClosed()); + } + public static void test_result_chunked_auto_closed_on_conn_close_prepared() throws Exception { DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); DuckDBPreparedStatement ps = conn.prepare("select 42");