tursodatabase/turso · error · SQLException
Exception while retrieving number of changes
Error message
Exception while retrieving number of changes
What it means
The Java wrapper throws this when the native changes() call returns -1. The native code returns -1 exclusively when the statement pointer fails to resolve to a live statement, meaning the statement was closed, finalized, or its handle is stale. The change count itself is read from the connection and cannot otherwise fail.
Source
Thrown at bindings/java/src/main/java/tech/turso/core/TursoStatement.java:268
final long result = totalChanges(statementPointer);
if (result == -1) {
throw new SQLException("Exception while retrieving total number of changes");
}
return result;
}
private native long totalChanges(long statementPointer) throws SQLException;
/**
* Returns number of changes.
*
* @throws SQLException If a database access error occurs
*/
public long changes() throws SQLException {
final long result = changes(statementPointer);
if (result == -1) {
throw new SQLException("Exception while retrieving number of changes");
}
return result;
}
private native long changes(long statementPointer) throws SQLException;
/**
* Returns the number of parameters in this statement. Parameters are the `?`'s that get replaced
* by the provided arguments.
*
* @throws SQLException If a database access error occurs
*/
public int parameterCount() throws SQLException {
final int result = parameterCount(statementPointer);
if (result == -1) {
throw new SQLException("Exception while retrieving parameter count");
}View on GitHub (pinned to bad083fafb)
Solutions
- Call changes() immediately after executing, before any close().
- Store the result locally instead of re-querying after cleanup.
- Guard with isClosed() when counters are read in error/recovery paths.
- Avoid sharing one statement object across components that each may close it.
Example fix
// before stmt.execute(); stmt.close(); long affected = stmt.changes(); // throws: statement closed // after stmt.execute(); long affected = stmt.changes(); // read before close stmt.close();
Defensive patterns
Strategy: validation
Validate before calling
long affected = -1;
if (!stmt.isClosed()) {
affected = stmt.changes();
} Try / catch
try {
affected = stmt.changes();
} catch (SQLException e) {
affected = -1;
LOG.warn("changes() failed; statement no longer open", e);
} Prevention
- Capture rows-affected right after execute/update returns.
- Design APIs to return the affected count from the method that owns the statement.
- Avoid double-close and shared statements so the native handle stays valid.
- In retry logic, re-prepare the statement instead of reusing a closed handle.
When it happens
Trigger: Calling stmt.changes() after stmt.close(); after connection close; on a statement reclaimed by a pool; in cleanup/finalizer code that runs after the normal close path.
Common situations: Getting 'rows affected' for the last INSERT/UPDATE/DELETE after a try-with-resources block exited; long-lived statement fields that an error handler already closed; double-close patterns where the second consumer of a shared statement hits a finalized handle.
Related errors
- Exception while retrieving total number of changes
- Exception while retrieving parameter count
- Exception while resetting statement
- database connection closed
- Failed to convert ${sql} into bytes
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/3438041c3f4672ba.
Report an issue: GitHub.