1# @effect/sql-pg
2
3An Effect SQL client for PostgreSQL with a native wire-protocol driver.
4
5## Installation
6
7```sh
8npm install effect @effect/sql-pg
9```
10
11## Links
12
13- [Website](https://effect.website): documentation, guides, and news.
14- [Reference](https://effect.website/docs/v4/api/sql-pg): API documentation for this package.
15- [Discord](https://discord.gg/effect-ts): ask questions, share what you're building, and talk to the core team.
16- [Community](https://effect.website/community-hub): meetups and events, or bring Effect to your own.
17- [Issues](https://github.com/Effect-TS/effect/issues): bug reports and feature requests.
18
19## Let's talk
20
21Whether your team is considering Effect, rolling it out, or already running it in production, we'd love to hear from you: what you're building, what works, and what you need from Effect next.
22
23- **Talk to the maintainers.** Introduce your team on [Discord](https://discord.gg/effect-ts) or email [contact@effectful.co](mailto:contact@effectful.co). We're happy to connect privately on Slack or Discord for feedback and help with adoption.
24- **Production support.** We're exploring how to better support teams running Effect in production. If your organization has specific support needs, let's discuss them.
25- **Adoption help.** Our [adoption partners](https://effect.website/adoption-partners) offer implementation, consulting, team extension, training, and commercial support.
26
27## Session defaults
28
29Use `startupParameters` to establish PostgreSQL session defaults when each physical
30connection opens, including replacement connections in a pool:
31
32```ts
33import { PgClient } from "@effect/sql-pg"
34import { Redacted } from "effect"
35
36const PostgresLive = PgClient.layer({
37 url: Redacted.make("postgres://user:password@localhost/app"),
38 startupParameters: {
39 statement_timeout: "5s",
40 search_path: "app, public"
41 },
42 startupOptions: "-c lock_timeout=1000"
43})
44```
45
46These settings are sent in the startup packet. PostgreSQL's `RESET ALL` restores
47the startup defaults after session-level changes. Both `PgConnection.Config` and
48`PgClientConfig` accept these fields, and pools inherit them.
49
50Parameter names are normalized to lowercase. Empty names and NUL bytes in names
51or values are rejected before connecting. The names `user`, `database`,
52`replication`, and `options` are reserved; use `username`, `database`, and `startupOptions`
53on the config for identity and opaque options. Replication mode is not supported
54through `startupParameters`. The driver accepts only `UTF8` or `UTF-8`
55(case-insensitive) for `client_encoding` and sends the canonical value `UTF8`.
56PostgreSQL validates other settings and reports invalid names or values at connect
57time.
58
59The `startupOptions` field is an opaque PostgreSQL options string. It can also come
60from the URL query, for example `?options=-c%20statement_timeout%3D5000`. Explicit
61config `startupOptions` overrides URL `options`, including when the explicit value
62is empty. The URL query key and startup packet field are both named `options`.
63Named parameters and opaque options can be sent together, but callers must not
64set the same GUC in both. The driver does not parse `-c` flags, detect conflicts,
65or read `PGOPTIONS`.
66
67The startup `application_name` is selected in this order:
68
691. Explicit `applicationName`
702. `startupParameters.application_name`
713. URL `application_name`
724. `"@effect/sql-pg"`
73
74The driver does not extract `application_name` from the opaque options string.
75
76## User-defined types
77
78Columns whose type OID has no codec are decoded as UTF-8 text, so user-defined
79enums read as plain strings:
80
81```ts
82import { PgClient } from "@effect/sql-pg"
83import { Effect } from "effect"
84
85// CREATE TYPE capability AS ENUM ('use_key', 'manage')
86const program = Effect.gen(function*() {
87 const sql = yield* PgClient.PgClient
88 const rows = yield* sql`SELECT 'use_key'::capability AS capability`
89 // rows[0].capability === "use_key"
90})
91```
92
93Strings bind to enum columns without any setup; the server infers the type.
94
95The fallback applies to every OID without a codec, not only enums, and it has
96two failure modes:
97
98- Invalid UTF-8 fails with `PgTypes.CodecError`, as it does for `text`. A codec
99 failure while reading a row is fatal to the connection, not just to the
100 query: the socket is destroyed, every other query pipelined on that
101 connection fails, an open transaction is lost, `LISTEN` channels on it are
102 torn down, and the pool replaces the connection. Columns of a binary
103 user-defined type such as a composite, a PostGIS geometry, or a pgvector
104 `vector` hold bytes that are usually not valid UTF-8, so register a codec
105 before querying them.
106- Bytes that happen to be valid UTF-8 decode as garbled text without any
107 error. Arrays of user-defined types, including enum arrays, land here: the
108 array header is all low bytes, so a `capability[]` column reads as a string
109 of control characters followed by the labels.
110
111Register a codec for a scalar user-defined type with
112`PgTypes.register(oid, codec)`; registered codecs take precedence over the
113fallback. `PgTypes.register` cannot attach an array type. For arrays, create a
114registry, register the element codec with its `arrayOid`, and pass the registry
115to the client as `types`. The registry builds the array codec from the element
116codec:
117
118```ts
119import { PgClient, PgTypes } from "@effect/sql-pg"
120import { Redacted, Result } from "effect"
121
122// SELECT oid, typarray FROM pg_type WHERE typname = 'capability'
123declare const capabilityOid: number
124declare const capabilityArrayOid: number
125
126const decoder = new TextDecoder("utf-8", { fatal: true })
127
128const utf8: PgTypes.Codec<string> = {
129 encode: (value) => Result.succeed(new TextEncoder().encode(value)),
130 decode: (bytes) => {
131 try {
132 return Result.succeed(decoder.decode(bytes))
133 } catch {
134 return Result.fail(new PgTypes.CodecError({ message: "Invalid UTF-8 in capability" }))
135 }
136 }
137}
138
139const types = PgTypes.makeRegistry()
140types.register(capabilityOid, utf8, { arrayOid: capabilityArrayOid })
141
142const PostgresLive = PgClient.layer({
143 url: Redacted.make("postgres://user:password@localhost/app"),
144 types
145})
146```
147
148OIDs of user-defined types are assigned per database, so look them up in
149`pg_type` at startup rather than hard-coding them.