Skip to content

fix(jdbc): close metadata statements through closeStatementOnClose (#119) - #127

Merged
magnusp merged 1 commit into
mainfrom
fix/i119-metadata-close
Oct 22, 2026
Merged

magnusp merged 1 commit into
mainfrom
fix/i119-metadata-close

Conversation

@magnusp

@magnusp magnusp commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Summary

Metadata result sets now close their statement through closeStatementOnClose instead of relying on closeOnCompletion. executeMetadataQuery no longer sets closeOnCompletion. It builds the result set with the flag set and attaches it to the statement. getColumns uses the same path, so every metadata result set exposes the statement that produced it and releases it when the caller closes the result set.

Contract basis. Hrana 3 is the source of truth for transport, values, and metadata, java.sql is the source of truth for the JDBC surface, and xerial/sqlite-jdbc is a behavioral reference only. This change is protocol-neutral. Metadata rows still come from execute responses, and JDBC owns the statement and result-set close contract.

Why

#91 closed metadata statements only because closeOnCompletion auto-closes when the result set closes. LibsqlResultSet.closeStatementOnClose already existed but no caller set it, so it was dead code. A metadata result set closed without that flag leaks its statement, and only MetadataLeakTest would notice, which exercises getTables alone.

#86 is resolved with the JDBC-first contract, so closeOnCompletion keeps its JDBC auto-close and is not changed to xerial's track-only behavior. This PR is about explicit statement ownership in the metadata code, not about defending against a #86 change that will not happen. The explicit flag is still the right contract because metadata code should not depend on a flag another class may set.

Metadata methods covered

Each method below has its own leak-count case in LibsqlDatabaseMetaDataTest. Every case asserts that the result set exposes a LibsqlStatement, that isCloseOnCompletion() is false, and that closing the result set closes the statement.

getTables, getColumns, getPrimaryKeys, getTableTypes, getSchemas, getSchemas(catalog, schemaPattern), getCatalogs, getImportedKeys, getExportedKeys, getCrossReference, getTypeInfo, getIndexInfo.

LibsqlDatabaseMetaData now has one result-set construction site, new LibsqlResultSet(statement, result, originSql, true), and all twelve methods reach it through executeMetadataQuery.

Red and green

Red at base 766e4c5 with only the new test file applied. Each method fails for the intended reason, eleven on must not rely on closeOnCompletion and getColumns on must expose the statement that produced it.

[ERROR] Tests run: 23, Failures: 12, Errors: 0, Skipped: 0, Time elapsed: 0.126 s <<< FAILURE! -- in com.github.magnusp.libsql.jdbc.LibsqlDatabaseMetaDataTest
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getTables must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:155 [getColumns must expose the statement that produced it]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getPrimaryKeys must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getTableTypes must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getSchemas must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getSchemas(catalog, schemaPattern) must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getCatalogs must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getTypeInfo must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getImportedKeys must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getExportedKeys must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getCrossReference must not rely on closeOnCompletion to close its statement]
[ERROR]   LibsqlDatabaseMetaDataTest.closesStatementWithoutCloseOnCompletion:159 [getIndexInfo must not rely on closeOnCompletion to close its statement]

Green at head d95594b.

[INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0 -- in com.github.magnusp.libsql.jdbc.LibsqlDatabaseMetaDataTest

Verification

Base 766e4c5, head d95594b4d90b8e41b6c0c449f48ba4f7234ad24a.

Lane Command Result
1. Regression ./mvnw -pl libsql-jdbc test -Dtest=LibsqlDatabaseMetaDataTest at base and head Base 12 failures, head 23 pass
2. Vendored leak test ./mvnw -pl libsql-conformance -am verify -Dit.test=MetadataLeakTest 1 pass, no reliance on closeOnCompletion
3. Flag off ./mvnw -pl libsql-jdbc test -Dtest='LibsqlDatabaseMetaDataTest#closesStatementWithoutCloseOnCompletion' 12 pass; isCloseOnCompletion() is false and counts return to baseline
4. Module suite ./mvnw -pl libsql-jdbc test 221 pass, BUILD SUCCESS
5. Conformance ./mvnw -pl libsql-conformance -am verify 115 pass, 1 skipped, BUILD SUCCESS, no new divergence
6. xerial control ./mvnw -pl libsql-conformance -am -Pxerial-control verify 100 pass, 1 skipped, BUILD SUCCESS, no differences to record
7. Format ./mvnw spotless:check BUILD SUCCESS, no violations
8. Reactor ./mvnw test 221 pass, BUILD SUCCESS
9. Construction sites grep -n "new LibsqlResultSet" LibsqlDatabaseMetaData.java One site, flag true; zero closeOnCompletion references
10. Package ./mvnw -pl libsql-jdbc -am package -DskipTests BUILD SUCCESS
Perf ./mvnw -pl libsql-jdbc test three times per side, interleaved Trunk median 2148 ms, head median 2163 ms, +0.7 percent, within the 10 percent rule

Divergences and scope

No protocol divergence. The protocol has no statement lifecycle field, so JDBC defines it.

The product-version statement in resolveDatabaseProductVersion still opens a statement and closes only its result set. The queue plan scopes that fix to #92, so it is not part of this PR and has no leak-count case here.

getColumns previously built its result set with no statement, so getStatement() returned null. It now has a statement like the other metadata methods. Its result set still passes a null origin SQL because the column labels are synthesized and must not drive column-origin resolution.

Files

libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java, libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java.

Fixes #119

)

Metadata result sets used to close their statement only because
executeMetadataQuery set closeOnCompletion, which made the
closeStatementOnClose flag in LibsqlResultSet dead code. Set the flag at
the single result-set construction site instead, and drop the
closeOnCompletion call so the close contract no longer depends on it.

getColumns now builds its result set through the same statement-backed
path, so every metadata result set exposes the statement that produced
it and carries the flag.

Fixes #119
@magnusp
magnusp merged commit 8754013 into main Oct 22, 2026
5 checks passed
@magnusp
magnusp deleted the fix/i119-metadata-close branch October 22, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metadata statement cleanup depends on closeOnCompletion behavior that #86 proposes to change

1 participant