Skip to main content

gRPC API

CamusDB exposes a client-facing gRPC API in addition to the REST/JSON API. The gRPC endpoint uses HTTP/2 on a dedicated port and reaches the same SQL, transaction, and row-operation engine as the HTTP endpoints.

It is enabled by default. Configure or disable it in config.yml:

grpc_enabled: true
grpc_port: 5096

If raft_certificate is configured, CamusDB reuses it for TLS on the gRPC listener. Otherwise the gRPC listener uses plaintext HTTP/2, which is suitable for local development and private test networks.

When authentication is enabled, use the CamusAuth service to exchange a username and password for a bearer token. gRPC clients then send that token in request metadata:

authorization: Bearer camus_<id>.<secret>

Authentication failures return UNAUTHENTICATED; privilege failures return PERMISSION_DENIED. See Authentication And Authorization.

The Protobuf contract lives in the CamusDB source tree at CamusDB.Grpc.Contracts/Protos/camus_sql.proto. Generate client bindings from that file with the standard gRPC toolchain for your language.

Services

The protocol defines three services:

ServiceUse it for
CamusSqlSQL queries, DML, DDL, explicit transactions, ping, and duplex batching.
CamusRowsTyped row CRUD without building SQL text.
CamusAuthLogin and logout for authenticated gRPC clients.

Use CamusSql for most application and ORM work. Use CamusRows when a client already has structured row values, filters, and ordering and wants to avoid constructing SQL strings.

Auth Service

CamusAuth provides the credential exchange used by gRPC-only deployments:

RPCPurpose
LoginAccepts LoginRequest { user, password } and returns LoginReply { token, expires_at_unix_ms, expires_in_seconds }.
LogoutRevokes the token supplied in authorization metadata. Missing or already-revoked tokens still produce the desired end state.

expires_at_unix_ms and expires_in_seconds describe the same deadline. Renew before that deadline instead of assuming a fixed token lifetime.

SQL Service

CamusSql provides:

RPCShapePurpose
ExecuteQueryserver streamExecute SELECT and SHOW; emits schema first, then rows.
ExecuteNonQueryunaryExecute INSERT, UPDATE, or DELETE; returns affected rows.
ExecuteDdlunaryExecute database, table, index, and schema statements.
StartTransactionunaryStart an explicit transaction and receive a TxnHandle.
CommitTransactionunaryCommit an explicit transaction by handle.
RollbackTransactionunaryRoll back an explicit transaction by handle.
BatchExecutebidirectional streamPipeline SQL operations, transaction lifecycle messages, and prepared-statement lifecycle messages on one stream.
PingunaryCheck liveness and round-trip connectivity.

SQL parameters are sent as a map<string, Value>. Prefer parameters over SQL string interpolation so typed values such as DATE, DATETIME, BYTES, and UUID cross the wire without loss.

Row Service

CamusRows provides typed CRUD operations:

RPCPurpose
InsertRowInsert one row from a column-value map.
QueryQuery rows by table, optional index name, filters, and ordering.
QueryByIdFetch one row by primary-key value.
UpdateRowsUpdate matching rows.
UpdateByIdUpdate one row by primary-key value.
DeleteRowsDelete matching rows.
DeleteByIdDelete one row by primary-key value.

Filters use QueryFilter { column_name, op, value }, where op is a string such as "=", ">", ">=", "<", "<=", or "LIKE". Ordering uses OrderBy { column_name, direction }, with ascending or descending direction.

QueryById, UpdateById, and DeleteById take a string key value. The server resolves the real primary-key column from the table schema, so the primary key does not need to be named id.

Value Encoding

All parameters, row values, filters, and result cells use the Protobuf Value message. It is a typed oneof that mirrors CamusDB's column types:

Column typeWire fieldEncoding
NULLnull_valueExplicit typed NULL sentinel.
ID / OIDid_value24 lowercase ObjectId hex characters.
INT64int64_valueSigned 64-bit integer.
STRINGstring_valueUTF-8 string.
BOOLbool_valueBoolean.
FLOAT64float64_valueIEEE-754 double.
FLOAT32float32_valueIEEE-754 float.
BYTESbytes_valueRaw bytes.
DATEdate_valueUTC .NET ticks, truncated to midnight.
DATETIMEdatetime_valueUTC .NET ticks.
ARRAYarray_valueElement type plus nested Value items.
UUID / GUIDuuid_valueExactly 16 bytes in canonical big-endian order.

Important rules for client implementers:

  • Send ObjectIds in id_value, not string_value.
  • Send UUIDs as 16 bytes, not as strings.
  • Send DATE and DATETIME as ticks, not ISO strings or Unix timestamps.
  • Preserve FLOAT32 and FLOAT64 as separate wire fields.
  • Include the array element type even when the array is empty.
  • Treat an unset Value and explicit null_value as NULL when decoding.

To convert Unix milliseconds to ticks:

ticks = 621355968000000000 + unix_millis * 10000

Query Streams

ExecuteQuery, CamusRows.Query, and CamusRows.QueryById are server-streaming calls with a schema-first contract:

QueryStreamMessage(schema)
QueryStreamMessage(row)
QueryStreamMessage(row)
...
QueryStreamMessage(cache_metadata)

The schema message is always first and appears exactly once, even when the result set is empty. Rows are positional: row.values[i] belongs to schema.columns[i]. Clients should take the column type from the schema, not from the first non-NULL row value.

A query with a {cache=...} hint may append one trailing cache_metadata message after the last row. Its absence means the statement carried no cache hint.

Transactions

Every SQL and row operation can run in autocommit mode or inside an explicit transaction.

Autocommit requests omit txn_handle. The server starts a short transaction, runs the operation, and commits it.

Explicit transactions use CamusSql.StartTransaction:

StartTransaction -> TxnHandle
ExecuteQuery / ExecuteNonQuery / ExecuteDdl with txn_handle
CommitTransaction(txn_handle)

StartTxnRequest and autocommit SqlRequest can set:

  • isolation_level: READ_COMMITTED or SERIALIZABLE
  • transaction_mode: READ_WRITE or READ_ONLY
  • locking: PESSIMISTIC or OPTIMISTIC

When a request resumes an existing txn_handle, these fields are ignored because the transaction properties were fixed when the transaction started.

Causal Tokens

Replies that advance transaction state include a causal token with three HLC components:

  • causal_token_n
  • causal_token_l
  • causal_token_c

Carry all three values into the next request in the same client session. The N component is part of HLC ordering and must not be dropped. Threading the token preserves read-your-writes behavior when a client talks to a cluster.

For explicit transactions, keep the latest causal token in the TxnHandle when resuming, committing, or rolling back the transaction.

Duplex Batching

CamusSql.BatchExecute lets a client pipeline many operations over one bidirectional stream. This is useful for drivers and ORMs that would otherwise pay one unary round trip per statement.

Each request contains:

  • request_id: client-assigned id echoed by every response for that operation
  • kind: QUERY, NON_QUERY, START, COMMIT, ROLLBACK, PREPARE, or CLOSE
  • request: the same SqlRequest shape used by unary SQL calls

Responses for different request_id values may interleave and arrive out of order. Clients must demultiplex by request_id.

A batched query emits:

schema
row...
query_complete

query_complete is the terminal message for that request and carries the row count plus causal token. For a {cache=...} hinted query, query_complete also carries the cache verdict. Non-query, start, commit, and rollback operations each emit one terminal success response. Failed operations emit one terminal BatchError { code, message }.

Operations that share the same transaction handle are ordered per batch stream. If a client uses multiple batch streams, pin all operations for a transaction to the same stream. Autocommit operations can use any stream.

grpc_batch_max_in_flight controls how many operations one batch stream may execute concurrently before the server applies backpressure.

Prepared Statements

Prepared statements are supported on CamusSql.BatchExecute.

Send a PREPARE operation with the target database and SQL text. The terminal PrepareReply returns:

  • statement_id: an integer handle scoped to that batch stream
  • parameter_names: the positional binding order for placeholders

Then send QUERY or NON_QUERY operations with statement_id and positional_parameters. When statement_id is set, do not also send SQL text, database name, or named parameter maps.

Use CLOSE to release a prepared statement id on the stream. CLOSE is idempotent.

Handles are stream-local and disappear when the BatchExecute stream closes or is rebuilt. If an execution fails with CADB0520 UnknownPreparedStatement, prepare again on the current stream and replay the operation once. Await the PrepareReply before executing with its id; batch requests may otherwise run concurrently and the execution can reach the server before registration.

Unary gRPC calls do not accept prepared handles because they have no stream scope. See Prepared Statements for supported statement types, binding rules, and configuration limits.

Errors And Retries

Unary and server-streaming RPCs surface domain errors as gRPC status errors with trailing metadata:

TrailerMeaning
camus-error-codeCamusDB CADBxxxx error code.
camus-error-messageHuman-readable error message.

Batched operations use in-band BatchError messages because trailers are per-call, not per operation.

Retry by camus-error-code, not by message text:

CodeRetry rule
CADB0502 TransactionConflictReplay the whole transaction from a fresh BEGIN.
CADB0504 TransactionMustRetryReplay the whole transaction from a fresh BEGIN.
CADB0505 TransactionLifetimeExceededReplay the whole transaction from a fresh BEGIN.
CADB0509 TransactionFinalizeUnresolvedRetry the same COMMIT or ROLLBACK on the same transaction handle.
CADB0520 UnknownPreparedStatementPrepare again on the current node or stream, then replay the execution once.

For streaming queries, only replay automatically if no rows have been surfaced to the caller yet. Once rows have been emitted, surface the error to the caller instead of silently replaying the query.