Skip to main content
Glama
HRANA_3_SPEC.md45.1 kB
# The Hrana protocol specification (version 3) Hrana (from Czech "hrana", which means "edge") is a protocol for connecting to a SQLite database over the network. It is designed to be used from edge functions and other environments where low latency and small overhead is important. This is a specification for version 3 of the Hrana protocol (Hrana 3). ## Overview The Hrana protocol provides SQL _streams_. Each stream corresponds to a SQLite connection and executes a sequence of SQL statements. ### Variants (WebSocket / HTTP) The protocol has two variants: - Hrana over WebSocket, which uses WebSocket as the underlying protocol. Multiple streams can be multiplexed over a single WebSocket. - Hrana over HTTP, which communicates with the server using HTTP requests. This is less efficient than WebSocket, but HTTP is the only reliable protocol in some environments. Each of these variants is described later. ### Encoding The protocol has two encodings: - [JSON][rfc8259] is the canonical encoding, backward compatible with Hrana 1 and 2. - Protobuf ([Protocol Buffers][protobuf]) is a more compact binary encoding, introduced in Hrana 3. [rfc8259]: https://datatracker.ietf.org/doc/html/rfc8259 [protobuf]: https://protobuf.dev/ This document defines protocol structures in JSON and specifies the schema using TypeScript type notation. The Protobuf schema is described in proto3 syntax in an appendix. The encoding is negotiated between the server and client. This process depends on the variant (WebSocket or HTTP) and is described later. All Hrana 3 servers must support both JSON and Protobuf; clients can choose which encodings to support and use. Both encodings support forward compatibility: when a peer (client or server) receives a protocol structure that includes an unrecognized field (object property in JSON or a message field in Protobuf), it must ignore this field. ## Hrana over WebSocket Hrana over WebSocket runs on top of the [WebSocket protocol][rfc6455]. ### Version and encoding negotiation The version of the protocol and the encoding is negotiated as a WebSocket subprotocol: the client includes a list of supported subprotocols in the `Sec-WebSocket-Protocol` request header in the opening handshake, and the server replies with the selected subprotocol in the same response header. The negotiation mechanism provides backward compatibility with older versions of the Hrana protocol and forward compatibility with newer versions. [rfc6455]: https://www.rfc-editor.org/rfc/rfc6455 The WebSocket subprotocols defined in all Hrana versions are as follows: | Subprotocol | Version | Encoding | |-------------|---------|----------| | `hrana1` | 1 | JSON | | `hrana2` | 2 | JSON | | `hrana3` | 3 | JSON | | `hrana3-protobuf` | 3 | Protobuf | This document describes version 3 of the Hrana protocol. Versions 1 and 2 are described in their own specifications. Version 3 of Hrana over WebSocket is designed to be a strict superset of versions 1 and 2: every server that implements Hrana 3 over WebSocket also implements versions 1 and 2 and should accept clients that indicate subprotocol `hrana1` or `hrana2`. ### Overview The client starts the connection by sending a _hello_ message, which authenticates the client to the server. The server responds with either a confirmation or with an error message, closing the connection. The client can choose not to wait for the confirmation and immediately send further messages to reduce latency. A single connection can host an arbitrary number of streams. In effect, one Hrana connection works as a "connection pool" in traditional SQL servers. After a stream is opened, the client can execute SQL statements on it. For the purposes of this protocol, the statements are arbitrary strings with optional parameters. To reduce the number of roundtrips, the protocol supports batches of statements that are executed conditionally, based on success or failure of previous statements. Clients can use this mechanism to implement non-interactive transactions in a single roundtrip. ### Messages If the negotiated encoding is JSON, all messages exchanged between the client and server are sent as text frames (opcode 0x1) on the WebSocket. If the negotiated encoding is Protobuf, messages are sent as binary frames (opcode 0x2). ```typescript type ClientMsg = | HelloMsg | RequestMsg type ServerMsg = | HelloOkMsg | HelloErrorMsg | ResponseOkMsg | ResponseErrorMsg ``` The client sends messages of type `ClientMsg`, and the server sends messages of type `ServerMsg`. The type of the message is determined by its `type` field. #### Hello ```typescript type HelloMsg = { "type": "hello", "jwt": string | null, } ``` The `hello` message is sent as the first message by the client. It authenticates the client to the server using the [Json Web Token (JWT)][rfc7519] passed in the `jwt` field. If no authentication is required (which might be useful for development and debugging, or when authentication is performed by other means, such as with mutual TLS), the `jwt` field might be set to `null`. [rfc7519]: https://www.rfc-editor.org/rfc/rfc7519 The client can also send the `hello` message again anytime during the lifetime of the connection to reauthenticate, by providing a new JWT. If the provided JWT expires and the client does not provide a new one in a `hello` message, the server may terminate the connection. ```typescript type HelloOkMsg = { "type": "hello_ok", } type HelloErrorMsg = { "type": "hello_error", "error": Error, } ``` The server waits for the `hello` message from the client and responds with a `hello_ok` message if the client can proceed, or with a `hello_error` message describing the failure. The client may choose not to wait for a response to its `hello` message before sending more messages to save a network roundtrip. If the server responds with `hello_error`, it must ignore all further messages sent by the client and it should close the WebSocket immediately. #### Request/response ```typescript type RequestMsg = { "type": "request", "request_id": int32, "request": Request, } ``` After sending the `hello` message, the client can start sending `request` messages. The client uses requests to open SQL streams and execute statements on them. The client assigns an identifier to every request, which is then used to match a response to the request. The `Request` structure represents the payload of the request and is defined later. ```typescript type ResponseOkMsg = { "type": "response_ok", "request_id": int32, "response": Response, } type ResponseErrorMsg = { "type": "response_error", "request_id": int32, "error": Error, } ``` When the server receives a `request` message, it must eventually send either a `response_ok` with the response or a `response_error` that describes a failure. The response from the server includes the same `request_id` that was provided by the client in the request. The server can send the responses in arbitrary order. The request ids are arbitrary 32-bit signed integers, the server does not interpret them in any way. The server should limit the number of outstanding requests to a reasonable value, and stop receiving messages when this limit is reached. This will cause the TCP flow control to kick in and apply back-pressure to the client. On the other hand, the client should always receive messages, to avoid deadlock. ### Requests Most of the work in the protocol happens in request/response interactions. ```typescript type Request = | OpenStreamReq | CloseStreamReq | ExecuteReq | BatchReq | OpenCursorReq | CloseCursorReq | FetchCursorReq | SequenceReq | DescribeReq | StoreSqlReq | CloseSqlReq | GetAutocommitReq type Response = | OpenStreamResp | CloseStreamResp | ExecuteResp | BatchResp | OpenCursorResp | CloseCursorResp | FetchCursorResp | SequenceResp | DescribeResp | StoreSqlReq | CloseSqlReq | GetAutocommitResp ``` The type of the request and response is determined by its `type` field. The `type` of the response must always match the `type` of the request. The individual requests and responses are defined in the rest of this section. #### Open stream ```typescript type OpenStreamReq = { "type": "open_stream", "stream_id": int32, } type OpenStreamResp = { "type": "open_stream", } ``` The client uses the `open_stream` request to open an SQL stream, which is then used to execute SQL statements. The streams are identified by arbitrary 32-bit signed integers assigned by the client. The client can optimistically send follow-up requests on a stream before it receives the response to its `open_stream` request. If the server receives a request that refers to a stream that failed to open, it should respond with an error, but it should not close the connection. Even if the `open_stream` request returns an error, the stream id is still considered as used, and the client cannot reuse it until it sends a `close_stream` request. The server can impose a reasonable limit to the number of streams opened at the same time. > This request was introduced in Hrana 1. #### Close stream ```typescript type CloseStreamReq = { "type": "close_stream", "stream_id": int32, } type CloseStreamResp = { "type": "close_stream", } ``` When the client is done with a stream, it should close it using the `close_stream` request. The client can safely reuse the stream id after it receives the response. The client should close even streams for which the `open_stream` request returned an error. If there is an open cursor for the stream, the cursor is closed together with the stream. > This request was introduced in Hrana 1. #### Execute a statement ```typescript type ExecuteReq = { "type": "execute", "stream_id": int32, "stmt": Stmt, } type ExecuteResp = { "type": "execute", "result": StmtResult, } ``` The client sends an `execute` request to execute an SQL statement on a stream. The server responds with the result of the statement. The `Stmt` and `StmtResult` structures are defined later. If the statement fails, the server responds with an error response (message of type `"response_error"`). > This request was introduced in Hrana 1. #### Execute a batch ```typescript type BatchReq = { "type": "batch", "stream_id": int32, "batch": Batch, } type BatchResp = { "type": "batch", "result": BatchResult, } ``` The `batch` request runs a batch of statements on a stream. The server responds with the result of the batch execution. If a statement in the batch fails, the error is returned inside the `BatchResult` structure in a normal response (message of type `"response_ok"`). However, if the server encounters a serious error that prevents it from executing the batch, it responds with an error response (message of type `"response_error"`). > This request was introduced in Hrana 1. #### Open a cursor executing a batch ```typescript type OpenCursorReq = { "type": "open_cursor", "stream_id": int32, "cursor_id": int32, "batch": Batch, } type OpenCursorResp = { "type": "open_cursor", } ``` The `open_cursor` request runs a batch of statements like the `batch` request, but instead of returning all statement results in the request response, it opens a _cursor_ which the client can then use to read the results incrementally. The `cursor_id` is an arbitrary 32-bit integer id assigned by the client. This id must be unique for the given connection and must not be used by another cursor that was not yet closed using the `close_cursor` request. Even if the `open_cursor` request returns an error, the cursor id is still considered as used, and the client cannot reuse it until it sends a `close_cursor` request. After the `open_cursor` request, the client must not send more requests on the stream until the cursor is closed using the `close_cursor` request. > This request was introduced in Hrana 3. #### Close a cursor ```typescript type CloseCursorReq = { "type": "close_cursor", "cursor_id": int32, } type CloseCursorResp = { "type": "close_cursor", } ``` The `close_cursor` request closes a cursor opened by an `open_cursor` request and allows the server to release resources and continue processing other requests for the given stream. > This request was introduced in Hrana 3. #### Fetch entries from a cursor ```typescript type FetchCursorReq = { "type": "fetch_cursor", "cursor_id": int32, "max_count": uint32, } type FetchCursorResp = { "type": "fetch_cursor", "entries": Array<CursorEntry>, "done": boolean, } ``` The `fetch_cursor` request reads data from a cursor previously opened with the `open_cursor` request. The cursor data is encoded as a sequence of entries (`CursorEntry` structure). `max_count` in the request specifies the maximum number of entries that the client wants to receive in the response; however, the server may decide to send fewer entries. If the `done` field in the response is set to true, then the cursor is finished and all subsequent calls to `fetch_cursor` are guaranteed to return zero entries. The client should then close the cursor by sending the `close_cursor` request. If the `cursor_id` refers to a cursor for which the `open_cursor` request returned an error, and the cursor hasn't yet been closed with `close_cursor`, then the server should return an error, but it must not close the connection (i.e., this is not a protocol error). > This request was introduced in Hrana 3. #### Store an SQL text on the server ```typescript type StoreSqlReq = { "type": "store_sql", "sql_id": int32, "sql": string, } type StoreSqlResp = { "type": "store_sql", } ``` The `store_sql` request stores an SQL text on the server. The client can then refer to this SQL text in other requests by its id, instead of repeatedly sending the same string over the network. SQL text ids are arbitrary 32-bit signed integers assigned by the client. It is a protocol error if the client tries to store an SQL text with an id which is already in use. > This request was introduced in Hrana 2. #### Close a stored SQL text ```typescript type CloseSqlReq = { "type": "close_sql", "sql_id": int32, } type CloseSqlResp = { "type": "close_sql", } ``` The `close_sql` request can be used to delete an SQL text stored on the server with `store_sql`. The client can safely reuse the SQL text id after it receives the response. It is not an error if the client attempts to close a SQL text id that is not used. > This request was introduced in Hrana 2. #### Execute a sequence of SQL statements ```typescript type SequenceReq = { "type": "sequence", "stream_id": int32, "sql"?: string | null, "sql_id"?: int32 | null, } type SequenceResp = { "type": "sequence", } ``` The `sequence` request executes a sequence of SQL statements separated by semicolons on the stream given by `stream_id`. `sql` or `sql_id` specify the SQL text; exactly one of these fields must be specified. Any rows returned by the statements are ignored. If any statement fails, the subsequent statements are not executed and the request returns an error response. > This request was introduced in Hrana 2. #### Describe a statement ```typescript type DescribeReq = { "type": "describe", "stream_id": int32, "sql"?: string | null, "sql_id"?: int32 | null, } type DescribeResp = { "type": "describe", "result": DescribeResult, } ``` The `describe` request is used to parse and analyze a SQL statement. `stream_id` specifies the stream on which the statement is parsed. `sql` or `sql_id` specify the SQL text: exactly one of these two fields must be specified, `sql` passes the SQL directly as a string, while `sql_id` refers to a SQL text previously stored with `store_sql`. In the response, `result` contains the result of describing a statement. > This request was introduced in Hrana 2. #### Get the autocommit state ```typescript type GetAutocommitReq = { "type": "get_autocommit", "stream_id": int32, } type GetAutocommitResp = { "type": "get_autocommit", "is_autocommit": bool, } ``` The `get_autocommit` request can be used to check whether the stream is in autocommit state (not inside an explicit transaction). > This request was introduced in Hrana 3. ### Errors If either peer detects that the protocol has been violated, it should close the WebSocket with an appropriate WebSocket close code and reason. Some examples of protocol violations include: - Text message payload that is not a valid JSON. - Data frame type that does not match the negotiated encoding (i.e., binary frame when the encoding is JSON or a text frame when the encoding is Protobuf). - Unrecognized `ClientMsg` or `ServerMsg` (the field `type` is unknown or missing) - Client receives a `ResponseOkMsg` or `ResponseErrorMsg` with a `request_id` that has not been sent in a `RequestMsg` or that has already received a response. ### Ordering The protocol allows the server to reorder the responses: it is not necessary to send the responses in the same order as the requests. However, the server must process requests related to a single stream id in order. For example, this means that a client can send an `open_stream` request immediately followed by a batch of `execute` requests on that stream and the server will always process them in correct order. ## Hrana over HTTP Hrana over HTTP runs on top of HTTP. Any version of the HTTP protocol can be used. ### Overview HTTP is a stateless protocol, so there is no concept of a connection like in the WebSocket protocol. However, Hrana needs to expose stateful streams, so it needs to ensure that requests on the same stream are tied together. This is accomplished by the use of a baton, which is similar to a session cookie. The server returns a baton in every response to a request on the stream, and the client then needs to include the baton in the subsequent request. The client must serialize the requests on a stream: it must wait for a response to the previous request before sending next request on the same stream. The server can also optionally specify a different URL that the client should use for the requests on the stream. This can be used to ensure that stream requests are "sticky" and reach the same server. If the client terminates without closing a stream, the server has no way of finding this out: with Hrana over WebSocket, the WebSocket connection is closed and the server can close the streams that belong to this connection, but there is no connection in Hrana over HTTP. Therefore, the server will close streams after a short period of inactivity, to make sure that abandoned streams don't accumulate on the server. ### Version and encoding negotiation With Hrana over HTTP, the client indicates the Hrana version and encoding in the URI path of the HTTP request. The client can check whether the server supports a given Hrana version by sending an HTTP request (described later). ### Endpoints The client communicates with the server by sending HTTP requests with a specified method and URL. #### Check support for version 3 (JSON) ```HTTP GET v3 ``` If the server supports version 3 of Hrana over HTTP with JSON encoding, it should return a 2xx response to this request. #### Check support for version 3 (Protobuf) ```text GET v3-protobuf ``` If the server supports version 3 of Hrana over HTTP with Protobuf encoding, it should return a 2xx response to this request. #### Execute a pipeline of requests (JSON) ```HTTP POST v3/pipeline -> JSON: PipelineReqBody <- JSON: PipelineRespBody ``` ```typescript type PipelineReqBody = { "baton": string | null, "requests": Array<StreamRequest>, } type PipelineRespBody = { "baton": string | null, "base_url": string | null, "results": Array<StreamResult> } type StreamResult = | StreamResultOk | StreamResultError type StreamResultOk = { "type": "ok", "response": StreamResponse, } type StreamResultError = { "type": "error", "error": Error, } ``` The `v3/pipeline` endpoint is used to execute a pipeline of requests on a stream. `baton` in the request specifies the stream. If the client sets `baton` to `null`, the server should create a new stream. Server responds with another `baton` value in the response. If the `baton` value in the response is `null`, it means that the server has closed the stream. The client must use this value to refer to this stream in the next request (the `baton` in the response should be different from the `baton` in the request). This forces the client to issue the requests serially: it must wait for the response from a previous `pipeline` request before issuing another request on the same stream. The server should ensure that the `baton` values are unpredictable and unforgeable, for example by cryptographically signing them. If the `base_url` in the response is not `null`, the client should use this URL when sending further requests on this stream. If it is `null`, the client should use the same URL that it has used for the previous request. The `base_url` must be an absolute URL with "http" or "https" scheme. The `requests` array in the request specifies a sequence of stream requests that should be executed on the stream. The server executes them in order and returns the results in the `results` array in the response. Result is either a success (`type` set to `"ok"`) or an error (`type` set to `"error"`). The server always executes all requests, even if some of them return errors. #### Execute a pipeline of requests (Protobuf) ```text POST v3-protobuf/pipeline -> Protobuf: PipelineReqBody <- Protobuf: PipelineRespBody ``` The `v3-protobuf/pipeline` endpoint is the same as `v3/pipeline`, but it encodes the request and response body using Protobuf. #### Execute a batch using a cursor (JSON) ```HTTP POST v3/cursor -> JSON: CursorReqBody <- line of JSON: CursorRespBody lines of JSON: CursorEntry ``` ```typescript type CursorReqBody = { "baton": string | null, "batch": Batch, } type CursorRespBody = { "baton": string | null, "base_url": string | null, } ``` The `v3/cursor` endpoint executes a batch of statements on a stream using a cursor, so the results can be streamed from the server to the client. The HTTP response is composed of JSON structures separated with a newline. The first line contains the `CursorRespBody` structure, and the following lines contain `CursorEntry` structures, which encode the result of the batch. The `baton` field in the request and the `baton` and `base_url` fields in the response have the same meaning as in the `v3/pipeline` endpoint. #### Execute a batch using a cursor (Protobuf) ```text POST v3-protobuf/cursor -> Protobuf: CursorReqBody <- length-delimited Protobuf: CursorRespBody length-delimited Protobufs: CursorEntry ``` The `v3-protobuf/cursor` endpoint is the same as `v3/cursor` endpoint, but the request and response are encoded using Protobuf. In the response body, the structures are prefixed with a length delimiter: a Protobuf variant that encodes the length of the structure. The first structure is `CursorRespBody`, followed by an arbitrary number of `CursorEntry` structures. ### Requests Requests in Hrana over HTTP closely mirror stream requests in Hrana over WebSocket: ```typescript type StreamRequest = | CloseStreamReq | ExecuteStreamReq | BatchStreamReq | SequenceStreamReq | DescribeStreamReq | StoreSqlStreamReq | CloseSqlStreamReq | GetAutocommitStreamReq type StreamResponse = | CloseStreamResp | ExecuteStreamResp | BatchStreamResp | SequenceStreamResp | DescribeStreamResp | StoreSqlStreamResp | CloseSqlStreamResp | GetAutocommitStreamReq ``` #### Close stream ```typescript type CloseStreamReq = { "type": "close", } type CloseStreamResp = { "type": "close", } ``` The `close` request closes the stream. It is an error if the client tries to execute more requests on the same stream. > This request was introduced in Hrana 2. #### Execute a statement ```typescript type ExecuteStreamReq = { "type": "execute", "stmt": Stmt, } type ExecuteStreamResp = { "type": "execute", "result": StmtResult, } ``` The `execute` request has the same semantics as the `execute` request in Hrana over WebSocket. > This request was introduced in Hrana 2. #### Execute a batch ```typescript type BatchStreamReq = { "type": "batch", "batch": Batch, } type BatchStreamResp = { "type": "batch", "result": BatchResult, } ``` The `batch` request has the same semantics as the `batch` request in Hrana over WebSocket. > This request was introduced in Hrana 2. #### Execute a sequence of SQL statements ```typescript type SequenceStreamReq = { "type": "sequence", "sql"?: string | null, "sql_id"?: int32 | null, } type SequenceStreamResp = { "type": "sequence", } ``` The `sequence` request has the same semantics as the `sequence` request in Hrana over WebSocket. > This request was introduced in Hrana 2. #### Describe a statement ```typescript type DescribeStreamReq = { "type": "describe", "sql"?: string | null, "sql_id"?: int32 | null, } type DescribeStreamResp = { "type": "describe", "result": DescribeResult, } ``` The `describe` request has the same semantics as the `describe` request in Hrana over WebSocket. > This request was introduced in Hrana 2. #### Store an SQL text on the server ```typescript type StoreSqlStreamReq = { "type": "store_sql", "sql_id": int32, "sql": string, } type StoreSqlStreamResp = { "type": "store_sql", } ``` The `store_sql` request has the same semantics as the `store_sql` request in Hrana over WebSocket, except that the scope of the SQL texts is just a single stream (with WebSocket, it is the whole connection). > This request was introduced in Hrana 2. #### Close a stored SQL text ```typescript type CloseSqlStreamReq = { "type": "close_sql", "sql_id": int32, } type CloseSqlStreamResp = { "type": "close_sql", } ``` The `close_sql` request has the same semantics as the `close_sql` request in Hrana over WebSocket, except that the scope of the SQL texts is just a single stream. > This request was introduced in Hrana 2. #### Get the autocommit state ```typescript type GetAutocommitStreamReq = { "type": "get_autocommit", } type GetAutocommitStreamResp = { "type": "get_autocommit", "is_autocommit": bool, } ``` The `get_autocommit` request has the same semantics as the `get_autocommit` request in Hrana over WebSocket. > This request was introduced in Hrana 3. ### Errors If the client receives an HTTP error (4xx or 5xx response), it means that the server encountered an internal error and the stream is no longer valid. The client should attempt to parse the response body as an `Error` structure (using the encoding indicated by the `Content-Type` response header), but the client must be able to handle responses with different bodies, such as plaintext or HTML, which might be returned by various components in the HTTP stack. ## Shared structures This section describes protocol structures that are common for both Hrana over WebSocket and Hrana over HTTP. ### Errors ```typescript type Error = { "message": string, "code"?: string | null, } ``` Errors can be returned by the server in many places in the protocol, and they are always represented with the `Error` structure. The `message` field contains an English human-readable description of the error. The `code` contains a machine-readable error code. At this moment, the error codes are not yet stabilized and depend on the server implementation. > This structure was introduced in Hrana 1. ### Statements ```typescript type Stmt = { "sql"?: string | null, "sql_id"?: int32 | null, "args"?: Array<Value>, "named_args"?: Array<NamedArg>, "want_rows"?: boolean, } type NamedArg = { "name": string, "value": Value, } ``` A SQL statement is represented by the `Stmt` structure. The text of the SQL statement is specified either by passing a string directly in the `sql` field, or by passing SQL text id that has previously been stored with the `store_sql` request. Exactly one of `sql` and `sql_id` must be passed. The arguments in `args` are bound to parameters in the SQL statement by position. The arguments in `named_args` are bound to parameters by name. In SQLite, the names of arguments include the prefix sign (`:`, `@` or `$`). If the name of the argument does not start with this prefix, the server will try to guess the correct prefix. If an argument is specified both as a positional argument and as a named argument, the named argument should take precedence. It is an error if the request specifies an argument that is not expected by the SQL statement, or if the request does not specify an argument that is expected by the SQL statement. Some servers may not support specifying both positional and named arguments. The `want_rows` field specifies whether the client is interested in the rows produced by the SQL statement. If it is set to `false`, the server should always reply with no rows, even if the statement produced some. If the field is omitted, the default value is `true`. The SQL text should contain just a single statement. Issuing multiple statements separated by a semicolon is not supported. > This structure was introduced in Hrana 1. In Hrana 2, the `sql_id` field was > added and the `sql` and `want_rows` fields were made optional. ### Statement results ```typescript type StmtResult = { "cols": Array<Col>, "rows": Array<Array<Value>>, "affected_row_count": uint64, "last_insert_rowid": string | null, "rows_read": uint64, "rows_written": uint64, "query_duration_ms": double, } type Col = { "name": string | null, "decltype": string | null, } ``` The result of executing an SQL statement is represented by the `StmtResult` structure and it contains information about the returned columns in `cols` and the returned rows in `rows` (the array is empty if the statement did not produce any rows or if `want_rows` was `false` in the request). `affected_row_count` counts the number of rows that were changed by the statement. This is meaningful only if the statement was an INSERT, UPDATE or DELETE, and the value is otherwise undefined. `last_insert_rowid` is the ROWID of the last successful insert into a rowid table. The rowid value is a 64-bit signed integer encoded as a string in JSON. For other statements, the value is undefined. > This structure was introduced in Hrana 1. The `decltype` field in the `Col` > structure was added in Hrana 2. ### Batches ```typescript type Batch = { "steps": Array<BatchStep>, } type BatchStep = { "condition"?: BatchCond | null, "stmt": Stmt, } ``` A batch is represented by the `Batch` structure. It is a list of steps (statements) which are always executed sequentially. If the `condition` of a step is present and evaluates to false, the statement is not executed. > This structure was introduced in Hrana 1. #### Conditions ```typescript type BatchCond = | { "type": "ok", "step": uint32 } | { "type": "error", "step": uint32 } | { "type": "not", "cond": BatchCond } | { "type": "and", "conds": Array<BatchCond> } | { "type": "or", "conds": Array<BatchCond> } | { "type": "is_autocommit" } ``` Conditions are expressions that evaluate to true or false: - `ok` evaluates to true if the `step` (referenced by its 0-based index) was executed successfully. If the statement was skipped, this condition evaluates to false. - `error` evaluates to true if the `step` (referenced by its 0-based index) has produced an error. If the statement was skipped, this condition evaluates to false. - `not` evaluates `cond` and returns the logical negative. - `and` evaluates `conds` and returns the logical conjunction of them. - `or` evaluates `conds` and returns the logical disjunction of them. - `is_autocommit` evaluates to true if the stream is currently in the autocommit state (not inside an explicit transaction) > This structure was introduced in Hrana 1. The `is_autocommit` type was added in Hrana 3. ### Batch results ```typescript type BatchResult = { "step_results": Array<StmtResult | null>, "step_errors": Array<Error | null>, } ``` The result of executing a batch is represented by `BatchResult`. The result contains the results or errors of statements from each step. For the step in `steps[i]`, `step_results[i]` contains the result of the statement if the statement was executed and succeeded, and `step_errors[i]` contains the error if the statement was executed and failed. If the statement was skipped because its condition evaluated to false, both `step_results[i]` and `step_errors[i]` will be `null`. > This structure was introduced in Hrana 1. ### Cursor entries ```typescript type CursorEntry = | StepBeginEntry | StepEndEntry | StepErrorEntry | RowEntry | ErrorEntry ``` Cursor entries are produced by cursors. A sequence of entries encodes the same information as a `BatchResult`, but it is sent to the client incrementally, so both peers don't need to keep the whole result in memory. > These structures were introduced in Hrana 3. #### Step results ```typescript type StepBeginEntry = { "type": "step_begin", "step": uint32, "cols": Array<Col>, } type StepEndEntry = { "type": "step_end", "affected_row_count": uint32, "last_insert_rowid": string | null, } type RowEntry = { "type": "row", "row": Array<Value>, } ``` At the beginning of every batch step that is executed, the server produces a `step_begin` entry. This entry specifies the index of the step (which refers to the `steps` array in the `Batch` structure). The server sends entries for steps in the order in which they are executed. If a step is skipped (because its condition evaluated to false), the server does not send any entry for it. After a `step_begin` entry, the server sends an arbitrary number of `row` entries that encode the individual rows produced by the statement, terminated by the `step_end` entry. Together, these entries encode the same information as the `StmtResult` structure. The server can send another `step_entry` only after the previous step was terminated by `step_end` or by `step_error`, described below. #### Errors ```typescript type StepErrorEntry = { "type": "step_error", "step": uint32, "error": Error, } type ErrorEntry = { "type": "error", "error": Error, } ``` The `step_error` entry indicates that the execution of a statement failed with an error. There are two ways in which the server may produce this entry: 1. Before a `step_begin` entry was sent: this means that the statement failed very early, without producing any results. The `step` field indicates which step has failed (similar to the `step_begin` entry). 2. After a `step_begin` entry was sent: in this case, the server has started executing the statement and produced `step_begin` (and perhaps a number of `row` entries), but then encountered an error. The `step` field must in this case be equal to the `step` of the currently processed step. The `error` entry means that the execution of the whole batch has failed. This can be produced by the server at any time, and it is always the last entry in the cursor. ### Result of describing a statement ```typescript type DescribeResult = { "params": Array<DescribeParam>, "cols": Array<DescribeCol>, "is_explain": boolean, "is_readonly": boolean, } ``` The `DescribeResult` structure is the result of describing a statement. `is_explain` is true if the statement was an `EXPLAIN` statement, and `is_readonly` is true if the statement does not modify the database. > This structure was introduced in Hrana 2. #### Parameters ```typescript type DescribeParam = { "name": string | null, } ``` Information about parameters of the statement is returned in `params`. SQLite indexes parameters from 1, so the first object in the `params` array describes parameter 1. For each parameter, the `name` field specifies the name of the parameter. For parameters of the form `?NNN`, `:AAA`, `@AAA` and `$AAA`, the name includes the initial `?`, `:`, `@` or `$` character. Parameters of the form `?` are nameless, their `name` is `null`. It is also possible that some parameters are not referenced in the statement, in which case the `name` is also `null`. > This structure was introduced in Hrana 2. #### Columns ```typescript type DescribeCol = { "name": string, "decltype": string | null, } ``` Information about columns of the statement is returned in `cols`. For each column, `name` specifies the name assigned by the SQL `AS` clause. For columns without `AS` clause, the name is not specified. For result columns that directly originate from tables in the database, `decltype` specifies the declared type of the column. For other columns (such as results of expressions), `decltype` is `null`. > This structure was introduced in Hrana 2. ### Values ```typescript type Value = | { "type": "null" } | { "type": "integer", "value": string } | { "type": "float", "value": number } | { "type": "text", "value": string } | { "type": "blob", "base64": string } ``` SQLite values are represented by the `Value` structure. The type of the value depends on the `type` field: - `null`: the SQL NULL value. - `integer`: a 64-bit signed integer. In JSON, the `value` is a string to avoid losing precision, because some JSON implementations treat all numbers as 64-bit floats. - `float`: a 64-bit float. - `text`: a UTF-8 string. - `blob`: a binary blob with. In JSON, the value is base64-encoded. > This structure was introduced in Hrana 1. ## Protobuf schema ### Hrana over WebSocket ```proto syntax = "proto3"; package hrana.ws; message ClientMsg { oneof msg { HelloMsg hello = 1; RequestMsg request = 2; } } message ServerMsg { oneof msg { HelloOkMsg hello_ok = 1; HelloErrorMsg hello_error = 2; ResponseOkMsg response_ok = 3; ResponseErrorMsg response_error = 4; } } message HelloMsg { optional string jwt = 1; } message HelloOkMsg { } message HelloErrorMsg { Error error = 1; } message RequestMsg { int32 request_id = 1; oneof request { OpenStreamReq open_stream = 2; CloseStreamReq close_stream = 3; ExecuteReq execute = 4; BatchReq batch = 5; OpenCursorReq open_cursor = 6; CloseCursorReq close_cursor = 7; FetchCursorReq fetch_cursor = 8; SequenceReq sequence = 9; DescribeReq describe = 10; StoreSqlReq store_sql = 11; CloseSqlReq close_sql = 12; GetAutocommitReq get_autocommit = 13; } } message ResponseOkMsg { int32 request_id = 1; oneof response { OpenStreamResp open_stream = 2; CloseStreamResp close_stream = 3; ExecuteResp execute = 4; BatchResp batch = 5; OpenCursorResp open_cursor = 6; CloseCursorResp close_cursor = 7; FetchCursorResp fetch_cursor = 8; SequenceResp sequence = 9; DescribeResp describe = 10; StoreSqlResp store_sql = 11; CloseSqlResp close_sql = 12; GetAutocommitResp get_autocommit = 13; } } message ResponseErrorMsg { int32 request_id = 1; Error error = 2; } message OpenStreamReq { int32 stream_id = 1; } message OpenStreamResp { } message CloseStreamReq { int32 stream_id = 1; } message CloseStreamResp { } message ExecuteReq { int32 stream_id = 1; Stmt stmt = 2; } message ExecuteResp { StmtResult result = 1; } message BatchReq { int32 stream_id = 1; Batch batch = 2; } message BatchResp { BatchResult result = 1; } message OpenCursorReq { int32 stream_id = 1; int32 cursor_id = 2; Batch batch = 3; } message OpenCursorResp { } message CloseCursorReq { int32 cursor_id = 1; } message CloseCursorResp { } message FetchCursorReq { int32 cursor_id = 1; uint32 max_count = 2; } message FetchCursorResp { repeated CursorEntry entries = 1; bool done = 2; } message StoreSqlReq { int32 sql_id = 1; string sql = 2; } message StoreSqlResp { } message CloseSqlReq { int32 sql_id = 1; } message CloseSqlResp { } message SequenceReq { int32 stream_id = 1; optional string sql = 2; optional int32 sql_id = 3; } message SequenceResp { } message DescribeReq { int32 stream_id = 1; optional string sql = 2; optional int32 sql_id = 3; } message DescribeResp { DescribeResult result = 1; } message GetAutocommitReq { int32 stream_id = 1; } message GetAutocommitResp { bool is_autocommit = 1; } ``` ### Hrana over HTTP ```proto syntax = "proto3"; package hrana.http; message PipelineReqBody { optional string baton = 1; repeated StreamRequest requests = 2; } message PipelineRespBody { optional string baton = 1; optional string base_url = 2; repeated StreamResult results = 3; } message StreamResult { oneof result { StreamResponse ok = 1; Error error = 2; } } message CursorReqBody { optional string baton = 1; Batch batch = 2; } message CursorRespBody { optional string baton = 1; optional string base_url = 2; } message StreamRequest { oneof request { CloseStreamReq close = 1; ExecuteStreamReq execute = 2; BatchStreamReq batch = 3; SequenceStreamReq sequence = 4; DescribeStreamReq describe = 5; StoreSqlStreamReq store_sql = 6; CloseSqlStreamReq close_sql = 7; GetAutocommitStreamReq get_autocommit = 8; } } message StreamResponse { oneof response { CloseStreamResp close = 1; ExecuteStreamResp execute = 2; BatchStreamResp batch = 3; SequenceStreamResp sequence = 4; DescribeStreamResp describe = 5; StoreSqlStreamResp store_sql = 6; CloseSqlStreamResp close_sql = 7; GetAutocommitStreamResp get_autocommit = 8; } } message CloseStreamReq { } message CloseStreamResp { } message ExecuteStreamReq { Stmt stmt = 1; } message ExecuteStreamResp { StmtResult result = 1; } message BatchStreamReq { Batch batch = 1; } message BatchStreamResp { BatchResult result = 1; } message SequenceStreamReq { optional string sql = 1; optional int32 sql_id = 2; } message SequenceStreamResp { } message DescribeStreamReq { optional string sql = 1; optional int32 sql_id = 2; } message DescribeStreamResp { DescribeResult result = 1; } message StoreSqlStreamReq { int32 sql_id = 1; string sql = 2; } message StoreSqlStreamResp { } message CloseSqlStreamReq { int32 sql_id = 1; } message CloseSqlStreamResp { } message GetAutocommitStreamReq { } message GetAutocommitStreamResp { bool is_autocommit = 1; } ``` ### Shared structures ```proto syntax = "proto3"; package hrana; message Error { string message = 1; optional string code = 2; } message Stmt { optional string sql = 1; optional int32 sql_id = 2; repeated Value args = 3; repeated NamedArg named_args = 4; optional bool want_rows = 5; } message NamedArg { string name = 1; Value value = 2; } message StmtResult { repeated Col cols = 1; repeated Row rows = 2; uint64 affected_row_count = 3; optional sint64 last_insert_rowid = 4; } message Col { optional string name = 1; optional string decltype = 2; } message Row { repeated Value values = 1; } message Batch { repeated BatchStep steps = 1; } message BatchStep { optional BatchCond condition = 1; Stmt stmt = 2; } message BatchCond { oneof cond { uint32 step_ok = 1; uint32 step_error = 2; BatchCond not = 3; CondList and = 4; CondList or = 5; IsAutocommit is_autocommit = 6; } message CondList { repeated BatchCond conds = 1; } message IsAutocommit { } } message BatchResult { map<uint32, StmtResult> step_results = 1; map<uint32, Error> step_errors = 2; } message CursorEntry { oneof entry { StepBeginEntry step_begin = 1; StepEndEntry step_end = 2; StepErrorEntry step_error = 3; Row row = 4; Error error = 5; } } message StepBeginEntry { uint32 step = 1; repeated Col cols = 2; } message StepEndEntry { uint64 affected_row_count = 1; optional sint64 last_insert_rowid = 2; } message StepErrorEntry { uint32 step = 1; Error error = 2; } message DescribeResult { repeated DescribeParam params = 1; repeated DescribeCol cols = 2; bool is_explain = 3; bool is_readonly = 4; } message DescribeParam { optional string name = 1; } message DescribeCol { string name = 1; optional string decltype = 2; } message Value { oneof value { Null null = 1; sint64 integer = 2; double float = 3; string text = 4; bytes blob = 5; } message Null {} } ```

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Xexr/mcp-libsql'

If you have feedback or need assistance with the MCP directory API, please join our Discord server