Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ build/
.coverage
node_modules/
runner.log
.coverage.*
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A community Python server SDK for [Zero](https://zero.rocicorp.dev/). It is not

It ports what `@rocicorp/zero/server` gives a TypeScript backend: the query endpoint (`ZERO_QUERY_URL`), the mutate endpoint (`ZERO_MUTATE_URL`), the ZQL query builder, `defineQueries` and `defineMutators`, and `tx.mutate` and `tx.run` on Postgres. The client and `zero-cache` stay in TypeScript, and a Python backend serves the same TypeScript client a TypeScript backend would.

Compatibility is pinned to one Zero release at a time. This version is tested against `@rocicorp/zero` **1.10.0** (see `ZERO_VERSION`).
Compatibility is pinned to one Zero release at a time. This version is tested against `@rocicorp/zero` **1.11.3** (see `ZERO_VERSION`).

## Usage

Expand Down Expand Up @@ -77,6 +77,9 @@ app = Starlette(
Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With FastAPI, `app.router.routes.extend(zero_routes(...))`. With another framework, call `handle_query_request` and `handle_mutate_request` yourself; `zero_server/integrations/starlette.py` is short enough to copy. The types to annotate those handlers (`QueryRequestHandler`, `TransactFn`, `MutateResponse`, `MutationResponse`, `ServerPushFailed` and the rest) import from `zero_server`, and `server_ast(query, schema)` returns the AST zero-cache receives for a query.

- **Databases.** `SQLAlchemyDatabase` (SQLAlchemy's asyncio engine on asyncpg or psycopg; the `sqlalchemy` extra installs neither driver), `AsyncpgDatabase` (an asyncpg pool) and `PsycopgDatabase` (a psycopg 3 async pool), under `zero_server.server.adapters`. A push's mutations run on one connection from the pool, taken at its first transaction and returned when the push ends. So mutators in one push share session state (a `SET` without `LOCAL`, session advisory locks, temp tables), and a handler that takes a second connection from the same pool while its push holds one can deadlock once the pool has fewer connections than there are concurrent pushes. Another driver plugs into `ZQLDatabase` as a `DBConnection` whose `query` takes `$1` placeholders.
- **Mutator results.** What a mutator returns, if not `None`, reaches the client as the mutation's result data, and is also written to zero-cache's `mutations` table so it survives a lost response. Python has one `None`, so a mutator cannot return JSON `null` as data the way a TypeScript mutator can.
- **Re-running a mutator.** `tx.retry_on(predicate)` asks `predicate(error, attempt)`, sync or async, whether to run the mutator again in a fresh transaction when the run fails, including at `COMMIT`. Only `True` means yes. A mutator runs at most 5 times per push, and `tx` is unusable once its transaction has ended, inside the predicate too.
- **Isolation level.** Each adapter takes `isolation_level="read committed" | "repeatable read" | "serializable"` for the transactions mutations run in. Unset, the database's default applies.
- **Validation.** `@define_query(Model)` and `@define_mutator(Model)` parse args with a Pydantic model or `TypeAdapter` first; a failure reaches the client as upstream's validation error.
- **Strict building.** `create_builder(schema, strict=True)` refuses an unknown column when the query is built. Upstream only refuses it when the endpoint maps the query, failing the whole batch, so this is for your tests.

Expand Down
2 changes: 1 addition & 1 deletion ZERO_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.10.0
1.11.3
26 changes: 26 additions & 0 deletions bench/fixtures/query_server_schema_rows.json
Original file line number Diff line number Diff line change
Expand Up @@ -518,5 +518,31 @@
"typename": "timestamptz",
"elemTyptype": null,
"elemTypname": null
},
{
"schema": "extra",
"table": "gadgets",
"column": "tag",
"dataType": "character",
"length": 3,
"precision": null,
"scale": null,
"typtype": "b",
"typename": "bpchar",
"elemTyptype": null,
"elemTypname": null
},
{
"schema": "extra",
"table": "gadgets",
"column": "pad",
"dataType": "character",
"length": null,
"precision": null,
"scale": null,
"typtype": "b",
"typename": "bpchar",
"elemTyptype": null,
"elemTypname": null
}
]
10 changes: 10 additions & 0 deletions bench/fixtures/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,16 @@
"type": "number",
"optional": true,
"customType": null
},
"tag": {
"type": "string",
"optional": true,
"customType": null
},
"pad": {
"type": "string",
"optional": true,
"customType": null
}
},
"primaryKey": [
Expand Down
8 changes: 8 additions & 0 deletions docs/compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ These are the cases where the proof holds Python to something narrower than "ide
- **Two failures in one query batch.** Upstream runs a batch's handlers together under `Promise.all`, and
so does Python. When two of them fail the whole batch, which failure is reported depends on how each
runtime orders its promise callbacks, and the two runtimes can disagree.
- **Interned queries.** Upstream's builder hands back the query it built before when a step repeats one,
keyed so that some values JavaScript's types rule out share a key: a `flip` of `'yes'` with `true`, any
`orderBy` direction but `'asc'` with `'desc'`. In one process, such a value can come back as whichever
was built first. Python builds what it is given. The harness gives each program a fresh schema, which
upstream interns by, so that no comparison depends on the programs before it.
- **A mutator returning `null`.** Upstream records a mutator's return value as result data unless it is
`undefined`, so a TypeScript mutator can return `null` as data. Python has only `None`, which the port
takes as returning nothing, as the tests' mutators never return `null`.
- **The JavaScript object model.** Upstream's registries also resolve `~`, their marker key, and names every
object inherits such as `toString`. A Python registry has neither.
- **The runtime.** The oracle runs on Node 24, and JSON parse messages are V8's. Bun or Deno word them
Expand Down
2 changes: 1 addition & 1 deletion e2e/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
retries: 60

zero-ts: &zero
image: rocicorp/zero:1.10.0
image: rocicorp/zero:1.11.3
depends_on:
postgres:
condition: service_healthy
Expand Down
Loading
Loading