@atscript/db-postgres 0.1.152 → 0.1.154
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/dist/index.cjs +102 -4
- package/dist/index.d.cts +37 -5
- package/dist/index.d.mts +37 -5
- package/dist/index.mjs +102 -4
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -192,6 +192,7 @@ const pgDialect = {
|
|
|
192
192
|
return zone === void 0 ? void 0 : (0, _atscript_db.bucketTimeZoneUnavailable)(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
|
|
193
193
|
},
|
|
194
194
|
nullsSortLargest: true,
|
|
195
|
+
nullsPlacementSyntax: true,
|
|
195
196
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
196
197
|
paramPlaceholder(index) {
|
|
197
198
|
return `$${index}`;
|
|
@@ -570,6 +571,12 @@ function parseCount(value) {
|
|
|
570
571
|
/** The suffix PostgreSQL gives an auto-named constraint of each `pg_constraint.contype`. */
|
|
571
572
|
/** Savepoint guarding one `insertManyIgnore` chunk (see {@link PostgresAdapter.insertManyIgnore}). */
|
|
572
573
|
const IGNORE_SAVEPOINT = "atscript_ignore_chunk";
|
|
574
|
+
/** Retryable concurrency SQLSTATEs (see `PostgresAdapter._mapConstraintError`). */
|
|
575
|
+
const PG_CONTENTION_CODES = {
|
|
576
|
+
"40P01": "DEADLOCK",
|
|
577
|
+
"55P03": "LOCK_TIMEOUT",
|
|
578
|
+
"40001": "SERIALIZATION_FAILURE"
|
|
579
|
+
};
|
|
573
580
|
const PG_CONSTRAINT_LABELS = {
|
|
574
581
|
p: "pkey",
|
|
575
582
|
u: "key",
|
|
@@ -788,6 +795,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
788
795
|
supportsAggregateExpressions() {
|
|
789
796
|
return true;
|
|
790
797
|
}
|
|
798
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
799
|
+
supportsNullsPlacement() {
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
791
802
|
/** Computed view columns and first-row joins. */
|
|
792
803
|
viewCapabilities() {
|
|
793
804
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -865,6 +876,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
865
876
|
* PostgreSQL uses SQLSTATE codes:
|
|
866
877
|
* - 23505 = unique_violation
|
|
867
878
|
* - 23503 = foreign_key_violation
|
|
879
|
+
* - 40P01 = deadlock_detected → `DEADLOCK`
|
|
880
|
+
* - 55P03 = lock_not_available (`lock_timeout`, `NOWAIT`) → `LOCK_TIMEOUT`
|
|
881
|
+
* - 40001 = serialization_failure → `SERIALIZATION_FAILURE`
|
|
868
882
|
*/
|
|
869
883
|
async _wrapConstraintError(fn) {
|
|
870
884
|
try {
|
|
@@ -873,7 +887,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
873
887
|
return this._mapConstraintError(error);
|
|
874
888
|
}
|
|
875
889
|
}
|
|
876
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
890
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock timeout, else as is. */
|
|
877
891
|
_mapConstraintError(error) {
|
|
878
892
|
if (error && typeof error === "object" && "code" in error) {
|
|
879
893
|
const err = error;
|
|
@@ -882,6 +896,11 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
882
896
|
message: err.detail ?? err.message
|
|
883
897
|
}]);
|
|
884
898
|
if (err.code === "23503") throw new _atscript_db.DbError("FK_VIOLATION", this._mapFkError(err.detail ?? err.message, err.constraint));
|
|
899
|
+
const contention = PG_CONTENTION_CODES[err.code];
|
|
900
|
+
if (contention) throw new _atscript_db.DbError(contention, [{
|
|
901
|
+
path: "",
|
|
902
|
+
message: err.message
|
|
903
|
+
}]);
|
|
885
904
|
}
|
|
886
905
|
throw error;
|
|
887
906
|
}
|
|
@@ -959,9 +978,21 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
959
978
|
* with the same `ON CONFLICT DO NOTHING`, so the result is always exact. A
|
|
960
979
|
* one-row chunk (or a table without keys) maps exactly by construction, so
|
|
961
980
|
* it needs no savepoint — and a single such chunk no transaction either.
|
|
981
|
+
* `ON CONFLICT DO NOTHING` takes no lock on a stored conflicting row, so a
|
|
982
|
+
* caller's transaction is left without any. `opts.lockConflicts` (since
|
|
983
|
+
* 0.1.153) locks them: one `SELECT … FOR UPDATE` of the stored rows matching
|
|
984
|
+
* the batch's keys, in primary-key order, BEFORE the insert, and one more
|
|
985
|
+
* for the skipped rows a concurrent writer committed in between.
|
|
962
986
|
*/
|
|
963
|
-
async insertManyIgnore(data) {
|
|
987
|
+
async insertManyIgnore(data, opts) {
|
|
964
988
|
if (data.length === 0) return [];
|
|
989
|
+
if (opts?.lockConflicts && this._table.uniqueKeySets.length > 0) return this.withTransaction(async () => {
|
|
990
|
+
const locked = await this._lockKeyedRows(data);
|
|
991
|
+
const slots = await this.insertManyIgnore(data);
|
|
992
|
+
const raced = data.filter((_, i) => slots[i] === null && !locked.has(i));
|
|
993
|
+
if (raced.length > 0) await this._lockKeyedRows(raced);
|
|
994
|
+
return slots;
|
|
995
|
+
});
|
|
965
996
|
const plan = this._ignorePlan();
|
|
966
997
|
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
967
998
|
const needsSavepoint = (batch) => batch.length > 1 && plan.returning.length > 0;
|
|
@@ -998,6 +1029,52 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
998
1029
|
return batches.length === 1 && !needsSavepoint(batches[0]) ? run() : this.withTransaction(run);
|
|
999
1030
|
}
|
|
1000
1031
|
/**
|
|
1032
|
+
* `SELECT <key columns> … FOR UPDATE` of the stored rows sharing a primary /
|
|
1033
|
+
* unique key tuple with one of `rows`, ordered by the primary key (so
|
|
1034
|
+
* concurrent lockers queue instead of deadlocking); PostgreSQL locks only
|
|
1035
|
+
* the rows returned. Returns the indices of `rows` a locked row matched.
|
|
1036
|
+
*/
|
|
1037
|
+
async _lockKeyedRows(rows) {
|
|
1038
|
+
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1039
|
+
const rowTuples = rows.map((row) => keySets.map((fields) => (0, _atscript_db.uniqueKeyTuple)(row, fields)));
|
|
1040
|
+
const order = (this._pk().cols.length > 0 ? this._pk().cols : keySets[0]).map((c) => qi(c)).join(", ");
|
|
1041
|
+
const selectCols = [...new Set(keySets.flat())].map((c) => qi(c)).join(", ");
|
|
1042
|
+
const lockedTuples = keySets.map(() => /* @__PURE__ */ new Set());
|
|
1043
|
+
const width = keySets.reduce((n, f) => n + f.length, 0);
|
|
1044
|
+
const sliceSize = Math.max(1, Math.floor(3e4 / width));
|
|
1045
|
+
for (let offset = 0; offset < rows.length; offset += sliceSize) {
|
|
1046
|
+
const end = Math.min(rows.length, offset + sliceSize);
|
|
1047
|
+
const clauses = [];
|
|
1048
|
+
const params = [];
|
|
1049
|
+
const ph = (value) => {
|
|
1050
|
+
params.push(pgDialect.toValue(value));
|
|
1051
|
+
return `$${params.length}`;
|
|
1052
|
+
};
|
|
1053
|
+
keySets.forEach((fields, k) => {
|
|
1054
|
+
const tuples = [];
|
|
1055
|
+
for (let i = offset; i < end; i++) {
|
|
1056
|
+
if (rowTuples[i][k] === void 0) continue;
|
|
1057
|
+
const row = rows[i];
|
|
1058
|
+
tuples.push(`(${fields.map((f) => ph(row[f])).join(", ")})`);
|
|
1059
|
+
}
|
|
1060
|
+
if (tuples.length > 0) clauses.push(`(${fields.map((f) => qi(f)).join(", ")}) IN (${tuples.join(", ")})`);
|
|
1061
|
+
});
|
|
1062
|
+
if (clauses.length === 0) continue;
|
|
1063
|
+
const sql = `SELECT ${selectCols} FROM ${quoteTableName(this.resolveTableName())} WHERE ${clauses.join(" OR ")} ORDER BY ${order} FOR UPDATE`;
|
|
1064
|
+
this._log(sql, params);
|
|
1065
|
+
const found = await this._wrapConstraintError(() => this._exec().all(sql, params));
|
|
1066
|
+
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1067
|
+
const tuple = (0, _atscript_db.uniqueKeyTuple)(doc, fields);
|
|
1068
|
+
if (tuple !== void 0) lockedTuples[k].add(tuple);
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const locked = /* @__PURE__ */ new Set();
|
|
1072
|
+
rowTuples.forEach((tuples, i) => {
|
|
1073
|
+
if (tuples.some((t, k) => t !== void 0 && lockedTuples[k].has(t))) locked.add(i);
|
|
1074
|
+
});
|
|
1075
|
+
return locked;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1001
1078
|
* The key columns of a conflict-ignoring insert (built once per table):
|
|
1002
1079
|
* every primary / unique key set, their RETURNING list, and the key columns
|
|
1003
1080
|
* whose physical type is not text (see {@link mapIgnoredBatch}). Keyed on the
|
|
@@ -2284,6 +2361,10 @@ function createCustomTypes(pgTypes) {
|
|
|
2284
2361
|
return pgTypes.getTypeParser(oid, format);
|
|
2285
2362
|
} };
|
|
2286
2363
|
}
|
|
2364
|
+
/** The warning logged for a pool client whose connection broke (`57P01`, `ECONNRESET`, …). */
|
|
2365
|
+
function lostConnectionMessage(err, where) {
|
|
2366
|
+
return `[atscript/db-postgres] ${where} pool connection lost (${err.code ?? err.name}): ${err.message}`;
|
|
2367
|
+
}
|
|
2287
2368
|
/**
|
|
2288
2369
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
2289
2370
|
*
|
|
@@ -2310,6 +2391,12 @@ function createCustomTypes(pgTypes) {
|
|
|
2310
2391
|
* const driver = new PgDriver(pool)
|
|
2311
2392
|
* ```
|
|
2312
2393
|
*
|
|
2394
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
2395
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
2396
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
2397
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
2398
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
2399
|
+
*
|
|
2313
2400
|
* Requires `pg` to be installed:
|
|
2314
2401
|
* ```bash
|
|
2315
2402
|
* pnpm add pg
|
|
@@ -2318,7 +2405,9 @@ function createCustomTypes(pgTypes) {
|
|
|
2318
2405
|
var PgDriver = class {
|
|
2319
2406
|
pool;
|
|
2320
2407
|
poolInit;
|
|
2321
|
-
|
|
2408
|
+
logger;
|
|
2409
|
+
constructor(poolOrConfig, options = {}) {
|
|
2410
|
+
this.logger = options.logger ?? console;
|
|
2322
2411
|
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
2323
2412
|
else this.poolInit = import("pg").then((pg) => {
|
|
2324
2413
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
@@ -2332,6 +2421,7 @@ var PgDriver = class {
|
|
|
2332
2421
|
...poolOrConfig,
|
|
2333
2422
|
types: customTypes
|
|
2334
2423
|
});
|
|
2424
|
+
this.pool.on("error", (err) => this.logger.warn(lostConnectionMessage(err, "idle")));
|
|
2335
2425
|
return this.pool;
|
|
2336
2426
|
});
|
|
2337
2427
|
}
|
|
@@ -2356,6 +2446,13 @@ var PgDriver = class {
|
|
|
2356
2446
|
}
|
|
2357
2447
|
async getConnection() {
|
|
2358
2448
|
const client = await (await this.getPool()).connect();
|
|
2449
|
+
let lost;
|
|
2450
|
+
const onError = (err) => {
|
|
2451
|
+
if (lost) return;
|
|
2452
|
+
lost = err;
|
|
2453
|
+
this.logger.warn(lostConnectionMessage(err, "checked-out"));
|
|
2454
|
+
};
|
|
2455
|
+
client.on("error", onError);
|
|
2359
2456
|
return {
|
|
2360
2457
|
async run(sql, params) {
|
|
2361
2458
|
const result = await client.query(sql, sanitizeParams(params));
|
|
@@ -2374,7 +2471,8 @@ var PgDriver = class {
|
|
|
2374
2471
|
await client.query(sql);
|
|
2375
2472
|
},
|
|
2376
2473
|
release() {
|
|
2377
|
-
client.
|
|
2474
|
+
client.removeListener("error", onError);
|
|
2475
|
+
client.release(lost);
|
|
2378
2476
|
}
|
|
2379
2477
|
};
|
|
2380
2478
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
3
3
|
import { TFilterVisitorOptions, TSqlFragment } from "@atscript/db-sql-tools";
|
|
4
4
|
|
|
@@ -176,6 +176,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
176
176
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
177
177
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
178
178
|
supportsAggregateExpressions(): boolean;
|
|
179
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
180
|
+
supportsNullsPlacement(): boolean;
|
|
179
181
|
/** Computed view columns and first-row joins. */
|
|
180
182
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
181
183
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -196,9 +198,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
196
198
|
* PostgreSQL uses SQLSTATE codes:
|
|
197
199
|
* - 23505 = unique_violation
|
|
198
200
|
* - 23503 = foreign_key_violation
|
|
201
|
+
* - 40P01 = deadlock_detected → `DEADLOCK`
|
|
202
|
+
* - 55P03 = lock_not_available (`lock_timeout`, `NOWAIT`) → `LOCK_TIMEOUT`
|
|
203
|
+
* - 40001 = serialization_failure → `SERIALIZATION_FAILURE`
|
|
199
204
|
*/
|
|
200
205
|
private _wrapConstraintError;
|
|
201
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
206
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock timeout, else as is. */
|
|
202
207
|
private _mapConstraintError;
|
|
203
208
|
private _extractFieldFromConstraint;
|
|
204
209
|
private _mapFkError;
|
|
@@ -216,8 +221,20 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
216
221
|
* with the same `ON CONFLICT DO NOTHING`, so the result is always exact. A
|
|
217
222
|
* one-row chunk (or a table without keys) maps exactly by construction, so
|
|
218
223
|
* it needs no savepoint — and a single such chunk no transaction either.
|
|
224
|
+
* `ON CONFLICT DO NOTHING` takes no lock on a stored conflicting row, so a
|
|
225
|
+
* caller's transaction is left without any. `opts.lockConflicts` (since
|
|
226
|
+
* 0.1.153) locks them: one `SELECT … FOR UPDATE` of the stored rows matching
|
|
227
|
+
* the batch's keys, in primary-key order, BEFORE the insert, and one more
|
|
228
|
+
* for the skipped rows a concurrent writer committed in between.
|
|
219
229
|
*/
|
|
220
|
-
insertManyIgnore(data: Array<Record<string, unknown
|
|
230
|
+
insertManyIgnore(data: Array<Record<string, unknown>>, opts?: TDbInsertIgnoreOptions): Promise<TDbInsertIgnoreSlot[]>;
|
|
231
|
+
/**
|
|
232
|
+
* `SELECT <key columns> … FOR UPDATE` of the stored rows sharing a primary /
|
|
233
|
+
* unique key tuple with one of `rows`, ordered by the primary key (so
|
|
234
|
+
* concurrent lockers queue instead of deadlocking); PostgreSQL locks only
|
|
235
|
+
* the rows returned. Returns the indices of `rows` a locked row matched.
|
|
236
|
+
*/
|
|
237
|
+
private _lockKeyedRows;
|
|
221
238
|
/**
|
|
222
239
|
* The key columns of a conflict-ignoring insert (built once per table):
|
|
223
240
|
* every primary / unique key set, their RETURNING list, and the key columns
|
|
@@ -425,6 +442,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
425
442
|
}
|
|
426
443
|
//#endregion
|
|
427
444
|
//#region src/pg-driver.d.ts
|
|
445
|
+
/** Options of {@link PgDriver}. */
|
|
446
|
+
interface TPgDriverOptions {
|
|
447
|
+
/**
|
|
448
|
+
* Receives a warning when a pool connection breaks — a terminated backend,
|
|
449
|
+
* failover or network reset (default `console`). See {@link PgDriver}.
|
|
450
|
+
*/
|
|
451
|
+
logger?: Pick<TGenericLogger, "warn">;
|
|
452
|
+
}
|
|
428
453
|
/**
|
|
429
454
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
430
455
|
*
|
|
@@ -451,6 +476,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
451
476
|
* const driver = new PgDriver(pool)
|
|
452
477
|
* ```
|
|
453
478
|
*
|
|
479
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
480
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
481
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
482
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
483
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
484
|
+
*
|
|
454
485
|
* Requires `pg` to be installed:
|
|
455
486
|
* ```bash
|
|
456
487
|
* pnpm add pg
|
|
@@ -459,7 +490,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
459
490
|
declare class PgDriver implements TPgDriver {
|
|
460
491
|
private pool;
|
|
461
492
|
private poolInit;
|
|
462
|
-
|
|
493
|
+
private readonly logger;
|
|
494
|
+
constructor(poolOrConfig: string | import("pg").Pool | import("pg").PoolConfig, options?: TPgDriverOptions);
|
|
463
495
|
private getPool;
|
|
464
496
|
run(sql: string, params?: unknown[]): Promise<TPgRunResult>;
|
|
465
497
|
all<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
@@ -500,4 +532,4 @@ declare function buildWhere(filter: FilterExpr, opts?: TFilterVisitorOptions): T
|
|
|
500
532
|
*/
|
|
501
533
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
502
534
|
//#endregion
|
|
503
|
-
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
|
535
|
+
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgDriverOptions, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TFilterVisitorOptions, TSqlFragment } from "@atscript/db-sql-tools";
|
|
3
3
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
4
4
|
|
|
@@ -176,6 +176,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
176
176
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
177
177
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
178
178
|
supportsAggregateExpressions(): boolean;
|
|
179
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
180
|
+
supportsNullsPlacement(): boolean;
|
|
179
181
|
/** Computed view columns and first-row joins. */
|
|
180
182
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
181
183
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -196,9 +198,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
196
198
|
* PostgreSQL uses SQLSTATE codes:
|
|
197
199
|
* - 23505 = unique_violation
|
|
198
200
|
* - 23503 = foreign_key_violation
|
|
201
|
+
* - 40P01 = deadlock_detected → `DEADLOCK`
|
|
202
|
+
* - 55P03 = lock_not_available (`lock_timeout`, `NOWAIT`) → `LOCK_TIMEOUT`
|
|
203
|
+
* - 40001 = serialization_failure → `SERIALIZATION_FAILURE`
|
|
199
204
|
*/
|
|
200
205
|
private _wrapConstraintError;
|
|
201
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
206
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock timeout, else as is. */
|
|
202
207
|
private _mapConstraintError;
|
|
203
208
|
private _extractFieldFromConstraint;
|
|
204
209
|
private _mapFkError;
|
|
@@ -216,8 +221,20 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
216
221
|
* with the same `ON CONFLICT DO NOTHING`, so the result is always exact. A
|
|
217
222
|
* one-row chunk (or a table without keys) maps exactly by construction, so
|
|
218
223
|
* it needs no savepoint — and a single such chunk no transaction either.
|
|
224
|
+
* `ON CONFLICT DO NOTHING` takes no lock on a stored conflicting row, so a
|
|
225
|
+
* caller's transaction is left without any. `opts.lockConflicts` (since
|
|
226
|
+
* 0.1.153) locks them: one `SELECT … FOR UPDATE` of the stored rows matching
|
|
227
|
+
* the batch's keys, in primary-key order, BEFORE the insert, and one more
|
|
228
|
+
* for the skipped rows a concurrent writer committed in between.
|
|
219
229
|
*/
|
|
220
|
-
insertManyIgnore(data: Array<Record<string, unknown
|
|
230
|
+
insertManyIgnore(data: Array<Record<string, unknown>>, opts?: TDbInsertIgnoreOptions): Promise<TDbInsertIgnoreSlot[]>;
|
|
231
|
+
/**
|
|
232
|
+
* `SELECT <key columns> … FOR UPDATE` of the stored rows sharing a primary /
|
|
233
|
+
* unique key tuple with one of `rows`, ordered by the primary key (so
|
|
234
|
+
* concurrent lockers queue instead of deadlocking); PostgreSQL locks only
|
|
235
|
+
* the rows returned. Returns the indices of `rows` a locked row matched.
|
|
236
|
+
*/
|
|
237
|
+
private _lockKeyedRows;
|
|
221
238
|
/**
|
|
222
239
|
* The key columns of a conflict-ignoring insert (built once per table):
|
|
223
240
|
* every primary / unique key set, their RETURNING list, and the key columns
|
|
@@ -425,6 +442,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
425
442
|
}
|
|
426
443
|
//#endregion
|
|
427
444
|
//#region src/pg-driver.d.ts
|
|
445
|
+
/** Options of {@link PgDriver}. */
|
|
446
|
+
interface TPgDriverOptions {
|
|
447
|
+
/**
|
|
448
|
+
* Receives a warning when a pool connection breaks — a terminated backend,
|
|
449
|
+
* failover or network reset (default `console`). See {@link PgDriver}.
|
|
450
|
+
*/
|
|
451
|
+
logger?: Pick<TGenericLogger, "warn">;
|
|
452
|
+
}
|
|
428
453
|
/**
|
|
429
454
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
430
455
|
*
|
|
@@ -451,6 +476,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
451
476
|
* const driver = new PgDriver(pool)
|
|
452
477
|
* ```
|
|
453
478
|
*
|
|
479
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
480
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
481
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
482
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
483
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
484
|
+
*
|
|
454
485
|
* Requires `pg` to be installed:
|
|
455
486
|
* ```bash
|
|
456
487
|
* pnpm add pg
|
|
@@ -459,7 +490,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
459
490
|
declare class PgDriver implements TPgDriver {
|
|
460
491
|
private pool;
|
|
461
492
|
private poolInit;
|
|
462
|
-
|
|
493
|
+
private readonly logger;
|
|
494
|
+
constructor(poolOrConfig: string | import("pg").Pool | import("pg").PoolConfig, options?: TPgDriverOptions);
|
|
463
495
|
private getPool;
|
|
464
496
|
run(sql: string, params?: unknown[]): Promise<TPgRunResult>;
|
|
465
497
|
all<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
@@ -500,4 +532,4 @@ declare function buildWhere(filter: FilterExpr, opts?: TFilterVisitorOptions): T
|
|
|
500
532
|
*/
|
|
501
533
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
502
534
|
//#endregion
|
|
503
|
-
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
|
535
|
+
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgDriverOptions, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.mjs
CHANGED
|
@@ -191,6 +191,7 @@ const pgDialect = {
|
|
|
191
191
|
return zone === void 0 ? void 0 : bucketTimeZoneUnavailable(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
|
|
192
192
|
},
|
|
193
193
|
nullsSortLargest: true,
|
|
194
|
+
nullsPlacementSyntax: true,
|
|
194
195
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
195
196
|
paramPlaceholder(index) {
|
|
196
197
|
return `$${index}`;
|
|
@@ -569,6 +570,12 @@ function parseCount(value) {
|
|
|
569
570
|
/** The suffix PostgreSQL gives an auto-named constraint of each `pg_constraint.contype`. */
|
|
570
571
|
/** Savepoint guarding one `insertManyIgnore` chunk (see {@link PostgresAdapter.insertManyIgnore}). */
|
|
571
572
|
const IGNORE_SAVEPOINT = "atscript_ignore_chunk";
|
|
573
|
+
/** Retryable concurrency SQLSTATEs (see `PostgresAdapter._mapConstraintError`). */
|
|
574
|
+
const PG_CONTENTION_CODES = {
|
|
575
|
+
"40P01": "DEADLOCK",
|
|
576
|
+
"55P03": "LOCK_TIMEOUT",
|
|
577
|
+
"40001": "SERIALIZATION_FAILURE"
|
|
578
|
+
};
|
|
572
579
|
const PG_CONSTRAINT_LABELS = {
|
|
573
580
|
p: "pkey",
|
|
574
581
|
u: "key",
|
|
@@ -787,6 +794,10 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
787
794
|
supportsAggregateExpressions() {
|
|
788
795
|
return true;
|
|
789
796
|
}
|
|
797
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
798
|
+
supportsNullsPlacement() {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
790
801
|
/** Computed view columns and first-row joins. */
|
|
791
802
|
viewCapabilities() {
|
|
792
803
|
return ALL_VIEW_CAPABILITIES;
|
|
@@ -864,6 +875,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
864
875
|
* PostgreSQL uses SQLSTATE codes:
|
|
865
876
|
* - 23505 = unique_violation
|
|
866
877
|
* - 23503 = foreign_key_violation
|
|
878
|
+
* - 40P01 = deadlock_detected → `DEADLOCK`
|
|
879
|
+
* - 55P03 = lock_not_available (`lock_timeout`, `NOWAIT`) → `LOCK_TIMEOUT`
|
|
880
|
+
* - 40001 = serialization_failure → `SERIALIZATION_FAILURE`
|
|
867
881
|
*/
|
|
868
882
|
async _wrapConstraintError(fn) {
|
|
869
883
|
try {
|
|
@@ -872,7 +886,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
872
886
|
return this._mapConstraintError(error);
|
|
873
887
|
}
|
|
874
888
|
}
|
|
875
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
889
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock timeout, else as is. */
|
|
876
890
|
_mapConstraintError(error) {
|
|
877
891
|
if (error && typeof error === "object" && "code" in error) {
|
|
878
892
|
const err = error;
|
|
@@ -881,6 +895,11 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
881
895
|
message: err.detail ?? err.message
|
|
882
896
|
}]);
|
|
883
897
|
if (err.code === "23503") throw new DbError("FK_VIOLATION", this._mapFkError(err.detail ?? err.message, err.constraint));
|
|
898
|
+
const contention = PG_CONTENTION_CODES[err.code];
|
|
899
|
+
if (contention) throw new DbError(contention, [{
|
|
900
|
+
path: "",
|
|
901
|
+
message: err.message
|
|
902
|
+
}]);
|
|
884
903
|
}
|
|
885
904
|
throw error;
|
|
886
905
|
}
|
|
@@ -958,9 +977,21 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
958
977
|
* with the same `ON CONFLICT DO NOTHING`, so the result is always exact. A
|
|
959
978
|
* one-row chunk (or a table without keys) maps exactly by construction, so
|
|
960
979
|
* it needs no savepoint — and a single such chunk no transaction either.
|
|
980
|
+
* `ON CONFLICT DO NOTHING` takes no lock on a stored conflicting row, so a
|
|
981
|
+
* caller's transaction is left without any. `opts.lockConflicts` (since
|
|
982
|
+
* 0.1.153) locks them: one `SELECT … FOR UPDATE` of the stored rows matching
|
|
983
|
+
* the batch's keys, in primary-key order, BEFORE the insert, and one more
|
|
984
|
+
* for the skipped rows a concurrent writer committed in between.
|
|
961
985
|
*/
|
|
962
|
-
async insertManyIgnore(data) {
|
|
986
|
+
async insertManyIgnore(data, opts) {
|
|
963
987
|
if (data.length === 0) return [];
|
|
988
|
+
if (opts?.lockConflicts && this._table.uniqueKeySets.length > 0) return this.withTransaction(async () => {
|
|
989
|
+
const locked = await this._lockKeyedRows(data);
|
|
990
|
+
const slots = await this.insertManyIgnore(data);
|
|
991
|
+
const raced = data.filter((_, i) => slots[i] === null && !locked.has(i));
|
|
992
|
+
if (raced.length > 0) await this._lockKeyedRows(raced);
|
|
993
|
+
return slots;
|
|
994
|
+
});
|
|
964
995
|
const plan = this._ignorePlan();
|
|
965
996
|
const { columns, batches } = chunkInsertRows(data);
|
|
966
997
|
const needsSavepoint = (batch) => batch.length > 1 && plan.returning.length > 0;
|
|
@@ -997,6 +1028,52 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
997
1028
|
return batches.length === 1 && !needsSavepoint(batches[0]) ? run() : this.withTransaction(run);
|
|
998
1029
|
}
|
|
999
1030
|
/**
|
|
1031
|
+
* `SELECT <key columns> … FOR UPDATE` of the stored rows sharing a primary /
|
|
1032
|
+
* unique key tuple with one of `rows`, ordered by the primary key (so
|
|
1033
|
+
* concurrent lockers queue instead of deadlocking); PostgreSQL locks only
|
|
1034
|
+
* the rows returned. Returns the indices of `rows` a locked row matched.
|
|
1035
|
+
*/
|
|
1036
|
+
async _lockKeyedRows(rows) {
|
|
1037
|
+
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1038
|
+
const rowTuples = rows.map((row) => keySets.map((fields) => uniqueKeyTuple(row, fields)));
|
|
1039
|
+
const order = (this._pk().cols.length > 0 ? this._pk().cols : keySets[0]).map((c) => qi(c)).join(", ");
|
|
1040
|
+
const selectCols = [...new Set(keySets.flat())].map((c) => qi(c)).join(", ");
|
|
1041
|
+
const lockedTuples = keySets.map(() => /* @__PURE__ */ new Set());
|
|
1042
|
+
const width = keySets.reduce((n, f) => n + f.length, 0);
|
|
1043
|
+
const sliceSize = Math.max(1, Math.floor(3e4 / width));
|
|
1044
|
+
for (let offset = 0; offset < rows.length; offset += sliceSize) {
|
|
1045
|
+
const end = Math.min(rows.length, offset + sliceSize);
|
|
1046
|
+
const clauses = [];
|
|
1047
|
+
const params = [];
|
|
1048
|
+
const ph = (value) => {
|
|
1049
|
+
params.push(pgDialect.toValue(value));
|
|
1050
|
+
return `$${params.length}`;
|
|
1051
|
+
};
|
|
1052
|
+
keySets.forEach((fields, k) => {
|
|
1053
|
+
const tuples = [];
|
|
1054
|
+
for (let i = offset; i < end; i++) {
|
|
1055
|
+
if (rowTuples[i][k] === void 0) continue;
|
|
1056
|
+
const row = rows[i];
|
|
1057
|
+
tuples.push(`(${fields.map((f) => ph(row[f])).join(", ")})`);
|
|
1058
|
+
}
|
|
1059
|
+
if (tuples.length > 0) clauses.push(`(${fields.map((f) => qi(f)).join(", ")}) IN (${tuples.join(", ")})`);
|
|
1060
|
+
});
|
|
1061
|
+
if (clauses.length === 0) continue;
|
|
1062
|
+
const sql = `SELECT ${selectCols} FROM ${quoteTableName(this.resolveTableName())} WHERE ${clauses.join(" OR ")} ORDER BY ${order} FOR UPDATE`;
|
|
1063
|
+
this._log(sql, params);
|
|
1064
|
+
const found = await this._wrapConstraintError(() => this._exec().all(sql, params));
|
|
1065
|
+
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1066
|
+
const tuple = uniqueKeyTuple(doc, fields);
|
|
1067
|
+
if (tuple !== void 0) lockedTuples[k].add(tuple);
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
const locked = /* @__PURE__ */ new Set();
|
|
1071
|
+
rowTuples.forEach((tuples, i) => {
|
|
1072
|
+
if (tuples.some((t, k) => t !== void 0 && lockedTuples[k].has(t))) locked.add(i);
|
|
1073
|
+
});
|
|
1074
|
+
return locked;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1000
1077
|
* The key columns of a conflict-ignoring insert (built once per table):
|
|
1001
1078
|
* every primary / unique key set, their RETURNING list, and the key columns
|
|
1002
1079
|
* whose physical type is not text (see {@link mapIgnoredBatch}). Keyed on the
|
|
@@ -2283,6 +2360,10 @@ function createCustomTypes(pgTypes) {
|
|
|
2283
2360
|
return pgTypes.getTypeParser(oid, format);
|
|
2284
2361
|
} };
|
|
2285
2362
|
}
|
|
2363
|
+
/** The warning logged for a pool client whose connection broke (`57P01`, `ECONNRESET`, …). */
|
|
2364
|
+
function lostConnectionMessage(err, where) {
|
|
2365
|
+
return `[atscript/db-postgres] ${where} pool connection lost (${err.code ?? err.name}): ${err.message}`;
|
|
2366
|
+
}
|
|
2286
2367
|
/**
|
|
2287
2368
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
2288
2369
|
*
|
|
@@ -2309,6 +2390,12 @@ function createCustomTypes(pgTypes) {
|
|
|
2309
2390
|
* const driver = new PgDriver(pool)
|
|
2310
2391
|
* ```
|
|
2311
2392
|
*
|
|
2393
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
2394
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
2395
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
2396
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
2397
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
2398
|
+
*
|
|
2312
2399
|
* Requires `pg` to be installed:
|
|
2313
2400
|
* ```bash
|
|
2314
2401
|
* pnpm add pg
|
|
@@ -2317,7 +2404,9 @@ function createCustomTypes(pgTypes) {
|
|
|
2317
2404
|
var PgDriver = class {
|
|
2318
2405
|
pool;
|
|
2319
2406
|
poolInit;
|
|
2320
|
-
|
|
2407
|
+
logger;
|
|
2408
|
+
constructor(poolOrConfig, options = {}) {
|
|
2409
|
+
this.logger = options.logger ?? console;
|
|
2321
2410
|
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
2322
2411
|
else this.poolInit = import("pg").then((pg) => {
|
|
2323
2412
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
@@ -2331,6 +2420,7 @@ var PgDriver = class {
|
|
|
2331
2420
|
...poolOrConfig,
|
|
2332
2421
|
types: customTypes
|
|
2333
2422
|
});
|
|
2423
|
+
this.pool.on("error", (err) => this.logger.warn(lostConnectionMessage(err, "idle")));
|
|
2334
2424
|
return this.pool;
|
|
2335
2425
|
});
|
|
2336
2426
|
}
|
|
@@ -2355,6 +2445,13 @@ var PgDriver = class {
|
|
|
2355
2445
|
}
|
|
2356
2446
|
async getConnection() {
|
|
2357
2447
|
const client = await (await this.getPool()).connect();
|
|
2448
|
+
let lost;
|
|
2449
|
+
const onError = (err) => {
|
|
2450
|
+
if (lost) return;
|
|
2451
|
+
lost = err;
|
|
2452
|
+
this.logger.warn(lostConnectionMessage(err, "checked-out"));
|
|
2453
|
+
};
|
|
2454
|
+
client.on("error", onError);
|
|
2358
2455
|
return {
|
|
2359
2456
|
async run(sql, params) {
|
|
2360
2457
|
const result = await client.query(sql, sanitizeParams(params));
|
|
@@ -2373,7 +2470,8 @@ var PgDriver = class {
|
|
|
2373
2470
|
await client.query(sql);
|
|
2374
2471
|
},
|
|
2375
2472
|
release() {
|
|
2376
|
-
client.
|
|
2473
|
+
client.removeListener("error", onError);
|
|
2474
|
+
client.release(lost);
|
|
2377
2475
|
}
|
|
2378
2476
|
};
|
|
2379
2477
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.154",
|
|
4
4
|
"description": "PostgreSQL adapter for @atscript/db with pg (node-postgres) driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -43,20 +43,20 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@atscript/core": "^0.1.
|
|
47
|
-
"@atscript/typescript": "^0.1.
|
|
46
|
+
"@atscript/core": "^0.1.103",
|
|
47
|
+
"@atscript/typescript": "^0.1.103",
|
|
48
48
|
"@types/pg": "^8.11.0",
|
|
49
|
-
"@uniqu/core": "^0.1.
|
|
49
|
+
"@uniqu/core": "^0.1.15",
|
|
50
50
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.1.
|
|
51
|
+
"unplugin-atscript": "^0.1.103"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1.
|
|
55
|
-
"@atscript/typescript": "^0.1.
|
|
56
|
-
"@uniqu/core": "^0.1.
|
|
54
|
+
"@atscript/core": "^0.1.103",
|
|
55
|
+
"@atscript/typescript": "^0.1.103",
|
|
56
|
+
"@uniqu/core": "^0.1.15",
|
|
57
57
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db": "^0.1.
|
|
59
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
58
|
+
"@atscript/db": "^0.1.154",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.154"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|