tursodatabase/turso · error · SQLException

Exception while retrieving total number of changes

Error message

Exception while retrieving total number of changes

What it means

The Java wrapper calls the native totalChanges and throws when it returns -1. In the native implementation, -1 is returned only when the statement pointer cannot be resolved to a live statement (closed, finalized, or stale handle); a valid statement always returns the connection's change count. So this exception almost always means the statement was used after close.

Source

Thrown at bindings/java/src/main/java/tech/turso/core/TursoStatement.java:252

      bindDouble(parameterIndex, (Float) x);
    } else if (x instanceof Double) {
      bindDouble(parameterIndex, (Double) x);
    } else if (x instanceof byte[]) {
      bindBlob(parameterIndex, (byte[]) x);
    } else {
      throw new SQLException("Unsupported object type in bindObject: " + x.getClass().getName());
    }
  }

  /**
   * Returns total number of changes.
   *
   * @throws SQLException If a database access error occurs
   */
  public long totalChanges() throws SQLException {
    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");
    }

View on GitHub (pinned to bad083fafb)

Solutions

  1. Read totalChanges() before closing the statement (and inside the same try block as execution).
  2. Check stmt.isClosed() before querying counters in defensive code paths.
  3. Capture the value into a local variable immediately after execute/update, then close.
  4. Ensure only the owning thread closes/uses the statement.

Example fix

// before
try (TursoStatement stmt = conn.prepare(sql)) {
    stmt.step();
}
long total = stmt.totalChanges(); // throws: statement already closed

// after
long total;
try (TursoStatement stmt = conn.prepare(sql)) {
    stmt.step();
    total = stmt.totalChanges(); // read while still open
}
Defensive patterns

Strategy: validation

Validate before calling

if (!stmt.isClosed()) {
    long total = stmt.totalChanges();
} else {
    // statement already closed: re-run via a fresh statement if the count is needed
}

Try / catch

try {
    total = stmt.totalChanges();
} catch (SQLException e) {
    // native handle gone (statement closed) - treat as unknown rather than crash reporting
    total = -1;
    LOG.warn("totalChanges unavailable; statement closed", e);
}

Prevention

When it happens

Trigger: Calling stmt.totalChanges() after stmt.close(); after the owning connection was closed; after a pool reclaimed the statement; from a finalizer or cleanup thread racing with close().

Common situations: Reporting rows-affected after a try-with-resources block has already auto-closed the statement; utility code that closes statements early for resource hygiene but still wants change totals; races between request timeouts that abort/close statements and code that then reads counters.

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/36b8b6e0856a0149. Report an issue: GitHub.