postgres

js/database/postgres.ts

fino:database/postgres — PostgreSQL protocol client.

This module is the Postgres-specific database engine used by fino:database. It speaks the PostgreSQL frontend/backend protocol (v3) directly rather than binding libpq, so every exchange runs on Fino's async socket and TLS primitives and never blocks the event loop.

Parameterized statements use the extended query protocol (Parse/Bind/Execute), while exec(), LISTEN/NOTIFY, and COPY use the simple protocol. TLS is negotiated with libpq-compatible sslmode semantics, and cleartext, MD5, and SCRAM-SHA-256 authentication are all supported. Each connection serializes its commands on an internal queue, so concurrent calls on one PostgresDatabase are safe but execute one at a time; use PostgresPool when you need real query concurrency.

Result values arrive in the text protocol and are decoded by type OID: boolean, int2/int4, and float4/float8 become JS primitives, int8 becomes a BigInt, json/jsonb are parsed, bytea values are returned as Uint8Array, and everything else is returned as a string.

import { PostgresDatabase } from 'fino:database/postgres';

await using db = await PostgresDatabase.open('postgres://user:pass@localhost/app');
const row = await db.prepare('SELECT $1::int AS value').get(1);
console.log(row?.value);

PostgreSQL Frontend/Backend Protocol: https://www.postgresql.org/docs/current/protocol.html

Types

type PostgresTlsMode = 'disable' | 'prefer' | 'require' | 'verify-ca' | 'verify-full'

TLS negotiation policy for a Postgres connection, mirroring libpq's sslmode values.

  • 'disable' — never request TLS.
  • 'prefer' (the default) — request TLS but fall back to plaintext when the server refuses.
  • 'require' — require TLS without verifying the server certificate.
  • 'verify-ca' / 'verify-full' — require TLS and verify the server certificate. This client treats the two verify modes identically.

Interfaces

interface PostgresOptions {

Connection settings for PostgresDatabase.open and PostgresPool.

All fields are optional. When open() is given a connection URL as well, values present in the URL win over these options. Defaults: user postgres, database named after the user, host 127.0.0.1, port 5432, TLS mode 'prefer'.

import { PostgresDatabase } from 'fino:database/postgres';

const db = await PostgresDatabase.open({
  host: 'db.internal',
  user: 'app',
  password: 'secret',
  database: 'app',
  tls: 'verify-full',
  applicationName: 'billing-worker'
});

Properties

user?: string

Role to authenticate as; defaults to postgres.

password?: string

Password for cleartext, MD5, or SCRAM authentication. Omit for servers that trust the connection.

database?: string

Database to connect to; defaults to the user name.

host?: string

Hostname, IP address, or Unix-socket directory (any path starting with /); defaults to 127.0.0.1. Hostnames are resolved via fino:net/dns.

port?: number

TCP port, which for Unix-socket hosts selects the .s.PGSQL.<port> socket file; defaults to 5432.

tls?: PostgresTlsMode

TLS negotiation policy; defaults to 'prefer'.

applicationName?: string

Value reported to the server as application_name, visible in pg_stat_activity and server logs.

interface PostgresNotification {

A NOTIFY message received from the server.

Delivered through PostgresDatabase.notifications and the iterators returned by PostgresDatabase.listen().

import { PostgresDatabase } from 'fino:database/postgres';

await using db = await PostgresDatabase.open('postgres://ada@localhost/app');
const sub = await db.listen('jobs');
for await (const note of sub) {
  console.log(`${note.channel} from pid ${note.processId}: ${note.payload}`);
}

Properties

processId: number

Backend process ID of the connection that issued the NOTIFY.

channel: string

Channel the notification was sent on.

payload: string

Payload string; empty when the notifier omitted one.

interface PostgresQueryResult extends QueryResult {

Result of a Postgres statement execution.

Extends the generic QueryResult from fino:database: changes is parsed from the trailing row count of the command tag (0 when the tag carries none), and lastInsertRowid is never populated — use a RETURNING clause to read generated keys.

import { PostgresDatabase } from 'fino:database/postgres';

await using db = await PostgresDatabase.open('postgres://ada@localhost/app');
const result = await db.prepare('INSERT INTO users (name) VALUES ($1)').run('Ada');
console.log(result.changes, result.command); // 1 'INSERT 0 1'

Properties

command?: string

Command tag reported by the server, such as 'INSERT 0 1' or 'SELECT 3'; absent when the statement completed without one.

Classes

class PostgresStatement {

A prepared statement bound to a PostgresDatabase connection.

Created by PostgresDatabase.prepare(). Each execution round-trips the extended query protocol (Parse/Bind/Execute/Sync) using the unnamed server-side statement, so the SQL is re-parsed by the server on every call. Positional parameters use Postgres $1, $2, ... placeholders and are sent in text format: Date values as ISO 8601 strings, Uint8Array values as raw bytes, null/undefined as SQL NULL, and everything else via String().

import { PostgresDatabase } from 'fino:database/postgres';

await using db = await PostgresDatabase.open('postgres://ada@localhost/app');
const insert = db.prepare('INSERT INTO users (name) VALUES ($1)');
await insert.run('Ada');

const byName = db.prepare('SELECT * FROM users WHERE name = $1');
const user = await byName.get('Ada');

Constructors

constructor(db: PostgresDatabase, sql: string, name = '')

Binds SQL text to a connection under an optional server-side statement name. Application code should use PostgresDatabase.prepare() rather than constructing statements directly.

Methods

run(...params: DbValue[]): Promise<PostgresQueryResult>

Executes the statement and resolves with the affected-row count and command tag, discarding any returned rows.

get(...params: DbValue[]): Promise<DbRow | undefined>

Executes the statement and resolves with the first returned row, or undefined when the query produces none.

all(...params: DbValue[]): Promise<DbRow[]>

Executes the statement and resolves with every returned row.

async *iterate(...params: DbValue[]): AsyncGenerator<DbRow>

Executes the statement and yields each returned row.

Rows are fetched eagerly in a single exchange and then yielded from memory — this is an iteration convenience, not a streaming cursor.

finalize(): void

Marks the statement finalized.

The unnamed server-side statement holds no per-statement resources, so this only flips the finalized flag for API parity with other fino:database drivers; it does not prevent further execution.

Getters

get finalized(): boolean

Whether finalize() has been called on this statement.

class PostgresDatabase {

A single PostgreSQL connection.

Open one with the static open() factory; the constructor alone produces an unconnected instance. open() performs TLS negotiation and authentication, after which every command is serialized on an internal queue — concurrent calls on the same connection are safe but execute one at a time. The class implements the DatabaseConnection contract from fino:database, so the same instance is what Database.open('postgres://...') returns.

Supports await using for scoped cleanup.

import { PostgresDatabase } from 'fino:database/postgres';

await using db = await PostgresDatabase.open('postgres://ada@localhost/app');
await db.exec('CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, body text)');
await db.prepare('INSERT INTO notes (body) VALUES ($1)').run('hello');
const notes = await db.prepare('SELECT * FROM notes').all();
console.log(db.parameters.get('server_version'), notes.length);

Readonly Properties

readonly driver

Driver discriminant used by the fino:database facade; always 'postgres'.

readonly parameters

Server-reported runtime parameters such as server_version and client_encoding, captured from ParameterStatus messages during startup and updated whenever the server reports a change mid-session.

Static Methods

static async open( target: string | URL | PostgresOptions, options: PostgresOptions = { } ): Promise<PostgresDatabase>

Connects, authenticates, and resolves with a ready connection.

The target may be a postgres:// URL (string or URL) or a PostgresOptions object. URLs take the user and password from the userinfo part and the database from the path, and understand the sslmode, host, and application_name query parameters; anything missing from the URL falls back to options, then to the defaults documented on PostgresOptions. A host beginning with / is treated as a Unix-socket directory and the client connects to <host>/.s.PGSQL.<port>. Hostnames are resolved via fino:net/dns before connecting.

Throws if the TCP or Unix-socket connection fails, if TLS is required but the server refuses it, or if authentication fails.

import { PostgresDatabase } from 'fino:database/postgres';

const fromUrl = await PostgresDatabase.open(
  'postgres://ada:secret@db.internal:5432/app?sslmode=verify-full'
);
const fromOptions = await PostgresDatabase.open({ host: '/var/run/postgresql', user: 'ada' });

Getters

get notifications(): ReadonlySignal<PostgresNotification | null>

Signal holding the most recent NOTIFY received on this connection, or null before the first one.

Notifications are decoded while the connection processes protocol traffic for other calls; an otherwise idle connection does not observe new notifications until its next query. Prefer listen() for a channel-scoped async iterator.

Methods

_serialize<T>(fn: () => Promise<T>): Promise<T>

Chains fn onto the connection's command queue so protocol exchanges never interleave on the socket. Backs every query method; part of the driver-internal surface shared with fino:database, not something application code needs to call.

prepare(query: string): PostgresStatement

Creates a prepared statement for later execution.

Preparation is lazy — nothing is sent to the server until the statement first runs. Throws if the connection has been closed.

const stmt = db.prepare('SELECT * FROM users WHERE id = $1');
const user = await stmt.get(7);
async exec(query: string): Promise<void>

Runs SQL through the simple query protocol, discarding any result rows.

Accepts multiple statements separated by semicolons, which makes it the right tool for DDL scripts and other parameterless commands. Throws with the server's error message if any statement fails.

await db.exec('CREATE TABLE a (id int); CREATE TABLE b (id int)');
async _simple(query: string): Promise<void>

Runs query on the simple query protocol and waits for the connection to become ready again, surfacing server errors. Backs exec(); part of the driver-internal surface shared with fino:database.

async _execute(query: string, params: DbValue[] = [], statement = ''): Promise<{ rows: DbRow[]; changes: number; command?: string; }>

Runs one extended-protocol exchange (Parse/Bind/Describe/Execute/Sync) and collects the decoded rows, affected-row count, and command tag. Backs PostgresStatement; part of the driver-internal surface shared with fino:database.

async transaction<T>(fn: () => Promise<T>): Promise<T>

Runs fn inside a transaction.

Issues BEGIN before calling fn, COMMIT when it resolves, and ROLLBACK when it rejects; rollback failures are swallowed so the original error propagates. Transactions do not nest — a nested call issues a second BEGIN, which Postgres warns about and ignores.

await db.transaction(async () => {
  await db.prepare('UPDATE accounts SET balance = balance - $1 WHERE id = $2').run(100, 1);
  await db.prepare('UPDATE accounts SET balance = balance + $1 WHERE id = $2').run(100, 2);
});
async listen(channel: string): Promise<AsyncIterable<PostgresNotification> & { close(): Promise<void>; signal: ReadonlySignal<PostgresNotification | null>; }>

Subscribes to a notification channel and returns an async iterable of matching notifications.

Issues LISTEN with the channel name safely quoted as an identifier; throws if the name is empty or contains a NUL byte. The returned object polls the connection's notification signal roughly every 10ms and yields the signal's current notification on each tick where it targets this channel. Call close() to stop iterating (no UNLISTEN is sent); the raw signal is exposed as signal for reactive composition.

Because notifications are only decoded while the connection processes protocol traffic, a connection that sits idle after listen() will not observe new notifications until it runs another query.

const sub = await db.listen('jobs');
for await (const note of sub) {
  console.log(note.channel, note.payload);
}
async notify(channel: string, payload = ''): Promise<void>

Sends a NOTIFY on channel with an optional payload.

The channel is quoted as an identifier and the payload as a string literal, so arbitrary values are passed safely. Throws if the channel name is empty or if either value contains a NUL byte.

await db.notify('jobs', JSON.stringify({ id: 42 }));
async copyFrom( query: string, source: Iterable<Uint8Array | string> | AsyncIterable<Uint8Array | string> ): Promise<PostgresQueryResult>

Streams data into the server with COPY ... FROM STDIN.

query must be a COPY statement that reads from STDIN. Chunks from source (strings are UTF-8 encoded) are forwarded as CopyData messages as they are produced, so large imports stream without buffering the whole payload. If source throws, the copy is aborted with CopyFail and the error is rethrown. Resolves with the server's command tag and the number of rows copied.

const result = await db.copyFrom('COPY users (name, age) FROM STDIN', [
  'Ada\t36\n',
  'Grace\t45\n'
]);
console.log(result.changes); // 2
async copyTo(query: string): Promise<Uint8Array[]>

Runs COPY ... TO STDOUT and resolves with the raw data chunks.

Chunks are collected in memory exactly as the server framed them; join or decode them as needed. Throws with the server's error message if the copy fails.

const chunks = await db.copyTo('COPY users TO STDOUT');
const text = chunks.map((chunk) => new TextDecoder().decode(chunk)).join('');
async cancel(): Promise<void>

Asks the server to cancel this connection's in-flight query.

Opens a separate plaintext connection to the same server and sends a CancelRequest carrying the backend key received at startup, as the protocol requires. Cancellation is best-effort: the server may already have finished the query, and when it does cancel, the interrupted call rejects with the server's cancellation error.

async close(): Promise<void>

Terminates the connection.

Sends the protocol Terminate message on a best-effort basis and closes the socket. Idempotent; after closing, prepare() throws.

class PostgresPool {

A lazy pool of PostgresDatabase connections.

Connections are opened on demand up to max (default 10) and kept for reuse after release(). When every connection is checked out, connect() queues until one is released. Idle connections are not health-checked or expired, so a connection that died while parked is handed out as-is and fails on first use.

import { PostgresPool } from 'fino:database/postgres';

const pool = new PostgresPool('postgres://ada@localhost/app', { max: 4 });
const users = await pool.run((db) => db.prepare('SELECT * FROM users').all());
await pool.close();

Constructors

constructor(target: string | URL | PostgresOptions, options: PostgresOptions & { max?: number; } = {})

Creates a pool for target, which accepts the same URL and option forms as PostgresDatabase.open. No connection is opened until first use; options.max caps concurrent connections (default 10).

Methods

async connect(): Promise<PostgresDatabase>

Checks a connection out of the pool.

Reuses an idle connection when one exists, opens a new one while under the max cap, and otherwise waits until a connection is released. Every successful connect() must be paired with release() — prefer run() for automatic pairing. Throws if the pool is closed.

release(db: PostgresDatabase): void

Returns a connection to the pool.

Hands the connection to the longest-waiting connect() caller if any, otherwise parks it idle. If the pool has been closed, the connection is closed instead.

async run<T>(fn: (db: PostgresDatabase) => Promise<T>): Promise<T>

Acquires a connection, invokes fn with it, and releases it when fn settles — the safe default for pooled queries.

const row = await pool.run((db) => db.prepare('SELECT now() AS ts').get());
async close(): Promise<void>

Closes the pool: closes every idle connection and rejects queued connect() calls. Checked-out connections are not interrupted; they are closed when released.