pg (PostgreSQL)
On this page:
- spider.pg: PostgreSQL:
init,query,queryOne,queryExecute,begin. Opt-in with-Dpg=true.
spider.pg
PostgreSQL for an app (spider.pg, opt-in with -Dpg=true): one connection pool per process, opened by init, then query, queryOne, queryExecute, begin and transaction from anywhere. Rows come back as structs whose fields are matched to columns by name.
spider.pg.ArrayParameter
pub fn ArrayParameter(comptime T: type) type
What array returns: a list of values to pass as one query parameter.
Fields
values: []const Ttype_name: []const u8
spider.pg.array
pub fn array(comptime T: type, values: []const T) ArrayParameter(T)
A list of values as one parameter, for = ANY($1). values is not copied: it must stay valid until the query call returns. A plain slice passed as the parameter does the same.
const ids = [_]i32{ 2, 4, 9 };
const posts = try spider.pg.query(
Post,
c.arena,
"SELECT id, title FROM posts WHERE id = ANY($1)",
.{spider.pg.array(i32, &ids)},
);
spider.pg.DbConfig
pub const DbConfig = struct
The options of init. A field left null comes from the environment (the .env files included), then from a built-in default.
Fields
host: ?[]const u8 = null: Server address. null:PG_HOST, or "localhost".port: ?u16 = null: Server port. null:PG_PORT, or 5432.database: ?[]const u8 = null: Database name. null:PG_DB, or "spider_db".user: ?[]const u8 = null: null:PG_USER, or "spider".password: ?[]const u8 = null: null:PG_PASSWORD, or "spider".pool_size: ?usize = null: Connections kept open. null: 10 (no variable is read).mapping: MappingMode = .fail: What typed mapping does with a column missing from the result or a NULL in a non-optional field (fields with a declared default use it either way)..failreturns error.ColumnMissing / error.UnexpectedNull;.warnlogs it (once per field) and keeps the old zero value (0, "", false) — a migration aid for code written against the old silent behavior. Under.warna value that cannot be decoded into its field (error.TypeMismatch, error.IntegerOverflow, error.InvalidEnumValue) is logged and becomes the zero value too.
spider.pg.MappingMode
pub const MappingMode = enum { fail, warn }
The values of DbConfig.mapping: see that field.
spider.pg.init
pub fn init(allocator: std.mem.Allocator, io: std.Io, overrides: DbConfig) !void
Opens the connection pool. Call it once at startup, before any query, and deinit() at exit. allocator and io must outlive the pool.
A connection that can't be opened is tried five times in all, waiting 1, 2, 4 and 8 seconds, then the error is returned. An answer from Postgres itself (wrong password, unknown database) is logged and returned at once as error.PG.
try spider.pg.init(allocator, io, .{});
defer spider.pg.deinit();
spider.pg.deinit
pub fn deinit() void
Closes the pool. No query may run after it.
spider.pg.QueryResult
pub fn QueryResult(comptime T: type) type
What query(T, ...) returns: nothing for void, one value for i32 and i64, a slice []T for a struct.
spider.pg.DbError
pub const DbError = error
Postgres failures, by SQLSTATE class. error.PG remains for codes not listed here. Use lastError() in the catch for message/detail/constraint.
Errors
UniqueViolationForeignKeyViolationNotNullViolationCheckViolationExclusionViolationIntegrityConstraintViolationInvalidTextRepresentationStringDataRightTruncationNumericValueOutOfRangeInvalidDatetimeFormatDatetimeFieldOverflowDivisionByZeroDataExceptionSerializationFailureDeadlockDetectedLockNotAvailableQueryCanceledUndefinedTableUndefinedColumnUndefinedFunctionSqlSyntaxErrorInsufficientPrivilegeRaisedExceptionPG
spider.pg.MappingError
pub const MappingError = error{ ColumnMissing, UnexpectedNull, TypeMismatch, IntegerOverflow }
Typed-mapping failures: what a row can fail to give a struct. error.ColumnMissing: no column with the field's name. error.UnexpectedNull: a NULL where the field is neither optional nor has a default. error.TypeMismatch: the column's type cannot become the field's type (a timestamp into an integer, 1.5 into an integer, a date before year 1 into a string). error.IntegerOverflow: the number does not fit the field. See DbConfig.mapping. An unknown enum label is error.InvalidEnumValue, which is not part of this set.
spider.pg.isDbError
pub fn isDbError(err: anyerror) bool
True when err is one of the errors of DbError: a failure reported by Postgres itself, as opposed to a mapping error or a lost connection.
spider.pg.ErrorInfo
pub const ErrorInfo = struct
Details of the last Postgres error raised on this thread, as lastError returns them. The strings live in a per-thread buffer of 2048 bytes (longer texts are cut) that the next Postgres error on the thread overwrites: copy what must be kept.
Fields
code: []const u8: The SQLSTATE, e.g. "23505".message: []const u8detail: ?[]const u8: Can contain row values: not for end users.constraint: ?[]const u8table: ?[]const u8column: ?[]const u8
spider.pg.lastError
pub fn lastError() ?ErrorInfo
Details (SQLSTATE, message, detail, constraint, table, column) of the Postgres error that the pg.* call which just failed returned. Read it in the catch, before any other I/O: it is per-thread and the next failing call on this thread replaces it. detail can contain row values (e.g. an email in a unique violation) — don't show it to end users.
null while no Postgres error happened on this thread. It is only set by a DbError, and a call that succeeds does not clear it: after a mapping error or a connection error it still holds the previous Postgres error.
spider.pg.query
pub fn query(
comptime T: type,
arena: std.mem.Allocator,
sql: []const u8,
params: anytype,
) !QueryResult(T)
Runs one statement with $1, $2... parameters on a connection of the pool and gives back what T asks for:
- a struct: every row as a
[]Tallocated inarena. A field takes the column of the same name. Fields may be[]const u8,bool, integers (i8toi64,u8tou64),f32,f64, enums (the label of a Postgres enum or a text), optionals of those, and slices of those for an array column:tags: []const []const u8takes aTEXT[],ids: []const i64aBIGINT[](one dimension; a NULL element needs an optional element type). A[]const u8field accepts any column: bool, integers, floats, numeric, uuid, date, time ("07:05:09", fractions dropped), timestamp and jsonb come as text ("2026-09-26T18:40:33.183Z" for a timestamptz, in UTC); any other type comes as the bytes Postgres sent, which is the text itself for text, varchar, json and enums. i32ori64: the first column of the first row; 0 when there is no row or the value is NULL.void: nothing; for INSERT, UPDATE and DELETE.
Fails with:
- a
DbErrorwhen Postgres refuses the statement (lastError()has the details), e.g. error.UniqueViolation; - a
MappingErrorwhen a row does not fitT: error.ColumnMissing and error.UnexpectedNull (a field that is neither optional nor has a default; seeDbConfig.mapping), error.TypeMismatch, error.IntegerOverflow; error.InvalidEnumValue for an unknown enum label; - error.UseBeginForTransactions for BEGIN, COMMIT, ROLLBACK or SAVEPOINT: each call may run on a different connection. Use
begin().
const posts = try spider.pg.query(Post, c.arena, "SELECT id, title FROM posts WHERE author_id = $1", .{author_id});
const total = try spider.pg.query(i64, c.arena, "SELECT count(*) FROM posts", .{});
try spider.pg.query(void, c.arena, "DELETE FROM posts WHERE id = $1", .{id});
spider.pg.queryOne
pub fn queryOne(
comptime T: type,
arena: std.mem.Allocator,
sql: []const u8,
params: anytype,
) !?T
The first row of a statement, or null when it returns no row. T is a struct, mapped as in query, or i32/i64 for the first column (null when that value is NULL). Further rows are discarded. Fails as query.
const post = try spider.pg.queryOne(Post, c.arena, "SELECT id, title FROM posts WHERE id = $1", .{id}) orelse return error.NotFound;
spider.pg.queryExecute
pub fn queryExecute(
comptime T: type,
arena: std.mem.Allocator,
sql: []const u8,
) !QueryResult(T)
Execute raw SQL without parameters. Supports multiple statements separated by ';'.
With void as T the script (DDL, migrations) runs statement by statement on one connection and stops at the first that fails; the ones before it stay applied unless the script has its own BEGIN; ...; COMMIT, which is allowed here when balanced: a failure inside it rolls the transaction back. A ; inside a string, a comment or a $$ ... $$ body does not split. With another T it is query for one statement without parameters.
try spider.pg.queryExecute(void, arena, "CREATE TABLE IF NOT EXISTS posts (id serial PRIMARY KEY, title text NOT NULL)");
spider.pg.queryOneExecute
pub fn queryOneExecute(
comptime T: type,
arena: std.mem.Allocator,
sql: []const u8,
) !?T
The first row of one statement without parameters: queryOne(T, arena, sql, .{}).
spider.pg.begin
pub fn begin() !Transaction
Starts a transaction: takes a connection from the pool, sends BEGIN and keeps the connection for the Transaction until commit() or rollback(). Always defer tx.rollback(): it does nothing after a commit and returns the connection in every other case.
var tx = try spider.pg.begin();
defer tx.rollback();
try tx.query(void, c.arena, "INSERT INTO users (name) VALUES ($1)", .{name});
try tx.commit();
spider.pg.transaction
pub fn transaction(
comptime R: type,
context: anytype,
comptime body: fn (*Transaction, @TypeOf(context)) anyerror!R,
) !R
Runs body(&tx, context) inside one transaction on one pinned connection: commits when it returns normally, rolls back when it returns an error (the error is passed through). Returns what body returned; when the COMMIT itself fails, the transaction is rolled back and that error is returned.
const NewPost = struct { arena: std.mem.Allocator, title: []const u8 };
const input: NewPost = .{ .arena = c.arena, .title = title };
const id = try spider.pg.transaction(i64, input, struct {
fn run(tx: *spider.pg.Transaction, in: NewPost) !i64 {
const sql = "INSERT INTO posts (title) VALUES ($1) RETURNING id";
return tx.query(i64, in.arena, sql, .{in.title});
}
}.run);
spider.pg.Transaction
pub const Transaction = struct
Database transaction on ONE pinned connection. Use begin() (or transaction()) to create one.
var tx = try pg.begin();
defer tx.rollback();
try tx.query(void, arena, "INSERT INTO users (name) VALUES ($1)", .{"Alice"});
try tx.commit();
After Postgres refuses a statement it aborts the transaction: further statements (and commit) return error.TransactionAborted until rollback().
Fields
conn: *pg_lib.Conn: The connection the transaction is pinned to. Set bybegin().committed: bool = false: True oncecommit()succeeded.rolled_back: bool = false: True oncerollback()ran.
pg.Transaction.exec
pub fn exec(self: *Transaction, sql: []const u8, params: anytype) !void
Deprecated: use tx.query(void, arena, sql, params) instead.
pg.Transaction.query
pub fn query(
self: *Transaction,
comptime T: type,
arena: std.mem.Allocator,
sql: []const u8,
params: anytype,
) !QueryResult(T)
The module's query, on the transaction's connection. Also fails with error.TransactionAborted after a statement of this transaction failed, and with error.TransactionAlreadyFinished after commit() or rollback().
pg.Transaction.queryOne
pub fn queryOne(
self: *Transaction,
comptime T: type,
arena: std.mem.Allocator,
sql: []const u8,
params: anytype,
) !?T
The module's queryOne, on the transaction's connection. Fails as Transaction.query.
pg.Transaction.commit
pub fn commit(self: *Transaction) !void
Sends COMMIT and returns the connection to the pool. Fails with error.TransactionAborted when a statement failed before, and with error.TransactionAlreadyFinished on a second call.
On failure the transaction stays open; the usual defer tx.rollback() then cleans it up and returns the connection.
pg.Transaction.rollback
pub fn rollback(self: *Transaction) void
Undoes the transaction and returns the connection to the pool. Does nothing after commit() or a first rollback().
Safe to call at any point (also from defer), including after a failed statement: it uses the driver's rollback, which works on an aborted transaction, so the connection goes back to the pool clean instead of being torn down and reopened.
spider.pg.exec
pub fn exec(sql: []const u8, params: anytype) !void
Deprecated: use query(void, arena, sql, params) instead.
spider.pg.execRaw
pub fn execRaw(sql: []const u8) !void
Deprecated: use queryExecute(void, arena, sql) instead.
spider.pg.queryWith
pub fn queryWith(sql: []const u8, params: anytype) !Result
Deprecated: use query(T, arena, sql, params) instead.
spider.pg.queryRaw
pub fn queryRaw(sql: []const u8, params: anytype) !Result
Deprecated: use query(T, arena, sql, params) instead.
spider.pg.queryRow
pub fn queryRow(sql: []const u8, params: anytype) !Result
Deprecated: use query(T, arena, sql, params) instead.
spider.pg.queryAs
pub fn queryAs(
comptime T: type,
allocator: std.mem.Allocator,
sql: []const u8,
params: anytype,
) !MappedRows(T)
Deprecated: use query(T, arena, sql, params) instead.
spider.pg.queryOneAs
pub fn queryOneAs(
comptime T: type,
allocator: std.mem.Allocator,
sql: []const u8,
params: anytype,
) !?MappedRows(T)
Deprecated: use queryOne(T, arena, sql, params) instead.
spider.pg.Result
pub const Result = struct
Deprecated: use query(T, arena, sql, params), which returns typed rows. The untyped result of queryWith, queryRaw and queryRow: every value as text, with NULL and the empty string both read as "".
pg.Result.deinit
pub fn deinit(self: *Result) void
Deprecated: use query (see Result). Frees the result.
pg.Result.rows
pub fn rows(self: *const Result) usize
Deprecated: use query (see Result). Number of rows.
pg.Result.columns
pub fn columns(self: *const Result) usize
Deprecated: use query (see Result). Number of columns.
pg.Result.columnName
pub fn columnName(self: *const Result, col: usize) []const u8
Deprecated: use query (see Result). Name of column col; "" when out of range.
pg.Result.columnTypeOid
pub fn columnTypeOid(_: *const Result, _: usize) i32
Deprecated: use query (see Result). Always 0: the type is not kept.
pg.Result.affectedRows
pub fn affectedRows(self: *const Result) usize
Deprecated: use query (see Result). The number of rows returned, the same as rows(); not the count of rows an UPDATE or DELETE changed.
pg.Result.getValue
pub fn getValue(self: *const Result, row: usize, col: usize) []const u8
Deprecated: use query (see Result). The value at row, col as text; "" for NULL or out of range.
pg.Result.get
pub fn get(self: *const Result, row: usize, comptime name: []const u8) []const u8
Deprecated: use query (see Result). The value of column name in row; "" for NULL or an unknown column.
pg.Result.isNull
pub fn isNull(self: *const Result, row: usize, col: usize) bool
Deprecated: use query (see Result). True for NULL and for an empty string.
pg.Result.mapAll
pub fn mapAll(self: *Result, comptime T: type, alloc: std.mem.Allocator) ![]T
Deprecated: use query (see Result). Every row as a T, allocated with alloc. A number that does not parse becomes 0.
pg.Result.mapOne
pub fn mapOne(self: *Result, comptime T: type, alloc: std.mem.Allocator) !?T
Deprecated: use query (see Result). The first row as a T, or null without rows.