0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2024-12-15 12:09:42 +00:00
libsql/docs/HRANA_3_SPEC.md
Lucio Franco 7708a43772
server: add query_duration_ms stats to query res (#1309)
* server: add `query_duration_ms` stats to query res

* hrana: default new stats fields
2024-04-15 16:37:13 +00:00

1714 lines
44 KiB
Markdown

# 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)
```
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)
```
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)
```
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)
```
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)
```
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)
```
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 varint 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`
> strucure 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 evalated 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 {}
}
```