{"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":"java.sql.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/244cde92a7df7f9b8b8b7a4075c35a12977e303e/bindings/java/src/main/java/tech/turso/core/TursoStatement.java#L250-L286","documentation":"TursoStatement.changes() asks the native layer (JNI, bindings/java/rs_src/turso_statement.rs:313) for the number of rows changed by the most recent statement execution on this connection. The native method returns -1 when the statement pointer cannot be resolved (zeroed pointer after close, or a native failure), and the Java wrapper at TursoStatement.java:268 converts any -1 into this generic SQLException. Seeing it almost always means the statement handle was invalid at call time, typically because the statement or its connection was already closed.","triggerScenarios":"Calling changes() on a TursoStatement after statement.close() has run (close() frees the native pointer); calling it after the parent TursoConnection.close(); reusing a statement object that a connection pool recycled and closed; any native error while reading the change counter that makes the JNI function return -1.","commonSituations":"try-with-resources blocks where changes() is read after the block ends; update-count reads moved into finally blocks that run after cleanup; wrapper/ORM layers (HikariCP proxy statements, MyBatis) that close or reset statements before the application reads the affected-row count; migration from sqlite-jdbc where lifetime rules differed slightly.","solutions":["Read changes() immediately after executeUpdate/step, inside the try (or try-with-resources) block while the statement is still open.","Guard the call: if (!stmt.isClosed()) { long n = stmt.changes(); } and skip or treat as 0 when closed.","If you need the value later, cache it in a local variable right after execution instead of querying the statement again.","For a connection-wide count use totalChanges() on a still-open statement, or open a fresh statement.","If the statement is verifiably open and it still fails, upgrade the turso Java bindings plus native library - a -1 with a live handle indicates a native-layer defect."],"exampleFix":"// before\ntry (TursoStatement stmt = conn.prepare(\"update t set x = 1\")) {\n    stmt.step();\n}\nlong n = stmt.changes(); // statement already closed -> SQLException\n\n// after\ntry (TursoStatement stmt = conn.prepare(\"update t set x = 1\")) {\n    stmt.step();\n    long n = stmt.changes(); // read while handle is alive\n    return n;\n}","handlingStrategy":"try-catch","validationCode":"if (stmt == null || stmt.isClosed()) {\n    // handle: statement already closed; skip or treat changes as 0\n} else {\n    long changed = stmt.changes();\n}","typeGuard":null,"tryCatchPattern":"try {\n    long n = stmt.changes();\n} catch (SQLException e) {\n    if (stmt.isClosed()) {\n        // closed-handle path: log and skip\n    } else {\n        throw e; // unexpected native failure\n    }\n}","preventionTips":["Read changes() inside the try-with-resources block, immediately after execution.","Cache the returned value in a local variable instead of re-querying later.","Never hold statement references across pool recycle boundaries.","Check isClosed() before calling metadata-style accessors in defensive paths."],"tags":["java","jdbc","turso","sqlite","statement-lifecycle","jni"],"backgroundTag":"closed-statement-handle","analyzedSha":"244cde92a7df7f9b8b8b7a4075c35a12977e303e","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}