@atscript/db-postgres 0.1.151 → 0.1.153
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 +79 -2
- package/dist/index.d.cts +20 -3
- package/dist/index.d.mts +20 -3
- package/dist/index.mjs +79 -2
- 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
|
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, 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
|
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, 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
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.153",
|
|
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
|
|
58
|
+
"@atscript/db-sql-tools": "^0.1.153",
|
|
59
|
+
"@atscript/db": "^0.1.153"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|