{"record":{"id":"3438041c3f4672ba","repo":"tursodatabase/turso","slug":"exception-while-retrieving-number-of-changes","errorCode":null,"errorMessage":"Exception while retrieving number of changes","messagePattern":"Exception while retrieving number of changes","errorType":"exception","errorClass":"SQLException","httpStatus":null,"severity":"error","filePath":"bindings/java/src/main/java/tech/turso/core/TursoStatement.java","lineNumber":268,"sourceCode":"    final long result = totalChanges(statementPointer);\n    if (result == -1) {\n      throw new SQLException(\"Exception while retrieving total number of changes\");\n    }\n\n    return result;\n  }\n\n  private native long totalChanges(long statementPointer) throws SQLException;\n\n  /**\n   * Returns number of changes.\n   *\n   * @throws SQLException If a database access error occurs\n   */\n  public long changes() throws SQLException {\n    final long result = changes(statementPointer);\n    if (result == -1) {\n      throw new SQLException(\"Exception while retrieving number of changes\");\n    }\n\n    return result;\n  }\n\n  private native long changes(long statementPointer) throws SQLException;\n\n  /**\n   * Returns the number of parameters in this statement. Parameters are the `?`'s that get replaced\n   * by the provided arguments.\n   *\n   * @throws SQLException If a database access error occurs\n   */\n  public int parameterCount() throws SQLException {\n    final int result = parameterCount(statementPointer);\n    if (result == -1) {\n      throw new SQLException(\"Exception while retrieving parameter count\");\n    }","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/bindings/java/src/main/java/tech/turso/core/TursoStatement.java#L250-L286","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nstmt.execute();\nstmt.close();\nlong affected = stmt.changes(); // throws: statement closed\n\n// after\nstmt.execute();\nlong affected = stmt.changes(); // read before close\nstmt.close();","handlingStrategy":"validation","validationCode":"long affected = -1;\nif (!stmt.isClosed()) {\n    affected = stmt.changes();\n}","typeGuard":null,"tryCatchPattern":"try {\n    affected = stmt.changes();\n} catch (SQLException e) {\n    affected = -1;\n    LOG.warn(\"changes() failed; statement no longer open\", e);\n}","preventionTips":["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."],"tags":["java","jdbc","changes","rows-affected","statement-lifecycle","use-after-close"],"backgroundTag":"closed-statement-access","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}