Sfoglia il codice sorgente

docs: document LISTEN/NOTIFY and channel scopes

Danilo Fragoso 23 ore fa
parent
commit
025dd1559b

+ 1 - 0
astro.config.mjs

@@ -94,6 +94,7 @@ export default defineConfig({
             { label: 'Expressions & operators', link: '/sql-reference/expressions/' },
             { label: 'Functions', link: '/sql-reference/functions/' },
             { label: 'Constraints', link: '/sql-reference/constraints/' },
+            { label: 'Notifications', link: '/sql-reference/notifications/' },
             { label: 'Compatibility', link: '/sql-reference/compatibility/' },
           ],
         },

+ 4 - 0
src/content/docs/clients/api-keys.md

@@ -44,9 +44,13 @@ Scopes constrain what a key can do. A key with no granted scope is denied everyt
 | `write` | `INSERT`, `UPDATE`, `DELETE` |
 | `alter_table` | `CREATE TABLE`, `ALTER TABLE` |
 | `drop_table` | `DROP TABLE` |
+| `listen:<pattern>` | `LISTEN`, `UNLISTEN`, and SSE channel subscriptions |
+| `notify:<pattern>` | `NOTIFY` |
 
 Statements are validated against these scopes on every request, over both HTTP and the Postgres protocol. A `DROP TABLE` requires the explicit `drop_table` scope; schema changes require `alter_table`.
 
+Channel scopes are separate from the data scopes: `read` does not grant `LISTEN`, and a `listen:orders.*` scope does not grant `SELECT`. Patterns are an exact channel name or a prefix ending in `*`. See [Notifications](/sql-reference/notifications/).
+
 ### `REST_API` scopes
 
 | Scope | Allows |

+ 2 - 1
src/content/docs/sql-reference/compatibility.md

@@ -202,7 +202,7 @@ PostgreSQL wire compatibility allows some PostgreSQL clients to connect. It does
 | `pg_catalog` | Unsupported | Unsupported | Supported |
 | Query cancellation | Unsupported | Not applicable | Supported |
 | `COPY` | Unsupported | Not applicable | Supported |
-| `LISTEN` and `NOTIFY` | Unsupported | Not applicable | Supported |
+| `LISTEN` and `NOTIFY` | Supported; no durable replay | Not applicable | Supported |
 | Node.js `pg` | Basic use verified | Not applicable | Supported |
 | psycopg, pgx, and lib/pq | Unverified | Not applicable | Supported |
 | ORMs and migration frameworks | Unverified | Varies | Supported |
@@ -215,6 +215,7 @@ Because the wire is PostgreSQL-compatible, drivers may issue introspection queri
 The managed database.pizza proxy layers additional behavior on top of PizzaSQL:
 
 - API keys with `read`, `write`, `alter_table`, and `drop_table` scopes gate statements.
+- Channel operations require `listen:<pattern>` and `notify:<pattern>` scopes; see [Notifications](/sql-reference/notifications/).
 - PostgreSQL TLS terminates at the managed proxy when `PGPROXY_TLS_CERT`/`PGPROXY_TLS_KEY` are configured. `PGPROXY_TLS_MODE=optional` (the default with a certificate) offers TLS while keeping `sslmode=disable` clients working; `required` rejects plaintext startup with `08P01`; `off` disables TLS. Clients can use `sslmode=require` (or `verify-full` with a trusted, hostname-matching certificate). If a deployment has not installed certificates, `SSLRequest` is declined and only `sslmode=disable` connects; use that only over a trusted hop. TLS ends at the proxy — the loopback hop to PizzaSQL is plaintext.
 - Exhausting the per-organization connection quota returns SQLSTATE `53300` with the current/limit numbers and a retry hint. Quota and idle-connection behavior are documented in [Limits & quotas](/guides/limits/#connections-and-idle-disconnects).
 - The PostgreSQL database name is `org/db`; the slash must be percent-encoded in a URI.

+ 120 - 0
src/content/docs/sql-reference/notifications.md

@@ -0,0 +1,120 @@
+---
+title: LISTEN, NOTIFY, and subscriptions
+description: PostgreSQL-compatible channels in PizzaSQL, their transaction semantics, limits, and the managed SSE fan-out endpoint.
+---
+
+PizzaSQL implements PostgreSQL-style `LISTEN`, `UNLISTEN`, and `NOTIFY` over the
+PostgreSQL wire protocol. Notifications are delivered asynchronously as
+`NotificationResponse` frames, so an idle client sees them without polling.
+
+## SQL syntax
+
+```sql
+LISTEN channel;
+UNLISTEN channel;
+UNLISTEN *;
+NOTIFY channel;
+NOTIFY channel, 'payload';
+```
+
+- `channel` is an identifier (quote it for mixed case or punctuation). Channel
+  names are limited to 63 bytes.
+- `payload` is an optional string literal of at most 8000 bytes.
+- A transaction may raise many notifications, but duplicate `(channel, payload)`
+  pairs collapse to one delivery.
+- Command tags are `LISTEN`, `UNLISTEN`, and `NOTIFY`.
+
+`LISTEN` and `UNLISTEN` take effect when the transaction commits. `NOTIFY` is
+delivered only after a successful commit; `ROLLBACK` discards pending
+notifications, and `ROLLBACK TO SAVEPOINT` discards notifications raised after
+that savepoint.
+
+```sql
+BEGIN;
+INSERT INTO jobs (id, state) VALUES (1, 'queued');
+NOTIFY jobs, '1 queued';
+COMMIT; -- the notification becomes visible here
+```
+
+A `NOTIFY` outside an explicit transaction is delivered immediately after the
+statement. Notifications raised by the same connection are delivered back to it
+when it also listens on the channel.
+
+## Commit cursor
+
+After a committed write transaction, PizzaSQL sends a `ParameterStatus` frame
+named `pizzasql.lsn` whose value is the commit LSN. Notifications carry a
+monotonically increasing LSN per database; the SSE endpoint exposes it as the
+event `id`. The LSN is an ordering and resume cursor, not a durable replay log.
+
+The HTTP query API returns the same value as `lsn` on a `COMMIT` response.
+
+## Managed service: channel scopes
+
+The managed PostgreSQL proxy classifies `LISTEN`, `UNLISTEN`, and `NOTIFY` and
+requires a matching channel scope on the API key. The data scopes (`read`,
+`write`, `alter_table`, `drop_table`) do not grant channel access.
+
+| Scope | Grants |
+| --- | --- |
+| `listen:<pattern>` | `LISTEN`/`UNLISTEN` on matching channels |
+| `notify:<pattern>` | `NOTIFY` on matching channels |
+
+Patterns are an exact channel name or a prefix ending in `*`:
+
+```text
+listen:orders
+listen:orders.*
+notify:*
+```
+
+`UNLISTEN *` is allowed when the key has any `listen:` scope. Long-lived
+listener connections hold one connection against the organization's quota until
+they disconnect; the proxy has no idle timeout that drops them.
+
+## SSE fan-out
+
+Browser and mobile clients should not each hold a database connection. The
+managed service exposes an SSE endpoint that shares a bounded listener pool per
+tenant and channel:
+
+```http
+GET /{org}/{db}/subscribe?channel=orders.created&channel=audit
+Authorization: Bearer pz_live_...
+Accept: text/event-stream
+```
+
+- Authenticate with a `DB_ACCESS` API key. Each requested channel must match a
+  `listen:` scope on that key.
+- Repeat the `channel` parameter or use a comma-separated list. At most 16
+  channels may be requested per connection.
+- The server holds at most `SUBSCRIPTION_MAX_LISTENERS_PER_ORG` database
+  listeners per organization (default 16), shared by all clients. A client
+  subscribing to four channels consumes four listener slots, not four database
+  connections per browser tab.
+- Events use the notification LSN as their SSE `id`:
+
+```text
+id: 42
+event: notification
+data: {"pid":123,"channel":"orders.created","payload":"17","lsn":42}
+```
+
+- A client that cannot keep up is disconnected with an `error` event rather than
+  blocking publishers. Reconnect with `EventSource` or a streaming `fetch`.
+- Keepalive comments are sent every 15 seconds.
+
+The SSE endpoint currently authenticates only with an API key (`Authorization:
+Bearer`), not a console session cookie. `EventSource` cannot set headers; use a
+streaming `fetch` implementation from browser code.
+
+## Limitations in v1
+
+- Notifications are **not durable**. There is no replay or catch-up after a
+  disconnect, and a server restart loses undelivered events.
+- Fan-out is in-process. One PizzaSQL process serves a database today; a future
+  multi-process deployment would need a shared notification bus.
+- Triggers are not implemented, so `NOTIFY` must be issued by an application
+  statement.
+- Channel names containing `"` are rejected by the SSE listener because the
+  SQLite-style identifier lexer does not support escaped quotes.