mirror of
https://github.com/tursodatabase/libsql.git
synced 2024-12-15 12:09:42 +00:00
453 lines
15 KiB
Markdown
453 lines
15 KiB
Markdown
# The Hrana protocol specification (version 1)
|
|
|
|
Hrana (from Czech "hrana", which means "edge") is a protocol for connecting to a
|
|
SQLite database over a WebSocket. It is designed to be used from edge functions,
|
|
where low latency and small overhead is important.
|
|
|
|
## Motivation
|
|
|
|
This protocol aims to provide several benefits over the Postgres wire protocol:
|
|
|
|
- Works in edge runtimes: WebSockets are available in all edge runtimes
|
|
(Cloudflare Workers, Deno Deploy, Lagon), but general TCP sockets are not
|
|
(notably, sockets are not supported by Cloudflare Workers).
|
|
|
|
- Fast cold start: the Postgres wire protocol requires [at least two
|
|
roundtrips][pgwire-flow] before the client can send queries, but Hrana needs
|
|
just a single roundtrip introduced by the WebSocket protocol. (In both cases,
|
|
additional roundtrips might be necessary due to TLS.)
|
|
|
|
- Multiplexing: a single Hrana connection can open multiple SQL streams, so an
|
|
application needs to open just a single connection even if it handles multiple
|
|
concurrent requests.
|
|
|
|
- Simplicity: Hrana is a simple protocol, so a client needs few lines of
|
|
code. This is important on edge runtimes that impose hard limits on code size
|
|
(usually just a few MB).
|
|
|
|
[pgwire-flow]: https://www.postgresql.org/docs/current/protocol-flow.html
|
|
|
|
## Usage
|
|
|
|
The Hrana protocol is intended to be used in one of two ways:
|
|
|
|
- Connecting to `sqld`: edge functions and other clients can connect directly
|
|
to `sqld` using Hrana, because it has native support for the protocol. This is
|
|
the approach with lowest latency, because no software in the middle is
|
|
necessary.
|
|
|
|
- Connecting to SQLite through a proxy: this allows edge functions
|
|
to efficiently connect to an existing SQLite databases.
|
|
|
|
## Overview
|
|
|
|
The protocol runs on top of the [WebSocket protocol][rfc6455] as a subprotocol
|
|
`hrana1`. The client includes `hrana1` in the `Sec-WebSocket-Protocol` request
|
|
header in the opening handshake, and the server replies with `hrana1` in the
|
|
same response header. Future versions of the Hrana protocol will be negotiated
|
|
as different WebSocket subprotocols.
|
|
|
|
[rfc6455]: https://www.rfc-editor.org/rfc/rfc6455
|
|
|
|
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_. A stream
|
|
corresponds to a "session" in PostgreSQL or a "connection" in SQLite: SQL
|
|
statements in a stream are executed sequentially and can affect stream-specific
|
|
state such as transactions (with SQL `BEGIN` or `SAVEPOINT`). 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. The protocol can thus work with any SQL dialect.
|
|
|
|
To reduce the number of roundtrips, the protocol supports batches of statements
|
|
that are executed conditionally, based on success or failure of previous
|
|
statements. This mechanism is used to implement non-interactive transactions in
|
|
a single roundtrip.
|
|
|
|
## Messages
|
|
|
|
All messages exchanged between the client and server are text messages encoded
|
|
in JSON. Future versions of the protocol might additionally support binary
|
|
messages with a more compact binary encoding.
|
|
|
|
This specification describes the JSON messages using TypeScript syntax as
|
|
follows:
|
|
|
|
```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.
|
|
|
|
To maintain backwards compatibility, the recipient must ignore any unrecognized
|
|
fields in the JSON messages. However, if the recipient receives a message with
|
|
unrecognized `type`, it must abort the connection.
|
|
|
|
### 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
|
|
|
|
```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.
|
|
|
|
```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.
|
|
|
|
### Errors
|
|
|
|
```typescript
|
|
type Error = {
|
|
"message": string,
|
|
"code"?: string | null,
|
|
}
|
|
```
|
|
|
|
When a server refuses to accept a client `hello` or fails to process a
|
|
`request`, it responds with a message that describes the error. The `message`
|
|
field contains an English human-readable description of the error. The `code`
|
|
contains a machine-readable error code.
|
|
|
|
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 that is not a valid JSON.
|
|
- 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.
|
|
|
|
## Requests
|
|
|
|
Most of the work in the protocol happens in request/response interactions.
|
|
|
|
```typescript
|
|
type Request =
|
|
| OpenStreamReq
|
|
| CloseStreamReq
|
|
| ExecuteReq
|
|
| BatchReq
|
|
|
|
type Response =
|
|
| OpenStreamResp
|
|
| CloseStreamResp
|
|
| ExecuteResp
|
|
| BatchResp
|
|
```
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
```typescript
|
|
type Stmt = {
|
|
"sql": string,
|
|
"args"?: Array<Value>,
|
|
"named_args"?: Array<NamedArg>,
|
|
"want_rows": boolean,
|
|
}
|
|
|
|
type NamedArg = {
|
|
"name": string,
|
|
"value": Value,
|
|
}
|
|
```
|
|
|
|
A statement contains the SQL text in `sql` and arguments.
|
|
|
|
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.
|
|
|
|
For 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.
|
|
|
|
The SQL text should contain just a single statement. Issuing multiple statements
|
|
separated by a semicolon is not supported.
|
|
|
|
```typescript
|
|
type StmtResult = {
|
|
"cols": Array<Col>,
|
|
"rows": Array<Array<Value>>,
|
|
"affected_row_count": int32,
|
|
"last_insert_rowid": string | null,
|
|
}
|
|
|
|
type Col = {
|
|
"name": string | null,
|
|
}
|
|
```
|
|
|
|
The result of executing an SQL statement 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. For
|
|
other statements, the value is undefined.
|
|
|
|
### 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.
|
|
|
|
```typescript
|
|
type Batch = {
|
|
"steps": Array<BatchStep>,
|
|
}
|
|
|
|
type BatchStep = {
|
|
"condition"?: BatchCond | null,
|
|
"stmt": Stmt,
|
|
}
|
|
|
|
type BatchResult = {
|
|
"step_results": Array<StmtResult | null>,
|
|
"step_errors": Array<Error | null>,
|
|
}
|
|
```
|
|
|
|
A batch 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
|
|
skipped.
|
|
|
|
The batch 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`.
|
|
|
|
```typescript
|
|
type BatchCond =
|
|
| { "type": "ok", "step": int32 }
|
|
| { "type": "error", "step": int32 }
|
|
| { "type": "not", "cond": BatchCond }
|
|
| { "type": "and", "conds": Array<BatchCond> }
|
|
| { "type": "or", "conds": Array<BatchCond> }
|
|
```
|
|
|
|
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.
|
|
|
|
### Values
|
|
|
|
```typescript
|
|
type Value =
|
|
| { "type": "null" }
|
|
| { "type": "integer", "value": string }
|
|
| { "type": "float", "value": number }
|
|
| { "type": "text", "value": string }
|
|
| { "type": "blob", "base64": string }
|
|
```
|
|
|
|
Values passed as arguments to SQL statements and returned in rows are one of
|
|
supported types:
|
|
|
|
- `null`: the SQL NULL value
|
|
- `integer`: a 64-bit signed integer, its `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 text string
|
|
- `blob`: a binary blob with base64-encoded value
|
|
|
|
These types exactly correspond to SQLite types. In the future, the protocol
|
|
might be extended with more types for compatibility with Postgres.
|
|
|
|
### 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.
|