From 0b33b7a93b292a9159bba1086a9078a34be80633 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:09:07 +0200 Subject: [PATCH] fix(jdbc): close the product-version statement (#92) resolveDatabaseProductVersion wrapped only the result set in try-with-resources, so the statement stayed open after the version was read. Wrap the statement too, and pin the leak with a statement-count test that also confirms the cached second call opens nothing. --- .../libsql/jdbc/LibsqlDatabaseMetaData.java | 3 +- .../jdbc/LibsqlDatabaseMetaDataTest.java | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java index e67e72b..0488771 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java @@ -35,7 +35,8 @@ public LibsqlDatabaseMetaData(LibsqlConnection connection) { private String resolveDatabaseProductVersion() throws SQLException { if (cachedDatabaseProductVersion == null) { - try (var rs = connection.createStatement().executeQuery("SELECT sqlite_version()")) { + try (var statement = connection.createStatement(); + var rs = statement.executeQuery("SELECT sqlite_version()")) { cachedDatabaseProductVersion = rs.next() ? rs.getString(1) : "3.0"; } catch (SQLException e) { cachedDatabaseProductVersion = "3.0"; diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java index 28fdf32..e870792 100644 --- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java @@ -13,6 +13,7 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.List; @@ -36,6 +37,33 @@ void getTablesClosesResultSetWhenStatementCloses() throws SQLException { assertThat(resultSet.isClosed()).isTrue(); } + @Test + void getDatabaseProductVersionClosesItsStatement() throws SQLException { + RecordingConnection connection = new RecordingConnection(versionResult("3.45.1")); + + new LibsqlDatabaseMetaData(connection).getDatabaseProductVersion(); + + assertThat(connection.statements) + .as("the product-version lookup must open exactly one statement") + .hasSize(1); + assertThat(connection.statements.get(0).isClosed()) + .as("the product-version lookup must close its statement") + .isTrue(); + } + + @Test + void getDatabaseProductVersionOpensOneStatementAcrossTwoCalls() throws SQLException { + RecordingConnection connection = new RecordingConnection(versionResult("3.45.1")); + LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(connection); + + assertThat(metadata.getDatabaseProductVersion()).isEqualTo("3.45.1"); + assertThat(metadata.getDatabaseProductVersion()).isEqualTo("3.45.1"); + + assertThat(connection.statements) + .as("the second product-version lookup must hit the cache and open nothing") + .hasSize(1); + } + @Test void escapesSingleQuoteInTableNamePattern() throws SQLException { RecordingConnection connection = new RecordingConnection(emptyResult()); @@ -280,6 +308,14 @@ private static StmtResult emptyResult() { return new StmtResult(List.of(new Col("TABLE_NAME", "TEXT")), List.of(), 0, null); } + private static StmtResult versionResult(String version) { + return new StmtResult( + List.of(new Col("sqlite_version()", "TEXT")), + List.of(new Row(List.of(Value.of(version)))), + 0, + null); + } + private static final class StmtResultConnection extends LibsqlConnection { private final StmtResult result; @@ -391,6 +427,7 @@ private static String literalAfter(String sql, String prefix, String suffix) thr private static final class RecordingConnection extends LibsqlConnection { private final StmtResult result; + private final List statements = new ArrayList<>(); private String lastSql; private RecordingConnection(StmtResult result) { @@ -398,6 +435,13 @@ private RecordingConnection(StmtResult result) { this.result = result; } + @Override + public Statement createStatement() throws SQLException { + LibsqlStatement statement = (LibsqlStatement) super.createStatement(); + statements.add(statement); + return statement; + } + @Override public StmtResult executeStatement(Stmt stmt) throws SQLException { lastSql = stmt.sql();