An Effect SQL client for PostgreSQL with a native wire-protocol driver.
npm install effect@rc @effect/sql-pg@rcUse startupParameters to establish PostgreSQL session defaults when each physical
connection opens, including replacement connections in a pool:
import { PgClient } from "@effect/sql-pg"
import { Redacted } from "effect"
const PostgresLive = PgClient.layer({
url: Redacted.make("postgres://user:password@localhost/app"),
startupParameters: {
statement_timeout: "5s",
search_path: "app, public"
},
startupOptions: "-c lock_timeout=1000"
})These settings are sent in the startup packet. PostgreSQL's RESET ALL restores
the startup defaults after session-level changes. Both PgConnection.Config and
PgClientConfig accept these fields, and pools inherit them.
Parameter names are normalized to lowercase. Empty names and NUL bytes in names
or values are rejected before connecting. The names user, database,
replication, and options are reserved; use username, database, and startupOptions
on the config for identity and opaque options. Replication mode is not supported
through startupParameters. The driver accepts only UTF8 or UTF-8
(case-insensitive) for client_encoding and sends the canonical value UTF8.
PostgreSQL validates other settings and reports invalid names or values at connect
time.
The startupOptions field is an opaque PostgreSQL options string. It can also come
from the URL query, for example ?options=-c%20statement_timeout%3D5000. Explicit
config startupOptions overrides URL options, including when the explicit value
is empty. The URL query key and startup packet field are both named options.
Named parameters and opaque options can be sent together, but callers must not
set the same GUC in both. The driver does not parse -c flags, detect conflicts,
or read PGOPTIONS.
The startup application_name is selected in this order:
- Explicit
applicationName startupParameters.application_name- URL
application_name "@effect/sql-pg"
The driver does not extract application_name from the opaque options string.
Columns whose type OID has no codec are decoded as UTF-8 text, so user-defined enums read as plain strings:
import { PgClient } from "@effect/sql-pg"
import { Effect } from "effect"
// CREATE TYPE capability AS ENUM ('use_key', 'manage')
const program = Effect.gen(function*() {
const sql = yield* PgClient.PgClient
const rows = yield* sql`SELECT 'use_key'::capability AS capability`
// rows[0].capability === "use_key"
})Strings bind to enum columns without any setup; the server infers the type.
The fallback applies to every OID without a codec, not only enums, and it has two failure modes:
- Invalid UTF-8 fails with
PgTypes.CodecError, as it does fortext. A codec failure while reading a row is fatal to the connection, not just to the query: the socket is destroyed, every other query pipelined on that connection fails, an open transaction is lost,LISTENchannels on it are torn down, and the pool replaces the connection. Columns of a binary user-defined type such as a composite, a PostGIS geometry, or a pgvectorvectorhold bytes that are usually not valid UTF-8, so register a codec before querying them. - Bytes that happen to be valid UTF-8 decode as garbled text without any
error. Arrays of user-defined types, including enum arrays, land here: the
array header is all low bytes, so a
capability[]column reads as a string of control characters followed by the labels.
Register a codec for a scalar user-defined type with
PgTypes.register(oid, codec); registered codecs take precedence over the
fallback. PgTypes.register cannot attach an array type. For arrays, create a
registry, register the element codec with its arrayOid, and pass the registry
to the client as types. The registry builds the array codec from the element
codec:
import { PgClient, PgTypes } from "@effect/sql-pg"
import { Redacted, Result } from "effect"
// SELECT oid, typarray FROM pg_type WHERE typname = 'capability'
declare const capabilityOid: number
declare const capabilityArrayOid: number
const decoder = new TextDecoder("utf-8", { fatal: true })
const utf8: PgTypes.Codec<string> = {
encode: (value) => Result.succeed(new TextEncoder().encode(value)),
decode: (bytes) => {
try {
return Result.succeed(decoder.decode(bytes))
} catch {
return Result.fail(new PgTypes.CodecError({ message: "Invalid UTF-8 in capability" }))
}
}
}
const types = PgTypes.makeRegistry()
types.register(capabilityOid, utf8, { arrayOid: capabilityArrayOid })
const PostgresLive = PgClient.layer({
url: Redacted.make("postgres://user:password@localhost/app"),
types
})OIDs of user-defined types are assigned per database, so look them up in
pg_type at startup rather than hard-coding them.