diff --git a/README.md b/README.md index 7e00db2..21fe934 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ An HTTP wire format for SQL. POST a SQL statement and parameters to an endpoint, get rows back. Stateless, edge-friendly, JSON over HTTP. Nothing more. -**Status:** v0.1 draft. The spec is being dogfooded against working implementations before being proposed as an RFC. Breaking changes possible until v1.0. +**Status:** v0.2 draft. The spec is being dogfooded against working implementations before being proposed as an RFC. Breaking changes possible until v1.0. ## The problem this solves @@ -20,7 +20,7 @@ The PostgreSQL wire protocol is a streaming socket protocol — not HTTP, not ed `http-sql` is that one format. -## Goals (v0.1) +## Goals (v0.2) - **One canonical request/response shape** that works for SELECT, INSERT, UPDATE, DELETE, DDL, and batched statements. - **Stateless HTTP**. No sessions, no connection objects, no websocket upgrades. One request, one response. @@ -29,9 +29,9 @@ The PostgreSQL wire protocol is a streaming socket protocol — not HTTP, not ed - **Vendor-neutral**. The spec doesn't mention any specific database or platform. - **JSON-native parameters and results**. Strings, numbers, booleans, null, with a tagged form for blobs and other extended types. -## Non-goals (v0.1) +## Non-goals (v0.2) -- **Streaming large result sets.** Pagination is the v0.1 answer. SSE / chunked responses can come in a later revision. +- **Streaming large result sets.** Pagination is the v0.2 answer. SSE / chunked responses can come in a later revision. - **Cross-request transactions.** One request is one autocommit unit. Batch requests can opt into atomicity. Multi-request transactions need session state and break statelessness; out of scope. - **Schema management primitives.** DDL is allowed as a normal SQL statement; the spec doesn't add `CREATE TABLE` helpers. - **Authentication scheme.** Use HTTP auth headers. `Bearer` is recommended but the spec doesn't mandate. @@ -42,7 +42,7 @@ The PostgreSQL wire protocol is a streaming socket protocol — not HTTP, not ed - [SPEC.md](./SPEC.md) — the wire format definition - [examples/](./examples) — curl invocations, reference client and server, and two full Cloudflare implementations (D1-backed and Durable-Object-backed) -- [conformance/](./conformance) — what a server must do to claim http-sql v0.1 conformance +- [conformance/](./conformance) — what a server must do to claim http-sql v0.2 conformance - [implementations.md](./implementations.md) — known servers and clients ## Prior art diff --git a/SPEC.md b/SPEC.md index 52449e1..c9e39b5 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,8 +1,8 @@ -# http-sql v0.1 +# http-sql v0.2 An HTTP wire format for submitting a SQL statement and receiving a result set. -**Status:** Draft, v0.1. +**Status:** Draft, v0.2. **Editor:** [@rafters-studio](https://github.com/rafters-studio) **License:** MIT @@ -69,13 +69,12 @@ The placeholder syntax itself is the server's, not this spec's: http-sql does no Servers MAY reject batches that exceed a server-defined statement count with HTTP status `413` and `error.code` of `payload_too_large`. -> **Non-normative note (not part of the v0.1 contract).** The `atomic` obligation above is -> one-directional, and v0.1 states that asymmetry deliberately rather than by oversight. +> **Non-normative note (not part of the normative contract).** The `atomic` obligation above is +> one-directional, and this spec states that asymmetry deliberately rather than by oversight. > > There is no conforming way to decline. The obligation on `atomic: true` is unqualified > here and in section 10.1, and section 7 registers no code meaning "atomicity unavailable." -> A server that cannot execute batches transactionally is simply non-conforming for batch in -> v0.1. Even if such a server declined with a `vendor:` code, the signal would not travel: +> A server that cannot execute batches transactionally is simply non-conforming for batch. Even if such a server declined with a `vendor:` code, the signal would not travel: > section 7 directs clients to treat unknown codes as the closest registered code by HTTP > status family, which collapses the decline into ordinary `bad_request` / `sql_error` > semantics. @@ -90,7 +89,7 @@ Servers MAY reject batches that exceed a server-defined statement count with HTT > `not_supported` code, an `atomic` echo field in the section 6.2 envelope, and capability > advertisement — are recorded in > [issue #13](https://github.com/rafters-studio/http-sql/issues/13) and are deliberately out -> of scope for v0.1. +> of scope for this version. ## 5. Parameter types @@ -116,7 +115,7 @@ For values that cannot be represented as a JSON primitive (binary blobs, integer - `$type` (REQUIRED, string) — one of the registered types listed below, or a vendor-namespaced type (`vendor:`). - `$value` (REQUIRED) — the encoded value as a JSON value. -Registered types in v0.1: +Registered types in v0.2: | `$type` | `$value` encoding | |----------|--------------------------------------------------------| @@ -214,7 +213,7 @@ HTTP status: `4xx` or `5xx`. - `error.message` (REQUIRED, string) — human-readable explanation. Servers SHOULD avoid leaking sensitive details. - `error.statementIndex` (REQUIRED for non-atomic batch statement failures, otherwise OPTIONAL, integer) — the zero-based index of the statement that failed. For a non-atomic batch failure it is the client's only means of determining which statements persisted (section 6.2.1), so it MUST be present. MUST be omitted for single-statement requests. -Registered error codes in v0.1: +Registered error codes in v0.2: | `code` | HTTP | Meaning | |--------------------------|------|-----------------------------------------------------------------| @@ -228,20 +227,20 @@ Registered error codes in v0.1: | `rate_limited` | 429 | Too many requests. | | `internal_error` | 500 | Server malfunction. | -`unsupported_media_type` is introduced in v0.2. Per section 11, a new registered error code is an additive change that increments the minor version; this spec has no patch level, so the code is not available to a server that advertises `0.1`. +`unsupported_media_type` was introduced in v0.2 (per section 11, a new registered error code is an additive change that increments the minor version). It is not available to a server that advertises `0.1`. Vendor codes carry the prefix `vendor:` (e.g. `vendor:cf_d1_quota_exceeded`). Clients SHOULD treat unknown `error.code` values as if they were the closest registered code by HTTP status family. ## 8. Pagination -http-sql v0.1 does not define pagination. Servers SHOULD enforce a server-defined maximum result row count and return `payload_too_large` if exceeded, with `error.message` suggesting `LIMIT` / `OFFSET` in the SQL. Cursor-based pagination is being considered for a future revision. +http-sql v0.2 does not define pagination. Servers SHOULD enforce a server-defined maximum result row count and return `payload_too_large` if exceeded, with `error.message` suggesting `LIMIT` / `OFFSET` in the SQL. Cursor-based pagination is being considered for a future revision. ## 9. Version negotiation Conforming servers MUST include the response header: ``` -X-Http-Sql-Version: 0.1 +X-Http-Sql-Version: 0.2 ``` on every response (including error responses). @@ -249,16 +248,16 @@ on every response (including error responses). Clients MAY send the request header: ``` -X-Http-Sql-Accept-Version: 0.1 +X-Http-Sql-Accept-Version: 0.2 ``` -to indicate the maximum spec version they understand. Servers MAY use this for forward-compatible behavior. v0.1 servers ignore the header. +to indicate the maximum spec version they understand. Servers MAY use this for forward-compatible behavior. v0.2 servers ignore the header. ## 10. Conformance ### 10.1 Server conformance -A v0.1 conforming server MUST: +A v0.2 conforming server MUST: 1. Accept POST requests with `Content-Type: application/json` at one or more endpoint URLs, and reject other media types per section 2. 2. Accept both single-statement (section 4.1) and batch (section 4.2) request shapes. @@ -270,14 +269,14 @@ A v0.1 conforming server MUST: 8. Accept the registered parameter types in section 5 (`blob`, `bigint`). 9. Emit the `X-Http-Sql-Version` response header. -A v0.1 conforming server MAY: +A v0.2 conforming server MAY: - Accept additional vendor-namespaced parameter types or error codes. - Apply tenancy, ACLs, row-level security, query whitelisting, or any other policy. http-sql is transport, not policy. ### 10.2 Client conformance -A v0.1 conforming client MUST: +A v0.2 conforming client MUST: 1. Send `Content-Type: application/json`. 2. Send exactly one of `sql` or `batch` in the request body. @@ -286,7 +285,7 @@ A v0.1 conforming client MUST: 5. On a non-atomic batch error, treat the statements preceding `error.statementIndex` as applied (section 6.2.1). A client MUST NOT assume no statements were applied. 6. Not require any vendor-specific request or response fields beyond those defined here. -A v0.1 conforming client SHOULD: +A v0.2 conforming client SHOULD: - Send the `X-Http-Sql-Accept-Version` header. - Treat `error.code` values it does not recognize as the closest registered code by HTTP status family. @@ -295,6 +294,11 @@ A v0.1 conforming client SHOULD: This spec uses `.` versioning. Until `1.0`, the minor version increments on any breaking change. After `1.0`, breaking changes increment the major version. Additive changes (new optional fields, new registered types, new registered error codes) increment the minor version. +### Version history + +- **0.2** — batch failure behavior made normative (sequential non-atomic execution, `statementIndex` REQUIRED on non-atomic statement failures, preceding statements persist); response-side tagged-value emission MUSTs; `unsupported_media_type` registered (415); `lastInsertId` narrowed to string-or-null; `atomic` obligation unconditional; dialect-neutral parameter typing; `X-Http-Sql-Version` MUST. +- **0.1** — initial draft. + ## 12. Security considerations http-sql carries arbitrary SQL strings. Servers MUST treat the SQL as untrusted input from the perspective of authorization, even if the network channel is authenticated. Practical implications: diff --git a/conformance/README.md b/conformance/README.md index d854ed4..f0786eb 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,8 +1,6 @@ # http-sql conformance -A conforming http-sql v0.1 server passes the test cases below when probed at its endpoint URL with a valid bearer token. - -Cases marked `(v0.2+)` exercise behavior introduced after v0.1 and are not required of a server that advertises `X-Http-Sql-Version: 0.1`. +A conforming http-sql v0.2 server passes the test cases below when probed at its endpoint URL with a valid bearer token. This directory will contain a runnable TypeScript test suite. The current document defines the test cases that runner must implement, so server implementers can self-check before installing the runner. @@ -31,7 +29,7 @@ Conformance is self-asserted. The community can call out failures via issues. | R-1 | Body contains both `sql` and `batch` | 400, `error.code` = `bad_request` | | R-2 | Body contains neither `sql` nor `batch` | 400, `error.code` = `bad_request` | | R-3 | Body is not valid JSON | 400, `error.code` = `bad_request` | -| R-4 | `Content-Type` other than `application/json` (v0.2+) | 415, `error.code` = `unsupported_media_type` | +| R-4 | `Content-Type` other than `application/json` | 415, `error.code` = `unsupported_media_type` | | R-5 | `Content-Type: application/json; charset=utf-8` | Executes normally -- media-type parameters are ignored | ### Single-statement execution @@ -86,8 +84,8 @@ Servers on a non-SQLite backend substitute their dialect's literal syntax for th | ID | Description | Expected response | |-------|------------------------------------------------------------------|---------------------------------------------| -| H-1 | Any successful response | Includes `X-Http-Sql-Version: 0.1` | -| H-2 | Any error response | Includes `X-Http-Sql-Version: 0.1` | +| H-1 | Any successful response | Includes `X-Http-Sql-Version: 0.2` | +| H-2 | Any error response | Includes `X-Http-Sql-Version: 0.2` | ## Optional / "nice to have" diff --git a/examples/cloudflare-durable-object/src/index.ts b/examples/cloudflare-durable-object/src/index.ts index b5c110f..a2b6298 100644 --- a/examples/cloudflare-durable-object/src/index.ts +++ b/examples/cloudflare-durable-object/src/index.ts @@ -1,4 +1,4 @@ -// http-sql v0.1 over Cloudflare Durable Objects, with Hono. +// http-sql v0.2 over Cloudflare Durable Objects, with Hono. // // Each tenant maps to its own DO instance, and each DO holds its own real // SQLite via ctx.storage.sql. The Worker is just a router: validate the @@ -17,7 +17,7 @@ export interface Env { TENANT_TOKEN_BOB: string; } -const VERSION = "0.1"; +const VERSION = "0.2"; const app = new Hono<{ Bindings: Env }>(); @@ -63,7 +63,7 @@ function resolveTenant(header: string, env: Env): string | null { // ============================================================================= // TenantDO: one Durable Object per tenant. Holds a real SQLite database via -// ctx.storage.sql. Receives http-sql v0.1 envelopes from the router and runs +// ctx.storage.sql. Receives http-sql v0.2 envelopes from the router and runs // them against its own SQLite. All access for a given tenant is serialized // through this single instance. // ============================================================================= diff --git a/examples/cloudflare-worker-to-d1/src/index.ts b/examples/cloudflare-worker-to-d1/src/index.ts index 55fe6f8..5ddda97 100644 --- a/examples/cloudflare-worker-to-d1/src/index.ts +++ b/examples/cloudflare-worker-to-d1/src/index.ts @@ -1,7 +1,7 @@ -// http-sql v0.1 over Cloudflare D1, with Hono. +// http-sql v0.2 over Cloudflare D1, with Hono. // -// POST any http-sql v0.1 request to this Worker; it runs the SQL against the -// bound D1 database and returns a v0.1 response. Tagged params (blob, bigint) +// POST any http-sql v0.2 request to this Worker; it runs the SQL against the +// bound D1 database and returns a v0.2 response. Tagged params (blob, bigint) // are decoded before binding; binary results are re-encoded going out. import { Hono } from "hono"; @@ -24,7 +24,7 @@ interface StatementResult { lastInsertId?: string | null; } -const VERSION = "0.1"; +const VERSION = "0.2"; const app = new Hono<{ Bindings: Env }>(); diff --git a/examples/reference-client.ts b/examples/reference-client.ts index 8430607..33c6134 100644 --- a/examples/reference-client.ts +++ b/examples/reference-client.ts @@ -1,4 +1,4 @@ -// Reference http-sql v0.1 client, ~40 lines. +// Reference http-sql v0.2 client, ~40 lines. // // Uses the platform `fetch`. No dependencies. @@ -37,7 +37,7 @@ export class HttpSqlClient { headers: { "content-type": "application/json", "authorization": `Bearer ${this.token}`, - "x-http-sql-accept-version": "0.1", + "x-http-sql-accept-version": "0.2", }, body: JSON.stringify(body), }); diff --git a/examples/reference-server.ts b/examples/reference-server.ts index a4504f1..3968d79 100644 --- a/examples/reference-server.ts +++ b/examples/reference-server.ts @@ -1,4 +1,4 @@ -// Reference http-sql v0.1 server, ~80 lines. +// Reference http-sql v0.2 server, ~80 lines. // // Runs on any platform with `fetch`-style Request/Response (Workers, Deno, // Bun, Node 20+ with the undici fetch globals). The SQL execution is faked @@ -16,7 +16,7 @@ interface Result { lastInsertId?: string | number | null; } -const VERSION_HEADER = { "X-Http-Sql-Version": "0.1" }; +const VERSION_HEADER = { "X-Http-Sql-Version": "0.2" }; const JSON_HEADERS = { "content-type": "application/json", ...VERSION_HEADER }; export async function handle(req: Request, auth: (req: Request) => boolean): Promise { diff --git a/implementations.md b/implementations.md index 47b1bfe..d51de23 100644 --- a/implementations.md +++ b/implementations.md @@ -1,6 +1,6 @@ # http-sql implementations -A directory of known servers and clients speaking the [http-sql v0.1 spec](./SPEC.md). The list is bootstrap-thin; PRs welcome. +A directory of known servers and clients speaking the [http-sql v0.2 spec](./SPEC.md). The list is bootstrap-thin; PRs welcome. ## Servers @@ -27,7 +27,7 @@ The combinations above let you enter at whichever end matches what you already h ## How to add yours -1. Implement the [v0.1 spec](./SPEC.md) (or the [conformance contract](./conformance/README.md) for self-check). +1. Implement the [v0.2 spec](./SPEC.md) (or the [conformance contract](./conformance/README.md) for self-check). 2. Open a PR adding a row to the table above. 3. Include: name, form (Worker / Node / library), backend (D1 / Turso / Postgres / DO / etc), and one-line notes.