Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/main/java/org/duckdb/DuckDBPreparedStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,41 @@ public int getUpdateCount() throws SQLException {
@Override
public boolean getMoreResults() throws SQLException {
checkOpen();
return moveToEnd(Statement.CLOSE_CURRENT_RESULT);
}

/**
* Moves the statement to its end-of-results state. DuckDB exposes at most one result per execution,
* so there is never a next result to expose and this always returns {@code false}.
*
* <p>{@code KEEP_CURRENT_RESULT} leaves the {@link ResultSet} handed out by {@link #getResultSet()}
* open/readable; it is no longer the statement's <i>current</i> result, so
* a following {@code CLOSE_CURRENT_RESULT} (or the no-argument overload) must not close it, while
* {@code CLOSE_ALL_RESULTS}, an explicit close, or the next execution still release the native result.
*/
private boolean moveToEnd(int flag) throws SQLException {
// returnsResultSet is only set while the last result is still the statement's "current" result.
// A kept result has been left behind; it is released only by CLOSE_ALL_RESULTS / close().
boolean hasCurrentResult = returnsResultSet;
DuckDBResultSet current = selectResult;

// Move to end-of-results before closing anything: closing can re-enter Statement#close() via
// closeOnCompletion, which must observe the updated state, not a result that is being closed.
returnsResultSet = false;
returnsChangedRows = false;
returnsNothing = false;
updateResult = -1;

if (Statement.KEEP_CURRENT_RESULT == flag) {
return false;
}
if (Statement.CLOSE_ALL_RESULTS == flag || hasCurrentResult) {
if (current != null) {
selectResult = null; // detach before close -> no closeOnCompletion recursion
selectResultReturned = false;
current.close();
}
}
return false;
}

Expand Down Expand Up @@ -870,7 +905,17 @@ public Connection getConnection() throws SQLException {
@Override
public boolean getMoreResults(int current) throws SQLException {
checkOpen();
return false;
switch (current) {
case Statement.CLOSE_CURRENT_RESULT:
case Statement.KEEP_CURRENT_RESULT:
case Statement.CLOSE_ALL_RESULTS:
return moveToEnd(current);
default:
throw createSQLException("Invalid getMoreResults flag " + current + ", expected Statement."
+ "CLOSE_CURRENT_RESULT, Statement.KEEP_CURRENT_RESULT or Statement."
+ "CLOSE_ALL_RESULTS",
ErrorCode.PREPARED_INVALID_MORE_RESULTS_FLAG);
}
}

@Override
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/duckdb/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ enum ErrorCode {
PREPARED_NO_QUERY(2414, "DuckDBPreparedStatement", SQLState.FUNCTION_SEQUENCE_ERROR,
"Query to execute not specified"),
PREPARED_STREAM_EXECUTE(2415, "DuckDBPreparedStatement", SQLState.HY000, "Streaming execute error"),
PREPARED_INVALID_MORE_RESULTS_FLAG(2416, "DuckDBPreparedStatement", SQLState.INVALID_ATTRIBUTE_VALUE,
"Invalid getMoreResults flag"),

// ------------------------------------------------------------------
// SCALAR / TABLE FUNCTION BUILDING (validation errors -> HY024 / HY000,
Expand Down
190 changes: 190 additions & 0 deletions src/test/java/org/duckdb/TestPrepare.java
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,194 @@ public static void test_prepared_statement_array_parameter() throws Exception {
}
}
}

public static void test_get_more_results_closes_current_result_set() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 42");
ResultSet rs = stmt.getResultSet();
assertNotNull(rs);
assertFalse(rs.isClosed());

// DuckDB returns a single result per execution, so there is no next result and the
// current ResultSet is closed implicitly, as required by JDBC.
assertFalse(stmt.getMoreResults());
assertTrue(rs.isClosed());

// The statement is in its end-of-results state.
assertNull(stmt.getResultSet());
assertEquals(stmt.getUpdateCount(), -1);
assertEquals(stmt.getLargeUpdateCount(), -1L);
}
}

public static void test_get_more_results_discards_update_count() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE t (i INT)");
assertFalse(stmt.execute("INSERT INTO t VALUES (42)"));

// The update count belongs to the current result. Moving past it must invalidate the
// count, otherwise a JDBC result loop sees a spurious update count at end-of-results.
assertFalse(stmt.getMoreResults());
assertEquals(stmt.getUpdateCount(), -1);
assertEquals(stmt.getLargeUpdateCount(), -1L);
assertNull(stmt.getResultSet());

// ... and it stays stable when called again.
assertFalse(stmt.getMoreResults());
assertEquals(stmt.getUpdateCount(), -1);
assertEquals(stmt.getLargeUpdateCount(), -1L);
}
}

public static void test_get_more_results_invalid_flag() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 42");

// JDBC: an argument that is not one of the three Statement constants is a SQLException.
// SQLFeatureNotSupportedException is reserved for unsupported KEEP/CLOSE_ALL flags.
try {
stmt.getMoreResults(42);
fail("expected SQLException for an invalid getMoreResults(int) flag");
} catch (SQLException e) {
assertFalse(e instanceof SQLFeatureNotSupportedException,
"invalid flag must be a plain SQLException but was " + e.getClass().getName());
assertTrue(e.getMessage().contains("42"),
"message should report the invalid flag but was: " + e.getMessage());
assertEquals(e.getErrorCode(), ErrorCode.PREPARED_INVALID_MORE_RESULTS_FLAG.getCode(), "error code");
assertEquals(e.getSQLState(), ErrorCode.PREPARED_INVALID_MORE_RESULTS_FLAG.getSQLState().getCode(),
"SQLState");
}

// The rejected call must not have consumed the current result.
ResultSet rs = stmt.getResultSet();
assertNotNull(rs);
assertFalse(stmt.getMoreResults());
assertTrue(rs.isClosed());
assertNull(stmt.getResultSet());
assertEquals(stmt.getUpdateCount(), -1);
}
}

public static void test_get_more_results_flags_close_results() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
for (int flag : new int[] {Statement.CLOSE_CURRENT_RESULT, Statement.CLOSE_ALL_RESULTS}) {
stmt.execute("SELECT 42");
ResultSet rs = stmt.getResultSet();
assertNotNull(rs);

assertFalse(stmt.getMoreResults(flag));
assertTrue(rs.isClosed(), "flag " + flag + " must close the current ResultSet");
assertNull(stmt.getResultSet());
assertEquals(stmt.getUpdateCount(), -1);
assertEquals(stmt.getLargeUpdateCount(), -1L);
}
}
}

public static void test_get_more_results_keep_current_result() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
// KEEP_CURRENT_RESULT is only allowed to throw SQLFeatureNotSupportedException when the
// driver cannot keep multiple results open; DuckDB reports that it can, so the flag is
// accepted and the ResultSet object handed out by getResultSet() stays usable.
assertTrue(conn.getMetaData().supportsMultipleOpenResults());

stmt.execute("SELECT 42");
ResultSet kept = stmt.getResultSet();
assertFalse(stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
assertFalse(kept.isClosed(), "KEEP_CURRENT_RESULT must not close the current ResultSet");
assertTrue(kept.next());
assertEquals(kept.getInt(1), 42);

// The statement itself still has no further result to expose.
assertNull(stmt.getResultSet());
assertEquals(stmt.getUpdateCount(), -1);
assertEquals(stmt.getLargeUpdateCount(), -1L);

kept.close();
assertTrue(kept.isClosed());
}
}

public static void test_get_more_results_statement_reusable() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 42");
assertNotNull(stmt.getResultSet());
assertFalse(stmt.getMoreResults());
assertNull(stmt.getResultSet());

// A new execution starts a fresh single-result lifecycle.
try (ResultSet rs = stmt.executeQuery("SELECT 43")) {
assertTrue(rs.next());
assertEquals(rs.getInt(1), 43);
assertFalse(rs.next());
}
assertFalse(stmt.isClosed());
assertEquals(stmt.getUpdateCount(), -1);
}
}

public static void test_get_more_results_close_on_completion() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
Statement stmt = conn.createStatement();
stmt.closeOnCompletion();
ResultSet rs = stmt.executeQuery("SELECT 42");
assertTrue(rs.next());

// The implicit close performed by getMoreResults() is a regular ResultSet close, so
// closeOnCompletion applies: the statement ends up closed, without recursion, detached
// reference or broken state.
assertFalse(stmt.getMoreResults());
assertTrue(rs.isClosed());
assertTrue(stmt.isClosed());
assertThrows(() -> stmt.getMoreResults(), SQLException.class);
}
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
PreparedStatement ps = conn.prepareStatement("SELECT 42");
ps.closeOnCompletion();
ResultSet rs = ps.executeQuery();
assertTrue(rs.next());
assertFalse(ps.getMoreResults());
assertTrue(rs.isClosed());
assertTrue(ps.isClosed());
}
}

public static void test_get_more_results_kept_result_survives_close_current() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 42");
ResultSet kept = stmt.getResultSet();
assertFalse(stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
assertFalse(kept.isClosed());

// A kept ResultSet is no longer the statement's *current* result, so neither the no-argument
// overload nor an explicit CLOSE_CURRENT_RESULT may close it.
assertFalse(stmt.getMoreResults());
assertFalse(kept.isClosed(), "no-arg getMoreResults() must not close a kept ResultSet");
assertFalse(stmt.getMoreResults(Statement.CLOSE_CURRENT_RESULT));
assertFalse(kept.isClosed(), "CLOSE_CURRENT_RESULT must not close a kept ResultSet");

// The kept object is still the live, readable result.
assertTrue(kept.next());
assertEquals(kept.getInt(1), 42);

// Only CLOSE_ALL_RESULTS (or an explicit close) releases the native result.
assertFalse(stmt.getMoreResults(Statement.CLOSE_ALL_RESULTS));
assertTrue(kept.isClosed(), "CLOSE_ALL_RESULTS must close the kept ResultSet");
}
}

public static void test_get_more_results_kept_result_released_by_statement_close() throws Exception {
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
Statement stmt = conn.createStatement();
stmt.execute("SELECT 42");
ResultSet kept = stmt.getResultSet();
assertFalse(stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
assertFalse(kept.isClosed());

// Closing the statement releases every result it handed out, kept ones included.
stmt.close();
assertTrue(stmt.isClosed());
assertTrue(kept.isClosed(), "Statement#close() must release a kept ResultSet");
}
}
}
Loading