@vibeorm/adapter-pg 5.1.0 → 5.2.1
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 +13 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +75 -16
- package/dist/index.js.map +4 -4
- package/dist/pool-config.d.ts +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ const db = VibeClient({ adapter: pgAdapter({ pool }) });
|
|
|
55
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 |
|
|
56
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 |
|
|
57
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 |
|
|
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` |
|
|
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), or with `PGOPTIONS` when the URL has none. Refused with `VIBE_CONFIG` next to a caller-supplied `pool` |
|
|
59
59
|
|
|
60
60
|
### A database far away
|
|
61
61
|
|
|
@@ -137,6 +137,18 @@ Outside transactions the adapter declares `connectionGroup` (when the driver can
|
|
|
137
137
|
|
|
138
138
|
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.
|
|
139
139
|
|
|
140
|
+
### Explicit deferred work
|
|
141
|
+
|
|
142
|
+
Transaction clients accept `tx.$queueRaw({ query, name? })`, where `query` is a bound `sql` fragment. Queued statements keep insertion order and travel before the next eligible statement, or before COMMIT when no statement follows. Each remains a separate SQL statement, so under READ COMMITTED a load after a lock wait takes its snapshot after acquisition. A queued failure rejects the companion statement and the transaction with the queued statement's typed error, even if the callback catches the companion rejection.
|
|
143
|
+
|
|
144
|
+
`$lazy(() => db.model.findMany(...))`, `$queryRawLazy({ query, access, name? })` and `$executeRawLazy({ query, name? })` stay unsent until awaited or explicitly claimed by `$all`, an array transaction or `commitWith`. Existing raw calls remain immediate. Search and `vector.nearest` contribute lazy reads; `vector.set` contributes a write. Inside an existing transaction, `$all` preserves object-key enumeration order, including locking reads. Eligible one-statement members still share an ordered batch.
|
|
145
|
+
|
|
146
|
+
`commitWith` accepts an ordered final-item list. Eligible application writes and a following flush can share the closing batch with COMMIT. Result-bearing tail items settle only after transaction completion, in list order. A failure rejects every item with the transaction's same error. A refusable client-side reply keeps COMMIT separate. Nested array transactions remain savepoints and never infer that they are final.
|
|
147
|
+
|
|
148
|
+
Count-only bulk writes accept `expect: n` on PostgreSQL. The SQL guard checks direct affected rows and raises `VIBE_ROW_COUNT_MISMATCH` before COMMIT when the count differs. Unsupported or multi-statement shapes refuse instead of substituting a client-only check.
|
|
149
|
+
|
|
150
|
+
`tx.$advisoryLocks({ keys, mode?, defer? })` takes sorted, deduplicated signed 64-bit keys in one statement. The immediate form confirms acquisition. `defer: true` confirms enqueueing only, with acquisition before the next statement. The top-level `advisoryLocks` transaction option queues the same statement before the first load. These explicit boundaries add no timer or microtask gathering window. Existing deadline, driver-timeout and pipeline capability checks still decide whether wire batching is safe.
|
|
151
|
+
|
|
140
152
|
## Parameter serialization: two paths
|
|
141
153
|
|
|
142
154
|
`pg` turns a JS array into a PostgreSQL array literal natively, which is what a raw query wants but not what the ORM path can assume. The adapter therefore keeps two helpers:
|
package/dist/index.d.ts
CHANGED
|
@@ -179,7 +179,8 @@ export type PgAdapterSettings = {
|
|
|
179
179
|
* statement is still prepared once, but planned with the real parameter
|
|
180
180
|
* values on every execution. Rides the `options` startup parameter, merged
|
|
181
181
|
* with whatever the connection string already carries (a URL that sets
|
|
182
|
-
* `plan_cache_mode` itself wins)
|
|
182
|
+
* `plan_cache_mode` itself wins), or with `PGOPTIONS` when it carries none.
|
|
183
|
+
* Refused with `VIBE_CONFIG` together with
|
|
183
184
|
* a caller-supplied `pool` — the adapter never reconfigures a pool it does
|
|
184
185
|
* not own; set it on that pool's own `options` instead.
|
|
185
186
|
*/
|
package/dist/index.js
CHANGED
|
@@ -792,13 +792,14 @@ function placePlanCacheMode(params) {
|
|
|
792
792
|
if (planCacheMode === "auto")
|
|
793
793
|
return { connectionString, options: undefined };
|
|
794
794
|
const setting = `-c plan_cache_mode=${planCacheMode}`;
|
|
795
|
+
const configOptions = [params.ambientOptions?.trim() ?? "", setting].filter(Boolean).join(" ");
|
|
795
796
|
if (connectionString === undefined || !URL.canParse(connectionString)) {
|
|
796
|
-
return { connectionString, options:
|
|
797
|
+
return { connectionString, options: configOptions };
|
|
797
798
|
}
|
|
798
799
|
const url = new URL(connectionString);
|
|
799
800
|
const existing = url.searchParams.get("options");
|
|
800
801
|
if (existing === null) {
|
|
801
|
-
return { connectionString, options:
|
|
802
|
+
return { connectionString, options: configOptions };
|
|
802
803
|
}
|
|
803
804
|
if (/(^|\s)-c\s*plan_cache_mode=/.test(existing)) {
|
|
804
805
|
return { connectionString, options: undefined };
|
|
@@ -831,7 +832,8 @@ function buildPoolConfig(params) {
|
|
|
831
832
|
const { max, min, statementTimeout, connectionTimeout, idleTimeout } = params;
|
|
832
833
|
const placement = placePlanCacheMode({
|
|
833
834
|
connectionString: params.connectionString,
|
|
834
|
-
planCacheMode: params.planCacheMode ?? "auto"
|
|
835
|
+
planCacheMode: params.planCacheMode ?? "auto",
|
|
836
|
+
...params.ambientOptions === undefined ? {} : { ambientOptions: params.ambientOptions }
|
|
835
837
|
});
|
|
836
838
|
const config = { connectionString: placement.connectionString, max };
|
|
837
839
|
if (min !== undefined)
|
|
@@ -1352,7 +1354,8 @@ function pgAdapter(options) {
|
|
|
1352
1354
|
...options?.statementTimeout === undefined ? {} : { statementTimeout: options.statementTimeout },
|
|
1353
1355
|
...options?.connectionTimeout === undefined ? {} : { connectionTimeout: options.connectionTimeout },
|
|
1354
1356
|
...idleTimeout === undefined ? {} : { idleTimeout },
|
|
1355
|
-
planCacheMode
|
|
1357
|
+
planCacheMode,
|
|
1358
|
+
...process.env.PGOPTIONS === undefined ? {} : { ambientOptions: process.env.PGOPTIONS }
|
|
1356
1359
|
});
|
|
1357
1360
|
const PoolCtor = pg3.Pool;
|
|
1358
1361
|
pool = new PoolCtor(config);
|
|
@@ -1452,6 +1455,25 @@ function pgAdapter(options) {
|
|
|
1452
1455
|
pipelining: pipelines === null ? "ordered" : PIPELINING,
|
|
1453
1456
|
connectionIdentity: connection,
|
|
1454
1457
|
...params.topLevel !== true || deferred === null ? {} : {
|
|
1458
|
+
queueStatement: (statement) => {
|
|
1459
|
+
budget.assertUsable({ stage: "statement" });
|
|
1460
|
+
if (scope.state.open !== null || final !== undefined)
|
|
1461
|
+
throw new VibeError9({ code: "VIBE_VALIDATION", message: "Queue statements on the active transaction handle, outside an open savepoint", meta: { reason: "queue-scope" } });
|
|
1462
|
+
scopeTurn({ scope });
|
|
1463
|
+
const name = statement.name === undefined ? undefined : assertStatementName({ name: statement.name, method: "$queueRaw" });
|
|
1464
|
+
if (name !== undefined)
|
|
1465
|
+
rawStatementNamesFor({ pool: getPool() }).admit({ name, text: statement.text });
|
|
1466
|
+
deferred.defer({
|
|
1467
|
+
text: statement.text,
|
|
1468
|
+
values: prepareBatchValues({ values: serializeParamsRaw({ values: [...statement.values] }) }),
|
|
1469
|
+
...name === undefined ? {} : { name },
|
|
1470
|
+
required: true,
|
|
1471
|
+
onError: statement.onError,
|
|
1472
|
+
...statement.onStart === undefined ? {} : { onStart: statement.onStart },
|
|
1473
|
+
...statement.onSettled === undefined ? {} : { onSettled: statement.onSettled },
|
|
1474
|
+
onSuccess: () => observeRowChanges({ scope, count: statement.effect === "read" ? 0 : "unknown" })
|
|
1475
|
+
});
|
|
1476
|
+
},
|
|
1455
1477
|
deferStatement: (statement) => {
|
|
1456
1478
|
budget.assertUsable({ stage: "statement" });
|
|
1457
1479
|
deferred.defer({ text: statement.text, values: prepareBatchValues({ values: statement.values }) });
|
|
@@ -1744,6 +1766,26 @@ function pgAdapter(options) {
|
|
|
1744
1766
|
return transactionSettingRefusedError({ provider: PROVIDER, settings, setting: findRefusedSetting({ settings, cause: mapped }), cause: mapped, deferred: true });
|
|
1745
1767
|
};
|
|
1746
1768
|
let openingFailure;
|
|
1769
|
+
let queuedFailure;
|
|
1770
|
+
const observeDeferred = (control, outcome) => {
|
|
1771
|
+
for (let index = 0;index < control.length; index++) {
|
|
1772
|
+
const entry = control[index];
|
|
1773
|
+
const step = outcome.steps[index];
|
|
1774
|
+
if (step?.status === "ok") {
|
|
1775
|
+
entry.onSuccess?.();
|
|
1776
|
+
entry.onSettled?.({ status: "fulfilled", value: { rows: step.result.rows, affectedRows: step.result.rowCount ?? 0 } });
|
|
1777
|
+
} else {
|
|
1778
|
+
const origin = mapPgDriverError({ error: step?.status === "failed" ? step.error : firstBatchFailure({ outcome })?.error ?? outcome.transportError });
|
|
1779
|
+
if (entry.required === true && step?.status === "failed") {
|
|
1780
|
+
queuedFailure ??= origin;
|
|
1781
|
+
if (entry.name !== undefined && isStaleStatementFailure({ error: queuedFailure }))
|
|
1782
|
+
staleNamedStatement = true;
|
|
1783
|
+
entry.onError?.(queuedFailure);
|
|
1784
|
+
}
|
|
1785
|
+
entry.onSettled?.({ status: "rejected", reason: queuedFailure ?? origin });
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1747
1789
|
let inFlight = null;
|
|
1748
1790
|
const commits = capable && budget.totalMs === null && !((client.connectionParameters?.query_timeout ?? 0) > 0);
|
|
1749
1791
|
let ridden;
|
|
@@ -1789,6 +1831,8 @@ function pgAdapter(options) {
|
|
|
1789
1831
|
dispatchParams.sent?.();
|
|
1790
1832
|
return cancellation.run({ config, execution: dispatchParams.execution, onReadControl: dispatchParams.onReadControl, send: () => onClient.query(config) });
|
|
1791
1833
|
};
|
|
1834
|
+
if (cleanup !== true && queuedFailure !== undefined)
|
|
1835
|
+
throw queuedFailure;
|
|
1792
1836
|
if (cleanup !== true && cancellationAttempted)
|
|
1793
1837
|
throw transactionAbortedError2({ cause: readCancelledError2({ stage: "running" }) });
|
|
1794
1838
|
if (cleanup !== true && openingFailure !== undefined)
|
|
@@ -1835,11 +1879,16 @@ function pgAdapter(options) {
|
|
|
1835
1879
|
if (statement.length > 0)
|
|
1836
1880
|
dispatchParams.sent?.();
|
|
1837
1881
|
const commitIndex = control.length + statement.length;
|
|
1838
|
-
const batch = runTurn(() =>
|
|
1882
|
+
const batch = runTurn(() => {
|
|
1883
|
+
for (const step of control)
|
|
1884
|
+
step.onStart?.();
|
|
1885
|
+
return runBatch({ client, steps: [...control, ...statement, ...rides ? [COMMIT_STEP] : []], types: EXACT_TYPES });
|
|
1886
|
+
});
|
|
1839
1887
|
if (rides)
|
|
1840
1888
|
trackRidden(batch, commitIndex);
|
|
1841
1889
|
const burst = batch.then((received) => {
|
|
1842
1890
|
const outcome = rides ? beforeCommit({ outcome: received, commitIndex }) : received;
|
|
1891
|
+
observeDeferred(control, outcome);
|
|
1843
1892
|
const failure = firstBatchFailure({ outcome });
|
|
1844
1893
|
if (failure !== undefined) {
|
|
1845
1894
|
const openingFailed = failure.index < openingSteps || outcome.transportError !== undefined && openingSteps > 0;
|
|
@@ -1851,6 +1900,10 @@ function pgAdapter(options) {
|
|
|
1851
1900
|
if (typeof mapped.meta.sqlstate === "string")
|
|
1852
1901
|
markTransactionAborted({ scope, cause: mapped });
|
|
1853
1902
|
}
|
|
1903
|
+
if (dispatchParams.stage === "commit" && failure.index < control.length && outcome.transportError === undefined)
|
|
1904
|
+
commitProgress = "not-sent";
|
|
1905
|
+
if (queuedFailure !== undefined)
|
|
1906
|
+
throw queuedFailure;
|
|
1854
1907
|
throw openingFailed && settingsDeferred && failure.index === opening.length && openingFailure !== undefined ? openingFailure : failure.error;
|
|
1855
1908
|
}
|
|
1856
1909
|
if (statement.length === 0)
|
|
@@ -1902,7 +1955,7 @@ function pgAdapter(options) {
|
|
|
1902
1955
|
});
|
|
1903
1956
|
return;
|
|
1904
1957
|
}
|
|
1905
|
-
const mapped = failure.index < openingSteps ? openingStepError(failure.index, failure.error) : mapPgDriverError({ error: failure.error });
|
|
1958
|
+
const mapped = queuedFailure ?? (failure.index < openingSteps ? openingStepError(failure.index, failure.error) : mapPgDriverError({ error: failure.error }));
|
|
1906
1959
|
if (failure.index < openingSteps || outcome.transportError !== undefined && openingSteps > 0)
|
|
1907
1960
|
openingFailure = mapped;
|
|
1908
1961
|
if (typeof mapped.meta.sqlstate === "string")
|
|
@@ -1933,7 +1986,7 @@ function pgAdapter(options) {
|
|
|
1933
1986
|
}
|
|
1934
1987
|
});
|
|
1935
1988
|
};
|
|
1936
|
-
const sendRefusal = () => ridden !== undefined ? commitSentRefusal() : cancellationAttempted ? transactionAbortedError2({ cause: readCancelledError2({ stage: "running" }) }) : openingFailure !== undefined ? transactionAbortedError2({ cause: openingFailure }) : scope.state.poisoned ?? (scope.state.aborted === null ? undefined : transactionAbortedError2({ cause: scope.state.aborted.cause }));
|
|
1989
|
+
const sendRefusal = () => queuedFailure !== undefined ? queuedFailure : ridden !== undefined ? commitSentRefusal() : cancellationAttempted ? transactionAbortedError2({ cause: readCancelledError2({ stage: "running" }) }) : openingFailure !== undefined ? transactionAbortedError2({ cause: openingFailure }) : scope.state.poisoned ?? (scope.state.aborted === null ? undefined : transactionAbortedError2({ cause: scope.state.aborted.cause }));
|
|
1937
1990
|
const ridesQueued = (live) => {
|
|
1938
1991
|
const last = live.at(-1);
|
|
1939
1992
|
if (last === undefined || last.commit !== true && last.lane.seat?.group.final !== true || queued.length > 0 || inFlight !== null || ordinaryOpen > 0 || !ridesNow())
|
|
@@ -1976,18 +2029,24 @@ function pgAdapter(options) {
|
|
|
1976
2029
|
const control = inTurn ? [] : deferred.takeSteps();
|
|
1977
2030
|
const openingSteps = carriesOpening ? openingLength : 0;
|
|
1978
2031
|
const commitIndex = control.length + live.length;
|
|
1979
|
-
const sendBatch = () =>
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2032
|
+
const sendBatch = () => {
|
|
2033
|
+
for (const step of control)
|
|
2034
|
+
step.onStart?.();
|
|
2035
|
+
return runBatch({
|
|
2036
|
+
client,
|
|
2037
|
+
steps: [...control, ...live.map((entry) => entry.name === undefined ? { text: entry.text, values: entry.values } : { text: entry.text, values: entry.values, name: entry.name }), ...rides ? [COMMIT_STEP] : []],
|
|
2038
|
+
types: EXACT_TYPES
|
|
2039
|
+
});
|
|
2040
|
+
};
|
|
1984
2041
|
const batch = inTurn ? new Promise((resolve) => {
|
|
1985
2042
|
resolve(sendBatch());
|
|
1986
2043
|
}) : runTurn(sendBatch);
|
|
1987
2044
|
if (rides)
|
|
1988
2045
|
trackRidden(batch, commitIndex);
|
|
1989
2046
|
const burst = batch.then((received) => {
|
|
1990
|
-
|
|
2047
|
+
const outcome = rides ? beforeCommit({ outcome: received, commitIndex }) : received;
|
|
2048
|
+
observeDeferred(control, outcome);
|
|
2049
|
+
distributeBurst({ outcome, live, controlSteps: control.length, openingSteps });
|
|
1991
2050
|
}).then(undefined, (error) => {
|
|
1992
2051
|
if (openingSteps > 0)
|
|
1993
2052
|
openingFailure ??= mapPgDriverError({ error });
|
|
@@ -2516,7 +2575,7 @@ function pgAdapter(options) {
|
|
|
2516
2575
|
throw riding.state === "skipped" ? transactionAbortedError2({ cause: scope.state.aborted?.cause }) : riding.error;
|
|
2517
2576
|
}
|
|
2518
2577
|
budget.assertUsable({ stage: "commit" });
|
|
2519
|
-
if (!deferred.opened()) {
|
|
2578
|
+
if (!deferred.opened() && !deferred.hasRequired()) {
|
|
2520
2579
|
budget.close({ outcome: "committed" });
|
|
2521
2580
|
return result;
|
|
2522
2581
|
}
|
|
@@ -2559,7 +2618,7 @@ function pgAdapter(options) {
|
|
|
2559
2618
|
budget.close({ outcome: "committed" });
|
|
2560
2619
|
throw caught;
|
|
2561
2620
|
}
|
|
2562
|
-
const error = riding?.state === "lost" ? riding.error : caught;
|
|
2621
|
+
const error = riding?.state === "lost" ? riding.error : queuedFailure ?? caught;
|
|
2563
2622
|
if (isSavepointRollbackFailedError(error) || scope.state.poisoned !== null) {
|
|
2564
2623
|
discard = true;
|
|
2565
2624
|
budget.close({ outcome: "unknown" });
|
|
@@ -2715,4 +2774,4 @@ export {
|
|
|
2715
2774
|
validateTransactionDeadline2 as validateTransactionDeadline
|
|
2716
2775
|
};
|
|
2717
2776
|
|
|
2718
|
-
//# debugId=
|
|
2777
|
+
//# debugId=F571608C9B212DA864756E2164756E21
|