PostgreSQL API
SQL (and the ready-made sql) is global — no import, no package. Native Zig client, extended protocol, lazy pool of up to 10 connections.
Quick look
new SQL(dsn) / global sql
new SQL(dsn) builds a pool around that DSN. The global sql is the same API preconfigured from the environment — checked in order: PG_TEST_DSN, DATABASE_URL, then PGHOST / PGPORT / PGUSER / PGPASSWORD / PGDATABASE, then defaults (127.0.0.1:5432, user postgres).
Connections are lazy: the first query opens them, idle connections are reused, and the pool caps at 10.
Caution: With no DSN argument and no environment set, queries reject with
TypeError: no database pool. ExportPG_TEST_DSN(or pass the DSN) before running.
Reading and writing: tag / unsafe
Rows map column names to values; NULL is null. Always prefer placeholders over string interpolation (no escaping bugs, no injection) — identifiers (table/column names) must never come from input, only values travel as params.
Parameter types
Arrays and objects round-trip jsonb and text[] columns without a manual JSON.stringify:
Result decoding
Errors
Failures reject the promise — await inside try/catch:
Transactions: begin + transaction(fn)
A transaction pins one connection: the tag, tx.unsafe(...), tx.commit(), and tx.rollback() all run on it. Await each query before starting the next — a transaction runs one query at a time, and using a finished transaction fails.
Caution: Don't interleave a transaction with plain
sqlqueries and assume they share state — outside queries use other pool connections and won't see uncommitted rows.
Tuning: close
Call it at shutdown (or nowhere — the process exit closes sockets anyway).
Practical example: notes API on Postgres
Full working app: examples/todo-app/ (CRUD + jsonb/arrays + transactions). Script tour: examples/sql-todo.js.
Troubleshooting
TypeError: no database pool — no DSN argument and no PG_TEST_DSN / DATABASE_URL / PG* variables. Export one, or use new SQL(dsn).
Connection refused — Postgres isn't listening (Docker not up? docker compose up -d), or the host/port in the DSN is wrong.
password authentication failed — credentials in the DSN don't match the server. Check PGUSER / PGPASSWORD; environment variables override nothing once a DSN argument is passed.
relation "x" does not exist — the migration never ran on this database (wrong database name? fresh server?). Run the CREATE TABLE before the queries.
transaction busy — two queries were fired inside one transaction without awaiting the first. Serialize them: await each statement.
no operator matches / type errors at the server — a parameter was encoded as a different type than the column expects (e.g. text param against an integer column). Cast in the SQL ($1::int) or pass the right JS type.
Queries after sql.close() fail — by design. Close only at shutdown, one pool per process (per worker).
