From 438cdac5f2b9931153a9359acc92aade4fb57a8d Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 11 Mar 2024 10:17:32 +0100 Subject: [PATCH] refactor: Make sure result set can only be consumed once, add test for empty result set meta data Closes #419. --- .../neo4j/jdbc/it/cp/PreparedStatementIT.java | 29 +++++++++++++++++++ .../java/org/neo4j/jdbc/StatementImpl.java | 8 +++++ 2 files changed, 37 insertions(+) diff --git a/neo4j-jdbc-it/neo4j-jdbc-it-cp/src/test/java/org/neo4j/jdbc/it/cp/PreparedStatementIT.java b/neo4j-jdbc-it/neo4j-jdbc-it-cp/src/test/java/org/neo4j/jdbc/it/cp/PreparedStatementIT.java index c33f4d1ab..299a884d3 100644 --- a/neo4j-jdbc-it/neo4j-jdbc-it-cp/src/test/java/org/neo4j/jdbc/it/cp/PreparedStatementIT.java +++ b/neo4j-jdbc-it/neo4j-jdbc-it-cp/src/test/java/org/neo4j/jdbc/it/cp/PreparedStatementIT.java @@ -34,6 +34,8 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -233,6 +235,33 @@ void executeShouldAutomaticallyTranslate() throws SQLException { } } + // GH-419 + @Test + void resultSetMustNotThrowWhenEmpty() throws SQLException { + try (var connection = getConnection(); + var stmt = connection.prepareStatement("MATCH(p:Person) WHERE p.date >= ? AND p.date < ? RETURN p")) { + + var ts = Timestamp.from(Instant.now()); + stmt.setTimestamp(1, ts); + stmt.setTimestamp(2, ts); + stmt.execute(); + var resultSet = stmt.getResultSet(); + var resultSetMetaData = resultSet.getMetaData(); + int columnCount = resultSetMetaData.getColumnCount(); + assertThat(columnCount).isOne(); + for (int c = 1; c < columnCount + 1; c++) { + var label = resultSetMetaData.getColumnLabel(c); + assertThat(label).isEqualTo("p"); + var type = resultSetMetaData.getColumnType(c); + assertThat(type).isEqualTo(Types.NULL); + var typeName = resultSetMetaData.getColumnTypeName(c); + assertThat(typeName).isEmpty(); + } + resultSet.close(); + + } + } + @Nested class SymmetricTypeConversionInPreparedStatementAndResultSet { diff --git a/neo4j-jdbc/src/main/java/org/neo4j/jdbc/StatementImpl.java b/neo4j-jdbc/src/main/java/org/neo4j/jdbc/StatementImpl.java index 79b3a71d3..d61aa4211 100644 --- a/neo4j-jdbc/src/main/java/org/neo4j/jdbc/StatementImpl.java +++ b/neo4j-jdbc/src/main/java/org/neo4j/jdbc/StatementImpl.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.Logger; @@ -84,6 +85,8 @@ non-sealed class StatementImpl implements Neo4jStatement { private final Warnings warnings; + private final AtomicBoolean resultSetAcquired = new AtomicBoolean(false); + StatementImpl(Connection connection, Neo4jTransactionSupplier transactionSupplier, UnaryOperator sqlProcessor, Warnings localWarnings) { this.connection = Objects.requireNonNull(connection); @@ -121,6 +124,7 @@ protected final ResultSet executeQuery0(String sql, boolean applyProcessor, Map< var runAndPull = transaction.runAndPull(sql, getParameters(parameters), fetchSize, this.queryTimeout); this.resultSet = new ResultSetImpl(this, transaction, runAndPull.runResponse(), runAndPull.pullResponse(), this.fetchSize, this.maxRows, this.maxFieldSize); + this.resultSetAcquired.set(false); return this.resultSet; } @@ -287,6 +291,9 @@ else if (entry.getValue() instanceof InputStream inputStream) { @Override public ResultSet getResultSet() throws SQLException { assertIsOpen(); + if (!this.resultSetAcquired.compareAndSet(false, true)) { + throw new SQLException("Result set has already been acquired"); + } return (this.multipleResultsApi && this.updateCount == -1) ? this.resultSet : null; } @@ -472,6 +479,7 @@ private void closeResultSet() throws SQLException { if (this.resultSet != null) { this.resultSet.close(); this.resultSet = null; + this.resultSetAcquired.set(false); } }