@voltro/sql-postgres 0.33.0 → 0.34.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/CHANGELOG.md +1801 -0
- package/dist/index.d.ts +106 -9
- package/dist/index.js +336 -199
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,29 @@ import { SqlDialect } from '@voltro/database';
|
|
|
20
20
|
import { SqlError } from '@effect/sql';
|
|
21
21
|
import { TransactionConnection } from '@effect/sql/SqlClient';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Every number the CDC consumer would otherwise pick on your behalf, with its
|
|
25
|
+
* default. Each is also readable from the environment, so an operator can tune
|
|
26
|
+
* a running deployment without a code change.
|
|
27
|
+
*/
|
|
28
|
+
declare interface CdcRehydrateTunables {
|
|
29
|
+
/**
|
|
30
|
+
* Total budget for recovering ONE oversized change, retries included. The
|
|
31
|
+
* LISTEN consumer is serial, so this is also the longest a single oversized
|
|
32
|
+
* row can hold up the change stream. Past it the event is delivered
|
|
33
|
+
* image-less and counted `unrecovered`.
|
|
34
|
+
* Default `5_000`ms. Env: `VOLTRO_CDC_REHYDRATE_TIMEOUT_MS`.
|
|
35
|
+
*/
|
|
36
|
+
readonly rehydrateTimeoutMs: number;
|
|
37
|
+
/**
|
|
38
|
+
* Re-reads AFTER the first attempt, inside the budget above (50 ms, doubling).
|
|
39
|
+
* A transient read failure is the one part of this that IS retryable — the
|
|
40
|
+
* loss it prevents is not.
|
|
41
|
+
* Default `2`. `0` disables retry. Env: `VOLTRO_CDC_REHYDRATE_RETRIES`.
|
|
42
|
+
*/
|
|
43
|
+
readonly rehydrateRetries: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
/**
|
|
24
47
|
* Parse a `ConnectionConfig` from the cross-dialect interface into the
|
|
25
48
|
* postgres-specific `PostgresConnection` shape.
|
|
@@ -161,6 +184,14 @@ export declare interface PostgresConnection {
|
|
|
161
184
|
* the pool would have freed up eventually: it names the pool as the cause, at
|
|
162
185
|
* the moment it is the cause, instead of surfacing as an unexplained latency
|
|
163
186
|
* spike in a place with no connection information in it.
|
|
187
|
+
*
|
|
188
|
+
* **It shipped bounded on ONE of the two layer paths**, which is the part
|
|
189
|
+
* worth remembering. The bound sat inside the hand-built `pg.Pool` branch —
|
|
190
|
+
* reached only when `DB_SCHEMA` or `DB_STATEMENT_TIMEOUT_MS` is set — while
|
|
191
|
+
* the default configuration went through `PgClient.layerConfig` with no
|
|
192
|
+
* `connectTimeout` at all. The fix is not "add a timeout", it is that BOTH
|
|
193
|
+
* paths now read one resolver (`resolveAcquireTimeoutMs`); a per-branch copy
|
|
194
|
+
* of a default is how the first one drifted.
|
|
164
195
|
*/
|
|
165
196
|
readonly acquireTimeoutMs?: number;
|
|
166
197
|
}
|
|
@@ -170,13 +201,33 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
170
201
|
private readonly runtime;
|
|
171
202
|
private readonly changeStrategy;
|
|
172
203
|
private readonly cdcChannel;
|
|
204
|
+
private readonly cdcRehydrate;
|
|
173
205
|
private readonly emitter;
|
|
174
206
|
private cdcFiber;
|
|
175
207
|
/** In-flight count of `transactional()` calls. `close()` waits for
|
|
176
208
|
* this to drop to 0 (or its grace period to expire) before disposing
|
|
177
209
|
* the runtime — preventing connection-pool teardown mid-COMMIT. */
|
|
178
210
|
private inflightTxns;
|
|
179
|
-
|
|
211
|
+
/** Reports the first oversized change per table + every unrecovered one.
|
|
212
|
+
* Instance-scoped so the once-per-table rate limit is the STORE's, not a
|
|
213
|
+
* module global that a second store in one process would silence. */
|
|
214
|
+
private readonly reportOversized;
|
|
215
|
+
/** PERF-23 — counts EVERY eager JSON-agg → walker degradation and logs a
|
|
216
|
+
* rate-limited line. Instance-scoped for the same reason as above. */
|
|
217
|
+
private readonly reportEagerFallback;
|
|
218
|
+
constructor(sql: SqlClient.SqlClient, runtime: ManagedRuntime.ManagedRuntime<SqlClient.SqlClient | PgClient.PgClient, never>, changeStrategy: ChangeStrategy, cdcChannel: string, cdcRehydrate?: CdcRehydrateTunables);
|
|
219
|
+
/**
|
|
220
|
+
* The DIALECT half of the shared transaction bracket
|
|
221
|
+
* (`runStoreTransaction` / `runRetryingTransaction` in `@voltro/database`).
|
|
222
|
+
*
|
|
223
|
+
* Postgres owns exactly four things here: its client's `withTransaction`, its
|
|
224
|
+
* managed runtime, its retryable-failure predicate and the span labels.
|
|
225
|
+
* Attribution threading, retry, commit-defect promotion and exit settling are
|
|
226
|
+
* NOT here on purpose — every one of them had already drifted between the four
|
|
227
|
+
* dialect stores, and `transactional()` and `runInNamespace()` had drifted
|
|
228
|
+
* from each other inside this very file. See `transactionOutcome.ts`.
|
|
229
|
+
*/
|
|
230
|
+
private txnSpec;
|
|
180
231
|
/**
|
|
181
232
|
* Bind a per-request tenant NAMESPACE (a postgres SCHEMA). The returned
|
|
182
233
|
* view runs every operation inside a transaction whose first statement
|
|
@@ -212,6 +263,14 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
212
263
|
* fully isolated. The public methods pass `null/null`; the view passes
|
|
213
264
|
* its own captured TxnContext + per-call events buffer.
|
|
214
265
|
*/
|
|
266
|
+
/**
|
|
267
|
+
* Run one SELECT.
|
|
268
|
+
*
|
|
269
|
+
* `namespace` is the physical-tenant schema when the read took the
|
|
270
|
+
* transaction-free fast path (see `PostgresNamespaceView.query`); `null` on
|
|
271
|
+
* every other path, including inside a `SET LOCAL search_path` transaction,
|
|
272
|
+
* where qualifying as well would be redundant.
|
|
273
|
+
*/
|
|
215
274
|
private executeQuery;
|
|
216
275
|
private executeInsert;
|
|
217
276
|
private executeUpdate;
|
|
@@ -256,13 +315,18 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
256
315
|
* The fallback-on-throw catches dialect quirks I couldn't predict
|
|
257
316
|
* up front — anything that breaks the JSON-agg query (driver
|
|
258
317
|
* compatibility, schema feature) silently degrades to walker
|
|
259
|
-
* instead of surfacing a routing-level error.
|
|
260
|
-
*
|
|
318
|
+
* instead of surfacing a routing-level error.
|
|
319
|
+
*
|
|
320
|
+
* PERF-23 — the degradation is COUNTED, not just logged. A log line makes it
|
|
321
|
+
* visible to whoever is watching the terminal; `voltro_db_eager_fallback_total`
|
|
322
|
+
* makes it visible to whoever looks in three weeks, which is when a
|
|
323
|
+
* per-query cliff that has been running the whole time actually gets noticed.
|
|
261
324
|
*/
|
|
262
325
|
private runWithEager;
|
|
263
326
|
/** Friend accessor for the transactional view — same eager-aware
|
|
264
327
|
* path as the public `query()` but pinned to the view's txn. */
|
|
265
328
|
getInternalRunWithEager(): (d: QueryDescriptor, txn: TxnContext | null) => Promise<ReadonlyArray<Row>>;
|
|
329
|
+
/* Excluded from this release type: queryInNamespace */
|
|
266
330
|
/**
|
|
267
331
|
* Bracket a NON-transactional write: capture the request identity before the
|
|
268
332
|
* first await (`withCapturedAttribution`), and hold this table's transport
|
|
@@ -349,16 +413,19 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
349
413
|
* 4. ChangeEvents queue in the view's buffer; on commit they drain
|
|
350
414
|
* to the parent's emitter. On throw they're dropped.
|
|
351
415
|
*
|
|
352
|
-
* Resilience:
|
|
416
|
+
* Resilience (all of it owned by the shared bracket, not by this file):
|
|
353
417
|
* - Automatic retry on `serialization_failure` (40001) and
|
|
354
418
|
* `deadlock_detected` (40P01) with exponential backoff (10ms base,
|
|
355
|
-
* up to 3
|
|
419
|
+
* up to 3 retries). Each retry rebuilds a fresh view — the
|
|
356
420
|
* aborted attempt's events were never drained, so the retry's
|
|
357
421
|
* subscribers see exactly one event-set (the winning attempt's).
|
|
358
|
-
* -
|
|
359
|
-
* `Effect.
|
|
360
|
-
*
|
|
361
|
-
*
|
|
422
|
+
* - A conflict raised at COMMIT arrives as a DEFECT (`@effect/sql` runs
|
|
423
|
+
* COMMIT as `Effect.orDie`) and is promoted back to a failure so the
|
|
424
|
+
* schedule can see it.
|
|
425
|
+
* - The whole boundary is one `Effect.withSpan('store.transactional')` so
|
|
426
|
+
* OpenTelemetry collectors see one span per logical mutation.
|
|
427
|
+
* - The Exit settles through `settleTransactionExit`, so a typed error
|
|
428
|
+
* reaches the caller with its `_tag` rather than as a `FiberFailure`.
|
|
362
429
|
*/
|
|
363
430
|
transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
|
|
364
431
|
onChange(listener: (event: ChangeEvent) => void): () => void;
|
|
@@ -391,10 +458,32 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
391
458
|
close(gracePeriodMs?: number): Promise<void>;
|
|
392
459
|
/** Liveness probe — `SELECT 1`. Rejects if the pool can't answer. */
|
|
393
460
|
ping(): Promise<void>;
|
|
461
|
+
/**
|
|
462
|
+
* Read one row back as the JSON the trigger could not send.
|
|
463
|
+
*
|
|
464
|
+
* `row_to_json(t)` on purpose: it is literally what `triggerFunctionSql`
|
|
465
|
+
* builds its images with, so a re-hydrated row and an ordinary CDC image are
|
|
466
|
+
* the same shape — dates as ISO strings, numerics as JSON numbers. A plain
|
|
467
|
+
* `SELECT *` would hand the driver's types to consumers that have only ever
|
|
468
|
+
* seen the JSON ones, which is a second shape to get right in every tap.
|
|
469
|
+
*
|
|
470
|
+
* SCHEMA-QUALIFIED, from the payload's own `schema`. Under namespace
|
|
471
|
+
* isolation the write landed in `tenant_<id>`, and an unqualified read
|
|
472
|
+
* resolves through this connection's `search_path` instead — which is another
|
|
473
|
+
* tenant's table of the same name, or none.
|
|
474
|
+
*/
|
|
475
|
+
private readRowAsCdcJson;
|
|
394
476
|
/**
|
|
395
477
|
* Subscribe to the Postgres NOTIFY channel and translate each notification
|
|
396
478
|
* into a ChangeEvent. Called automatically from `makePostgresDataStore`
|
|
397
479
|
* when `changeStrategy: 'cdc'`.
|
|
480
|
+
*
|
|
481
|
+
* An OVERSIZED notification is re-hydrated HERE, before anything downstream
|
|
482
|
+
* sees it. This is the one place every consumer is downstream of — the
|
|
483
|
+
* dispatcher, the plugin taps, the analytics mirror, the DevTools CDC
|
|
484
|
+
* producer and the ISR invalidator all hang off the emitter this feeds — so
|
|
485
|
+
* it is the only place the repair can be made once. See `cdcRehydrate.ts` for
|
|
486
|
+
* what re-hydration can and cannot recover.
|
|
398
487
|
*/
|
|
399
488
|
startCdcConsumer(): Promise<void>;
|
|
400
489
|
}
|
|
@@ -418,6 +507,14 @@ export declare interface PostgresDataStoreOptions {
|
|
|
418
507
|
readonly changeStrategy?: ChangeStrategy;
|
|
419
508
|
/** Postgres NOTIFY channel name when changeStrategy is 'cdc'. Defaults to 'framework_changes'. */
|
|
420
509
|
readonly cdcChannel?: string;
|
|
510
|
+
/** Budget for re-reading ONE row whose change exceeded the NOTIFY payload cap
|
|
511
|
+
* — see {@link CdcRehydrateTunables.rehydrateTimeoutMs}. Defaults to 5000ms
|
|
512
|
+
* (env `VOLTRO_CDC_REHYDRATE_TIMEOUT_MS`). */
|
|
513
|
+
readonly cdcRehydrateTimeoutMs?: number;
|
|
514
|
+
/** Re-reads after the first attempt, inside that budget — see
|
|
515
|
+
* {@link CdcRehydrateTunables.rehydrateRetries}. Defaults to 2
|
|
516
|
+
* (env `VOLTRO_CDC_REHYDRATE_RETRIES`). */
|
|
517
|
+
readonly cdcRehydrateRetries?: number;
|
|
421
518
|
}
|
|
422
519
|
|
|
423
520
|
/**
|