Prepared Statements
Prepared statements let a client register a SQL statement once and execute it many times with different values.
Use them when an application repeats the same parameterized statement shape, for example a hot lookup, an insert loop, an update by primary key, or an ORM query that runs many times per process.
Prepared execution has the same behavior as inline execution:
- same rows and affected-row counts
- same Serializable isolation and retry behavior
- same transaction, locking, and read-only semantics
- same query result cache hints and cache metadata
- same authorization checks at execution time
The optimization is at the client/protocol boundary. After a statement is registered, later executions send a handle and positional values instead of sending the SQL text and parameter-name map again.
In the current CamusDB implementation, a benchmarked five-column insert used a 43-byte prepared execution payload instead of a 198-byte inline request, and the server avoided the repeated transport parse needed before routing the statement.
What Can Be Prepared
CamusDB can prepare:
SELECTINSERTUPDATEDELETESHOW ...
Schema, database, and user administration statements are one-shot operations
and cannot be prepared. /execute-sql-ddl and unary gRPC DDL calls reject
prepared handles.
Prepared statements are a client API feature. There is no SQL PREPARE
statement to type in camus-cli.
Parameter Binding
Prepare replies include the parameter names found in the SQL text. The order is the first time each distinct placeholder appears.
SELECT id, name
FROM robots
WHERE kind = @kind OR backup_kind = @kind
AND year >= @min_year;
The binding order for that statement is:
["@kind", "@min_year"]
Prepared executions send values by ordinal:
- value
0binds to@kind - value
1binds to@min_year
If the same placeholder appears more than once, it still has one slot. The
execution must send exactly the number of values returned by prepare. Parameter
names are returned verbatim, including the @ prefix.
REST Lifecycle
Register a statement with /prepare-sql-statement:
POST /prepare-sql-statement
Content-Type: application/json
{
"databaseName": "factory",
"sql": "SELECT id, name FROM robots WHERE year >= @year"
}
Response:
{
"status": "ok",
"statementId": "opaque-node-local-handle",
"parameterNames": ["@year"]
}
Execute it through the normal SQL endpoints by sending statementId and
positionalParameters:
POST /execute-sql-query
Content-Type: application/json
{
"statementId": "opaque-node-local-handle",
"positionalParameters": [
{ "type": 2, "longValue": 1980 }
],
"isolationLevel": "Serializable",
"transactionMode": "ReadOnly"
}
Prepared execution is accepted by:
/execute-sql-query/execute-sql-query-stream/execute-sql-non-query
When statementId is present, omit sql, databaseName, and named
parameters. The database and SQL text are the ones captured by the prepared
handle.
Close a REST handle when the client no longer needs it:
POST /close-sql-statement
Content-Type: application/json
{ "statementId": "opaque-node-local-handle" }
Close is idempotent.
gRPC Lifecycle
Prepared statements live on CamusSql.BatchExecute, the bidirectional SQL
batch stream.
Use a PREPARE batch operation with (database, sql). The terminal
PrepareReply returns:
statement_id: an integer handle scoped to that batch streamparameter_names: the ordinal binding order
Then send a QUERY or NON_QUERY operation with:
statement_idpositional_parameters
Do not send sql, database, or named parameters on an execution that uses
statement_id.
Use a CLOSE batch operation to release a stream-local handle. Close is
idempotent.
Wait for the PrepareReply before sending an execution that references the new
statement_id. Batch operations may run concurrently, so an execution can
arrive before registration if the client pipelines both messages without
waiting.
Unary gRPC calls do not support prepared handles because they have no stream scope for handle ownership.
Handle Scope
Prepared handles are node-local. They are not replicated to other nodes.
REST handles are scoped to:
- the node that prepared them
- the authenticated principal, or the anonymous principal when authentication is disabled
- the handle's idle lifetime
gRPC handles are scoped to:
- the node that owns the
BatchExecutestream - the specific
BatchExecutestream that prepared them
A gRPC handle disappears when the stream closes or is rebuilt.
Authorization
Preparing a statement parses and registers it. Authorization runs when the statement is executed, using the principal that executes it.
This means a statement can prepare successfully and later fail with
CADB0517 InsufficientPrivilege if the executing user does not have the
required privileges for the affected database, table, or statement.
Unknown Handles
An execution can fail with CADB0520 UnknownPreparedStatement when the node
does not recognize the handle. This is a routine condition, not data loss.
Common causes include:
- the handle expired after sitting idle
- the server restarted
- a load balancer sent a REST execution to a different node
- a gRPC stream was rebuilt
- the handle was closed
- the handle belongs to another authenticated principal
Clients should prepare the statement again and replay the execution once. For a streaming query, only replay automatically if no rows have been returned to the caller yet.
Limits
CamusDB refuses new registrations that exceed prepared-statement caps. It does not silently evict an existing handle that a client may still use.
The relevant settings are:
prepared_statement_idle_timeout_ms: 600000
prepared_statement_sweep_interval_ms: 60000
grpc_max_prepared_statements_per_stream: 512
rest_max_prepared_statements_per_principal: 512
rest_max_prepared_statements: 8192
max_prepared_statement_bytes: 65536
grpc_max_prepared_statement_bytes_per_stream: 8388608
rest_max_prepared_statement_bytes_per_principal: 8388608
rest_max_prepared_statement_bytes: 67108864
Exceeding a statement-count or retained-byte cap returns CADB0521
PreparedStatementLimitExceeded. Close handles you no longer need, reduce the
number of distinct statement shapes, or raise the relevant cap.
max_prepared_statement_bytes bounds the size of one prepared SQL text. A
statement larger than that limit is rejected at registration.
See Configuration for the full setting descriptions.
.NET Driver
The .NET ADO.NET driver prepares repeated statements automatically. Once the same SQL has been seen enough times, later executions use prepared handles without application code changes.
You can also call Prepare() or PrepareAsync() on CamusCommand for a hot
statement you know should be registered immediately.
See .NET Driver and EF Core Provider for driver-level behavior and tuning.