@vibeorm/adapter-pg 2.4.0 → 3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeORM contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -16,31 +16,35 @@ bun add @vibeorm/adapter-pg pg
16
16
  import { VibeClient } from "./generated/vibeorm/index.js";
17
17
  import { pgAdapter } from "@vibeorm/adapter-pg";
18
18
 
19
- // connectionString defaults to process.env.DATABASE_URL
20
- const db = VibeClient({ adapter: pgAdapter({ connectionString: process.env.DATABASE_URL, max: 20 }) });
19
+ // The adapter reads no environment variable: pass the address you mean.
20
+ const connectionString = process.env.DATABASE_URL;
21
+ if (!connectionString) throw new Error("DATABASE_URL is not set");
22
+ const db = VibeClient({ adapter: pgAdapter({ connectionString, max: 20 }) });
21
23
 
22
24
  const users = await db.user.findMany({ where: { email: { contains: "@example.com" } }, take: 10 });
23
25
 
24
26
  await db.$disconnect();
25
27
  ```
26
28
 
27
- To reuse a pool you already own, pass it in. The adapter never ends a pool it did not create, so `disconnect()` is a no-op in that mode:
29
+ `disconnect()` ends the adapter for good: work that already started finishes, the pool it created is ended once, and every later call refuses with `VIBE_ADAPTER_CLOSED`. Create a new adapter to connect again.
30
+
31
+ To reuse a pool you already own, pass it in. The adapter never ends a pool it did not create: in that mode `disconnect()` only ends the adapter, and the pool stays usable by its owner and by other adapters:
28
32
 
29
33
  ```ts
30
34
  import pg from "pg";
31
35
 
32
- const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
36
+ const pool = new pg.Pool({ connectionString });
33
37
  const db = VibeClient({ adapter: pgAdapter({ pool }) });
34
38
  ```
35
39
 
36
40
  ## Options
37
41
 
38
- `pgAdapter(options?: PgAdapterOptions)`:
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.
39
43
 
40
44
  | Option | Type | Default | Meaning |
41
45
  |---|---|---|---|
42
- | `connectionString` | `string` | `process.env.DATABASE_URL` | PostgreSQL connection URL |
43
- | `pool` | `PgPoolLike` | — | An existing `pg.Pool` to use instead of creating one; it is never ended by the adapter |
46
+ | `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 |
44
48
  | `max` | `number` | `10` | Maximum pooled connections; ignored when `pool` is given |
45
49
  | `statementTimeout` | `number` (ms) | — | Pool-wide `statement_timeout`; a per-transaction `timeout` overrides it |
46
50
  | `connectionTimeout` | `number` (ms) | — | Wait for a pool checkout before failing (pg's `connectionTimeoutMillis`) |
@@ -71,7 +75,7 @@ Why it stays opt-in: named statements live on the **backend** connection. Behind
71
75
 
72
76
  The adapter surfaces the failure as `VIBE_ADAPTER` with `meta.sqlstate: "0A000"` and the driver error as `cause`. It does not retry the statement, replay a transaction callback, or issue `DEALLOCATE`. Inside a transaction, the error leaves PostgreSQL in its aborted state until rollback. SQLSTATE `0A000` also covers other unsupported features, so it is not a safe generic retry signal.
73
77
 
74
- For an incompatible schema deployment, stop admitting work on the old pool, let active transactions settle, apply the migration, then recycle the connections before resuming traffic. If the adapter owns its pool, await `disconnect()` after draining and reconnect it, or create a new adapter. If you supplied `pool`, its owner must await `pool.end()` and supply a new pool to a new adapter. Disconnecting or replacing only the adapter leaves an injected pool's retained statements intact. Never run `DEALLOCATE ALL` behind node-postgres' remembered statement-name registry.
78
+ For an incompatible schema deployment, stop admitting work on the old pool, let active transactions settle, apply the migration, then recycle the connections before resuming traffic. If the adapter owns its pool, await `disconnect()` after draining and create a new adapter (a disconnected adapter does not reconnect). If you supplied `pool`, its owner must await `pool.end()` and supply a new pool to a new adapter. Disconnecting or replacing only the adapter leaves an injected pool's retained statements intact. Never run `DEALLOCATE ALL` behind node-postgres' remembered statement-name registry.
75
79
 
76
80
  The timings above are historical measurements, not a schema-recovery or current performance guarantee. Prepared statements remain opt-in; applications without a coordinated connection lifecycle should keep the default `preparedStatements: false`.
77
81
 
@@ -99,7 +103,7 @@ A raw script that produces multiple PostgreSQL results returns only the **final
99
103
  - `serializeParams` (ORM path): plain objects **and** plain arrays are JSON-stringified, since at that point they can only be `json`/`jsonb` values. Scalar-list values are tagged by `formatArrayParam` with `PgArrayParam` and unwrapped, reaching the driver as the untouched array.
100
104
  - `serializeParamsRaw` (raw path, `$queryRaw*` / `$executeRaw*` / DDL): plain JS arrays pass straight through, so `$queryRawUnsafe('… WHERE "id" = ANY($1)', [1, 2, 3])` matches rows. Binding an array *into* a `jsonb` column from a raw query means calling `JSON.stringify` yourself.
101
105
 
102
- 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 — on `@vibeorm/adapter-bun` the same statement stores a jsonb *string*, because bun:sql 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).
106
+ 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).
103
107
 
104
108
  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` is unaffected. See `docs/dialect-notes.md` "DateTime without time zone on postgres".
105
109
 
@@ -0,0 +1,152 @@
1
+ /**
2
+ * One-Sync batch for node-postgres (round-trip EPIC 6, pipelining).
3
+ *
4
+ * Several statements go to the server as Parse / Bind / Describe / Execute
5
+ * each, followed by ONE Sync — one wait instead of one per statement. It is a
6
+ * `Submittable` (the extension point `client.query(object)` documents and
7
+ * pg-cursor uses) running on an ordinary, NON-pipeline client: the batch is one
8
+ * query object, so the client's own queue, ReadyForQuery handling and status
9
+ * bookkeeping stay node-postgres'.
10
+ *
11
+ * PostgreSQL's extended protocol skips every message after an error until the
12
+ * Sync (as libpq pipeline mode does), so a failing step leaves every later step
13
+ * UNRUN — a failed `BEGIN` can never let a later write run in autocommit, and
14
+ * no later step answers 25P02 (spike-report §1.3, "pg one-Sync batch").
15
+ *
16
+ * Only API that node-postgres exposes is used: `connection.parse / bind /
17
+ * describe / execute / sync` (declared on `Connection` in @types/pg), the
18
+ * message handlers of the Submittable contract, and the driver's own
19
+ * `utils.prepareValue` (a property of the `pg` module object, feature-detected)
20
+ * so parameters serialize exactly as `client.query` serializes them. Rows are
21
+ * decoded HERE, with the adapter's query-local type parsers — never through
22
+ * node-postgres' internal `lib/result.js`.
23
+ */
24
+ /** One statement of a batch. Values are the adapter's already-serialized params. */
25
+ export type BatchStep = {
26
+ readonly text: string;
27
+ readonly values?: readonly unknown[];
28
+ };
29
+ /** A step's decoded result, shaped like the node-postgres result the adapter reads. */
30
+ export type BatchStepResult = {
31
+ readonly rows: Record<string, unknown>[];
32
+ readonly rowCount: number | null;
33
+ readonly command?: string;
34
+ };
35
+ /** Per-step outcome after the batch's ReadyForQuery. */
36
+ export type BatchStepOutcome = {
37
+ readonly status: "ok";
38
+ readonly result: BatchStepResult;
39
+ } | {
40
+ readonly status: "failed";
41
+ readonly error: unknown;
42
+ } | {
43
+ readonly status: "skipped";
44
+ };
45
+ /** What a batch reports once it settled. */
46
+ export type BatchOutcome = {
47
+ /** One entry per step, in order. */
48
+ readonly steps: readonly BatchStepOutcome[];
49
+ /**
50
+ * A failure WITHOUT a server answer — the socket broke, or the client's
51
+ * `query_timeout` fired while the batch was still on the wire. What the
52
+ * server did with the batch is unknown; `steps` lists only what was seen.
53
+ */
54
+ readonly transportError?: unknown;
55
+ };
56
+ /** The query-local type parser surface (the adapter's `EXACT_TYPES`). */
57
+ export type BatchTypeParsers = {
58
+ getTypeParser(oid: number, format?: "text" | "binary"): (value: string) => unknown;
59
+ };
60
+ type RowDescriptionField = {
61
+ readonly name: string;
62
+ readonly dataTypeID: number;
63
+ readonly format?: "text" | "binary";
64
+ };
65
+ /**
66
+ * Whether `client` can run a one-Sync batch: a node-postgres client whose
67
+ * connection exposes the protocol writers, NOT in pipeline mode (pipeline mode
68
+ * sends a Sync per query and was rejected by the spike), with the driver's
69
+ * value serializer available. Test doubles and foreign clients answer false
70
+ * and keep today's one-statement-per-wait path.
71
+ */
72
+ export declare function supportsBatch(params: {
73
+ client: unknown;
74
+ }): boolean;
75
+ /**
76
+ * Serialize a step's parameters with the driver's own `prepareValue`. Runs
77
+ * BEFORE anything is written, so a value the driver cannot serialize refuses
78
+ * the whole batch with nothing sent (node-postgres' own path closes and syncs
79
+ * instead; the caller-visible error is the same driver error).
80
+ */
81
+ export declare function prepareBatchValues(params: {
82
+ values: readonly unknown[];
83
+ }): unknown[];
84
+ /**
85
+ * The Submittable. Construct with steps whose values are already prepared
86
+ * ({@link prepareBatchValues}); pass to `client.query(batch)`; await
87
+ * {@link PgBatch.done}.
88
+ */
89
+ export declare class PgBatch {
90
+ /** Settles once: at the batch's ReadyForQuery, at its first server error, or at a transport failure. */
91
+ readonly done: Promise<BatchOutcome>;
92
+ /**
93
+ * Set by node-postgres when the client has a `query_timeout`: its wrapper
94
+ * clears the read-timeout timer. Called once when the batch settles, so a
95
+ * timer never outlives the batch (fix lane FX-A, RV5 MI-6).
96
+ */
97
+ callback: ((error: unknown, result?: unknown) => void) | undefined;
98
+ private readonly steps;
99
+ private readonly types;
100
+ private readonly outcomes;
101
+ private parsers;
102
+ private fields;
103
+ private rows;
104
+ /** The first type-parser failure of the current step; its remaining rows are skipped. */
105
+ private rowError;
106
+ private settled;
107
+ private resolve;
108
+ constructor(params: {
109
+ steps: readonly BatchStep[];
110
+ types: BatchTypeParsers;
111
+ });
112
+ /** Called by node-postgres when the batch reaches the head of the client's queue. */
113
+ submit(connection: unknown): void;
114
+ handleRowDescription(message: {
115
+ fields: readonly RowDescriptionField[];
116
+ }): void;
117
+ handleDataRow(message: {
118
+ fields: readonly (string | null)[];
119
+ }): void;
120
+ handleCommandComplete(message: {
121
+ text?: string;
122
+ command?: string;
123
+ }): void;
124
+ handleEmptyQuery(): void;
125
+ handleError(error: unknown): void;
126
+ handleReadyForQuery(): void;
127
+ handlePortalSuspended(): void;
128
+ handleCopyInResponse(connection: unknown): void;
129
+ handleCopyData(): void;
130
+ private finish;
131
+ }
132
+ /** Run `steps` as one batch on `client` and wait for it to settle. */
133
+ export declare function runBatch(params: {
134
+ /** A checked-out client that passed {@link supportsBatch}. */
135
+ client: {
136
+ query: (...args: never[]) => unknown;
137
+ };
138
+ steps: readonly BatchStep[];
139
+ types: BatchTypeParsers;
140
+ }): Promise<BatchOutcome>;
141
+ /**
142
+ * The first failure of a settled batch: the transport error, or the first
143
+ * failed step with its index. `undefined` when every step succeeded.
144
+ */
145
+ export declare function firstBatchFailure(params: {
146
+ outcome: BatchOutcome;
147
+ }): {
148
+ readonly index: number;
149
+ readonly error: unknown;
150
+ } | undefined;
151
+ export {};
152
+ //# sourceMappingURL=batch.d.ts.map
package/dist/index.d.ts CHANGED
@@ -63,15 +63,22 @@ export type PgPoolLike = {
63
63
  on?(event: "error", listener: (error: unknown, client: PgPoolClientLike) => void): void;
64
64
  };
65
65
  /**
66
- * Options for {@link pgAdapter}. Either point it at a connection string (the
67
- * adapter then owns the pool it creates) or hand it a pool you already own.
66
+ * Every {@link pgAdapter} option. {@link PgAdapterOptions} adds the rule that
67
+ * one of `connectionString` and `pool` is present.
68
68
  */
69
- export type PgAdapterOptions = {
70
- /** PostgreSQL connection URL. Defaults to `process.env.DATABASE_URL`. */
69
+ export type PgAdapterSettings = {
70
+ /**
71
+ * PostgreSQL connection URL. The adapter never reads `DATABASE_URL` (or any
72
+ * other environment variable) itself: pass the URL you mean. Give a complete
73
+ * URL — node-postgres fills parts the URL omits (host, user, database, …)
74
+ * from the `PG*` environment variables.
75
+ */
71
76
  readonly connectionString?: string;
72
77
  /**
73
78
  * An existing `pg.Pool` to use instead of creating one. The adapter never
74
- * ends a pool it did not create, so `disconnect()` is a no-op in this mode.
79
+ * ends a pool it did not create: `disconnect()` only ends this adapter (and
80
+ * rolls back and releases its own raw session); the pool stays usable.
81
+ * Wins over `connectionString` when both are given.
75
82
  */
76
83
  readonly pool?: PgPoolLike;
77
84
  /** Maximum pooled connections (default 10). Ignored when `pool` is given. */
@@ -130,12 +137,27 @@ export type PgAdapterOptions = {
130
137
  */
131
138
  readonly planCacheMode?: PlanCacheMode;
132
139
  };
140
+ /**
141
+ * Options for {@link pgAdapter}: an explicit address is required. Either point
142
+ * it at a connection string (the adapter then owns the pool it creates) or hand
143
+ * it a pool you already own. Without either, the factory throws `VIBE_CONFIG`
144
+ * before any pool or connection exists.
145
+ */
146
+ export type PgAdapterOptions = PgAdapterSettings & ({
147
+ readonly connectionString: string;
148
+ } | {
149
+ readonly pool: PgPoolLike;
150
+ });
133
151
  /**
134
152
  * Create a VibeORM adapter backed by node-postgres.
135
153
  *
154
+ * Requires an explicit address (`connectionString` or `pool`); see
155
+ * {@link PgAdapterOptions}. Construction opens nothing — the pool is created
156
+ * on first use.
157
+ *
136
158
  * @example
137
159
  * ```ts
138
- * const adapter = pgAdapter({ connectionString: process.env.DATABASE_URL, max: 20 });
160
+ * const adapter = pgAdapter({ connectionString: "postgresql://app:secret@db.internal:5432/app", max: 20 });
139
161
  * await adapter.connect();
140
162
  * const rows = await adapter.execute({ text: 'SELECT * FROM "User" WHERE "id" = $1', values: [1] });
141
163
  * ```
@@ -147,7 +169,7 @@ export type PgAdapterOptions = {
147
169
  * const adapter = pgAdapter({ pool });
148
170
  * ```
149
171
  */
150
- export declare function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter;
172
+ export declare function pgAdapter(options: PgAdapterOptions): DatabaseAdapter;
151
173
  export { PG_SQLSTATE_ERROR_CODES, PG_SQLSTATE_REASONS, mapPgDriverError, pgErrorSqlstate } from "./errors.ts";
152
174
  export { PgArrayParam, serializeParams, serializeParamsRaw } from "./params.ts";
153
175
  export { createSavepointCounter, nextSavepointName } from "@vibeorm/runtime/adapter-kit";