tursodatabase/turso · error · SQLException

Failed to convert ${sql} into bytes

Error message

Failed to convert ${sql} into bytes

What it means

SQLException thrown in TursoConnection.prepare when stringToUtf8ByteArray(sql) returns null — which happens only when the SQL string itself is null. The statement text must be UTF-8 encoded bytes before being passed to the native prepareUtf8, so a null statement cannot be compiled and is rejected with this message (the runtime text interpolates the null sql).

Source

Thrown at bindings/java/src/main/java/tech/turso/core/TursoConnection.java:135

  /**
   * Compiles an SQL statement with optional transaction start check.
   *
   * @param sql An SQL statement.
   * @param checkTransaction Whether to check and start transaction if needed.
   * @return Pointer to statement.
   * @throws SQLException if a database access error occurs.
   */
  private TursoStatement prepare(String sql, boolean checkTransaction) throws SQLException {
    logger.trace("DriverManager [{}] [SQLite EXEC] {}", Thread.currentThread().getName(), sql);

    // Ensure transaction is started if needed (lazy transaction start)
    if (checkTransaction) {
      ensureTransactionStarted(sql);
    }

    byte[] sqlBytes = stringToUtf8ByteArray(sql);
    if (sqlBytes == null) {
      throw new SQLException("Failed to convert " + sql + " into bytes");
    }
    return new TursoStatement(sql, prepareUtf8(connectionPtr, sqlBytes));
  }

  private native long prepareUtf8(long connectionPtr, byte[] sqlUtf8) throws SQLException;

  // TODO: check whether this is still valid for turso
  /**
   * Checks whether the type, concurrency, and holdability settings for a {@link ResultSet} are
   * supported by the SQLite interface. Supported settings are:
   *
   * <ul>
   *   <li>type: {@link ResultSet#TYPE_FORWARD_ONLY}
   *   <li>concurrency: {@link ResultSet#CONCUR_READ_ONLY})
   *   <li>holdability: {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}
   * </ul>
   *
   * @param resultSetType the type setting.

View on GitHub (pinned to bad083fafb)

Solutions

  1. Null-check the SQL before preparing and fail fast with a meaningful application error or skip the statement
  2. Trace the null to its source — usually a missing config value or an absent mapping
  3. Make SQL constants non-null final so absence is caught at startup, not at query time

Example fix

// before
String sql = config.get("query.delete"); // null when the key is missing
try (TursoStatement st = conn.prepare(sql)) { st.execute(); } // Failed to convert null into bytes

// after
String sql = Objects.requireNonNull(config.get("query.delete"), "missing query.delete config");
try (TursoStatement st = conn.prepare(sql)) { st.execute(); }
Defensive patterns

Strategy: validation

Validate before calling

if (sql == null || sql.isBlank()) {
  throw new SQLException("SQL must be a non-empty string");
}
try (TursoStatement st = conn.prepare(sql)) {
  st.execute();
}

Type guard

static boolean isPreparable(String sql) {
  return sql != null && !sql.isBlank();
}

Prevention

When it happens

Trigger: conn.prepare(null) or createStatement-level paths passing a null SQL string; a String variable that is null because a lookup, config key, or optional mapping produced nothing; an ORM/query-builder path that forwards null for a skipped query.

Common situations: Optional SQL sourced from configuration that was never set; refactors that moved SQL constants and left a null default; NPE-avoidance patterns that substitute null and push the failure to prepare time.

Related errors


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