These libraries are experimental. APIs may change without notice. Generated from source with koruc 0.1.7 on 11/4/2026.
pq
@korulang/pq@0.0.1libpq (PostgreSQL) bindings for Koru, lifted behind phantom-typed connection and result obligations
pq/index.kz · 30 tors
@korulang/postgres - PostgreSQL client for Koru
A semantic redesign of libpq using phantom obligations to make
an entire class of database bugs impossible at compile time.
Phantom lifecycles
Derived from the phantom labels in the declarations below — state! issues an
obligation the compiler will chase, !state discharges it, a bare state holds it without moving it. Nothing here is hand-drawn.
Conn 5 states connected!connecting!in_tx!in_failed_tx!Result 1 state result!// Connection Lifecycle
~pub tor connect { conninfo: string }
| ok *Conn<connected!>
| err string~pub tor disconnect { conn: *Conn<!connected> }// Query Execution
//
// Fire-and-forget: DDL, INSERT/UPDATE/DELETE where you don't need rows back.
// Error message is connection-owned — valid until the next operation on conn.
~pub tor exec { conn: *Conn<connected>, sql: string }
| ok
| err string// Query returning rows. Result carries a <result!> obligation — must be cleared.
~pub tor query { conn: *Conn<connected>, sql: string }
| rows *Result<result!>
| empty
| err string// Number of rows in a result.
~pub tor result.nrows { result: *Result<result> } -> usize// Get a column value by name. Returns the text representation (libpq's native form).
// Branches: some = value present, null = SQL NULL, no-such-col = column name not found.
~pub tor result.col { result: *Result<result>, row: usize, col: string }
| some string
| is-null
| no-such-col// Discharge the result obligation — must be called when done with a result.
~pub tor result.clear { result: *Result<!result> }// ASYNC SURFACE — the nonblocking half of libpq
// The sync tors above block the calling thread for the round trip — right
// for a CLI, wrong for a server that owns thousands of sockets per worker.
// libpq's nonblocking API exists so the connection's own fd can live on the
// caller's event loop: `socket` hands the descriptor out, `connect-poll` /
// `is-busy` say which readiness to wait for, `consume-input` + `get-result`
// drive the state machine, and `set-nonblocking` makes every send queue
// instead of stall. A `*Conn<connecting!>` born by `connect-start` settles
// through `connect-poll` into `connected` — the same phantom transition the
// sync path performs in one call, spelled out for the reactor.
~pub tor connect-start { conninfo: string }
| ok *Conn<connecting!>
| err string// Step the async connect — consumes the <connecting!> obligation and
// re-arms it: `read`/`write` hand the conn back still carrying
// <connecting!> so the next poll can consume it again, `ok` settles into
// <connected!>, and `failed` frees the handle inside (nothing to poll —
// the string is all that's left).
~pub tor connect-poll { conn: *Conn<!connecting> }
| ok *Conn<connected!>
| read *Conn<connecting!>
| write *Conn<connecting!>
| failed string// Abort a conn in ANY state — a still-connecting socket, a failed-tx conn,
// whatever `disconnect` cannot take because its obligation is the wrong
// shape. PQfinish + free; the obligation is discharged.
~pub tor conn.close { conn: *Conn<!connecting|!in_tx|!in_failed_tx> }// The descriptor the event loop registers. Same fd for the whole conn's
// life — libpq may recycle it internally on reconnect, which this client
// does not do, so the value is stable.
~pub tor socket { conn: *Conn<*> } -> i32// Make every send queue instead of stall. Must precede send-query use on
// an event loop; without it a full socket buffer blocks the caller — the
// very thing the async surface exists to prevent.
~pub tor set-nonblocking { conn: *Conn<*> }
| ok
| err string// Queue a query without waiting for the socket. libpq returns 0 when the
// send could not be queued at all — not "not yet sent", so there is no
// pending state to drain later, just an error to report.
~pub tor send-query { conn: *Conn<connected>, sql: string }
| sent
| err string// Read whatever the socket delivered into libpq's buffers. Call when the
// fd reports readable; pairs with `is-busy` to know whether a result is
// actually ready.
~pub tor consume-input { conn: *Conn<connected> }
| ok
| err string// True while a result is still in flight — get-result would block, so the
// caller must wait for more input first. The event-loop gate.
~pub tor is-busy { conn: *Conn<connected> } -> bool// Pop the next completed result — call only when is-busy is false. Every
// query produces a TERMINATING null result after its last PGresult, so
// `done` marks the query finished; intermediate `rows`/`empty` arms must
// still be collected until then, exactly like the sync query's contract.
// PGresult ordering is per-query, so pipelined queries come back in send
// order.
~pub tor get-result { conn: *Conn<connected> }
| rows *Result<result!>
| empty
| done
| err string// Push queued sends toward the socket — returns 1 while bytes remain
// queued, which is the caller's signal to arm POLLOUT instead of POLLIN.
~pub tor flush { conn: *Conn<connected> } -> i32// TRANSACTIONS — the phantom state machine made real
// `begin` consumes [connected], produces [in_tx]; `commit` consumes
// [in_tx], produces [connected] — a failed commit demotes the conn to
// [in_failed_tx], where the ONLY valid move is `rollback`. `rollback`
// accepts either tx state and always produces [connected] on success.
// Queries inside a transaction use `tx.query`: the same shape as `query`,
// but its error arm carries the conn demoted to [in_failed_tx], because a
// query failure inside a transaction is exactly what puts it there.
~pub tor tx.begin { conn: *Conn<!connected> }
| ok *Conn<in_tx!>
| err string~pub tor tx.commit { conn: *Conn<!in_tx> }
| ok *Conn<connected!>
| err *Conn<in_failed_tx!>// Valid from either transaction state — a committed-but-failed conn has
// nowhere else to go. The rollback itself can still error (dead conn):
// then the conn is destroyed and the string says why.
~pub tor tx.rollback { conn: *Conn<*> }
| ok *Conn<connected!>
| err string// Query inside a transaction. On error the conn comes back demoted — the
// phantom tag does the bookkeeping `PQtransactionStatus` would otherwise
// have to be asked for at runtime.
~pub tor tx.query { conn: *Conn<in_tx>, sql: string }
| rows *Result<result!>
| empty
| err string// PREPARED + PARAMETERIZED — no string-interpolated queries
// `exec-params` binds values server-side: the SQL text never carries user
// bytes, so injection is not a sanitization problem, it is a non-problem.
// `prepare`/`exec-prepared` name a statement once and reuse it.
~pub tor exec-params { conn: *Conn<connected>, sql: string, params: []string }
| rows *Result<result!>
| empty
| err string~pub tor prepare { conn: *Conn<connected>, name: string, sql: string }
| ok
| err string~pub tor exec-prepared { conn: *Conn<connected>, name: string, params: []string }
| rows *Result<result!>
| empty
| err string// RESULT ACCESSORS — complete the read side
~pub tor result.ncols { result: *Result<result> } -> usize// Column name by index — the order RowDescription shipped them.
~pub tor result.field-name { result: *Result<result>, col: usize } -> ?string// Column value by position — text form, libpq's native representation.
// Tagged like `result.col`: `null` is a real NULL, never the string "NULL".
~pub tor result.getv { result: *Result<result>, row: usize, col: usize }
| some string
| null// The server's own error detail for a failed result — severity, code,
// message, position — richer than the conn-level error string.
~pub tor result.error { result: *Result<result> } -> string// The conn-level error string — valid until the next operation on conn.
~pub tor conn.error { conn: *Conn<*> } -> string~[comptime] pub tor sql { conninfo: string, source: Source }
| ok
| err string