Skip to content

feat(jdbc): implement getGeneratedKeys for inserts - #82

Merged
magnusp merged 3 commits into
mainfrom
fix/39-generated-keys
Oct 21, 2026
Merged

magnusp merged 3 commits into
mainfrom
fix/39-generated-keys

Conversation

@magnusp

@magnusp magnusp commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Closes #39. Part of #54.

What changed

  • LibsqlStatement.getGeneratedKeys() now returns the generated key after an INSERT or REPLACE (including WITH ... INSERT and INSERT ... RETURNING). It returns a fresh empty one-column result when no insert has run.
  • LibsqlPreparedStatement produces the same generated-key result for prepared inserts.
  • Generated-key results are cleared by a later statement execution and when the statement closes, matching sqlite-jdbc.
  • LibsqlDatabaseMetaData.supportsGetGeneratedKeys() now returns true, matching the implemented behavior.
  • Added LibsqlGeneratedKeysTest (unit) and GeneratedKeysIT (live sqld).

Contract decision

Matches xerial 3.53.4.0 CoreStatement.java:181-210:

  • After an insert, the result has one column named last_insert_rowid() and one row containing the key.
  • Without an insert, getGeneratedKeys() returns one empty column instead of throwing.
  • Insert detection accepts the same INSERT/REPLACE-with-optional-CTE forms as xerial, using a linear scanner instead of xerial's backtracking regex.
  • executeQuery does not replace an earlier generated key, matching xerial's updateGeneratedKeys call sites.

The key comes from the Hrana execute result's last_insert_rowid field instead of a follow-up SELECT last_insert_rowid(). Auto-commit statements run on a one-shot Hrana stream, so a follow-up query observes a different session. Live sqld confirms the insert result carries the key while a separate pipeline's select last_insert_rowid() returns 0. This is not an observable deviation; it is how the same-session value is obtained before the stream closes.

Evidence

Failing before the fix (./mvnw -pl libsql-jdbc -am test -Dtest=LibsqlGeneratedKeysTest -Dsurefire.failIfNoSpecifiedTests=false):

Tests run: 8, Failures: 1, Errors: 7, Skipped: 0
ERROR ... getGeneratedKeysWithoutExecuteIsEmpty ... SQLException: getGeneratedKeys not supported directly
ERROR ... statementInsertReturnsGeneratedKey ... SQLException: getGeneratedKeys not supported directly
ERROR ... preparedStatementInsertReturnsGeneratedKey ... SQLException: getGeneratedKeys not supported directly
FAILURE ... supportsGetGeneratedKeysMatchesImplementedBehavior ... expected true but was false

Passing after the fix:

  • ./mvnw -pl libsql-jdbc -am test — Tests run: 148, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.
  • DOCKER_CONFIG=/tmp/docker-config-empty ./mvnw -pl libsql-itests -am verify — Tests run: 21, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.
  • ./mvnw -pl libsql-conformance -am verify — Tests run: 115, Failures: 0, Errors: 0, Skipped: 1, BUILD SUCCESS.
  • ./mvnw spotless:apply then ./mvnw spotless:check — BUILD SUCCESS.

Docker config workaround used: nono denies reads of ~/.docker/config.json, so the integration run used DOCKER_CONFIG=/tmp/docker-config-empty with /tmp/docker-config-empty/config.json containing {"auths":{}}. The ghcr.io/tursodatabase/libsql-server:latest image is public, so no credentials were needed.

The live test covers statement inserts, prepared inserts, inserts inside a transaction, empty results for non-inserts, and two concurrent connections each getting their own key. It also shows the session-state risk directly: a separate one-shot select last_insert_rowid() returns 0, so generated keys must come from the insert's own result.

Deviations

None.

@magnusp
magnusp merged commit 8b7f967 into main Oct 21, 2026
5 checks passed
@magnusp
magnusp deleted the fix/39-generated-keys branch October 21, 2026 23:32
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.

getGeneratedKeys is unimplemented

2 participants