0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2024-09-22 07:20:55 +00:00
libsql/docs/HRANA_2_SPEC.md
2023-11-14 10:28:20 -05:00

6.0 KiB

The Hrana protocol specification (version 2)

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.

In this specification, version 2 of the protocol is described as a set of extensions to version 1.

Version 2 is designed to be a strict superset of version 1: every server that implements version 2 also implements version 1.

Version negotiation

The Hrana protocol version 2 uses a WebSocket subprotocol hrana2. The WebSocket subprotocol negotiation allows the client and server to use version 2 of the protocol if both peers support it, but fall back to version 1 if the client or the server don't support version 2.

Messages

Hello

The hello message has the same format as in version 1. The client must send it as the first message, but in version 2, the client can also send it again anytime during the lifetime of the connection to reauthenticate, by providing a new JWT.

This feature was introduced because, in long-living connections, the JWT used to authenticate the client may expire and the server may terminate the connection. Using this feature, the client can provide a fresh JWT, thus keeping the connection properly authenticated.

Requests

Version 2 introduces four new requests:

type Request =
    | ...
    | SequenceReq
    | DescribeReq
    | StoreSqlReq
    | CloseSqlReq

type Response =
    | ...
    | SequenceResp
    | DescribeResp
    | StoreSqlReq
    | CloseSqlReq

Store an SQL text on the server

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 an error if the client tries to store an SQL text with an id which is already in use.

Close a stored SQL text

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.

Execute a sequence of SQL statements

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.

Describe a statement

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.

type DescribeResult = {
    "params": Array<DescribeParam>,
    "cols": Array<DescribeCol>,
    "is_explain": boolean,
    "is_readonly": boolean,
}

In the result, is_explain is true if the statement was an EXPLAIN statement, and is_readonly is true if the statement does not modify the database.

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.

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.

Other changes

Statement

type Stmt = {
    "sql"?: string | undefined,
    "sql_id"?: int32 | undefined,
    "args"?: Array<Value>,
    "named_args"?: Array<NamedArg>,
    "want_rows"?: boolean,
}

In version 2 of the protocol, the SQL text of a statement can be 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.

Also, the want_rows field is now optional and defaults to true.

Statement result

type Col = {
    "name": string | null,
    "decltype": string | null,
}

In version 2 of the protocol, the column descriptor in the statement result also includes the declared type of the column (if available).