@vibeorm/adapter-pg 4.2.0 → 5.0.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 +35 -5
- package/dist/batch.d.ts +8 -0
- package/dist/client-turns.d.ts +44 -0
- package/dist/connection-group.d.ts +62 -0
- package/dist/connection-string.d.ts +55 -0
- package/dist/errors.d.ts +8 -0
- package/dist/index.d.ts +42 -5
- package/dist/index.js +1326 -75
- package/dist/index.js.map +13 -9
- package/dist/pool-config.d.ts +25 -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
|
@@ -41,18 +41,36 @@ const db = VibeClient({ adapter: pgAdapter({ pool }) });
|
|
|
41
41
|
|
|
42
42
|
`pgAdapter(options: PgAdapterOptions)` — `connectionString` or `pool` is required. Without either, the factory throws `VIBE_CONFIG` (`meta.reason: "missing-address"`) before any pool or connection exists. The adapter never reads `DATABASE_URL`. Give a complete URL: node-postgres fills parts a URL leaves out (host, user, database, …) from the `PG*` environment variables.
|
|
43
43
|
|
|
44
|
+
**TLS with `sslrootcert=system`.** Managed providers such as PlanetScale hand out URLs ending in `?sslmode=verify-full&sslrootcert=system`. In libpq, `sslrootcert=system` means "trust the CA roots of the system"; node-postgres reads it as a file named `system` and fails with `ENOENT`. The adapter accepts it: the connection verifies the server certificate's chain and host name (`sslmode=verify-full`) against the runtime's default trust store — Node's bundled Mozilla roots plus `NODE_EXTRA_CA_CERTS`. To use the roots libpq itself would load, start Node with `--use-openssl-ca` (OpenSSL's default locations, honouring `SSL_CERT_FILE` / `SSL_CERT_DIR`), or with `--use-system-ca` for the operating system's store. As in libpq, an omitted `sslmode` means `verify-full`, and a weaker `sslmode` next to `sslrootcert=system` is refused at construction with `VIBE_CONFIG` (`meta.reason: "sslrootcert-system-weak-sslmode"`). `cancellationConnectionString` follows the same rule. A caller-supplied `pool` is configured by its owner: build it from the URL with `sslmode=verify-full` and without `sslrootcert=system`.
|
|
45
|
+
|
|
44
46
|
| Option | Type | Default | Meaning |
|
|
45
47
|
|---|---|---|---|
|
|
46
48
|
| `connectionString` | `string` | — (required without `pool`) | PostgreSQL connection URL |
|
|
47
|
-
| `pool` | `PgPoolLike` | — (required without `connectionString`) | An existing `pg.Pool` to use instead of creating one; it is never ended by the adapter, and it wins when both are given |
|
|
49
|
+
| `pool` | `PgPoolLike` | — (required without `connectionString`) | An existing `pg.Pool` to use instead of creating one; it is never ended by the adapter, and it wins when both are given. A pool-like object must return a distinct client object from `connect()` for each physical connection, never the same object for another one (`pg.Pool` does): native RLS `verify: "per-connection"` and `mode: "operator"` key their verdicts on it |
|
|
48
50
|
| `max` | `number` | `10` | Maximum pooled connections; ignored when `pool` is given |
|
|
51
|
+
| `min` | `number` | pg-pool's `0` | Connections the pool keeps open however long they sit idle (pg-pool's `min`). `db.$connect()` opens this many at once, so the first burst finds them warm (see [A database far away](#a-database-far-away)). An integer from `0` through `max`; other values throw `VIBE_CONFIG`. Refused with `VIBE_CONFIG` next to a caller-supplied `pool`: set `min` on that pool instead |
|
|
49
52
|
| `statementTimeout` | `number` (ms) | — | Pool-wide `statement_timeout`; a per-transaction `timeout` overrides it |
|
|
50
53
|
| `connectionTimeout` | `number` (ms) | — | Wait for a pool checkout before failing (pg's `connectionTimeoutMillis`) |
|
|
54
|
+
| `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
55
|
| `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
|
|
56
|
+
| `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`. Works with an injected `pool` too; the name budget (`stmtCacheMax`) then belongs to that pool and is shared by every adapter on it |
|
|
53
57
|
| `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
58
|
| `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
59
|
|
|
60
|
+
### A database far away
|
|
61
|
+
|
|
62
|
+
A new connection costs TCP, the startup message and authentication: about **three round trips** before the first query, more with TLS. On a local database that is noise (≈ 2.4 ms); across a network it is not. The framework team's report measured three parallel `findUnique` calls at 35 ms per round trip: **150 ms on a cold pool, 37 ms on a warm one** — the cold pool opened three connections first.
|
|
63
|
+
|
|
64
|
+
- Keep connections open: set `idleTimeout` long, or `0` (never close an idle connection). pg-pool's default closes a connection after 10 s idle, so the first query after a quiet spell pays the connect again.
|
|
65
|
+
- Keep a floor: `min: 3` keeps three connections open however long they idle.
|
|
66
|
+
- Warm up at start: `await db.$connect()` opens `min` connections at once (at least one) and returns them to the pool, so the first burst does not pay for them.
|
|
67
|
+
- With your own `pool`, set `min` and `idleTimeoutMillis` on that `pg.Pool`; the adapter's `min` is refused there, and `$connect()` opens one connection of it.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const db = VibeClient({ adapter: pgAdapter({ connectionString, max: 10, min: 3, idleTimeout: 0 }) });
|
|
71
|
+
await db.$connect(); // three connections open before the first request
|
|
72
|
+
```
|
|
73
|
+
|
|
56
74
|
### Prepared statements: the measured trade
|
|
57
75
|
|
|
58
76
|
Interleaved rounds on this repository's benchmark database (postgres 16, `apps/benchmark/scratch/probe-join-agg.ts`, p50 of 7 rounds):
|
|
@@ -68,7 +86,17 @@ The saving is the server's parse/analyze and the protocol Parse/Describe round o
|
|
|
68
86
|
|
|
69
87
|
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
88
|
|
|
71
|
-
|
|
89
|
+
**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.
|
|
90
|
+
|
|
91
|
+
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:
|
|
92
|
+
|
|
93
|
+
- a direct connection;
|
|
94
|
+
- session pooling (each client keeps one backend for the session);
|
|
95
|
+
- 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.
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
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
100
|
|
|
73
101
|
### Schema changes with prepared statements
|
|
74
102
|
|
|
@@ -93,7 +121,9 @@ The timings above are historical measurements, not a schema-recovery or current
|
|
|
93
121
|
|
|
94
122
|
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
123
|
|
|
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.
|
|
124
|
+
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). When the statement in flight returns, every statement that waited for it leaves as ONE batch (one Sync), in the order issued: a callback whose whole body is `Promise.all` of three creates costs 3 round trips instead of 4. Nothing waits for more statements to arrive — a statement issued while nothing runs goes at once, and a single waiting statement goes alone, as before. Not under a `deadline`, on a client with a local `query_timeout`, behind a read with `cancellation: "running"`, or for raw SQL that may hold several statements (it keeps its own round trip, and what was issued after it waits for it). A statement that fails on the server rejects with its own error; the statements the server skipped behind it in the same batch, and two or more statements still waiting when it failed, get the aborted-transaction refusal and never run (a single waiting statement is still sent, as before). The top-level transaction handle of a transaction without a `deadline`, on a client without a local `query_timeout`, and the savepoint handles it opens (a nested `tx.$transaction([...])`), declare `orderedPipeline` — also under a `timeout`, which is only the server's `SET LOCAL statement_timeout` and is applied to every statement of a batch on its own: 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, and `COMMIT` travels with the last statement when the last operation is such a write — `[deleteMany, createMany, updateMany]` costs 1 round trip instead of 4, with or without a `timeout`. Otherwise `COMMIT` is sent alone after the last operation settled; the callback form can end with one statement of your own and `COMMIT` in one round trip through the `commitWith` option (see [COMMIT travels with the last statement](https://github.com/vibeorm/vibeorm/blob/master/docs/queries.md#transactions)). 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.
|
|
125
|
+
|
|
126
|
+
Outside transactions the adapter declares `connectionGroup` (when the driver can batch and no local `query_timeout` is configured — the same conditions as `oneRoundTripTransactions`): the `"query"` strategy's sibling relation loads of one read leave together on ONE pooled connection as one batch (one Sync), so `findUnique` with three sibling includes costs 2 round trips instead of 4. The statements of one such batch run in one implicit transaction of the server: under `READ COMMITTED` each still reads its own snapshot, a statement that fails makes PostgreSQL skip the rest of its batch (the read rejects with the same first failure as before), and a server-side `transaction_timeout` (PostgreSQL 17+) covers the whole batch. A checked-out client that cannot batch is handed back unused and the statements go one after another. See [Sibling relation loads outside transactions](https://github.com/vibeorm/vibeorm/blob/master/docs/queries.md#sibling-relation-loads-outside-transactions-adapter-pg).
|
|
97
127
|
|
|
98
128
|
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
129
|
|
|
@@ -106,7 +136,7 @@ A raw script that produces multiple PostgreSQL results returns only the **final
|
|
|
106
136
|
|
|
107
137
|
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
138
|
|
|
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`
|
|
139
|
+
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
140
|
|
|
111
141
|
## Caller cancellation of reads
|
|
112
142
|
|
package/dist/batch.d.ts
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
export type BatchStep = {
|
|
26
26
|
readonly text: string;
|
|
27
27
|
readonly values?: readonly unknown[];
|
|
28
|
+
/**
|
|
29
|
+
* Nobody reads this step's rows (statement-count pass EPIC 5: the final
|
|
30
|
+
* statement that rides with COMMIT): they are not decoded, so no type parser
|
|
31
|
+
* can fail on them; the step reports no rows and its command tag's count.
|
|
32
|
+
*/
|
|
33
|
+
readonly discardRows?: true;
|
|
28
34
|
};
|
|
29
35
|
/** A step's decoded result, shaped like the node-postgres result the adapter reads. */
|
|
30
36
|
export type BatchStepResult = {
|
|
@@ -62,6 +68,8 @@ type RowDescriptionField = {
|
|
|
62
68
|
readonly dataTypeID: number;
|
|
63
69
|
readonly format?: "text" | "binary";
|
|
64
70
|
};
|
|
71
|
+
/** Whether this `pg` build exports what a one-Sync batch needs (`utils.prepareValue`); per-client checks stay in {@link supportsBatch}. */
|
|
72
|
+
export declare function batchDriverAvailable(): boolean;
|
|
65
73
|
/**
|
|
66
74
|
* Whether `client` can run a one-Sync batch: a node-postgres client whose
|
|
67
75
|
* connection exposes the protocol writers, NOT in pipeline mode (pipeline mode
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
* The rejection of the call that settled LAST, or `undefined` when it
|
|
33
|
+
* fulfilled (or nothing settled yet). Read inside a turn, it is the outcome
|
|
34
|
+
* of the call right before it — already known there, before any of that
|
|
35
|
+
* call's own continuations ran (a burst of waiting statements reads it to
|
|
36
|
+
* refuse what the server would answer with 25P02).
|
|
37
|
+
*/
|
|
38
|
+
readonly lastFailure: () => {
|
|
39
|
+
readonly error: unknown;
|
|
40
|
+
} | undefined;
|
|
41
|
+
};
|
|
42
|
+
/** Start the turn keeper for one checked-out client. */
|
|
43
|
+
export declare function createClientTurns(): ClientTurns;
|
|
44
|
+
//# sourceMappingURL=client-turns.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root connection groups (round-trip follow-up, lane A2;
|
|
3
|
+
* `DatabaseAdapter.connectionGroup`).
|
|
4
|
+
*
|
|
5
|
+
* A read's sibling relation loads outside any transaction: the runtime opens
|
|
6
|
+
* one sub-lane per statement, each sub-lane issues ONE statement, and once the
|
|
7
|
+
* group is sealed and every sub-lane issued or closed, the statements leave
|
|
8
|
+
* TOGETHER, in sub-lane order, on ONE pooled connection — one one-Sync batch
|
|
9
|
+
* (`./batch.ts`), so one round trip instead of one per sibling.
|
|
10
|
+
*
|
|
11
|
+
* This module is the group's bookkeeping only: the adapter hands it how to
|
|
12
|
+
* prepare a statement and how to send the prepared steps (checkout, batch,
|
|
13
|
+
* release — or one statement after another where the checked-out client cannot
|
|
14
|
+
* batch). Failure semantics are those of sequential dispatch in sub-lane
|
|
15
|
+
* order: a failed step's caller gets its mapped error; the steps PostgreSQL
|
|
16
|
+
* skipped behind it reject with the aborted refusal naming that failure.
|
|
17
|
+
*/
|
|
18
|
+
import { VibeError } from "@vibeorm/schema";
|
|
19
|
+
import type { DatabaseAdapter, PipelineGroup } from "@vibeorm/runtime";
|
|
20
|
+
import type { BatchOutcome, BatchStep } from "./batch.ts";
|
|
21
|
+
/** The `execute` parameters a sub-lane receives (the root's `execute` contract). */
|
|
22
|
+
export type ConnectionGroupExecute = Parameters<DatabaseAdapter["execute"]>[0];
|
|
23
|
+
/** One statement of a group, ready to send: its batch step and the call it came from. */
|
|
24
|
+
export type ConnectionGroupEntry = {
|
|
25
|
+
/** The step for the one-Sync batch (values already serialized for the driver). */
|
|
26
|
+
readonly step: BatchStep;
|
|
27
|
+
/** The sub-lane's original `execute` call (for a send that cannot batch: `root.execute`). */
|
|
28
|
+
readonly call: ConnectionGroupExecute;
|
|
29
|
+
};
|
|
30
|
+
/** What the adapter provides to a group. */
|
|
31
|
+
export type ConnectionGroupDriver = {
|
|
32
|
+
/** The fields every sub-lane adapter carries (the root's declarations). */
|
|
33
|
+
readonly base: Pick<DatabaseAdapter, "dialect" | "provider" | "wire" | "budgets" | "roundTrips" | "formatArrayParam">;
|
|
34
|
+
/**
|
|
35
|
+
* Serialize one statement for the batch. A value the driver refuses throws
|
|
36
|
+
* (mapped): that sub-lane's call rejects with it and it holds nothing.
|
|
37
|
+
*/
|
|
38
|
+
readonly prepare: (call: ConnectionGroupExecute) => BatchStep;
|
|
39
|
+
/**
|
|
40
|
+
* Send the group's statements, in order, as one round trip — or one after
|
|
41
|
+
* another where the checked-out connection cannot batch, reporting the
|
|
42
|
+
* statements after the first failure as skipped. Rejects only when nothing
|
|
43
|
+
* was sent (a failed checkout, a closed adapter): every statement then
|
|
44
|
+
* rejects with that error, mapped.
|
|
45
|
+
*/
|
|
46
|
+
readonly send: (entries: readonly ConnectionGroupEntry[]) => Promise<BatchOutcome>;
|
|
47
|
+
/** Map a driver failure to the adapter's typed error. */
|
|
48
|
+
readonly mapError: (error: unknown) => VibeError;
|
|
49
|
+
};
|
|
50
|
+
/** A broken use of a connection group — a runtime defect, refused before anything is sent. */
|
|
51
|
+
export declare function connectionGroupMisuse(params: {
|
|
52
|
+
message: string;
|
|
53
|
+
}): VibeError;
|
|
54
|
+
/**
|
|
55
|
+
* Open one connection group ({@link PipelineGroup}). Synchronous and free:
|
|
56
|
+
* nothing is checked out until the group is sealed and every sub-lane issued
|
|
57
|
+
* its statement or closed; a group that ends without a statement sends nothing.
|
|
58
|
+
*/
|
|
59
|
+
export declare function openConnectionGroup(params: {
|
|
60
|
+
driver: ConnectionGroupDriver;
|
|
61
|
+
}): PipelineGroup;
|
|
62
|
+
//# sourceMappingURL=connection-group.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* libpq's `sslrootcert=system` for node-postgres.
|
|
3
|
+
*
|
|
4
|
+
* Managed PostgreSQL providers (PlanetScale, among others) hand out URLs such
|
|
5
|
+
* as `postgresql://…@host:5432/postgres?sslmode=verify-full&sslrootcert=system`.
|
|
6
|
+
* In libpq (PostgreSQL 16+) the value `system` means "load the trusted CA roots
|
|
7
|
+
* of the SSL implementation"; libpq then defaults `sslmode` to `verify-full`
|
|
8
|
+
* and refuses any weaker mode (libpq-connect, `sslrootcert`). node-postgres
|
|
9
|
+
* (pg 8.23 / pg-connection-string 2.14.1, the latest releases on 18.10.26, and
|
|
10
|
+
* its master branch) reads every `sslrootcert` as a FILE path —
|
|
11
|
+
* `fs.readFileSync("system")` — so the provider's URL fails before connecting
|
|
12
|
+
* with `ENOENT: no such file or directory, open 'system'`.
|
|
13
|
+
*
|
|
14
|
+
* {@link honourSystemRootCert} rewrites such a URL into node-postgres' own
|
|
15
|
+
* spelling of the same contract: `sslrootcert` removed, `sslmode=verify-full`.
|
|
16
|
+
* pg-connection-string then builds `ssl: {}` (plus any `sslcert`/`sslkey`),
|
|
17
|
+
* and `tls.connect` runs with its defaults: `rejectUnauthorized: true` (chain
|
|
18
|
+
* verified), the default `checkServerIdentity` against the host name
|
|
19
|
+
* node-postgres passes as `servername` (an IP address is matched against the
|
|
20
|
+
* certificate's IP entries, as in libpq), and — no `ca` given — the runtime's
|
|
21
|
+
* default trust store.
|
|
22
|
+
*
|
|
23
|
+
* "System roots" in that store: Node's default store is its bundled Mozilla
|
|
24
|
+
* root set (the program most operating-system stores derive from) plus
|
|
25
|
+
* `NODE_EXTRA_CA_CERTS`; started with `--use-openssl-ca` it loads OpenSSL's
|
|
26
|
+
* default locations, honouring `SSL_CERT_FILE` / `SSL_CERT_DIR` exactly as
|
|
27
|
+
* libpq-with-OpenSSL does, and with `--use-system-ca` the operating system's
|
|
28
|
+
* store. Bun's `node:tls` default is likewise its bundled root set plus
|
|
29
|
+
* `NODE_EXTRA_CA_CERTS`. The adapter passes no `ca`, so whichever of these the
|
|
30
|
+
* process chose applies — and connections reuse the runtime's parsed root
|
|
31
|
+
* store instead of re-parsing ~150 certificates per connection.
|
|
32
|
+
*/
|
|
33
|
+
/** The connection-string option a value came from, named in a refusal. */
|
|
34
|
+
export type ConnectionStringOption = "connectionString" | "cancellationConnectionString";
|
|
35
|
+
/**
|
|
36
|
+
* Rewrite a connection string carrying `sslrootcert=system` (libpq's "trust the
|
|
37
|
+
* system CA roots") into the form node-postgres understands: the parameter
|
|
38
|
+
* removed and `sslmode=verify-full` set, so the server certificate's chain and
|
|
39
|
+
* host name are both verified against the runtime's default trust store (see
|
|
40
|
+
* the module header). Any other string — no query, another `sslrootcert`, no
|
|
41
|
+
* `sslrootcert` — is returned byte-for-byte unchanged.
|
|
42
|
+
*
|
|
43
|
+
* Follows libpq's rule: with `system`, an omitted `sslmode` means
|
|
44
|
+
* `verify-full`, and every weaker mode is an error. node-postgres' own weaker
|
|
45
|
+
* spellings count too: `sslmode=no-verify`, and an `ssl` parameter other than
|
|
46
|
+
* `true` / `1`.
|
|
47
|
+
*
|
|
48
|
+
* @throws VibeError `VIBE_CONFIG` (`meta.reason: "sslrootcert-system-weak-sslmode"`)
|
|
49
|
+
* for a weaker mode. Neither the message nor `meta` carries the URL.
|
|
50
|
+
*/
|
|
51
|
+
export declare function honourSystemRootCert(params: {
|
|
52
|
+
connectionString: string;
|
|
53
|
+
option: ConnectionStringOption;
|
|
54
|
+
}): string;
|
|
55
|
+
//# sourceMappingURL=connection-string.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
|
@@ -78,6 +78,11 @@ export type PgAdapterSettings = {
|
|
|
78
78
|
* other environment variable) itself: pass the URL you mean. Give a complete
|
|
79
79
|
* URL — node-postgres fills parts the URL omits (host, user, database, …)
|
|
80
80
|
* from the `PG*` environment variables.
|
|
81
|
+
*
|
|
82
|
+
* libpq's `sslrootcert=system` (as in PlanetScale's URLs) is accepted:
|
|
83
|
+
* the adapter verifies the server certificate's chain and host name against
|
|
84
|
+
* the runtime's default trust store (`sslmode=verify-full`), and refuses a
|
|
85
|
+
* weaker `sslmode` next to it with `VIBE_CONFIG`, as libpq does.
|
|
81
86
|
*/
|
|
82
87
|
readonly connectionString?: string;
|
|
83
88
|
/**
|
|
@@ -85,10 +90,26 @@ export type PgAdapterSettings = {
|
|
|
85
90
|
* ends a pool it did not create: `disconnect()` only ends this adapter (and
|
|
86
91
|
* rolls back and releases its own raw session); the pool stays usable.
|
|
87
92
|
* Wins over `connectionString` when both are given.
|
|
93
|
+
*
|
|
94
|
+
* `connect()` must return a distinct client object for each physical
|
|
95
|
+
* connection and never hand the same object out for another one (node-postgres
|
|
96
|
+
* `pg.Pool` does: a `Client` never reconnects). The returned object is the
|
|
97
|
+
* transaction's `connectionIdentity`, which native-RLS per-connection
|
|
98
|
+
* verdicts and the operator login check are keyed on.
|
|
88
99
|
*/
|
|
89
100
|
readonly pool?: PgPoolLike;
|
|
90
101
|
/** Maximum pooled connections (default 10). Ignored when `pool` is given. */
|
|
91
102
|
readonly max?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Connections the pool keeps open however long they sit idle (pg-pool's
|
|
105
|
+
* `min`; default 0). `adapter.connect()` (`db.$connect()`) opens this many
|
|
106
|
+
* at once, so a burst of parallel queries finds them warm instead of paying
|
|
107
|
+
* TCP + startup + authentication per connection — about three round trips
|
|
108
|
+
* each, more with TLS. Must be an integer from 0 through `max`, else the
|
|
109
|
+
* factory throws `VIBE_CONFIG`. Refused next to `pool` (`VIBE_CONFIG`): the
|
|
110
|
+
* adapter cannot change a caller's pool — set `min` on that pool instead.
|
|
111
|
+
*/
|
|
112
|
+
readonly min?: number;
|
|
92
113
|
/**
|
|
93
114
|
* Pool-wide `statement_timeout` in milliseconds. Every connection inherits
|
|
94
115
|
* it; a per-transaction `timeout` overrides it for that transaction.
|
|
@@ -96,6 +117,18 @@ export type PgAdapterSettings = {
|
|
|
96
117
|
readonly statementTimeout?: number;
|
|
97
118
|
/** Milliseconds to wait for a pool checkout before failing (pg's `connectionTimeoutMillis`). */
|
|
98
119
|
readonly connectionTimeout?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Milliseconds a pooled connection may sit idle before the pool closes it
|
|
122
|
+
* (pg-pool's `idleTimeoutMillis`). Unset keeps pg-pool's default of 10 000:
|
|
123
|
+
* the next query after a quiet spell then opens a new connection (TCP,
|
|
124
|
+
* startup, authentication — ≈ 2.4 ms locally, more over TLS). `0` keeps idle
|
|
125
|
+
* connections open until the server or network closes them; a longer value
|
|
126
|
+
* trades held server connections for fewer reconnects. Must be integer
|
|
127
|
+
* milliseconds from 0 through 2,147,483,647, else the factory throws
|
|
128
|
+
* `VIBE_CONFIG`. Ignored when
|
|
129
|
+
* `pool` is given: set `idleTimeoutMillis` on that pool.
|
|
130
|
+
*/
|
|
131
|
+
readonly idleTimeout?: number;
|
|
99
132
|
/** Separate direct/session-pinned PostgreSQL control endpoint for running reads.
|
|
100
133
|
* Required with an injected pool; never inferred from that pool or ambient env.
|
|
101
134
|
* The same role must be allowed to signal its target. Server support needs live acceptance.
|
|
@@ -112,11 +145,15 @@ export type PgAdapterSettings = {
|
|
|
112
145
|
* picking worse indexes for range/LIKE/skewed filters).
|
|
113
146
|
*
|
|
114
147
|
* 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
|
-
*
|
|
148
|
+
* behind a transaction-mode pooler (PgBouncer before 1.21 or with
|
|
149
|
+
* `max_prepared_statements = 0`, Supavisor, Neon's pooler) a client that
|
|
150
|
+
* remembers a name from an earlier transaction is handed another backend
|
|
151
|
+
* later. Names are collision-resistant (`vibeorm_` + the SHA-256 of the exact
|
|
152
|
+
* text, see `statementNameFor`), so a name can only ever stand for its own
|
|
153
|
+
* text: the remaining failure is a typed `VIBE_ADAPTER` error (SQLSTATE 26000
|
|
154
|
+
* or 42P05), never another statement's rows. Turn it on for direct
|
|
155
|
+
* connections, session pooling, or PgBouncer 1.21+ with
|
|
156
|
+
* `max_prepared_statements > 0`.
|
|
120
157
|
*
|
|
121
158
|
* Schema changes can invalidate a retained result descriptor (SQLSTATE
|
|
122
159
|
* 0A000). No statement or transaction callback is retried. Drain traffic and
|