apache/shardingsphere · error · InvalidStatementHandleException

Invalid statement handle: %d

Error message

Invalid statement handle: %d

What it means

InvalidStatementHandleException thrown by FirebirdBatchCreateCommandExecutor when the client asks to create a batch for a statement handle that is not in the connection's ServerPreparedStatementRegistry. It mirrors Firebird's native 'Invalid statement handle' error: you cannot batch-execute a statement that was never prepared (or was already freed) on this connection.

Source

Thrown at proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/batch/FirebirdBatchCreateCommandExecutor.java:77

    
    private static final int TAG_BLOB_POLICY = 4;
    
    // private static final int BLOB_STREAM = 3;
    
    private static final int WIDE_CLUMPLET_LENGTH_SIZE = 4;
    
    private static final int INTEGER_VALUE_LENGTH = 4;
    
    private final FirebirdBatchCreateCommandPacket packet;
    
    private final ConnectionSession connectionSession;
    
    @Override
    public Collection<DatabasePacket> execute() throws SQLException {
        int connectionId = connectionSession.getConnectionId();
        int statementId = packet.getStatementHandle();
        if (null == connectionSession.getServerPreparedStatementRegistry().getPreparedStatement(statementId)) {
            throw new InvalidStatementHandleException(statementId);
        }
        if (null != FirebirdBatchRegistry.getInstance().getBatchStatement(connectionId, statementId)) {
            throw new BatchAlreadyOpenedException(statementId);
        }
        ByteBuf batchBlr = packet.getBatchBlr();
        int blrLength = batchBlr.readableBytes();
        FirebirdParseBatchBlr messageFormat = FirebirdParseBatchBlr.parse(batchBlr, blrLength);
        if (messageFormat.getFields().isEmpty()) {
            throw new BatchParametersRequiredException(statementId);
        }
        if (packet.getBatchMessageLength() != messageFormat.getMessageLength()) {
            throw new InvalidBatchMessageFormatException(
                    String.format("invalid message length: computed %d from BLR but client sent %d", messageFormat.getMessageLength(), packet.getBatchMessageLength()));
        }
        ByteBuf batchParametersBuffer = packet.getBatchParametersBuffer();
        BatchParameters batchParameters = BatchParameters.parse(batchParametersBuffer);
        FirebirdBatchRegistry.getInstance().registerBatchStatement(connectionId, statementId,
                new FirebirdBatchStatement(statementId, messageFormat.getFields(), batchParameters.getBufferSize(), batchParameters.isRecordCounts(), batchParameters.isMultiError()));

View on GitHub (pinned to e952770a21)

Solutions

  1. Always run PREPARE first and use the statement handle returned in the prepare response for the subsequent CREATE BATCH on the same connection.
  2. Do not send CREATE BATCH after FREE STATEMENT (option DROP/UNPREPARE) for that handle; re-prepare instead.
  3. Ensure pooled clients do not carry statement handles across physical Firebird proxy connections.
  4. If writing a raw-protocol client, verify you parse the statement handle from the correct response field (int4 in the generic response data).

Example fix

// before: batch created with a stale/foreign handle
stmtHandle = 0; // never prepared
sendCreateBatch(stmtHandle);

// after: prepare first, then batch
prepResp = sendPrepare(sql);
sendCreateBatch(prepResp.statementHandle);
Defensive patterns

Strategy: validation

Validate before calling

// Client: only batch handles you actually prepared on this connection
Set<Integer> prepared = new HashSet<>();
prepared.add(sendPrepare(sql).statementHandle);
if (!prepared.contains(stmtHandle)) throw new IllegalStateException("handle not prepared");

Try / catch

catch (SQLException e) {
    if (e.getMessage().contains("Invalid statement handle")) { int h = sendPrepare(sql).statementHandle; sendCreateBatch(h); }
    else throw e;
}

Prevention

When it happens

Trigger: FirebirdBatchCreateCommandExecutor.execute() reads packet.getStatementHandle() and looks up connectionSession.getServerPreparedStatementRegistry().getPreparedStatement(statementId); a null result (never prepared, already dropped/unprepared via FREE STATEMENT, or handle from a different connection) triggers the throw.

Common situations: client sends CREATE BATCH before PREPARE; handle reuse after the statement was freed; statement handle created on another connection in a connection pool that reuses handles across physical sessions; client bug mis-parsing the handle returned by the prepare response.

Related errors


AI-assisted analysis of apache/shardingsphere@e952770a21 (2026-08-14). Data as JSON: /api/errors/152735bf5712b202. Report an issue: GitHub.