├── .gitignore ├── 1713449025236-HRANA_3_SPEC.md ├── LICENSE ├── README.md ├── _tests ├── perf.ts ├── test1.ts ├── test2.ts └── test3.ts ├── check_api_level_diff ├── get_new_api_level ├── js_test ├── package.json ├── src ├── functions.ts ├── main.ts └── types.ts ├── tsconfig.json └── tsup.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | # Project specific 178 | lib-cjs/ 179 | lib-esm/ 180 | _conf.ts 181 | wiki/ 182 | *.lockb 183 | 184 | 185 | package-lock.json 186 | -------------------------------------------------------------------------------- /1713449025236-HRANA_3_SPEC.md: -------------------------------------------------------------------------------- 1 | # The Hrana protocol specification (version 3) 2 | 3 | Hrana (from Czech "hrana", which means "edge") is a protocol for connecting to a 4 | SQLite database over the network. It is designed to be used from edge functions 5 | and other environments where low latency and small overhead is important. 6 | 7 | This is a specification for version 3 of the Hrana protocol (Hrana 3). 8 | 9 | ## Overview 10 | 11 | The Hrana protocol provides SQL _streams_. Each stream corresponds to a SQLite 12 | connection and executes a sequence of SQL statements. 13 | 14 | ### Variants (WebSocket / HTTP) 15 | 16 | The protocol has two variants: 17 | 18 | - Hrana over WebSocket, which uses WebSocket as the underlying protocol. 19 | Multiple streams can be multiplexed over a single WebSocket. 20 | - Hrana over HTTP, which communicates with the server using HTTP requests. This 21 | is less efficient than WebSocket, but HTTP is the only reliable protocol in 22 | some environments. 23 | 24 | Each of these variants is described later. 25 | 26 | ### Encoding 27 | 28 | The protocol has two encodings: 29 | 30 | - [JSON][rfc8259] is the canonical encoding, backward compatible with Hrana 1 31 | and 2. 32 | - Protobuf ([Protocol Buffers][protobuf]) is a more compact binary encoding, 33 | introduced in Hrana 3. 34 | 35 | [rfc8259]: https://datatracker.ietf.org/doc/html/rfc8259 36 | [protobuf]: https://protobuf.dev/ 37 | 38 | This document defines protocol structures in JSON and specifies the schema using 39 | TypeScript type notation. The Protobuf schema is described in proto3 syntax in 40 | an appendix. 41 | 42 | The encoding is negotiated between the server and client. This process depends 43 | on the variant (WebSocket or HTTP) and is described later. All Hrana 3 servers 44 | must support both JSON and Protobuf; clients can choose which encodings to 45 | support and use. 46 | 47 | Both encodings support forward compatibility: when a peer (client or server) 48 | receives a protocol structure that includes an unrecognized field (object 49 | property in JSON or a message field in Protobuf), it must ignore this field. 50 | 51 | 52 | 53 | ## Hrana over WebSocket 54 | 55 | Hrana over WebSocket runs on top of the [WebSocket protocol][rfc6455]. 56 | 57 | ### Version and encoding negotiation 58 | 59 | The version of the protocol and the encoding is negotiated as a WebSocket 60 | subprotocol: the client includes a list of supported subprotocols in the 61 | `Sec-WebSocket-Protocol` request header in the opening handshake, and the server 62 | replies with the selected subprotocol in the same response header. 63 | 64 | The negotiation mechanism provides backward compatibility with older versions of 65 | the Hrana protocol and forward compatibility with newer versions. 66 | 67 | [rfc6455]: https://www.rfc-editor.org/rfc/rfc6455 68 | 69 | The WebSocket subprotocols defined in all Hrana versions are as follows: 70 | 71 | | Subprotocol | Version | Encoding | 72 | |-------------|---------|----------| 73 | | `hrana1` | 1 | JSON | 74 | | `hrana2` | 2 | JSON | 75 | | `hrana3` | 3 | JSON | 76 | | `hrana3-protobuf` | 3 | Protobuf | 77 | 78 | This document describes version 3 of the Hrana protocol. Versions 1 and 2 are 79 | described in their own specifications. 80 | 81 | Version 3 of Hrana over WebSocket is designed to be a strict superset of 82 | versions 1 and 2: every server that implements Hrana 3 over WebSocket also 83 | implements versions 1 and 2 and should accept clients that indicate subprotocol 84 | `hrana1` or `hrana2`. 85 | 86 | ### Overview 87 | 88 | The client starts the connection by sending a _hello_ message, which 89 | authenticates the client to the server. The server responds with either a 90 | confirmation or with an error message, closing the connection. The client can 91 | choose not to wait for the confirmation and immediately send further messages to 92 | reduce latency. 93 | 94 | A single connection can host an arbitrary number of streams. In effect, one 95 | Hrana connection works as a "connection pool" in traditional SQL servers. 96 | 97 | After a stream is opened, the client can execute SQL statements on it. For the 98 | purposes of this protocol, the statements are arbitrary strings with optional 99 | parameters. 100 | 101 | To reduce the number of roundtrips, the protocol supports batches of statements 102 | that are executed conditionally, based on success or failure of previous 103 | statements. Clients can use this mechanism to implement non-interactive 104 | transactions in a single roundtrip. 105 | 106 | ### Messages 107 | 108 | If the negotiated encoding is JSON, all messages exchanged between the client 109 | and server are sent as text frames (opcode 0x1) on the WebSocket. If the 110 | negotiated encoding is Protobuf, messages are sent as binary frames (opcode 111 | 0x2). 112 | 113 | ```typescript 114 | type ClientMsg = 115 | | HelloMsg 116 | | RequestMsg 117 | 118 | type ServerMsg = 119 | | HelloOkMsg 120 | | HelloErrorMsg 121 | | ResponseOkMsg 122 | | ResponseErrorMsg 123 | ``` 124 | 125 | The client sends messages of type `ClientMsg`, and the server sends messages of 126 | type `ServerMsg`. The type of the message is determined by its `type` field. 127 | 128 | #### Hello 129 | 130 | ```typescript 131 | type HelloMsg = { 132 | "type": "hello", 133 | "jwt": string | null, 134 | } 135 | ``` 136 | 137 | The `hello` message is sent as the first message by the client. It authenticates 138 | the client to the server using the [Json Web Token (JWT)][rfc7519] passed in the 139 | `jwt` field. If no authentication is required (which might be useful for 140 | development and debugging, or when authentication is performed by other means, 141 | such as with mutual TLS), the `jwt` field might be set to `null`. 142 | 143 | [rfc7519]: https://www.rfc-editor.org/rfc/rfc7519 144 | 145 | The client can also send the `hello` message again anytime during the lifetime 146 | of the connection to reauthenticate, by providing a new JWT. If the provided JWT 147 | expires and the client does not provide a new one in a `hello` message, the 148 | server may terminate the connection. 149 | 150 | ```typescript 151 | type HelloOkMsg = { 152 | "type": "hello_ok", 153 | } 154 | 155 | type HelloErrorMsg = { 156 | "type": "hello_error", 157 | "error": Error, 158 | } 159 | ``` 160 | 161 | The server waits for the `hello` message from the client and responds with a 162 | `hello_ok` message if the client can proceed, or with a `hello_error` message 163 | describing the failure. 164 | 165 | The client may choose not to wait for a response to its `hello` message before 166 | sending more messages to save a network roundtrip. If the server responds with 167 | `hello_error`, it must ignore all further messages sent by the client and it 168 | should close the WebSocket immediately. 169 | 170 | #### Request/response 171 | 172 | ```typescript 173 | type RequestMsg = { 174 | "type": "request", 175 | "request_id": int32, 176 | "request": Request, 177 | } 178 | ``` 179 | 180 | After sending the `hello` message, the client can start sending `request` 181 | messages. The client uses requests to open SQL streams and execute statements on 182 | them. The client assigns an identifier to every request, which is then used to 183 | match a response to the request. 184 | 185 | The `Request` structure represents the payload of the request and is defined 186 | later. 187 | 188 | ```typescript 189 | type ResponseOkMsg = { 190 | "type": "response_ok", 191 | "request_id": int32, 192 | "response": Response, 193 | } 194 | 195 | type ResponseErrorMsg = { 196 | "type": "response_error", 197 | "request_id": int32, 198 | "error": Error, 199 | } 200 | ``` 201 | 202 | When the server receives a `request` message, it must eventually send either a 203 | `response_ok` with the response or a `response_error` that describes a failure. 204 | The response from the server includes the same `request_id` that was provided by 205 | the client in the request. The server can send the responses in arbitrary order. 206 | 207 | The request ids are arbitrary 32-bit signed integers, the server does not 208 | interpret them in any way. 209 | 210 | The server should limit the number of outstanding requests to a reasonable 211 | value, and stop receiving messages when this limit is reached. This will cause 212 | the TCP flow control to kick in and apply back-pressure to the client. On the 213 | other hand, the client should always receive messages, to avoid deadlock. 214 | 215 | ### Requests 216 | 217 | Most of the work in the protocol happens in request/response interactions. 218 | 219 | ```typescript 220 | type Request = 221 | | OpenStreamReq 222 | | CloseStreamReq 223 | | ExecuteReq 224 | | BatchReq 225 | | OpenCursorReq 226 | | CloseCursorReq 227 | | FetchCursorReq 228 | | SequenceReq 229 | | DescribeReq 230 | | StoreSqlReq 231 | | CloseSqlReq 232 | | GetAutocommitReq 233 | 234 | type Response = 235 | | OpenStreamResp 236 | | CloseStreamResp 237 | | ExecuteResp 238 | | BatchResp 239 | | OpenCursorResp 240 | | CloseCursorResp 241 | | FetchCursorResp 242 | | SequenceResp 243 | | DescribeResp 244 | | StoreSqlReq 245 | | CloseSqlReq 246 | | GetAutocommitResp 247 | ``` 248 | 249 | The type of the request and response is determined by its `type` field. The 250 | `type` of the response must always match the `type` of the request. The 251 | individual requests and responses are defined in the rest of this section. 252 | 253 | #### Open stream 254 | 255 | ```typescript 256 | type OpenStreamReq = { 257 | "type": "open_stream", 258 | "stream_id": int32, 259 | } 260 | 261 | type OpenStreamResp = { 262 | "type": "open_stream", 263 | } 264 | ``` 265 | 266 | The client uses the `open_stream` request to open an SQL stream, which is then 267 | used to execute SQL statements. The streams are identified by arbitrary 32-bit 268 | signed integers assigned by the client. 269 | 270 | The client can optimistically send follow-up requests on a stream before it 271 | receives the response to its `open_stream` request. If the server receives a 272 | request that refers to a stream that failed to open, it should respond with an 273 | error, but it should not close the connection. 274 | 275 | Even if the `open_stream` request returns an error, the stream id is still 276 | considered as used, and the client cannot reuse it until it sends a 277 | `close_stream` request. 278 | 279 | The server can impose a reasonable limit to the number of streams opened at the 280 | same time. 281 | 282 | > This request was introduced in Hrana 1. 283 | 284 | #### Close stream 285 | 286 | ```typescript 287 | type CloseStreamReq = { 288 | "type": "close_stream", 289 | "stream_id": int32, 290 | } 291 | 292 | type CloseStreamResp = { 293 | "type": "close_stream", 294 | } 295 | ``` 296 | 297 | When the client is done with a stream, it should close it using the 298 | `close_stream` request. The client can safely reuse the stream id after it 299 | receives the response. 300 | 301 | The client should close even streams for which the `open_stream` request 302 | returned an error. 303 | 304 | If there is an open cursor for the stream, the cursor is closed together with 305 | the stream. 306 | 307 | > This request was introduced in Hrana 1. 308 | 309 | #### Execute a statement 310 | 311 | ```typescript 312 | type ExecuteReq = { 313 | "type": "execute", 314 | "stream_id": int32, 315 | "stmt": Stmt, 316 | } 317 | 318 | type ExecuteResp = { 319 | "type": "execute", 320 | "result": StmtResult, 321 | } 322 | ``` 323 | 324 | The client sends an `execute` request to execute an SQL statement on a stream. 325 | The server responds with the result of the statement. The `Stmt` and 326 | `StmtResult` structures are defined later. 327 | 328 | If the statement fails, the server responds with an error response (message of 329 | type `"response_error"`). 330 | 331 | > This request was introduced in Hrana 1. 332 | 333 | #### Execute a batch 334 | 335 | ```typescript 336 | type BatchReq = { 337 | "type": "batch", 338 | "stream_id": int32, 339 | "batch": Batch, 340 | } 341 | 342 | type BatchResp = { 343 | "type": "batch", 344 | "result": BatchResult, 345 | } 346 | ``` 347 | 348 | The `batch` request runs a batch of statements on a stream. The server responds 349 | with the result of the batch execution. 350 | 351 | If a statement in the batch fails, the error is returned inside the 352 | `BatchResult` structure in a normal response (message of type `"response_ok"`). 353 | However, if the server encounters a serious error that prevents it from 354 | executing the batch, it responds with an error response (message of type 355 | `"response_error"`). 356 | 357 | > This request was introduced in Hrana 1. 358 | 359 | #### Open a cursor executing a batch 360 | 361 | ```typescript 362 | type OpenCursorReq = { 363 | "type": "open_cursor", 364 | "stream_id": int32, 365 | "cursor_id": int32, 366 | "batch": Batch, 367 | } 368 | 369 | type OpenCursorResp = { 370 | "type": "open_cursor", 371 | } 372 | ``` 373 | 374 | The `open_cursor` request runs a batch of statements like the `batch` request, 375 | but instead of returning all statement results in the request response, it opens 376 | a _cursor_ which the client can then use to read the results incrementally. 377 | 378 | The `cursor_id` is an arbitrary 32-bit integer id assigned by the client. This 379 | id must be unique for the given connection and must not be used by another 380 | cursor that was not yet closed using the `close_cursor` request. 381 | 382 | Even if the `open_cursor` request returns an error, the cursor id is still 383 | considered as used, and the client cannot reuse it until it sends a 384 | `close_cursor` request. 385 | 386 | After the `open_cursor` request, the client must not send more requests on the 387 | stream until the cursor is closed using the `close_cursor` request. 388 | 389 | > This request was introduced in Hrana 3. 390 | 391 | #### Close a cursor 392 | 393 | ```typescript 394 | type CloseCursorReq = { 395 | "type": "close_cursor", 396 | "cursor_id": int32, 397 | } 398 | 399 | type CloseCursorResp = { 400 | "type": "close_cursor", 401 | } 402 | ``` 403 | 404 | The `close_cursor` request closes a cursor opened by an `open_cursor` request 405 | and allows the server to release resources and continue processing other 406 | requests for the given stream. 407 | 408 | > This request was introduced in Hrana 3. 409 | 410 | #### Fetch entries from a cursor 411 | 412 | ```typescript 413 | type FetchCursorReq = { 414 | "type": "fetch_cursor", 415 | "cursor_id": int32, 416 | "max_count": uint32, 417 | } 418 | 419 | type FetchCursorResp = { 420 | "type": "fetch_cursor", 421 | "entries": Array, 422 | "done": boolean, 423 | } 424 | ``` 425 | 426 | The `fetch_cursor` request reads data from a cursor previously opened with the 427 | `open_cursor` request. The cursor data is encoded as a sequence of entries 428 | (`CursorEntry` structure). `max_count` in the request specifies the maximum 429 | number of entries that the client wants to receive in the response; however, the 430 | server may decide to send fewer entries. 431 | 432 | If the `done` field in the response is set to true, then the cursor is finished 433 | and all subsequent calls to `fetch_cursor` are guaranteed to return zero 434 | entries. The client should then close the cursor by sending the `close_cursor` 435 | request. 436 | 437 | If the `cursor_id` refers to a cursor for which the `open_cursor` request 438 | returned an error, and the cursor hasn't yet been closed with `close_cursor`, 439 | then the server should return an error, but it must not close the connection 440 | (i.e., this is not a protocol error). 441 | 442 | > This request was introduced in Hrana 3. 443 | 444 | #### Store an SQL text on the server 445 | 446 | ```typescript 447 | type StoreSqlReq = { 448 | "type": "store_sql", 449 | "sql_id": int32, 450 | "sql": string, 451 | } 452 | 453 | type StoreSqlResp = { 454 | "type": "store_sql", 455 | } 456 | ``` 457 | 458 | The `store_sql` request stores an SQL text on the server. The client can then 459 | refer to this SQL text in other requests by its id, instead of repeatedly 460 | sending the same string over the network. 461 | 462 | SQL text ids are arbitrary 32-bit signed integers assigned by the client. It is 463 | a protocol error if the client tries to store an SQL text with an id which is 464 | already in use. 465 | 466 | > This request was introduced in Hrana 2. 467 | 468 | #### Close a stored SQL text 469 | 470 | ```typescript 471 | type CloseSqlReq = { 472 | "type": "close_sql", 473 | "sql_id": int32, 474 | } 475 | 476 | type CloseSqlResp = { 477 | "type": "close_sql", 478 | } 479 | ``` 480 | 481 | The `close_sql` request can be used to delete an SQL text stored on the server 482 | with `store_sql`. The client can safely reuse the SQL text id after it receives 483 | the response. 484 | 485 | It is not an error if the client attempts to close a SQL text id that is not 486 | used. 487 | 488 | > This request was introduced in Hrana 2. 489 | 490 | #### Execute a sequence of SQL statements 491 | 492 | ```typescript 493 | type SequenceReq = { 494 | "type": "sequence", 495 | "stream_id": int32, 496 | "sql"?: string | null, 497 | "sql_id"?: int32 | null, 498 | } 499 | 500 | type SequenceResp = { 501 | "type": "sequence", 502 | } 503 | ``` 504 | 505 | The `sequence` request executes a sequence of SQL statements separated by 506 | semicolons on the stream given by `stream_id`. `sql` or `sql_id` specify the SQL 507 | text; exactly one of these fields must be specified. 508 | 509 | Any rows returned by the statements are ignored. If any statement fails, the 510 | subsequent statements are not executed and the request returns an error 511 | response. 512 | 513 | > This request was introduced in Hrana 2. 514 | 515 | #### Describe a statement 516 | 517 | ```typescript 518 | type DescribeReq = { 519 | "type": "describe", 520 | "stream_id": int32, 521 | "sql"?: string | null, 522 | "sql_id"?: int32 | null, 523 | } 524 | 525 | type DescribeResp = { 526 | "type": "describe", 527 | "result": DescribeResult, 528 | } 529 | ``` 530 | 531 | The `describe` request is used to parse and analyze a SQL statement. `stream_id` 532 | specifies the stream on which the statement is parsed. `sql` or `sql_id` specify 533 | the SQL text: exactly one of these two fields must be specified, `sql` passes 534 | the SQL directly as a string, while `sql_id` refers to a SQL text previously 535 | stored with `store_sql`. In the response, `result` contains the result of 536 | describing a statement. 537 | 538 | > This request was introduced in Hrana 2. 539 | 540 | #### Get the autocommit state 541 | 542 | ```typescript 543 | type GetAutocommitReq = { 544 | "type": "get_autocommit", 545 | "stream_id": int32, 546 | } 547 | 548 | type GetAutocommitResp = { 549 | "type": "get_autocommit", 550 | "is_autocommit": bool, 551 | } 552 | ``` 553 | 554 | The `get_autocommit` request can be used to check whether the stream is in 555 | autocommit state (not inside an explicit transaction). 556 | 557 | > This request was introduced in Hrana 3. 558 | 559 | ### Errors 560 | 561 | If either peer detects that the protocol has been violated, it should close the 562 | WebSocket with an appropriate WebSocket close code and reason. Some examples of 563 | protocol violations include: 564 | 565 | - Text message payload that is not a valid JSON. 566 | - Data frame type that does not match the negotiated encoding (i.e., binary frame when 567 | the encoding is JSON or a text frame when the encoding is Protobuf). 568 | - Unrecognized `ClientMsg` or `ServerMsg` (the field `type` is unknown or 569 | missing) 570 | - Client receives a `ResponseOkMsg` or `ResponseErrorMsg` with a `request_id` 571 | that has not been sent in a `RequestMsg` or that has already received a 572 | response. 573 | 574 | ### Ordering 575 | 576 | The protocol allows the server to reorder the responses: it is not necessary to 577 | send the responses in the same order as the requests. However, the server must 578 | process requests related to a single stream id in order. 579 | 580 | For example, this means that a client can send an `open_stream` request 581 | immediately followed by a batch of `execute` requests on that stream and the 582 | server will always process them in correct order. 583 | 584 | 585 | 586 | ## Hrana over HTTP 587 | 588 | Hrana over HTTP runs on top of HTTP. Any version of the HTTP protocol can be 589 | used. 590 | 591 | ### Overview 592 | 593 | HTTP is a stateless protocol, so there is no concept of a connection like in the 594 | WebSocket protocol. However, Hrana needs to expose stateful streams, so it needs 595 | to ensure that requests on the same stream are tied together. 596 | 597 | This is accomplished by the use of a baton, which is similar to a session cookie. 598 | The server returns a baton in every response to a request on the stream, and the 599 | client then needs to include the baton in the subsequent request. The client 600 | must serialize the requests on a stream: it must wait for a response to the 601 | previous request before sending next request on the same stream. 602 | 603 | The server can also optionally specify a different URL that the client should 604 | use for the requests on the stream. This can be used to ensure that stream 605 | requests are "sticky" and reach the same server. 606 | 607 | If the client terminates without closing a stream, the server has no way of 608 | finding this out: with Hrana over WebSocket, the WebSocket connection is closed 609 | and the server can close the streams that belong to this connection, but there 610 | is no connection in Hrana over HTTP. Therefore, the server will close streams 611 | after a short period of inactivity, to make sure that abandoned streams don't 612 | accumulate on the server. 613 | 614 | ### Version and encoding negotiation 615 | 616 | With Hrana over HTTP, the client indicates the Hrana version and encoding in the 617 | URI path of the HTTP request. The client can check whether the server supports a 618 | given Hrana version by sending an HTTP request (described later). 619 | 620 | ### Endpoints 621 | 622 | The client communicates with the server by sending HTTP requests with a 623 | specified method and URL. 624 | 625 | #### Check support for version 3 (JSON) 626 | 627 | ``` 628 | GET v3 629 | ``` 630 | 631 | If the server supports version 3 of Hrana over HTTP with JSON encoding, it 632 | should return a 2xx response to this request. 633 | 634 | #### Check support for version 3 (Protobuf) 635 | 636 | ``` 637 | GET v3-protobuf 638 | ``` 639 | 640 | If the server supports version 3 of Hrana over HTTP with Protobuf encoding, it 641 | should return a 2xx response to this request. 642 | 643 | #### Execute a pipeline of requests (JSON) 644 | 645 | ``` 646 | POST v3/pipeline 647 | -> JSON: PipelineReqBody 648 | <- JSON: PipelineRespBody 649 | ``` 650 | 651 | ```typescript 652 | type PipelineReqBody = { 653 | "baton": string | null, 654 | "requests": Array, 655 | } 656 | 657 | type PipelineRespBody = { 658 | "baton": string | null, 659 | "base_url": string | null, 660 | "results": Array 661 | } 662 | 663 | type StreamResult = 664 | | StreamResultOk 665 | | StreamResultError 666 | 667 | type StreamResultOk = { 668 | "type": "ok", 669 | "response": StreamResponse, 670 | } 671 | 672 | type StreamResultError = { 673 | "type": "error", 674 | "error": Error, 675 | } 676 | ``` 677 | 678 | The `v3/pipeline` endpoint is used to execute a pipeline of requests on a 679 | stream. `baton` in the request specifies the stream. If the client sets `baton` 680 | to `null`, the server should create a new stream. 681 | 682 | Server responds with another `baton` value in the response. If the `baton` value 683 | in the response is `null`, it means that the server has closed the stream. The 684 | client must use this value to refer to this stream in the next request (the 685 | `baton` in the response should be different from the `baton` in the request). 686 | This forces the client to issue the requests serially: it must wait for the 687 | response from a previous `pipeline` request before issuing another request on 688 | the same stream. 689 | 690 | The server should ensure that the `baton` values are unpredictable and 691 | unforgeable, for example by cryptographically signing them. 692 | 693 | If the `base_url` in the response is not `null`, the client should use this URL 694 | when sending further requests on this stream. If it is `null`, the client should 695 | use the same URL that it has used for the previous request. The `base_url` 696 | must be an absolute URL with "http" or "https" scheme. 697 | 698 | The `requests` array in the request specifies a sequence of stream requests that 699 | should be executed on the stream. The server executes them in order and returns 700 | the results in the `results` array in the response. Result is either a success 701 | (`type` set to `"ok"`) or an error (`type` set to `"error"`). The server always 702 | executes all requests, even if some of them return errors. 703 | 704 | #### Execute a pipeline of requests (Protobuf) 705 | 706 | ``` 707 | POST v3-protobuf/pipeline 708 | -> Protobuf: PipelineReqBody 709 | <- Protobuf: PipelineRespBody 710 | ``` 711 | 712 | The `v3-protobuf/pipeline` endpoint is the same as `v3/pipeline`, but it encodes 713 | the request and response body using Protobuf. 714 | 715 | #### Execute a batch using a cursor (JSON) 716 | 717 | ``` 718 | POST v3/cursor 719 | -> JSON: CursorReqBody 720 | <- line of JSON: CursorRespBody 721 | lines of JSON: CursorEntry 722 | ``` 723 | 724 | ```typescript 725 | type CursorReqBody = { 726 | "baton": string | null, 727 | "batch": Batch, 728 | } 729 | 730 | type CursorRespBody = { 731 | "baton": string | null, 732 | "base_url": string | null, 733 | } 734 | ``` 735 | 736 | The `v3/cursor` endpoint executes a batch of statements on a stream using a 737 | cursor, so the results can be streamed from the server to the client. 738 | 739 | The HTTP response is composed of JSON structures separated with a newline. The 740 | first line contains the `CursorRespBody` structure, and the following lines 741 | contain `CursorEntry` structures, which encode the result of the batch. 742 | 743 | The `baton` field in the request and the `baton` and `base_url` fields in the 744 | response have the same meaning as in the `v3/pipeline` endpoint. 745 | 746 | #### Execute a batch using a cursor (Protobuf) 747 | 748 | ``` 749 | POST v3-protobuf/cursor 750 | -> Protobuf: CursorReqBody 751 | <- length-delimited Protobuf: CursorRespBody 752 | length-delimited Protobufs: CursorEntry 753 | ``` 754 | 755 | The `v3-protobuf/cursor` endpoint is the same as `v3/cursor` endpoint, but the 756 | request and response are encoded using Protobuf. 757 | 758 | In the response body, the structures are prefixed with a length delimiter: a 759 | Protobuf varint that encodes the length of the structure. The first structure is 760 | `CursorRespBody`, followed by an arbitrary number of `CursorEntry` structures. 761 | 762 | ### Requests 763 | 764 | Requests in Hrana over HTTP closely mirror stream requests in Hrana over 765 | WebSocket: 766 | 767 | ```typescript 768 | type StreamRequest = 769 | | CloseStreamReq 770 | | ExecuteStreamReq 771 | | BatchStreamReq 772 | | SequenceStreamReq 773 | | DescribeStreamReq 774 | | StoreSqlStreamReq 775 | | CloseSqlStreamReq 776 | | GetAutocommitStreamReq 777 | 778 | type StreamResponse = 779 | | CloseStreamResp 780 | | ExecuteStreamResp 781 | | BatchStreamResp 782 | | SequenceStreamResp 783 | | DescribeStreamResp 784 | | StoreSqlStreamResp 785 | | CloseSqlStreamResp 786 | | GetAutocommitStreamReq 787 | ``` 788 | 789 | #### Close stream 790 | 791 | ```typescript 792 | type CloseStreamReq = { 793 | "type": "close", 794 | } 795 | 796 | type CloseStreamResp = { 797 | "type": "close", 798 | } 799 | ``` 800 | 801 | The `close` request closes the stream. It is an error if the client tries to 802 | execute more requests on the same stream. 803 | 804 | > This request was introduced in Hrana 2. 805 | 806 | #### Execute a statement 807 | 808 | ```typescript 809 | type ExecuteStreamReq = { 810 | "type": "execute", 811 | "stmt": Stmt, 812 | } 813 | 814 | type ExecuteStreamResp = { 815 | "type": "execute", 816 | "result": StmtResult, 817 | } 818 | ``` 819 | 820 | The `execute` request has the same semantics as the `execute` request in Hrana 821 | over WebSocket. 822 | 823 | > This request was introduced in Hrana 2. 824 | 825 | #### Execute a batch 826 | 827 | ```typescript 828 | type BatchStreamReq = { 829 | "type": "batch", 830 | "batch": Batch, 831 | } 832 | 833 | type BatchStreamResp = { 834 | "type": "batch", 835 | "result": BatchResult, 836 | } 837 | ``` 838 | 839 | The `batch` request has the same semantics as the `batch` request in Hrana over 840 | WebSocket. 841 | 842 | > This request was introduced in Hrana 2. 843 | 844 | #### Execute a sequence of SQL statements 845 | 846 | ```typescript 847 | type SequenceStreamReq = { 848 | "type": "sequence", 849 | "sql"?: string | null, 850 | "sql_id"?: int32 | null, 851 | } 852 | 853 | type SequenceStreamResp = { 854 | "type": "sequence", 855 | } 856 | ``` 857 | 858 | The `sequence` request has the same semantics as the `sequence` request in 859 | Hrana over WebSocket. 860 | 861 | > This request was introduced in Hrana 2. 862 | 863 | #### Describe a statement 864 | 865 | ```typescript 866 | type DescribeStreamReq = { 867 | "type": "describe", 868 | "sql"?: string | null, 869 | "sql_id"?: int32 | null, 870 | } 871 | 872 | type DescribeStreamResp = { 873 | "type": "describe", 874 | "result": DescribeResult, 875 | } 876 | ``` 877 | 878 | The `describe` request has the same semantics as the `describe` request in 879 | Hrana over WebSocket. 880 | 881 | > This request was introduced in Hrana 2. 882 | 883 | #### Store an SQL text on the server 884 | 885 | ```typescript 886 | type StoreSqlStreamReq = { 887 | "type": "store_sql", 888 | "sql_id": int32, 889 | "sql": string, 890 | } 891 | 892 | type StoreSqlStreamResp = { 893 | "type": "store_sql", 894 | } 895 | ``` 896 | 897 | The `store_sql` request has the same semantics as the `store_sql` request in 898 | Hrana over WebSocket, except that the scope of the SQL texts is just a single 899 | stream (with WebSocket, it is the whole connection). 900 | 901 | > This request was introduced in Hrana 2. 902 | 903 | #### Close a stored SQL text 904 | 905 | ```typescript 906 | type CloseSqlStreamReq = { 907 | "type": "close_sql", 908 | "sql_id": int32, 909 | } 910 | 911 | type CloseSqlStreamResp = { 912 | "type": "close_sql", 913 | } 914 | ``` 915 | 916 | The `close_sql` request has the same semantics as the `close_sql` request in 917 | Hrana over WebSocket, except that the scope of the SQL texts is just a single 918 | stream. 919 | 920 | > This request was introduced in Hrana 2. 921 | 922 | #### Get the autocommit state 923 | 924 | ```typescript 925 | type GetAutocommitStreamReq = { 926 | "type": "get_autocommit", 927 | } 928 | 929 | type GetAutocommitStreamResp = { 930 | "type": "get_autocommit", 931 | "is_autocommit": bool, 932 | } 933 | ``` 934 | 935 | The `get_autocommit` request has the same semantics as the `get_autocommit` 936 | request in Hrana over WebSocket. 937 | 938 | > This request was introduced in Hrana 3. 939 | 940 | ### Errors 941 | 942 | If the client receives an HTTP error (4xx or 5xx response), it means that the 943 | server encountered an internal error and the stream is no longer valid. The 944 | client should attempt to parse the response body as an `Error` structure (using 945 | the encoding indicated by the `Content-Type` response header), but the client 946 | must be able to handle responses with different bodies, such as plaintext or 947 | HTML, which might be returned by various components in the HTTP stack. 948 | 949 | 950 | 951 | ## Shared structures 952 | 953 | This section describes protocol structures that are common for both Hrana over 954 | WebSocket and Hrana over HTTP. 955 | 956 | ### Errors 957 | 958 | ```typescript 959 | type Error = { 960 | "message": string, 961 | "code"?: string | null, 962 | } 963 | ``` 964 | 965 | Errors can be returned by the server in many places in the protocol, and they 966 | are always represented with the `Error` structure. The `message` field contains 967 | an English human-readable description of the error. The `code` contains a 968 | machine-readable error code. 969 | 970 | At this moment, the error codes are not yet stabilized and depend on the server 971 | implementation. 972 | 973 | > This structure was introduced in Hrana 1. 974 | 975 | ### Statements 976 | 977 | ```typescript 978 | type Stmt = { 979 | "sql"?: string | null, 980 | "sql_id"?: int32 | null, 981 | "args"?: Array, 982 | "named_args"?: Array, 983 | "want_rows"?: boolean, 984 | } 985 | 986 | type NamedArg = { 987 | "name": string, 988 | "value": Value, 989 | } 990 | ``` 991 | 992 | A SQL statement is represented by the `Stmt` structure. The text of the SQL 993 | statement is specified either by passing a string directly in the `sql` field, 994 | or by passing SQL text id that has previously been stored with the `store_sql` 995 | request. Exactly one of `sql` and `sql_id` must be passed. 996 | 997 | The arguments in `args` are bound to parameters in the SQL statement by 998 | position. The arguments in `named_args` are bound to parameters by name. 999 | 1000 | In SQLite, the names of arguments include the prefix sign (`:`, `@` or `$`). If 1001 | the name of the argument does not start with this prefix, the server will try to 1002 | guess the correct prefix. If an argument is specified both as a positional 1003 | argument and as a named argument, the named argument should take precedence. 1004 | 1005 | It is an error if the request specifies an argument that is not expected by the 1006 | SQL statement, or if the request does not specify an argument that is expected 1007 | by the SQL statement. Some servers may not support specifying both positional 1008 | and named arguments. 1009 | 1010 | The `want_rows` field specifies whether the client is interested in the rows 1011 | produced by the SQL statement. If it is set to `false`, the server should always 1012 | reply with no rows, even if the statement produced some. If the field is 1013 | omitted, the default value is `true`. 1014 | 1015 | The SQL text should contain just a single statement. Issuing multiple statements 1016 | separated by a semicolon is not supported. 1017 | 1018 | > This structure was introduced in Hrana 1. In Hrana 2, the `sql_id` field was 1019 | > added and the `sql` and `want_rows` fields were made optional. 1020 | 1021 | ### Statement results 1022 | 1023 | ```typescript 1024 | type StmtResult = { 1025 | "cols": Array, 1026 | "rows": Array>, 1027 | "affected_row_count": uint64, 1028 | "last_insert_rowid": string | null, 1029 | "rows_read": uint64, 1030 | "rows_written": uint64, 1031 | "query_duration_ms": double, 1032 | } 1033 | 1034 | type Col = { 1035 | "name": string | null, 1036 | "decltype": string | null, 1037 | } 1038 | ``` 1039 | 1040 | The result of executing an SQL statement is represented by the `StmtResult` 1041 | structure and it contains information about the returned columns in `cols` and 1042 | the returned rows in `rows` (the array is empty if the statement did not produce 1043 | any rows or if `want_rows` was `false` in the request). 1044 | 1045 | `affected_row_count` counts the number of rows that were changed by the 1046 | statement. This is meaningful only if the statement was an INSERT, UPDATE or 1047 | DELETE, and the value is otherwise undefined. 1048 | 1049 | `last_insert_rowid` is the ROWID of the last successful insert into a rowid 1050 | table. The rowid value is a 64-bit signed integer encoded as a string in JSON. 1051 | For other statements, the value is undefined. 1052 | 1053 | > This structure was introduced in Hrana 1. The `decltype` field in the `Col` 1054 | > strucure was added in Hrana 2. 1055 | 1056 | ### Batches 1057 | 1058 | ```typescript 1059 | type Batch = { 1060 | "steps": Array, 1061 | } 1062 | 1063 | type BatchStep = { 1064 | "condition"?: BatchCond | null, 1065 | "stmt": Stmt, 1066 | } 1067 | ``` 1068 | 1069 | A batch is represented by the `Batch` structure. It is a list of steps 1070 | (statements) which are always executed sequentially. If the `condition` of a 1071 | step is present and evaluates to false, the statement is not executed. 1072 | 1073 | > This structure was introduced in Hrana 1. 1074 | 1075 | #### Conditions 1076 | 1077 | ```typescript 1078 | type BatchCond = 1079 | | { "type": "ok", "step": uint32 } 1080 | | { "type": "error", "step": uint32 } 1081 | | { "type": "not", "cond": BatchCond } 1082 | | { "type": "and", "conds": Array } 1083 | | { "type": "or", "conds": Array } 1084 | | { "type": "is_autocommit" } 1085 | ``` 1086 | 1087 | Conditions are expressions that evaluate to true or false: 1088 | 1089 | - `ok` evaluates to true if the `step` (referenced by its 0-based index) was 1090 | executed successfully. If the statement was skipped, this condition evaluates to 1091 | false. 1092 | - `error` evaluates to true if the `step` (referenced by its 0-based index) has 1093 | produced an error. If the statement was skipped, this condition evaluates to 1094 | false. 1095 | - `not` evaluates `cond` and returns the logical negative. 1096 | - `and` evaluates `conds` and returns the logical conjunction of them. 1097 | - `or` evaluates `conds` and returns the logical disjunction of them. 1098 | - `is_autocommit` evaluates to true if the stream is currently in the autocommit 1099 | state (not inside an explicit transaction) 1100 | 1101 | > This structure was introduced in Hrana 1. The `is_autocommit` type was added in Hrana 3. 1102 | 1103 | ### Batch results 1104 | 1105 | ```typescript 1106 | type BatchResult = { 1107 | "step_results": Array, 1108 | "step_errors": Array, 1109 | } 1110 | ``` 1111 | 1112 | The result of executing a batch is represented by `BatchResult`. The result 1113 | contains the results or errors of statements from each step. For the step in 1114 | `steps[i]`, `step_results[i]` contains the result of the statement if the 1115 | statement was executed and succeeded, and `step_errors[i]` contains the error if 1116 | the statement was executed and failed. If the statement was skipped because its 1117 | condition evaluated to false, both `step_results[i]` and `step_errors[i]` will 1118 | be `null`. 1119 | 1120 | > This structure was introduced in Hrana 1. 1121 | 1122 | ### Cursor entries 1123 | 1124 | ```typescript 1125 | type CursorEntry = 1126 | | StepBeginEntry 1127 | | StepEndEntry 1128 | | StepErrorEntry 1129 | | RowEntry 1130 | | ErrorEntry 1131 | ``` 1132 | 1133 | Cursor entries are produced by cursors. A sequence of entries encodes the same 1134 | information as a `BatchResult`, but it is sent to the client incrementally, so 1135 | both peers don't need to keep the whole result in memory. 1136 | 1137 | > These structures were introduced in Hrana 3. 1138 | 1139 | #### Step results 1140 | 1141 | ```typescript 1142 | type StepBeginEntry = { 1143 | "type": "step_begin", 1144 | "step": uint32, 1145 | "cols": Array, 1146 | } 1147 | 1148 | type StepEndEntry = { 1149 | "type": "step_end", 1150 | "affected_row_count": uint32, 1151 | "last_insert_rowid": string | null, 1152 | } 1153 | 1154 | type RowEntry = { 1155 | "type": "row", 1156 | "row": Array, 1157 | } 1158 | ``` 1159 | 1160 | At the beginning of every batch step that is executed, the server produces a 1161 | `step_begin` entry. This entry specifies the index of the step (which refers to 1162 | the `steps` array in the `Batch` structure). The server sends entries for steps 1163 | in the order in which they are executed. If a step is skipped (because its 1164 | condition evalated to false), the server does not send any entry for it. 1165 | 1166 | After a `step_begin` entry, the server sends an arbitrary number of `row` 1167 | entries that encode the individual rows produced by the statement, terminated by 1168 | the `step_end` entry. Together, these entries encode the same information as the 1169 | `StmtResult` structure. 1170 | 1171 | The server can send another `step_entry` only after the previous step was 1172 | terminated by `step_end` or by `step_error`, described below. 1173 | 1174 | #### Errors 1175 | 1176 | ```typescript 1177 | type StepErrorEntry = { 1178 | "type": "step_error", 1179 | "step": uint32, 1180 | "error": Error, 1181 | } 1182 | 1183 | type ErrorEntry = { 1184 | "type": "error", 1185 | "error": Error, 1186 | } 1187 | ``` 1188 | 1189 | The `step_error` entry indicates that the execution of a statement failed with 1190 | an error. There are two ways in which the server may produce this entry: 1191 | 1192 | 1. Before a `step_begin` entry was sent: this means that the statement failed 1193 | very early, without producing any results. The `step` field indicates which 1194 | step has failed (similar to the `step_begin` entry). 1195 | 2. After a `step_begin` entry was sent: in this case, the server has started 1196 | executing the statement and produced `step_begin` (and perhaps a number of 1197 | `row` entries), but then encountered an error. The `step` field must in this 1198 | case be equal to the `step` of the currently processed step. 1199 | 1200 | The `error` entry means that the execution of the whole batch has failed. This 1201 | can be produced by the server at any time, and it is always the last entry in 1202 | the cursor. 1203 | 1204 | ### Result of describing a statement 1205 | 1206 | ```typescript 1207 | type DescribeResult = { 1208 | "params": Array, 1209 | "cols": Array, 1210 | "is_explain": boolean, 1211 | "is_readonly": boolean, 1212 | } 1213 | ``` 1214 | 1215 | The `DescribeResult` structure is the result of describing a statement. 1216 | `is_explain` is true if the statement was an `EXPLAIN` statement, and 1217 | `is_readonly` is true if the statement does not modify the database. 1218 | 1219 | > This structure was introduced in Hrana 2. 1220 | 1221 | #### Parameters 1222 | 1223 | ```typescript 1224 | type DescribeParam = { 1225 | "name": string | null, 1226 | } 1227 | ``` 1228 | 1229 | Information about parameters of the statement is returned in `params`. SQLite 1230 | indexes parameters from 1, so the first object in the `params` array describes 1231 | parameter 1. 1232 | 1233 | For each parameter, the `name` field specifies the name of the parameter. For 1234 | parameters of the form `?NNN`, `:AAA`, `@AAA` and `$AAA`, the name includes the 1235 | initial `?`, `:`, `@` or `$` character. Parameters of the form `?` are nameless, 1236 | their `name` is `null`. 1237 | 1238 | It is also possible that some parameters are not referenced in the statement, in 1239 | which case the `name` is also `null`. 1240 | 1241 | > This structure was introduced in Hrana 2. 1242 | 1243 | #### Columns 1244 | 1245 | ```typescript 1246 | type DescribeCol = { 1247 | "name": string, 1248 | "decltype": string | null, 1249 | } 1250 | ``` 1251 | 1252 | Information about columns of the statement is returned in `cols`. 1253 | 1254 | For each column, `name` specifies the name assigned by the SQL `AS` clause. For 1255 | columns without `AS` clause, the name is not specified. 1256 | 1257 | For result columns that directly originate from tables in the database, 1258 | `decltype` specifies the declared type of the column. For other columns (such as 1259 | results of expressions), `decltype` is `null`. 1260 | 1261 | > This structure was introduced in Hrana 2. 1262 | 1263 | ### Values 1264 | 1265 | ```typescript 1266 | type Value = 1267 | | { "type": "null" } 1268 | | { "type": "integer", "value": string } 1269 | | { "type": "float", "value": number } 1270 | | { "type": "text", "value": string } 1271 | | { "type": "blob", "base64": string } 1272 | ``` 1273 | 1274 | SQLite values are represented by the `Value` structure. The type of the value 1275 | depends on the `type` field: 1276 | 1277 | - `null`: the SQL NULL value. 1278 | - `integer`: a 64-bit signed integer. In JSON, the `value` is a string to avoid 1279 | losing precision, because some JSON implementations treat all numbers as 1280 | 64-bit floats. 1281 | - `float`: a 64-bit float. 1282 | - `text`: a UTF-8 string. 1283 | - `blob`: a binary blob with. In JSON, the value is base64-encoded. 1284 | 1285 | > This structure was introduced in Hrana 1. 1286 | 1287 | 1288 | 1289 | 1290 | ## Protobuf schema 1291 | 1292 | ### Hrana over WebSocket 1293 | 1294 | ```proto 1295 | syntax = "proto3"; 1296 | package hrana.ws; 1297 | 1298 | message ClientMsg { 1299 | oneof msg { 1300 | HelloMsg hello = 1; 1301 | RequestMsg request = 2; 1302 | } 1303 | } 1304 | 1305 | message ServerMsg { 1306 | oneof msg { 1307 | HelloOkMsg hello_ok = 1; 1308 | HelloErrorMsg hello_error = 2; 1309 | ResponseOkMsg response_ok = 3; 1310 | ResponseErrorMsg response_error = 4; 1311 | } 1312 | } 1313 | 1314 | message HelloMsg { 1315 | optional string jwt = 1; 1316 | } 1317 | 1318 | message HelloOkMsg { 1319 | } 1320 | 1321 | message HelloErrorMsg { 1322 | Error error = 1; 1323 | } 1324 | 1325 | message RequestMsg { 1326 | int32 request_id = 1; 1327 | oneof request { 1328 | OpenStreamReq open_stream = 2; 1329 | CloseStreamReq close_stream = 3; 1330 | ExecuteReq execute = 4; 1331 | BatchReq batch = 5; 1332 | OpenCursorReq open_cursor = 6; 1333 | CloseCursorReq close_cursor = 7; 1334 | FetchCursorReq fetch_cursor = 8; 1335 | SequenceReq sequence = 9; 1336 | DescribeReq describe = 10; 1337 | StoreSqlReq store_sql = 11; 1338 | CloseSqlReq close_sql = 12; 1339 | GetAutocommitReq get_autocommit = 13; 1340 | } 1341 | } 1342 | 1343 | message ResponseOkMsg { 1344 | int32 request_id = 1; 1345 | oneof response { 1346 | OpenStreamResp open_stream = 2; 1347 | CloseStreamResp close_stream = 3; 1348 | ExecuteResp execute = 4; 1349 | BatchResp batch = 5; 1350 | OpenCursorResp open_cursor = 6; 1351 | CloseCursorResp close_cursor = 7; 1352 | FetchCursorResp fetch_cursor = 8; 1353 | SequenceResp sequence = 9; 1354 | DescribeResp describe = 10; 1355 | StoreSqlResp store_sql = 11; 1356 | CloseSqlResp close_sql = 12; 1357 | GetAutocommitResp get_autocommit = 13; 1358 | } 1359 | } 1360 | 1361 | message ResponseErrorMsg { 1362 | int32 request_id = 1; 1363 | Error error = 2; 1364 | } 1365 | 1366 | message OpenStreamReq { 1367 | int32 stream_id = 1; 1368 | } 1369 | 1370 | message OpenStreamResp { 1371 | } 1372 | 1373 | message CloseStreamReq { 1374 | int32 stream_id = 1; 1375 | } 1376 | 1377 | message CloseStreamResp { 1378 | } 1379 | 1380 | message ExecuteReq { 1381 | int32 stream_id = 1; 1382 | Stmt stmt = 2; 1383 | } 1384 | 1385 | message ExecuteResp { 1386 | StmtResult result = 1; 1387 | } 1388 | 1389 | message BatchReq { 1390 | int32 stream_id = 1; 1391 | Batch batch = 2; 1392 | } 1393 | 1394 | message BatchResp { 1395 | BatchResult result = 1; 1396 | } 1397 | 1398 | message OpenCursorReq { 1399 | int32 stream_id = 1; 1400 | int32 cursor_id = 2; 1401 | Batch batch = 3; 1402 | } 1403 | 1404 | message OpenCursorResp { 1405 | } 1406 | 1407 | message CloseCursorReq { 1408 | int32 cursor_id = 1; 1409 | } 1410 | 1411 | message CloseCursorResp { 1412 | } 1413 | 1414 | message FetchCursorReq { 1415 | int32 cursor_id = 1; 1416 | uint32 max_count = 2; 1417 | } 1418 | 1419 | message FetchCursorResp { 1420 | repeated CursorEntry entries = 1; 1421 | bool done = 2; 1422 | } 1423 | 1424 | message StoreSqlReq { 1425 | int32 sql_id = 1; 1426 | string sql = 2; 1427 | } 1428 | 1429 | message StoreSqlResp { 1430 | } 1431 | 1432 | message CloseSqlReq { 1433 | int32 sql_id = 1; 1434 | } 1435 | 1436 | message CloseSqlResp { 1437 | } 1438 | 1439 | message SequenceReq { 1440 | int32 stream_id = 1; 1441 | optional string sql = 2; 1442 | optional int32 sql_id = 3; 1443 | } 1444 | 1445 | message SequenceResp { 1446 | } 1447 | 1448 | message DescribeReq { 1449 | int32 stream_id = 1; 1450 | optional string sql = 2; 1451 | optional int32 sql_id = 3; 1452 | } 1453 | 1454 | message DescribeResp { 1455 | DescribeResult result = 1; 1456 | } 1457 | 1458 | message GetAutocommitReq { 1459 | int32 stream_id = 1; 1460 | } 1461 | 1462 | message GetAutocommitResp { 1463 | bool is_autocommit = 1; 1464 | } 1465 | ``` 1466 | 1467 | ### Hrana over HTTP 1468 | 1469 | ```proto 1470 | syntax = "proto3"; 1471 | package hrana.http; 1472 | 1473 | message PipelineReqBody { 1474 | optional string baton = 1; 1475 | repeated StreamRequest requests = 2; 1476 | } 1477 | 1478 | message PipelineRespBody { 1479 | optional string baton = 1; 1480 | optional string base_url = 2; 1481 | repeated StreamResult results = 3; 1482 | } 1483 | 1484 | message StreamResult { 1485 | oneof result { 1486 | StreamResponse ok = 1; 1487 | Error error = 2; 1488 | } 1489 | } 1490 | 1491 | message CursorReqBody { 1492 | optional string baton = 1; 1493 | Batch batch = 2; 1494 | } 1495 | 1496 | message CursorRespBody { 1497 | optional string baton = 1; 1498 | optional string base_url = 2; 1499 | } 1500 | 1501 | message StreamRequest { 1502 | oneof request { 1503 | CloseStreamReq close = 1; 1504 | ExecuteStreamReq execute = 2; 1505 | BatchStreamReq batch = 3; 1506 | SequenceStreamReq sequence = 4; 1507 | DescribeStreamReq describe = 5; 1508 | StoreSqlStreamReq store_sql = 6; 1509 | CloseSqlStreamReq close_sql = 7; 1510 | GetAutocommitStreamReq get_autocommit = 8; 1511 | } 1512 | } 1513 | 1514 | message StreamResponse { 1515 | oneof response { 1516 | CloseStreamResp close = 1; 1517 | ExecuteStreamResp execute = 2; 1518 | BatchStreamResp batch = 3; 1519 | SequenceStreamResp sequence = 4; 1520 | DescribeStreamResp describe = 5; 1521 | StoreSqlStreamResp store_sql = 6; 1522 | CloseSqlStreamResp close_sql = 7; 1523 | GetAutocommitStreamResp get_autocommit = 8; 1524 | } 1525 | } 1526 | 1527 | message CloseStreamReq { 1528 | } 1529 | 1530 | message CloseStreamResp { 1531 | } 1532 | 1533 | message ExecuteStreamReq { 1534 | Stmt stmt = 1; 1535 | } 1536 | 1537 | message ExecuteStreamResp { 1538 | StmtResult result = 1; 1539 | } 1540 | 1541 | message BatchStreamReq { 1542 | Batch batch = 1; 1543 | } 1544 | 1545 | message BatchStreamResp { 1546 | BatchResult result = 1; 1547 | } 1548 | 1549 | message SequenceStreamReq { 1550 | optional string sql = 1; 1551 | optional int32 sql_id = 2; 1552 | } 1553 | 1554 | message SequenceStreamResp { 1555 | } 1556 | 1557 | message DescribeStreamReq { 1558 | optional string sql = 1; 1559 | optional int32 sql_id = 2; 1560 | } 1561 | 1562 | message DescribeStreamResp { 1563 | DescribeResult result = 1; 1564 | } 1565 | 1566 | message StoreSqlStreamReq { 1567 | int32 sql_id = 1; 1568 | string sql = 2; 1569 | } 1570 | 1571 | message StoreSqlStreamResp { 1572 | } 1573 | 1574 | message CloseSqlStreamReq { 1575 | int32 sql_id = 1; 1576 | } 1577 | 1578 | message CloseSqlStreamResp { 1579 | } 1580 | 1581 | message GetAutocommitStreamReq { 1582 | } 1583 | 1584 | message GetAutocommitStreamResp { 1585 | bool is_autocommit = 1; 1586 | } 1587 | ``` 1588 | 1589 | ### Shared structures 1590 | 1591 | ```proto 1592 | syntax = "proto3"; 1593 | package hrana; 1594 | 1595 | message Error { 1596 | string message = 1; 1597 | optional string code = 2; 1598 | } 1599 | 1600 | message Stmt { 1601 | optional string sql = 1; 1602 | optional int32 sql_id = 2; 1603 | repeated Value args = 3; 1604 | repeated NamedArg named_args = 4; 1605 | optional bool want_rows = 5; 1606 | } 1607 | 1608 | message NamedArg { 1609 | string name = 1; 1610 | Value value = 2; 1611 | } 1612 | 1613 | message StmtResult { 1614 | repeated Col cols = 1; 1615 | repeated Row rows = 2; 1616 | uint64 affected_row_count = 3; 1617 | optional sint64 last_insert_rowid = 4; 1618 | } 1619 | 1620 | message Col { 1621 | optional string name = 1; 1622 | optional string decltype = 2; 1623 | } 1624 | 1625 | message Row { 1626 | repeated Value values = 1; 1627 | } 1628 | 1629 | message Batch { 1630 | repeated BatchStep steps = 1; 1631 | } 1632 | 1633 | message BatchStep { 1634 | optional BatchCond condition = 1; 1635 | Stmt stmt = 2; 1636 | } 1637 | 1638 | message BatchCond { 1639 | oneof cond { 1640 | uint32 step_ok = 1; 1641 | uint32 step_error = 2; 1642 | BatchCond not = 3; 1643 | CondList and = 4; 1644 | CondList or = 5; 1645 | IsAutocommit is_autocommit = 6; 1646 | } 1647 | 1648 | message CondList { 1649 | repeated BatchCond conds = 1; 1650 | } 1651 | 1652 | message IsAutocommit { 1653 | } 1654 | } 1655 | 1656 | message BatchResult { 1657 | map step_results = 1; 1658 | map step_errors = 2; 1659 | } 1660 | 1661 | message CursorEntry { 1662 | oneof entry { 1663 | StepBeginEntry step_begin = 1; 1664 | StepEndEntry step_end = 2; 1665 | StepErrorEntry step_error = 3; 1666 | Row row = 4; 1667 | Error error = 5; 1668 | } 1669 | } 1670 | 1671 | message StepBeginEntry { 1672 | uint32 step = 1; 1673 | repeated Col cols = 2; 1674 | } 1675 | 1676 | message StepEndEntry { 1677 | uint64 affected_row_count = 1; 1678 | optional sint64 last_insert_rowid = 2; 1679 | } 1680 | 1681 | message StepErrorEntry { 1682 | uint32 step = 1; 1683 | Error error = 2; 1684 | } 1685 | 1686 | message DescribeResult { 1687 | repeated DescribeParam params = 1; 1688 | repeated DescribeCol cols = 2; 1689 | bool is_explain = 3; 1690 | bool is_readonly = 4; 1691 | } 1692 | 1693 | message DescribeParam { 1694 | optional string name = 1; 1695 | } 1696 | 1697 | message DescribeCol { 1698 | string name = 1; 1699 | optional string decltype = 2; 1700 | } 1701 | 1702 | message Value { 1703 | oneof value { 1704 | Null null = 1; 1705 | sint64 integer = 2; 1706 | double float = 3; 1707 | string text = 4; 1708 | bytes blob = 5; 1709 | } 1710 | 1711 | message Null {} 1712 | } 1713 | ``` 1714 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Localbox Crox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libsql-stateless 2 | 3 | > Thin libSQL stateless HTTP driver for TypeScript and JavaScript for the edge 🚀 4 | - ✅ **Supported runtime environments:** Web API (browser, serverless), Bun, Node.js (>=18) 5 | - ✅ **Extremely thin:** Has no dependency, only has a few functions that implement the [`Hrana v3 HTTP`](https://github.com/tursodatabase/libsql/blob/main/libsql-server/docs/HRANA_3_SPEC.md) protocol from scratch, and has no classes (tend to duplicate memory and/or perform long memory traversals). 6 | - ✅ **Does no extra computation.** 7 | - ✅ **Has no premature optimizations.** 8 | - ✅ **Is extremely light:** 1.15kB (unpacked)* / 548B (gzipped) 9 | - ✅ Unlike `@libsql/client/web`, **every function performs complete execution in exactly 1 roundtrip.** 10 | - ✅ **Is built for:** Quick stateless query execution. (Mainly for serverless and edge functions.) 11 | - ✅ **Supports everything in** `@libsql/client/web` 12 | - ⚠️ **Interactive transactions are not supported** because this lib is stateless but [`transactions`](https://github.com/DaBigBlob/libsql-stateless/wiki/transactions) are supported. 13 | - ⚠️ **The API provided by `libsql-stateless` is raw and explicit** for reducing (computational and memory) overheads. 14 | 15 | \* The actual js that is included with your project. (Excluding the type definitions and 2 copies of the main js for esm and cjs. (because you're gonna use one of them)) 16 |
17 | 18 | **For easier DX, consider using [`libsql-stateless-easy`](https://github.com/DaBigBlob/libsql-stateless-easy) instead**: it, however, comes with the cost of non-zero-dependency and (computational and memory) overheads potentially unneeded by you. But is still very very very slim compared to `@libsql/client`. 19 | 20 | # Why not just use `@libsql/client/web`? 21 | 1. Not everyone needs stateful DB connection or the overheads that come with it. 22 | 2. To provide a simpler API, `@libsql/client/web` does a lot of, I'd argue unnecessary, computation under the hood.\ 23 | Many people would rather use a more complex API than have worse performance. 24 | 25 | # Installation 26 | ```sh 27 | $ npm i libsql-stateless #pnpm, yarn, etc. 28 | # or 29 | $ bun add libsql-stateless 30 | ``` 31 | 32 | # Goto [`WIKI`](https://github.com/DaBigBlob/libsql-stateless/wiki) for Specifications and Examples 33 | 34 | ## API Level 35 | > NOTE: -HRANA_3_SPEC.md is the current API level. 36 | Downloaded from: https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md at . 37 | Servers using older API levels may not be compatible. In that case downgrade to an earlier of this package. -------------------------------------------------------------------------------- /_tests/perf.ts: -------------------------------------------------------------------------------- 1 | import { libsqlExecute, libsqlBatch} from "../src/main.js"; 2 | import { skjdgfksg } from "./_conf.js"; 3 | 4 | (async () => { 5 | const conf = skjdgfksg; 6 | 7 | console.time("libsqlBatch"); 8 | const res = await libsqlBatch(conf, [ 9 | {stmt: {sql: "select * from contacts where contact_id = 3;"}}, 10 | { 11 | stmt: {sql: "select first_name, last_name, email from contacts where contact_id = 2;"}, 12 | // condition: { 13 | // type: "and", 14 | // conds: [ 15 | // {type:"ok", step: 1}, 16 | // {type:"ok", step: 0}, 17 | // ] 18 | // } 19 | }, 20 | {stmt: {sql: "select * from contacts where wife_id = ?;", args: [{type: "null"}]}} 21 | ]); 22 | console.timeEnd("libsqlBatch"); 23 | 24 | console.log(res.isOk); 25 | 26 | 27 | 28 | console.time("libsqlExecute"); 29 | const res2 = await libsqlExecute(conf, {sql: "select first_name, last_name, email from contacts where contact_id = 1;"}); 30 | console.timeEnd("libsqlExecute"); 31 | 32 | console.log(res2.isOk); 33 | })(); -------------------------------------------------------------------------------- /_tests/test1.ts: -------------------------------------------------------------------------------- 1 | type rawValues = null|bigint|number|string|Uint8Array; 2 | 3 | 4 | (() => { 5 | let a: Record = { 6 | 1: "aa", 7 | null: "bb", 8 | ll: 1 9 | } 10 | let b: Array = ["ll"] 11 | console.log(typeof(a.length)); 12 | console.log(typeof(b.length)); 13 | for (const m in a) { 14 | console.log(m+" "+a[m]); 15 | } 16 | })(); -------------------------------------------------------------------------------- /_tests/test2.ts: -------------------------------------------------------------------------------- 1 | import { libsqlExecute, libsqlBatch, libsqlServerCompatCheck, type libsqlSQLValue} from "../src/main.js"; 2 | import { conf } from "./_conf.js"; 3 | 4 | (async () => { 5 | console.log("##libsqlBatch##") 6 | const res = await libsqlBatch(conf, [ 7 | //{stmt: {sql: "BEGIN DEFERRED"}}, 8 | {stmt: {sql: "select * from contacts where contact_id = ?;", args: [{type: "integer", value: "3"}]}}, 9 | {stmt: {sql: "select first_name, last_name, email from contacts where contact_id = 2;"}}, 10 | {stmt: {sql: `insert into contacts (contact_id,first_name,last_name,email,phone) values (6,"glomm","feru","moca@doro.co","001");`}}, 11 | {stmt: {sql: `delete from contacts where contact_id = 6;`}} 12 | ]); 13 | if (res.isOk) console.log(JSON.stringify(res.val.step_results.map(r => { 14 | if (!r) return undefined; 15 | if (r.rows.length===0) return r; 16 | else return r.rows; 17 | }), null, 4)); 18 | else console.error(res.err); 19 | 20 | console.log("##libsqlBatch##") 21 | const resb = await libsqlBatch(conf, [ 22 | {stmt: {sql: "BEGIN DEFERRED"}}, 23 | {stmt: {sql: "select * from contacts where contact_id = ?;", args: [{type: "integer", value: "3"}]}, 24 | condition: {type: "and", conds: [ 25 | {type: "ok", step: 0}, 26 | {type: "not", cond: {type: "is_autocommit"}} 27 | ]} 28 | }, 29 | {stmt: {sql: "select first_name, last_name, email from contacts where contact_id = 2;"}, 30 | condition: {type: "and", conds: [ 31 | {type: "ok", step: 1}, 32 | {type: "not", cond: {type: "is_autocommit"}} 33 | ]} 34 | }, 35 | {stmt: {sql: `insert into contacts (contact_id,first_name,last_name,email,phone) values (7,"glomm","feru","moca@doro.co","001");`}, 36 | condition: {type: "and", conds: [ 37 | {type: "ok", step: 2}, 38 | {type: "not", cond: {type: "is_autocommit"}} 39 | ]} 40 | }, 41 | {stmt: {sql: `delete from contacts where contact_id = 7;`}, 42 | condition: {type: "and", conds: [ 43 | {type: "ok", step: 3}, 44 | {type: "not", cond: {type: "is_autocommit"}} 45 | ]} 46 | }, 47 | {stmt: {sql: "COMMIT"}}, 48 | {stmt: {sql: "ROLLBACK"}, condition: {type: "not", cond: {type: "ok", step: 5}}} 49 | ]); 50 | if (resb.isOk) console.log(JSON.stringify(resb.val.step_results.map(r => { 51 | if (!r) return undefined; 52 | if (r.rows.length===0) return r; 53 | else return r.rows; 54 | }), null, 4)); 55 | else console.error(resb.err); 56 | 57 | 58 | console.log("##libsqlExecute##") 59 | const res2 = await libsqlExecute(conf, {sql: "select first_name, last_name, email from contacts where contact_id = 1;"}); 60 | if (res2.isOk) { 61 | console.log(JSON.stringify(res2.val, null, 4)); 62 | 63 | const idx = res2.val.cols.indexOf(res2.val.cols.find(c => c.name=="email")!); 64 | for (const row of res2.val.rows as libsqlSQLValue[][]) { 65 | console.log(row[idx]); 66 | } 67 | } 68 | else console.error(res2.err); 69 | 70 | console.log("##libsqlServerCompatCheck##") 71 | const res3 = await libsqlServerCompatCheck(conf); 72 | if (res3.isOk) console.log("Server Compat Check OK"); 73 | else console.error("Server Compat Check NOT OK"); 74 | })(); -------------------------------------------------------------------------------- /_tests/test3.ts: -------------------------------------------------------------------------------- 1 | import { libsqlExecute, type libsqlSQLValue } from "../src/main.js"; 2 | import { conf } from "./_conf.js"; 3 | 4 | (async () => { 5 | console.log("##libsqlExecute##") 6 | const res2 = await libsqlExecute(conf, {sql: "select first_name, last_name, email from contacts where contact_id = 1;"}); 7 | if (res2.isOk) { 8 | console.log(JSON.stringify(res2.val, null, 4)); 9 | 10 | const idx = res2.val.cols.indexOf(res2.val.cols.find(c => c.name=="email")!); 11 | for (const row of res2.val.rows as libsqlSQLValue[][]) { 12 | console.log(row[idx]); 13 | } 14 | } 15 | else console.error(res2.err); 16 | })(); -------------------------------------------------------------------------------- /check_api_level_diff: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -s "https://raw.githubusercontent.com/tursodatabase/libsql/main/docs/HRANA_3_SPEC.md" > HRANA_3_SPEC-transient.md 4 | diff HRANA_3_SPEC-transient.md ./*-HRANA_3_SPEC.md 5 | rm HRANA_3_SPEC-transient.md -------------------------------------------------------------------------------- /get_new_api_level: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | UNIXMILI=$(node -e "console.log(Date.now())") 4 | curl -s "https://raw.githubusercontent.com/tursodatabase/libsql/main/docs/HRANA_3_SPEC.md" > "${UNIXMILI}-HRANA_3_SPEC.md" -------------------------------------------------------------------------------- /js_test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bun build "$1" --target node > _test_node_run.mjs 4 | bun build "$1" --target browser > _test_web_run.js 5 | bun build "$1" --target bun > _test_bun_run.js 6 | 7 | echo "running node..." 8 | node _test_node_run.mjs 9 | echo "running deno..." 10 | deno run --allow-net _test_web_run.js 11 | echo "running bun..." 12 | bun run _test_bun_run.js 13 | 14 | rm _test_node_run.mjs _test_web_run.js _test_bun_run.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-stateless", 3 | "version": "2.9.1", 4 | "description": "thin libSQL stateless http driver for TypeScript and JavaScript", 5 | "homepage": "https://github.com/DaBigBlob/libsql-stateless#readme", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/DaBigBlob/libsql-stateless.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/DaBigBlob/libsql-stateless/issues", 12 | "email": "libsqlstateless@hman.io" 13 | }, 14 | "author": { 15 | "name": "LocalBox Crox", 16 | "email": "libsqlstateless@hman.io" 17 | }, 18 | "license": "MIT", 19 | "type": "module", 20 | "main": "./dist/main.js", 21 | "types": "./dist/main.d.ts", 22 | "files": [ 23 | "./dist/*", 24 | "./LICENSE", 25 | "./package.json", 26 | "./README.md" 27 | ], 28 | "exports": { 29 | ".": { 30 | "types": "./dist/main.d.ts", 31 | "import": "./dist/main.js", 32 | "require": "./dist/main.cjs" 33 | } 34 | }, 35 | "devDependencies": { 36 | "tsup": "^8.0.2", 37 | "typescript": "^5.0.0" 38 | }, 39 | "scripts": { 40 | "prepublishOnly": "npm run build", 41 | "prebuild": "rm -rf ./dist", 42 | "build": "tsup && rm ./dist/main.d.cts", 43 | "typecheck": "tsc --noEmit", 44 | "test": "bun run _tests/test2.ts", 45 | "perf": "bun run _tests/perf.ts", 46 | "clean": "npm run prebuild", 47 | "prod": "npm publish && npm run clean" 48 | }, 49 | "keywords": [ 50 | "libsql", 51 | "database", 52 | "sqlite", 53 | "serverless", 54 | "vercel", 55 | "netlify", 56 | "lambda", 57 | "http", 58 | "https", 59 | "webapi", 60 | "cloudflare-workers", 61 | "cloudflare-pages", 62 | "edge" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import type { libsqlBatchReqStep, libsqlBatchStreamResOk, libsqlBatchStreamResOkData, libsqlConfig, libsqlError, libsqlExecuteStreamResOk, libsqlFetchLike, libsqlPipelineReq, libsqlPipelineRes, libsqlResult, libsqlSQLStatement, libsqlStatementResOkData } from "./types.js"; 2 | 3 | async function hranaFetch(s: { 4 | conf: libsqlConfig, 5 | req_json: libsqlPipelineReq 6 | }): Promise> { 7 | const res = await (s.conf.fetch ?? (globalThis as unknown as {fetch: libsqlFetchLike}).fetch)( 8 | `${s.conf.url}/v3/pipeline`, //line 646 from 1713449025236-HRANA_3_SPEC.md 9 | { 10 | method: 'POST', 11 | headers: (s.conf.authToken) ? {'Authorization': 'Bearer '+s.conf.authToken} : undefined, 12 | body: JSON.stringify(s.req_json) 13 | } 14 | ); 15 | if (res.ok) return {isOk: true, val: (await res.json() as libsqlPipelineRes)}; 16 | else return {isOk: false, err: { 17 | kind: "LIBSQL_SERVER_ERROR", 18 | server_message: await (async () => { 19 | try { 20 | return await res.text() 21 | } catch { 22 | return null; 23 | } 24 | })(), 25 | http_status_code: res.status 26 | }}; 27 | } 28 | 29 | /** 30 | * @async 31 | * @description Executes exactly one (1) SQL statements. 32 | * @param {libsqlConfig} conf libsql's config for DB connection: {@link libsqlConfig} 33 | * @param {libsqlSQLStatement} stmt libsql's raw API sql statement: {@link libsqlSQLStatement} 34 | */ 35 | export async function libsqlExecute(conf: libsqlConfig, stmt: libsqlSQLStatement): Promise> { 36 | const res = await hranaFetch({conf, req_json: { 37 | baton: null, 38 | requests: [ 39 | { 40 | type: "execute", 41 | stmt: stmt, 42 | }, 43 | { 44 | type: "close", 45 | } 46 | ] 47 | }}); 48 | 49 | if (res.isOk) { 50 | const resu = res.val.results[0]; //this because [0] is where we executed the statement 51 | if (resu.type=="ok") return { 52 | isOk: true, 53 | val: (resu.response as libsqlExecuteStreamResOk).result // cast because is guaranteed to be execute by line: 699 54 | }; 55 | else return {isOk: false, err: { 56 | kind: "LIBSQL_RESPONSE_ERROR", 57 | data: resu.error //has to be StreamResErr 58 | }}; 59 | } 60 | else return res; //whatever server error returned 61 | } 62 | 63 | /** 64 | * @async 65 | * @description Executes many SQL statements. Can be used to perform implicit transactions. 66 | * @param {libsqlConfig} conf libsql's config for DB connection: {@link libsqlConfig} 67 | * @param {Array} batch_steps array of libsql's raw API sql batch steps: {@link BatchReqSteps} 68 | */ 69 | export async function libsqlBatch(conf: libsqlConfig, batch_steps: Array): Promise> { 70 | const res = await hranaFetch({conf, req_json: { 71 | baton: null, 72 | requests: [ 73 | { 74 | type: "batch", 75 | batch: { steps: batch_steps } 76 | }, 77 | { 78 | type: "close" 79 | } 80 | ] 81 | }}); 82 | 83 | if (res.isOk) { 84 | const resu = res.val.results[0]; //this because [0] is where we executed the statement 85 | if (resu.type=="ok") return { 86 | isOk: true, 87 | val: (resu.response as libsqlBatchStreamResOk).result // cast because is guaranteed to be execute by line: 1070 88 | }; 89 | else return {isOk: false, err: { 90 | kind: "LIBSQL_RESPONSE_ERROR", 91 | data: resu.error //has to be StreamResErr 92 | }}; 93 | } 94 | else return res; //whatever server error returned 95 | } 96 | 97 | /** 98 | * @async 99 | * @description Check if the server supports this library 100 | * @param {Config} conf libsql's config for DB connection: {@link libsqlConfig} 101 | */ 102 | export async function libsqlServerCompatCheck(conf: libsqlConfig): Promise> { 103 | if ((await (conf.fetch ?? (globalThis as unknown as {fetch: libsqlFetchLike}).fetch)( 104 | `${conf.url}/v3`, 105 | { 106 | method: 'GET' 107 | } 108 | )).ok) return {isOk: true, val: null}; 109 | else return {isOk: false, err: null}; 110 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | export * from './functions.js'; 2 | export * from './types.js'; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | //### Resul type 2 | export type libsqlResult = { isOk: true, val: T}|{ isOk: false, err: E}; 3 | 4 | //### Config Type 5 | export type libsqlConfig = { 6 | url: string, 7 | authToken?: string, 8 | fetch?: libsqlFetchLike 9 | } 10 | 11 | //### Error Type 12 | //the final wrapper for error in this library for what is returned by hrana server 13 | export type libsqlError = { 14 | kind: "LIBSQL_SERVER_ERROR", 15 | server_message: string|null, 16 | http_status_code: number 17 | }|{ 18 | kind: "LIBSQL_RESPONSE_ERROR", 19 | data: libsqlStreamResErrData 20 | } 21 | 22 | //### Fetch Def 23 | //the only features of WHATHG fetch required in this library 24 | export type libsqlFetchLike = ( 25 | input: string, 26 | init?: { 27 | body?: string, 28 | headers?: Record, 29 | method?: 'POST'|'GET' 30 | } 31 | ) => Promise<{ 32 | readonly ok: boolean, 33 | readonly status: number, 34 | json(): Promise, 35 | text(): Promise 36 | }>; 37 | 38 | //### Hrana Types 39 | // "//line:" from 1713449025236-HRANA_3_SPEC.md 40 | //## Pipeline Intractions ====================================================== 41 | //line: 652 42 | export type libsqlPipelineReq = { 43 | baton: string | null, 44 | requests: Array 45 | } 46 | //line: 657 47 | export type libsqlPipelineRes = { 48 | baton: string | null, 49 | base_url: string | null, 50 | results: Array 51 | } 52 | 53 | //## StreamReqKind ============================================================= 54 | //line: 792 55 | export type libsqlCloseStreamReq = { 56 | type: "close", 57 | } 58 | //line: 809 59 | export type libsqlExecuteStreamReq = { 60 | type: "execute", 61 | stmt: libsqlSQLStatement 62 | } 63 | //line: 828 64 | export type libsqlBatchStreamReq = { 65 | type: "batch", 66 | // line: 1059 67 | batch: { 68 | steps: Array, 69 | } 70 | } 71 | //other types are not dealt with in this lib 72 | 73 | //## StreamResKind ============================================================= 74 | //line: 667 75 | export type libsqlStreamResOk = { 76 | type: "ok", 77 | response: libsqlCloseStreamResOk|libsqlExecuteStreamResOk|libsqlBatchStreamResOk 78 | } 79 | //line: 672 80 | export type libsqlStreamResErr = { 81 | type: "error", 82 | error: libsqlStreamResErrData 83 | } 84 | 85 | //## SQLStatement ============================================================== 86 | //line: 978 87 | export type libsqlSQLStatement = { 88 | sql: string, 89 | // sql_id: number | null, // not useful in stateless 90 | args?: Array, 91 | named_args?: Array<{ 92 | name: string, 93 | value: libsqlSQLValue, 94 | }>, 95 | want_rows?: boolean, 96 | } 97 | 98 | //## BatchReqSteps ============================================================= 99 | // line: 1063 100 | export type libsqlBatchReqStep = { 101 | condition?: libsqlBatchReqStepExecCond | null, 102 | stmt: libsqlSQLStatement, 103 | } 104 | 105 | //## Stream Res Ok Kinds ======================================================= 106 | //line: 796 107 | export type libsqlCloseStreamResOk = { 108 | type: "close", 109 | } 110 | //line: 814 111 | export type libsqlExecuteStreamResOk = { 112 | type: "execute", 113 | result: libsqlStatementResOkData 114 | } 115 | //line: 833 116 | export type libsqlBatchStreamResOk = { 117 | type: "batch", 118 | result: libsqlBatchStreamResOkData, 119 | } 120 | //other types are not dealt with in this lib 121 | 122 | //## StreamResErrData ========================================================== 123 | // line: 959 124 | export type libsqlStreamResErrData = { 125 | message: string, 126 | code?: string | null 127 | } 128 | 129 | //## SQLValues ================================================================= 130 | // line: 1266 131 | export type libsqlSQLValue = 132 | { type: "null" } | 133 | { type: "integer", value: string } | 134 | { type: "float", value: number } | 135 | { type: "text", value: string } | 136 | { type: "blob", base64: string }; 137 | 138 | //## BatchReqStepExecCond ====================================================== 139 | // line: 1078 140 | export type libsqlBatchReqStepExecCond = 141 | { type: "ok", step: number } | //uint32: 0-based index in the steps array 142 | { type: "error", step: number } | //uint32: 0-based index in the steps array 143 | { type: "not", cond: libsqlBatchReqStepExecCond } | 144 | { type: "and", conds: Array } | 145 | { type: "or", conds: Array } | 146 | { type: "is_autocommit" }; 147 | 148 | //## StatementResOkData ======================================================== 149 | //line: 1024 150 | export type libsqlStatementResOkData = { 151 | cols: Array, 152 | rows: Array>, 153 | affected_row_count: number, //uint32 154 | last_insert_rowid: string | null, 155 | rows_read: number, 156 | rows_written: number, 157 | query_duration_ms: number 158 | } 159 | 160 | //## BatchStreamResOkData ====================================================== 161 | //line: 1106 162 | export type libsqlBatchStreamResOkData = { 163 | step_results: Array, 164 | step_errors: Array 165 | } 166 | 167 | //## SQLColumn ================================================================= 168 | // line: 1034 169 | export type libsqlSQLColumnElm = { 170 | name: string | null, 171 | decltype: string | null 172 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitAny": true, 5 | "strictNullChecks": true, 6 | "strictFunctionTypes": true, 7 | "strictBindCallApply": true, 8 | "strictPropertyInitialization": true, 9 | "verbatimModuleSyntax": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | 18 | "lib": ["ES2023"], 19 | "module": "Node16", 20 | // "target": "ES2019", 21 | "declaration": true, 22 | "outDir": "./dist" 23 | }, 24 | "include": ["./src/*"] 25 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['./src/main.ts'], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: true, 8 | minify: true, 9 | minifyWhitespace: true, 10 | outDir: './dist', 11 | platform: 'neutral', 12 | treeshake: 'safest', 13 | cjsInterop: true, 14 | dts: true, 15 | shims: true, 16 | format: ['cjs', 'esm'], 17 | target: ['deno1', 'node18', 'chrome120', 'edge120', 'firefox120', 'safari16', 'es2020'] 18 | }) --------------------------------------------------------------------------------