@vibeorm/adapter-pg 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/client-turns.d.ts +34 -0
- package/dist/errors.d.ts +8 -0
- package/dist/index.d.ts +21 -5
- package/dist/index.js +668 -25
- package/dist/index.js.map +10 -8
- package/dist/pool-config.d.ts +12 -0
- package/dist/statement-cache.d.ts +24 -5
- package/dist/timestamp-text.d.ts +34 -0
- package/dist/transaction-sql.d.ts +22 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -48,8 +48,9 @@ const db = VibeClient({ adapter: pgAdapter({ pool }) });
|
|
|
48
48
|
| `max` | `number` | `10` | Maximum pooled connections; ignored when `pool` is given |
|
|
49
49
|
| `statementTimeout` | `number` (ms) | — | Pool-wide `statement_timeout`; a per-transaction `timeout` overrides it |
|
|
50
50
|
| `connectionTimeout` | `number` (ms) | — | Wait for a pool checkout before failing (pg's `connectionTimeoutMillis`) |
|
|
51
|
+
| `idleTimeout` | `number` (ms) | pg-pool's `10000` | How long a pooled connection may sit idle before the pool closes it (pg's `idleTimeoutMillis`). Integer milliseconds from `0` through `2147483647`; other values throw `VIBE_CONFIG`. The next query after a quiet spell opens a new connection (TCP, startup, authentication — about 2.4 ms locally, more over TLS); a longer value, or `0` (never close), keeps connections open at the cost of held server connections. Ignored when `pool` is given |
|
|
51
52
|
| `cancellationConnectionString` | `string` | Owned `connectionString`; none for an injected `pool` | Separate control endpoint for running read cancellation; same PostgreSQL server/database, with permission to signal the target role |
|
|
52
|
-
| `preparedStatements` | `boolean` | `false` | Send ORM-path queries as named prepared statements. Measured −17…−25 % on reads and −6 % on a by-key update (see below). Off by default because of transaction-mode poolers — turn it on for direct connections
|
|
53
|
+
| `preparedStatements` | `boolean` | `false` | Send ORM-path queries as named prepared statements. Measured −17…−25 % on reads and −6 % on a by-key update (see below). Off by default because of transaction-mode poolers — turn it on for direct connections, session pooling, or PgBouncer 1.21+ with `max_prepared_statements > 0` |
|
|
53
54
|
| `stmtCacheMax` | `number` | `1000` | Budget of prepared-statement names per pool. Names never evict (an evicted name stays prepared on the backend), so this bounds server-side retention per connection; a text arriving after the budget is spent runs unnamed |
|
|
54
55
|
| `planCacheMode` | `"auto" \| "force_custom_plan" \| "force_generic_plan"` | `"auto"` | PostgreSQL `plan_cache_mode` for every connection of the pool the adapter builds; sent in the `options` startup parameter, merged with an `options` the URL already carries (a URL that sets `plan_cache_mode` itself wins). Refused with `VIBE_CONFIG` next to a caller-supplied `pool` |
|
|
55
56
|
|
|
@@ -68,7 +69,17 @@ The saving is the server's parse/analyze and the protocol Parse/Describe round o
|
|
|
68
69
|
|
|
69
70
|
A name is allocated on a text's **second** sighting, never its first: statement names never evict (an evicted name stays prepared on every backend that saw it), so first-sight allocation let a stream of one-off texts — offset pagination emits a distinct text per page — spend the whole `stmtCacheMax` budget for the pool's lifetime and leave the genuinely hot statements unnamed. A repeated statement therefore runs unprepared exactly once; a one-off statement costs the server nothing.
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
**Statement names are collision-resistant.** A name is `vibeorm_` followed by the SHA-256 of the exact statement text in lowercase base32 (60 bytes, inside PostgreSQL's 63-byte identifier limit). The same text gets the same name in every pool, process and adapter version, and two different texts get the same name only if SHA-256 collides. So when a client reaches a backend that another pool prepared, a name can only stand for its own text: the outcome is the correct rows or an error, never another statement's rows (pinned in `tests/prepared-backend-switch.live.test.ts`). Before this change names were counters (`vibeorm_0`, `vibeorm_1`, …), and two independent pools could mean different texts by one name.
|
|
73
|
+
|
|
74
|
+
Why it stays opt-in: named statements live on the **backend** connection, and node-postgres sends only Bind/Execute for a name it has parsed before. Supported setups:
|
|
75
|
+
|
|
76
|
+
- a direct connection;
|
|
77
|
+
- session pooling (each client keeps one backend for the session);
|
|
78
|
+
- PgBouncer 1.21 or later in transaction mode with `max_prepared_statements` above 0, which prepares each statement on the backend it links the client to.
|
|
79
|
+
|
|
80
|
+
Behind any other transaction-mode pooler (PgBouncer before 1.21 or with `max_prepared_statements = 0`, Supavisor, Neon's pooler) a client that remembers a name from an earlier transaction can be handed a different backend. The query then fails with SQLSTATE `26000` (the backend does not know the name) or `42P05` (the backend already holds the name on Parse). Both are `VIBE_ADAPTER` errors whose message ends with a hint about these setups, and the adapter discards that connection; no statement is retried. Turn `preparedStatements` off there.
|
|
81
|
+
|
|
82
|
+
One case stays silent with every naming scheme: a statement prepared before DDL **shadows** a table. If a migration creates a table with the same name in a schema that comes earlier in the unchanged `search_path`, the prepared statement keeps reading the table it was parsed against until the connection is recycled. (A changed `search_path` itself is safe: PostgreSQL re-parses the statement.) Recycle connections after such a migration, as for the result-shape changes below.
|
|
72
83
|
|
|
73
84
|
### Schema changes with prepared statements
|
|
74
85
|
|
|
@@ -93,7 +104,7 @@ The timings above are historical measurements, not a schema-recovery or current
|
|
|
93
104
|
|
|
94
105
|
This is a postgres-dialect adapter, so the full capability set is available: `RETURNING`, scalar array columns, native enums, `ILIKE`, `DISTINCT ON`, partial indexes, transactional DDL, `createManyAndReturn`, a real JSON column type, native `NULLS FIRST`/`NULLS LAST`, and the `"join"` relation strategy (`LEFT JOIN LATERAL` plus JSON aggregation). The generated capability table lives in [docs/dialects.md](https://github.com/vibeorm/vibeorm/blob/master/docs/dialects.md).
|
|
95
106
|
|
|
96
|
-
Transactions run on one checked-out client; nested `transaction()` calls become savepoints on that same client. Raw transaction control (`BEGIN` / `COMMIT` / `ROLLBACK` sent through the raw path, as `@vibeorm/migrate` does for transactional DDL) pins one client for the duration, so a raw transaction never splits across connections. Concurrent raw transactions on a single adapter are unsupported.
|
|
107
|
+
Transactions run on one checked-out client; nested `transaction()` calls become savepoints on that same client. Statements issued at the same time on one transaction (`Promise.all` on a transaction handle, a statement the callback did not await) wait in the adapter's own queue and reach node-postgres one at a time, in the order they were issued: the adapter never calls `client.query()` while that client still runs a query (node-postgres' own queue for such calls is deprecated and goes away in pg@9). The top-level transaction handle of a transaction without a `deadline` or a `timeout`, on a client without a local `query_timeout`, and the savepoint handles it opens (a nested `tx.$transaction([...])`), declare `orderedPipeline`: the array form of `$transaction` then sends a count-only write (`createMany`, `updateMany`, `deleteMany`, `upsertMany` as one statement) and the next operation's first statement in one round trip — `[deleteMany, createMany, updateMany]` costs 2 round trips instead of 4 — and still sends `COMMIT` alone. Its lanes keep lane order, never send a statement behind a lane that closed without issuing one, and never send anything behind a failed statement, also when that failure had no server answer (no SQLSTATE `25P02` ever reaches a caller); see `DatabaseAdapter.pipeline` and [Transactions](https://github.com/vibeorm/vibeorm/blob/master/docs/queries.md#transactions) for what a caller can observe. Inside a transaction, raw SQL without bound values that is provably ONE statement (no `;`, no `$`, no comment, not transaction control and not `COPY`) travels with a pending `BEGIN` or `SAVEPOINT` like a bound statement, so a framework's `SELECT set_config('lock_timeout', '5000', true)` no longer costs a round trip of its own; raw SQL that may hold several statements keeps its own round trip. Raw transaction control (`BEGIN` / `COMMIT` / `ROLLBACK` sent through the raw path, as `@vibeorm/migrate` does for transactional DDL) pins one client for the duration, so a raw transaction never splits across connections. Concurrent raw transactions on a single adapter are unsupported.
|
|
97
108
|
|
|
98
109
|
A raw script that produces multiple PostgreSQL results returns only the **final command's** rows and affected-row count. Earlier results are not concatenated or summed; a final DDL command returns empty rows and zero affected rows.
|
|
99
110
|
|
|
@@ -106,7 +117,7 @@ A raw script that produces multiple PostgreSQL results returns only the **final
|
|
|
106
117
|
|
|
107
118
|
A string bound to a `::jsonb` slot on the raw path is sent as text and parsed by the server, so pre-encoded JSON text stores the document here, and on `@vibeorm/adapter-bun` with its default unnamed statements. With adapter-bun's `preparedStatements: true` the same statement stores a jsonb *string*, because bun:sql then JSON-encodes whatever is bound to a jsonb slot. Spell the slot `::text::jsonb` when a raw statement must run on both drivers; that is what the ORM path renders for `Json` fields (pinned live in `tests/raw-jsonb.live.test.ts` on both adapters).
|
|
108
119
|
|
|
109
|
-
On both paths a `Date` is spelled as UTC ISO text (`2026-03-08T19:38:08.538Z`) rather than node-postgres' host-local form, and the result side attaches per-query parsers (never `pg.types` globally) that read `timestamp without time zone`, `date` and their arrays as **UTC** — pg-types reads them as host-local time. Together they give zone-less columns (Prisma's `@db.Timestamp(n)` / `@db.Date`) the UTC wall-clock contract shared with bun:sql, PGlite and Prisma, whatever the process `TZ` is; `timestamptz`
|
|
120
|
+
On both paths a `Date` is spelled as UTC ISO text (`2026-03-08T19:38:08.538Z`) rather than node-postgres' host-local form, and the result side attaches per-query parsers (never `pg.types` globally) that read `timestamp without time zone`, `date` and their arrays as **UTC** — pg-types reads them as host-local time. Together they give zone-less columns (Prisma's `@db.Timestamp(n)` / `@db.Date`) the UTC wall-clock contract shared with bun:sql, PGlite and Prisma, whatever the process `TZ` is; `timestamptz` keeps pg-types' instants. The same per-query parsers decode the plain wire form of `timestamptz`, `timestamp` and `date` (`YYYY-MM-DD HH:MM:SS[.ffffff][±HH[:MM]]`, years 1000–9999) by character codes, about 3× cheaper per cell, and hand every other text (infinity, BC, earlier years, seconds offsets) to the parser that decoded it before; an application that registered its own global `timestamptz` parser through `pg.types` keeps it. See `docs/dialect-notes.md` "DateTime without time zone on postgres".
|
|
110
121
|
|
|
111
122
|
## Caller cancellation of reads
|
|
112
123
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One query at a time on a transaction's checked-out client (pg@9).
|
|
3
|
+
*
|
|
4
|
+
* node-postgres 8 queues a `client.query()` made while the client still runs
|
|
5
|
+
* another one, and warns that this queue goes away in pg@9.0 ("Calling
|
|
6
|
+
* client.query() when the client is already executing a query is deprecated
|
|
7
|
+
* and will be removed in pg@9.0"). A transaction handle receives statements
|
|
8
|
+
* concurrently — `Promise.all` on a callback-transaction handle, an un-awaited
|
|
9
|
+
* call, a ROLLBACK behind a failed batch — so the adapter keeps that queue
|
|
10
|
+
* itself: every call waits until the previous one SETTLED (the `await` pattern
|
|
11
|
+
* node-postgres documents), and the calls reach the client in the order they
|
|
12
|
+
* were made — the order the driver's own queue used. A call made while nothing
|
|
13
|
+
* is outstanding reaches the client synchronously, as before.
|
|
14
|
+
*
|
|
15
|
+
* "Settled" is the driver's own promise (or a batch's `done`): on success it
|
|
16
|
+
* settles at the statement's ReadyForQuery, so the next call finds the client
|
|
17
|
+
* idle; after a server error it settles at the ErrorResponse, and the next call
|
|
18
|
+
* waits inside node-postgres for that ReadyForQuery alone — never behind a
|
|
19
|
+
* second queued query.
|
|
20
|
+
*/
|
|
21
|
+
/** The turn keeper of ONE checked-out client. */
|
|
22
|
+
export type ClientTurns = {
|
|
23
|
+
/**
|
|
24
|
+
* Run `send` (one `client.query()` call, or one batch) once every earlier
|
|
25
|
+
* call of this client settled. Resolves or rejects with `send`'s own
|
|
26
|
+
* outcome; a synchronous throw of `send` becomes its rejection.
|
|
27
|
+
*/
|
|
28
|
+
readonly run: <T>(send: () => Promise<T>) => Promise<T>;
|
|
29
|
+
/** Calls handed in that have not settled yet (the running one included). */
|
|
30
|
+
readonly outstanding: () => number;
|
|
31
|
+
};
|
|
32
|
+
/** Start the turn keeper for one checked-out client. */
|
|
33
|
+
export declare function createClientTurns(): ClientTurns;
|
|
34
|
+
//# sourceMappingURL=client-turns.d.ts.map
|
package/dist/errors.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ import { VibeError, type VibeErrorCode } from "@vibeorm/schema";
|
|
|
14
14
|
export declare const PG_SQLSTATE_ERROR_CODES: Readonly<Record<string, VibeErrorCode>>;
|
|
15
15
|
/** SQLSTATEs that additionally explain themselves through `meta.reason`. */
|
|
16
16
|
export declare const PG_SQLSTATE_REASONS: Readonly<Record<string, string>>;
|
|
17
|
+
/**
|
|
18
|
+
* SQLSTATEs whose message gets a remedy appended. The code stays what the
|
|
19
|
+
* SQLSTATE table says (`VIBE_ADAPTER` for both); only the human-readable text
|
|
20
|
+
* grows. Frozen and mirrored by tests. `0A000` ("cached plan must not change
|
|
21
|
+
* result type") is deliberately absent: it is the generic feature_not_supported
|
|
22
|
+
* class, so a prepared-statement hint would mislead on its other causes.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PG_SQLSTATE_HINTS: Readonly<Record<string, string>>;
|
|
17
25
|
/**
|
|
18
26
|
* Extract the SQLSTATE from a pg driver error. `pg` puts the server's SQLSTATE
|
|
19
27
|
* on `error.code`; client-side failures (`ECONNREFUSED`, `ETIMEDOUT`, …) use
|
package/dist/index.d.ts
CHANGED
|
@@ -96,6 +96,18 @@ export type PgAdapterSettings = {
|
|
|
96
96
|
readonly statementTimeout?: number;
|
|
97
97
|
/** Milliseconds to wait for a pool checkout before failing (pg's `connectionTimeoutMillis`). */
|
|
98
98
|
readonly connectionTimeout?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Milliseconds a pooled connection may sit idle before the pool closes it
|
|
101
|
+
* (pg-pool's `idleTimeoutMillis`). Unset keeps pg-pool's default of 10 000:
|
|
102
|
+
* the next query after a quiet spell then opens a new connection (TCP,
|
|
103
|
+
* startup, authentication — ≈ 2.4 ms locally, more over TLS). `0` keeps idle
|
|
104
|
+
* connections open until the server or network closes them; a longer value
|
|
105
|
+
* trades held server connections for fewer reconnects. Must be integer
|
|
106
|
+
* milliseconds from 0 through 2,147,483,647, else the factory throws
|
|
107
|
+
* `VIBE_CONFIG`. Ignored when
|
|
108
|
+
* `pool` is given: set `idleTimeoutMillis` on that pool.
|
|
109
|
+
*/
|
|
110
|
+
readonly idleTimeout?: number;
|
|
99
111
|
/** Separate direct/session-pinned PostgreSQL control endpoint for running reads.
|
|
100
112
|
* Required with an injected pool; never inferred from that pool or ambient env.
|
|
101
113
|
* The same role must be allowed to signal its target. Server support needs live acceptance.
|
|
@@ -112,11 +124,15 @@ export type PgAdapterSettings = {
|
|
|
112
124
|
* picking worse indexes for range/LIKE/skewed filters).
|
|
113
125
|
*
|
|
114
126
|
* Off by default because named statements live on the BACKEND connection:
|
|
115
|
-
* behind a transaction-mode pooler (PgBouncer
|
|
116
|
-
*
|
|
117
|
-
* earlier transaction is handed another backend
|
|
118
|
-
*
|
|
119
|
-
*
|
|
127
|
+
* behind a transaction-mode pooler (PgBouncer before 1.21 or with
|
|
128
|
+
* `max_prepared_statements = 0`, Supavisor, Neon's pooler) a client that
|
|
129
|
+
* remembers a name from an earlier transaction is handed another backend
|
|
130
|
+
* later. Names are collision-resistant (`vibeorm_` + the SHA-256 of the exact
|
|
131
|
+
* text, see `statementNameFor`), so a name can only ever stand for its own
|
|
132
|
+
* text: the remaining failure is a typed `VIBE_ADAPTER` error (SQLSTATE 26000
|
|
133
|
+
* or 42P05), never another statement's rows. Turn it on for direct
|
|
134
|
+
* connections, session pooling, or PgBouncer 1.21+ with
|
|
135
|
+
* `max_prepared_statements > 0`.
|
|
120
136
|
*
|
|
121
137
|
* Schema changes can invalidate a retained result descriptor (SQLSTATE
|
|
122
138
|
* 0A000). No statement or transaction callback is retried. Drain traffic and
|