@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 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>>): Promise<TDbInsertIgnoreSlot[]>;
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>>): Promise<TDbInsertIgnoreSlot[]>;
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.151",
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.102",
47
- "@atscript/typescript": "^0.1.102",
46
+ "@atscript/core": "^0.1.103",
47
+ "@atscript/typescript": "^0.1.103",
48
48
  "@types/pg": "^8.11.0",
49
- "@uniqu/core": "^0.1.14",
49
+ "@uniqu/core": "^0.1.15",
50
50
  "pg": "^8.13.0",
51
- "unplugin-atscript": "^0.1.102"
51
+ "unplugin-atscript": "^0.1.103"
52
52
  },
53
53
  "peerDependencies": {
54
- "@atscript/core": "^0.1.102",
55
- "@atscript/typescript": "^0.1.102",
56
- "@uniqu/core": "^0.1.14",
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.151",
59
- "@atscript/db-sql-tools": "^0.1.151"
58
+ "@atscript/db-sql-tools": "^0.1.153",
59
+ "@atscript/db": "^0.1.153"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "pg": {