@atscript/db-postgres 0.1.147 → 0.1.148
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 +195 -38
- package/dist/index.d.cts +20 -10
- package/dist/index.d.mts +20 -10
- package/dist/index.mjs +197 -40
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,77 @@ let _atscript_db = require("@atscript/db");
|
|
|
3
3
|
let _atscript_db_agg = require("@atscript/db/agg");
|
|
4
4
|
let _atscript_db_sql_tools = require("@atscript/db-sql-tools");
|
|
5
5
|
let _uniqu_core = require("@uniqu/core");
|
|
6
|
+
//#region src/insert-ignore.ts
|
|
7
|
+
/**
|
|
8
|
+
* Whether two key values are the same value written differently: a number
|
|
9
|
+
* and its decimal text (`5` / `"5.00"` of a NUMERIC column), a string and its
|
|
10
|
+
* blank-padded form (`"ab"` / `"ab "` of a CHAR(n) column), or in another
|
|
11
|
+
* letter case (`uuid`, `citext`).
|
|
12
|
+
*/
|
|
13
|
+
function looselySame(a, b) {
|
|
14
|
+
const textA = typeof a === "number" || typeof a === "string" ? `${a}` : void 0;
|
|
15
|
+
const textB = typeof b === "number" || typeof b === "string" ? `${b}` : void 0;
|
|
16
|
+
if (textA === void 0 || textB === void 0) return false;
|
|
17
|
+
const na = Number(textA);
|
|
18
|
+
const nb = Number(textB);
|
|
19
|
+
if (textA.trim() !== "" && textB.trim() !== "" && Number.isFinite(na) && na === nb) return true;
|
|
20
|
+
return typeof a === "string" && typeof b === "string" && a.trimEnd().toLowerCase() === b.trimEnd().toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
/** A key value the server may store in another form than it was sent (NUMERIC / decimal rounding). */
|
|
23
|
+
function mayNormalize(value, col, formCols) {
|
|
24
|
+
if (typeof value === "string" && col !== void 0 && formCols?.has(col)) return true;
|
|
25
|
+
return typeof value === "number" && !Number.isInteger(value) || typeof value === "string" && /^-?\d+\.\d+$/.test(value);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Maps the rows a batched `INSERT … ON CONFLICT DO NOTHING RETURNING <keys>`
|
|
29
|
+
* returned back to the batch's input rows.
|
|
30
|
+
*
|
|
31
|
+
* PostgreSQL returns the inserted rows in VALUES order and omits skipped ones,
|
|
32
|
+
* so `returned` is an in-order subsequence of `batch`. A row is inserted
|
|
33
|
+
* exactly when the next returned row carries EVERY key set the row fully
|
|
34
|
+
* defines (key sets with an absent / null input component are never compared:
|
|
35
|
+
* a generated key's value is unknown to the input). Result: per input row, the
|
|
36
|
+
* index of its returned row, or `-1` when it was skipped.
|
|
37
|
+
*
|
|
38
|
+
* `undefined` when the mapping is not provably exact — a row that defines no
|
|
39
|
+
* complete key set (it may match a returned row only by a generated value), a
|
|
40
|
+
* skipped-looking row whose key a server could have normalized (non-integer
|
|
41
|
+
* numbers / decimals), a key equal to the returned one only when written
|
|
42
|
+
* differently (`5` / `"5.00"`, blank-padded CHAR, a `uuid` / `citext` in another
|
|
43
|
+
* letter case — the server lower-cases a returned uuid, so `"AAA"` / `"aaa"` could
|
|
44
|
+
* swap the mapping), or returned rows left unaccounted for (counts or keys that
|
|
45
|
+
* do not line up with an in-order subsequence). The caller must then redo the
|
|
46
|
+
* chunk row by row. An empty `returned` (everything skipped) and a fully-returned
|
|
47
|
+
* batch (everything inserted) are exact without any matching.
|
|
48
|
+
*
|
|
49
|
+
* @param keySets Physical column names of the primary key and every unique index.
|
|
50
|
+
* @param formCols Key columns whose physical type is not text (uuid, inet, cidr,
|
|
51
|
+
* macaddr, …, `@db.pg.type` overrides): a string key there may come back
|
|
52
|
+
* normalized, so a skipped-looking row of such a key is ambiguous.
|
|
53
|
+
*/
|
|
54
|
+
function mapIgnoredBatch(batch, returned, keySets, formCols) {
|
|
55
|
+
if (returned.length === 0) return batch.map(() => -1);
|
|
56
|
+
if (returned.length === batch.length) return batch.map((_, i) => i);
|
|
57
|
+
const out = [];
|
|
58
|
+
let next = 0;
|
|
59
|
+
for (const row of batch) {
|
|
60
|
+
const tuples = keySets.map((cols) => (0, _atscript_db.uniqueKeyTuple)(row, cols));
|
|
61
|
+
if (tuples.every((t) => t === void 0)) return void 0;
|
|
62
|
+
const candidate = returned[next];
|
|
63
|
+
if (candidate !== void 0) {
|
|
64
|
+
const strict = keySets.map((cols, k) => tuples[k] === void 0 || tuples[k] === (0, _atscript_db.uniqueKeyTuple)(candidate, cols));
|
|
65
|
+
if (strict.every(Boolean)) {
|
|
66
|
+
out.push(next++);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (keySets.every((cols, k) => tuples[k] === void 0 || strict[k] || cols.every((c) => looselySame(row[c], candidate[c])))) return void 0;
|
|
70
|
+
}
|
|
71
|
+
if (keySets.some((cols, k) => tuples[k] !== void 0 && cols.some((c) => mayNormalize(row[c], c, formCols)))) return;
|
|
72
|
+
out.push(-1);
|
|
73
|
+
}
|
|
74
|
+
return next === returned.length ? out : void 0;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
6
77
|
//#region src/sql-builder.ts
|
|
7
78
|
/**
|
|
8
79
|
* PostgreSQL-aware default value for a given design type.
|
|
@@ -80,6 +151,25 @@ const pgDialect = {
|
|
|
80
151
|
calendarBucket: pgCalendarBucket,
|
|
81
152
|
jsonExtract: pgJsonExtract,
|
|
82
153
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE PRECISION)`,
|
|
154
|
+
anyValue: pgAnyValue,
|
|
155
|
+
booleanAggregates: {
|
|
156
|
+
min: "BOOL_AND",
|
|
157
|
+
max: "BOOL_OR"
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* SQLSTATE 22003 (`numeric_value_out_of_range`), a double overflow in
|
|
161
|
+
* aggregate arithmetic, is `INVALID_QUERY`; SQLSTATE 22023
|
|
162
|
+
* (`invalid_parameter_value`, `time zone "…" not recognized`), raised by a
|
|
163
|
+
* calendar bucket's `AT TIME ZONE '<tz>'`, is `BUCKET_TZ_UNAVAILABLE` — the
|
|
164
|
+
* zone passed the core's IANA validation, so the server's tz database is
|
|
165
|
+
* older than the runtime's.
|
|
166
|
+
*/
|
|
167
|
+
mapQueryError(error, arithmetic) {
|
|
168
|
+
const err = error;
|
|
169
|
+
if (err?.code === "22003") return arithmetic ? (0, _atscript_db_sql_tools.arithOverflowError)() : (0, _atscript_db_sql_tools.numericOutOfRangeError)();
|
|
170
|
+
const zone = err?.code === "22023" && typeof err.message === "string" ? /time zone "([^"]*)" not recognized/.exec(err.message)?.[1] : void 0;
|
|
171
|
+
return zone === void 0 ? void 0 : (0, _atscript_db.bucketTimeZoneUnavailable)(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
|
|
172
|
+
},
|
|
83
173
|
nullsSortLargest: true,
|
|
84
174
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
85
175
|
paramPlaceholder(index) {
|
|
@@ -257,6 +347,30 @@ function intTypeFromTags(tags) {
|
|
|
257
347
|
if (tags?.has("int64") || tags?.has("uint64")) return "BIGINT";
|
|
258
348
|
return "INTEGER";
|
|
259
349
|
}
|
|
350
|
+
/** Physical types `MIN` aggregates (base name, lower-cased): the streaming pick applies. */
|
|
351
|
+
const MIN_ORDERED_TYPE = /^(smallint|int2|integer|int|int4|bigint|int8|real|float4|double precision|float8|numeric|decimal|money|text|varchar|character varying|char|character|bpchar|date|time|timetz|timestamp|timestamptz|interval|time (with|without) time zone|timestamp (with|without) time zone)\b/;
|
|
352
|
+
/** Text key types: a value comes back in the form it was sent (modulo case for `citext`), so a returned key maps to its input row. */
|
|
353
|
+
const TEXT_KEY_TYPE = /^(text|varchar|character varying|char|character|bpchar|citext)\b/;
|
|
354
|
+
/** `true` when the physical column type `type` is a text type (see {@link TEXT_KEY_TYPE}). */
|
|
355
|
+
function isTextKeyType(type) {
|
|
356
|
+
return TEXT_KEY_TYPE.test(type.trim().toLowerCase());
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The "any value of the group" pick of a `first` / `last` derived column
|
|
360
|
+
* (constant within its group) over a PostgreSQL column: `MIN` (streaming — one
|
|
361
|
+
* accumulator per group) for the ordered types and for every array column
|
|
362
|
+
* (`MIN(anyarray)`; `ARRAY_AGG` over arrays builds a 2-D array and rejects NULL /
|
|
363
|
+
* empty ones), `BOOL_AND` for a boolean, and `(ARRAY_AGG(x))[1]` only where the
|
|
364
|
+
* physical type has no `MIN` (uuid, bytea, point, json / jsonb, citext,
|
|
365
|
+
* `@db.pg.type` overrides, …) or is unknown.
|
|
366
|
+
*/
|
|
367
|
+
function pgAnyValue(expr, field) {
|
|
368
|
+
const type = field ? pgTypeFromField(field).trim().toLowerCase() : "";
|
|
369
|
+
if (type.endsWith("[]")) return `MIN(${expr})`;
|
|
370
|
+
if (/^bool(ean)?\b/.test(type)) return `BOOL_AND(${expr})`;
|
|
371
|
+
if (MIN_ORDERED_TYPE.test(type)) return `MIN(${expr})`;
|
|
372
|
+
return `(ARRAY_AGG(${expr}))[1]`;
|
|
373
|
+
}
|
|
260
374
|
/**
|
|
261
375
|
* Maps an Atscript field descriptor to a PostgreSQL column type.
|
|
262
376
|
*
|
|
@@ -312,11 +426,11 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
312
426
|
continue;
|
|
313
427
|
}
|
|
314
428
|
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
315
|
-
if (options?.incrementFields?.has(field.
|
|
429
|
+
if (options?.incrementFields?.has(field.path)) {
|
|
316
430
|
const start = options.autoIncrementStart;
|
|
317
431
|
def += start !== void 0 ? ` GENERATED BY DEFAULT AS IDENTITY (START WITH ${start})` : " GENERATED BY DEFAULT AS IDENTITY";
|
|
318
432
|
}
|
|
319
|
-
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.
|
|
433
|
+
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.path)) def += " NOT NULL";
|
|
320
434
|
if (field.defaultValue?.kind === "value") def += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
321
435
|
else if (field.defaultValue?.kind === "fn") {
|
|
322
436
|
if (field.defaultValue.fn === "uuid") def += " DEFAULT gen_random_uuid()";
|
|
@@ -386,6 +500,8 @@ function parseCount(value) {
|
|
|
386
500
|
* ```
|
|
387
501
|
*/
|
|
388
502
|
/** The suffix PostgreSQL gives an auto-named constraint of each `pg_constraint.contype`. */
|
|
503
|
+
/** Savepoint guarding one `insertManyIgnore` chunk (see {@link PostgresAdapter.insertManyIgnore}). */
|
|
504
|
+
const IGNORE_SAVEPOINT = "atscript_ignore_chunk";
|
|
389
505
|
const PG_CONSTRAINT_LABELS = {
|
|
390
506
|
p: "pkey",
|
|
391
507
|
u: "key",
|
|
@@ -528,10 +644,14 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
528
644
|
calendarBucketUnits() {
|
|
529
645
|
return _atscript_db.ALL_BUCKET_UNITS;
|
|
530
646
|
}
|
|
531
|
-
/** Every aggregate function
|
|
647
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
532
648
|
aggregateFns() {
|
|
533
649
|
return _atscript_db.ALL_AGGREGATE_FNS;
|
|
534
650
|
}
|
|
651
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
652
|
+
supportsAggregateExpressions() {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
535
655
|
/** Computed view columns and first-row joins. */
|
|
536
656
|
viewCapabilities() {
|
|
537
657
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -601,16 +721,20 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
601
721
|
try {
|
|
602
722
|
return await fn();
|
|
603
723
|
} catch (error) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
724
|
+
return this._mapConstraintError(error);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
728
|
+
_mapConstraintError(error) {
|
|
729
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
730
|
+
const err = error;
|
|
731
|
+
if (err.code === "23505") throw new _atscript_db.DbError("CONFLICT", [{
|
|
732
|
+
path: this._extractFieldFromConstraint(err.constraint) ?? "",
|
|
733
|
+
message: err.detail ?? err.message
|
|
734
|
+
}]);
|
|
735
|
+
if (err.code === "23503") throw new _atscript_db.DbError("FK_VIOLATION", this._mapFkError(err.detail ?? err.message, err.constraint));
|
|
613
736
|
}
|
|
737
|
+
throw error;
|
|
614
738
|
}
|
|
615
739
|
_extractFieldFromConstraint(constraint) {
|
|
616
740
|
if (!constraint) return;
|
|
@@ -653,11 +777,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
653
777
|
const tableName = this.resolveTableName();
|
|
654
778
|
const pkCols = this._pkColumns();
|
|
655
779
|
const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
|
|
656
|
-
const columns = (0, _atscript_db_sql_tools.
|
|
657
|
-
const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
|
|
780
|
+
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
658
781
|
const allIds = [];
|
|
659
|
-
for (
|
|
660
|
-
const batch = data.slice(offset, offset + maxRowsPerBatch);
|
|
782
|
+
for (const batch of batches) {
|
|
661
783
|
const insert = buildInsertMany(tableName, batch, columns);
|
|
662
784
|
const sql = insert.sql + returningSuffix;
|
|
663
785
|
const params = insert.params;
|
|
@@ -674,6 +796,56 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
674
796
|
};
|
|
675
797
|
});
|
|
676
798
|
}
|
|
799
|
+
supportsInsertIgnore() {
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Batched `INSERT … VALUES (…), (…) ON CONFLICT DO NOTHING RETURNING <pk +
|
|
804
|
+
* unique key columns>`: one statement per chunk (a conflict never raises, so
|
|
805
|
+
* the surrounding transaction survives). Skipped rows are the ones missing
|
|
806
|
+
* from RETURNING — mapped back by key values, see {@link mapIgnoredBatch}.
|
|
807
|
+
* Each chunk runs inside a SAVEPOINT: when the mapping is ambiguous (a key
|
|
808
|
+
* the server returns in another form than it was sent) the chunk is rolled
|
|
809
|
+
* back to the savepoint and redone row by row with the same
|
|
810
|
+
* `ON CONFLICT DO NOTHING`, so the result is always exact.
|
|
811
|
+
*/
|
|
812
|
+
async insertManyIgnore(data) {
|
|
813
|
+
if (data.length === 0) return [];
|
|
814
|
+
return this.withTransaction(async () => {
|
|
815
|
+
const tableName = this.resolveTableName();
|
|
816
|
+
const pkCols = this._pkColumns();
|
|
817
|
+
const keySets = this._table.uniqueKeySets;
|
|
818
|
+
const returning = [...new Set(keySets.flat())];
|
|
819
|
+
const returningCols = new Set(returning);
|
|
820
|
+
const nonTextKeyCols = new Set(this._table.fieldDescriptors.filter((f) => returningCols.has(f.physicalName) && !isTextKeyType(this.typeMapper(f))).map((f) => f.physicalName));
|
|
821
|
+
const returningSuffix = returning.length > 0 ? ` RETURNING ${returning.map((c) => qi(c)).join(", ")}` : "";
|
|
822
|
+
const insertedId = (row, returned) => ({ insertedId: this._resolveInsertedId(row, pkCols.length > 0 ? returned?.[pkCols[0]] : void 0) });
|
|
823
|
+
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
824
|
+
const slots = [];
|
|
825
|
+
for (const batch of batches) {
|
|
826
|
+
const insert = buildInsertMany(tableName, batch, columns);
|
|
827
|
+
const sql = `${insert.sql} ON CONFLICT DO NOTHING${returningSuffix}`;
|
|
828
|
+
await this._exec().run(`SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
829
|
+
this._log(sql, insert.params);
|
|
830
|
+
const returned = (await this._wrapConstraintError(() => this._exec().run(sql, insert.params))).rows ?? [];
|
|
831
|
+
const mapping = returning.length === 0 ? batch.map((_, i) => i) : mapIgnoredBatch(batch, returned, keySets, nonTextKeyCols);
|
|
832
|
+
if (mapping) {
|
|
833
|
+
await this._exec().run(`RELEASE SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
834
|
+
mapping.forEach((hit, i) => slots.push(hit < 0 ? null : insertedId(batch[i], returned[hit])));
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
await this._exec().run(`ROLLBACK TO SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
838
|
+
for (const row of batch) {
|
|
839
|
+
const single = buildInsert(tableName, row);
|
|
840
|
+
const rowSql = `${single.sql} ON CONFLICT DO NOTHING${returningSuffix}`;
|
|
841
|
+
this._log(rowSql, single.params);
|
|
842
|
+
const rowResult = await this._wrapConstraintError(() => this._exec().run(rowSql, single.params));
|
|
843
|
+
slots.push(rowResult.rows?.length ? insertedId(row, rowResult.rows[0]) : null);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return slots;
|
|
847
|
+
});
|
|
848
|
+
}
|
|
677
849
|
async findOne(query) {
|
|
678
850
|
const where = buildWhere(query.filter);
|
|
679
851
|
const controls = {
|
|
@@ -716,28 +888,11 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
716
888
|
if (query.controls.$count) {
|
|
717
889
|
const { sql, params } = buildAggregateCount(tableName, where, query.controls);
|
|
718
890
|
this._log(sql, params);
|
|
719
|
-
return [{ count: parseCount((await
|
|
891
|
+
return [{ count: parseCount((await (0, _atscript_db_sql_tools.mapQueryErrors)(pgDialect, () => this._exec().get(sql, params), query.controls))?.count) }];
|
|
720
892
|
}
|
|
721
893
|
const { sql, params } = buildAggregateSelect(tableName, where, query.controls);
|
|
722
894
|
this._log(sql, params);
|
|
723
|
-
return
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
|
|
727
|
-
* (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
|
|
728
|
-
* calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
|
|
729
|
-
* zone passed the core's IANA validation, so the server's tz database is
|
|
730
|
-
* older than the runtime's. Other errors propagate unchanged.
|
|
731
|
-
*/
|
|
732
|
-
async _wrapBucketZoneError(fn) {
|
|
733
|
-
try {
|
|
734
|
-
return await fn();
|
|
735
|
-
} catch (error) {
|
|
736
|
-
const err = error;
|
|
737
|
-
const zone = err?.code === "22023" && typeof err.message === "string" ? /time zone "([^"]*)" not recognized/.exec(err.message)?.[1] : void 0;
|
|
738
|
-
if (zone !== void 0) throw (0, _atscript_db.bucketTimeZoneUnavailable)(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
|
|
739
|
-
throw error;
|
|
740
|
-
}
|
|
895
|
+
return (0, _atscript_db_sql_tools.mapQueryErrors)(pgDialect, () => this._exec().all(sql, params), query.controls);
|
|
741
896
|
}
|
|
742
897
|
/** Physical primary-key columns (`@db.column` renames applied). */
|
|
743
898
|
_pkColumns() {
|
|
@@ -1856,8 +2011,10 @@ var PgDriver = class {
|
|
|
1856
2011
|
}
|
|
1857
2012
|
};
|
|
1858
2013
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
2014
|
+
_closing;
|
|
2015
|
+
/** Idempotent: every call returns the first call's promise. */
|
|
2016
|
+
close() {
|
|
2017
|
+
return this._closing ??= Promise.resolve(this.getPool()).then((pool) => pool.end());
|
|
1861
2018
|
}
|
|
1862
2019
|
};
|
|
1863
2020
|
//#endregion
|
|
@@ -1874,7 +2031,7 @@ function createAdapter(uri, options) {
|
|
|
1874
2031
|
connectionString: uri,
|
|
1875
2032
|
...options
|
|
1876
2033
|
});
|
|
1877
|
-
return new _atscript_db.DbSpace(() => new PostgresAdapter(driver));
|
|
2034
|
+
return new _atscript_db.DbSpace(() => new PostgresAdapter(driver), { onClose: () => driver.close() });
|
|
1878
2035
|
}
|
|
1879
2036
|
//#endregion
|
|
1880
2037
|
exports.PgDriver = PgDriver;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, 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, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, 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
|
|
|
@@ -125,8 +125,10 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
125
125
|
* server's tz database (an unknown one maps to `BUCKET_TZ_UNAVAILABLE`).
|
|
126
126
|
*/
|
|
127
127
|
calendarBucketUnits(): ReadonlySet<BucketUnit>;
|
|
128
|
-
/** Every aggregate function
|
|
128
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
129
129
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
130
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
131
|
+
supportsAggregateExpressions(): boolean;
|
|
130
132
|
/** Computed view columns and first-row joins. */
|
|
131
133
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
132
134
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -149,10 +151,24 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
149
151
|
* - 23503 = foreign_key_violation
|
|
150
152
|
*/
|
|
151
153
|
private _wrapConstraintError;
|
|
154
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
155
|
+
private _mapConstraintError;
|
|
152
156
|
private _extractFieldFromConstraint;
|
|
153
157
|
private _mapFkError;
|
|
154
158
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
155
159
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
160
|
+
supportsInsertIgnore(): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Batched `INSERT … VALUES (…), (…) ON CONFLICT DO NOTHING RETURNING <pk +
|
|
163
|
+
* unique key columns>`: one statement per chunk (a conflict never raises, so
|
|
164
|
+
* the surrounding transaction survives). Skipped rows are the ones missing
|
|
165
|
+
* from RETURNING — mapped back by key values, see {@link mapIgnoredBatch}.
|
|
166
|
+
* Each chunk runs inside a SAVEPOINT: when the mapping is ambiguous (a key
|
|
167
|
+
* the server returns in another form than it was sent) the chunk is rolled
|
|
168
|
+
* back to the savepoint and redone row by row with the same
|
|
169
|
+
* `ON CONFLICT DO NOTHING`, so the result is always exact.
|
|
170
|
+
*/
|
|
171
|
+
insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
|
|
156
172
|
findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
|
|
157
173
|
findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
|
|
158
174
|
/**
|
|
@@ -162,14 +178,6 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
162
178
|
findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
|
|
163
179
|
count(query: DbQuery): Promise<number>;
|
|
164
180
|
aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
|
|
165
|
-
/**
|
|
166
|
-
* Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
|
|
167
|
-
* (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
|
|
168
|
-
* calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
|
|
169
|
-
* zone passed the core's IANA validation, so the server's tz database is
|
|
170
|
-
* older than the runtime's. Other errors propagate unchanged.
|
|
171
|
-
*/
|
|
172
|
-
private _wrapBucketZoneError;
|
|
173
181
|
/** Physical primary-key columns (`@db.column` renames applied). */
|
|
174
182
|
private _pkColumns;
|
|
175
183
|
/**
|
|
@@ -345,6 +353,8 @@ declare class PgDriver implements TPgDriver {
|
|
|
345
353
|
get<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
|
|
346
354
|
exec(sql: string): Promise<void>;
|
|
347
355
|
getConnection(): Promise<TPgConnection>;
|
|
356
|
+
private _closing?;
|
|
357
|
+
/** Idempotent: every call returns the first call's promise. */
|
|
348
358
|
close(): Promise<void>;
|
|
349
359
|
}
|
|
350
360
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, 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, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, 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
|
|
|
@@ -125,8 +125,10 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
125
125
|
* server's tz database (an unknown one maps to `BUCKET_TZ_UNAVAILABLE`).
|
|
126
126
|
*/
|
|
127
127
|
calendarBucketUnits(): ReadonlySet<BucketUnit>;
|
|
128
|
-
/** Every aggregate function
|
|
128
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
129
129
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
130
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
131
|
+
supportsAggregateExpressions(): boolean;
|
|
130
132
|
/** Computed view columns and first-row joins. */
|
|
131
133
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
132
134
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -149,10 +151,24 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
149
151
|
* - 23503 = foreign_key_violation
|
|
150
152
|
*/
|
|
151
153
|
private _wrapConstraintError;
|
|
154
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
155
|
+
private _mapConstraintError;
|
|
152
156
|
private _extractFieldFromConstraint;
|
|
153
157
|
private _mapFkError;
|
|
154
158
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
155
159
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
160
|
+
supportsInsertIgnore(): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Batched `INSERT … VALUES (…), (…) ON CONFLICT DO NOTHING RETURNING <pk +
|
|
163
|
+
* unique key columns>`: one statement per chunk (a conflict never raises, so
|
|
164
|
+
* the surrounding transaction survives). Skipped rows are the ones missing
|
|
165
|
+
* from RETURNING — mapped back by key values, see {@link mapIgnoredBatch}.
|
|
166
|
+
* Each chunk runs inside a SAVEPOINT: when the mapping is ambiguous (a key
|
|
167
|
+
* the server returns in another form than it was sent) the chunk is rolled
|
|
168
|
+
* back to the savepoint and redone row by row with the same
|
|
169
|
+
* `ON CONFLICT DO NOTHING`, so the result is always exact.
|
|
170
|
+
*/
|
|
171
|
+
insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
|
|
156
172
|
findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
|
|
157
173
|
findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
|
|
158
174
|
/**
|
|
@@ -162,14 +178,6 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
162
178
|
findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
|
|
163
179
|
count(query: DbQuery): Promise<number>;
|
|
164
180
|
aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
|
|
165
|
-
/**
|
|
166
|
-
* Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
|
|
167
|
-
* (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
|
|
168
|
-
* calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
|
|
169
|
-
* zone passed the core's IANA validation, so the server's tz database is
|
|
170
|
-
* older than the runtime's. Other errors propagate unchanged.
|
|
171
|
-
*/
|
|
172
|
-
private _wrapBucketZoneError;
|
|
173
181
|
/** Physical primary-key columns (`@db.column` renames applied). */
|
|
174
182
|
private _pkColumns;
|
|
175
183
|
/**
|
|
@@ -345,6 +353,8 @@ declare class PgDriver implements TPgDriver {
|
|
|
345
353
|
get<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
|
|
346
354
|
exec(sql: string): Promise<void>;
|
|
347
355
|
getConnection(): Promise<TPgConnection>;
|
|
356
|
+
private _closing?;
|
|
357
|
+
/** Idempotent: every call returns the first call's promise. */
|
|
348
358
|
close(): Promise<void>;
|
|
349
359
|
}
|
|
350
360
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,78 @@
|
|
|
1
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, fkColumns, isColumnTypeChanged, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
1
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, fkColumns, isColumnTypeChanged, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
2
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
3
|
-
import { SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls,
|
|
3
|
+
import { SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, parseRegexString, quotedJsonPathSegments, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, vectorDistanceSource } from "@atscript/db-sql-tools";
|
|
4
4
|
import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
|
|
5
|
+
//#region src/insert-ignore.ts
|
|
6
|
+
/**
|
|
7
|
+
* Whether two key values are the same value written differently: a number
|
|
8
|
+
* and its decimal text (`5` / `"5.00"` of a NUMERIC column), a string and its
|
|
9
|
+
* blank-padded form (`"ab"` / `"ab "` of a CHAR(n) column), or in another
|
|
10
|
+
* letter case (`uuid`, `citext`).
|
|
11
|
+
*/
|
|
12
|
+
function looselySame(a, b) {
|
|
13
|
+
const textA = typeof a === "number" || typeof a === "string" ? `${a}` : void 0;
|
|
14
|
+
const textB = typeof b === "number" || typeof b === "string" ? `${b}` : void 0;
|
|
15
|
+
if (textA === void 0 || textB === void 0) return false;
|
|
16
|
+
const na = Number(textA);
|
|
17
|
+
const nb = Number(textB);
|
|
18
|
+
if (textA.trim() !== "" && textB.trim() !== "" && Number.isFinite(na) && na === nb) return true;
|
|
19
|
+
return typeof a === "string" && typeof b === "string" && a.trimEnd().toLowerCase() === b.trimEnd().toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
/** A key value the server may store in another form than it was sent (NUMERIC / decimal rounding). */
|
|
22
|
+
function mayNormalize(value, col, formCols) {
|
|
23
|
+
if (typeof value === "string" && col !== void 0 && formCols?.has(col)) return true;
|
|
24
|
+
return typeof value === "number" && !Number.isInteger(value) || typeof value === "string" && /^-?\d+\.\d+$/.test(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Maps the rows a batched `INSERT … ON CONFLICT DO NOTHING RETURNING <keys>`
|
|
28
|
+
* returned back to the batch's input rows.
|
|
29
|
+
*
|
|
30
|
+
* PostgreSQL returns the inserted rows in VALUES order and omits skipped ones,
|
|
31
|
+
* so `returned` is an in-order subsequence of `batch`. A row is inserted
|
|
32
|
+
* exactly when the next returned row carries EVERY key set the row fully
|
|
33
|
+
* defines (key sets with an absent / null input component are never compared:
|
|
34
|
+
* a generated key's value is unknown to the input). Result: per input row, the
|
|
35
|
+
* index of its returned row, or `-1` when it was skipped.
|
|
36
|
+
*
|
|
37
|
+
* `undefined` when the mapping is not provably exact — a row that defines no
|
|
38
|
+
* complete key set (it may match a returned row only by a generated value), a
|
|
39
|
+
* skipped-looking row whose key a server could have normalized (non-integer
|
|
40
|
+
* numbers / decimals), a key equal to the returned one only when written
|
|
41
|
+
* differently (`5` / `"5.00"`, blank-padded CHAR, a `uuid` / `citext` in another
|
|
42
|
+
* letter case — the server lower-cases a returned uuid, so `"AAA"` / `"aaa"` could
|
|
43
|
+
* swap the mapping), or returned rows left unaccounted for (counts or keys that
|
|
44
|
+
* do not line up with an in-order subsequence). The caller must then redo the
|
|
45
|
+
* chunk row by row. An empty `returned` (everything skipped) and a fully-returned
|
|
46
|
+
* batch (everything inserted) are exact without any matching.
|
|
47
|
+
*
|
|
48
|
+
* @param keySets Physical column names of the primary key and every unique index.
|
|
49
|
+
* @param formCols Key columns whose physical type is not text (uuid, inet, cidr,
|
|
50
|
+
* macaddr, …, `@db.pg.type` overrides): a string key there may come back
|
|
51
|
+
* normalized, so a skipped-looking row of such a key is ambiguous.
|
|
52
|
+
*/
|
|
53
|
+
function mapIgnoredBatch(batch, returned, keySets, formCols) {
|
|
54
|
+
if (returned.length === 0) return batch.map(() => -1);
|
|
55
|
+
if (returned.length === batch.length) return batch.map((_, i) => i);
|
|
56
|
+
const out = [];
|
|
57
|
+
let next = 0;
|
|
58
|
+
for (const row of batch) {
|
|
59
|
+
const tuples = keySets.map((cols) => uniqueKeyTuple(row, cols));
|
|
60
|
+
if (tuples.every((t) => t === void 0)) return void 0;
|
|
61
|
+
const candidate = returned[next];
|
|
62
|
+
if (candidate !== void 0) {
|
|
63
|
+
const strict = keySets.map((cols, k) => tuples[k] === void 0 || tuples[k] === uniqueKeyTuple(candidate, cols));
|
|
64
|
+
if (strict.every(Boolean)) {
|
|
65
|
+
out.push(next++);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (keySets.every((cols, k) => tuples[k] === void 0 || strict[k] || cols.every((c) => looselySame(row[c], candidate[c])))) return void 0;
|
|
69
|
+
}
|
|
70
|
+
if (keySets.some((cols, k) => tuples[k] !== void 0 && cols.some((c) => mayNormalize(row[c], c, formCols)))) return;
|
|
71
|
+
out.push(-1);
|
|
72
|
+
}
|
|
73
|
+
return next === returned.length ? out : void 0;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
5
76
|
//#region src/sql-builder.ts
|
|
6
77
|
/**
|
|
7
78
|
* PostgreSQL-aware default value for a given design type.
|
|
@@ -79,6 +150,25 @@ const pgDialect = {
|
|
|
79
150
|
calendarBucket: pgCalendarBucket,
|
|
80
151
|
jsonExtract: pgJsonExtract,
|
|
81
152
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE PRECISION)`,
|
|
153
|
+
anyValue: pgAnyValue,
|
|
154
|
+
booleanAggregates: {
|
|
155
|
+
min: "BOOL_AND",
|
|
156
|
+
max: "BOOL_OR"
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* SQLSTATE 22003 (`numeric_value_out_of_range`), a double overflow in
|
|
160
|
+
* aggregate arithmetic, is `INVALID_QUERY`; SQLSTATE 22023
|
|
161
|
+
* (`invalid_parameter_value`, `time zone "…" not recognized`), raised by a
|
|
162
|
+
* calendar bucket's `AT TIME ZONE '<tz>'`, is `BUCKET_TZ_UNAVAILABLE` — the
|
|
163
|
+
* zone passed the core's IANA validation, so the server's tz database is
|
|
164
|
+
* older than the runtime's.
|
|
165
|
+
*/
|
|
166
|
+
mapQueryError(error, arithmetic) {
|
|
167
|
+
const err = error;
|
|
168
|
+
if (err?.code === "22003") return arithmetic ? arithOverflowError() : numericOutOfRangeError();
|
|
169
|
+
const zone = err?.code === "22023" && typeof err.message === "string" ? /time zone "([^"]*)" not recognized/.exec(err.message)?.[1] : void 0;
|
|
170
|
+
return zone === void 0 ? void 0 : bucketTimeZoneUnavailable(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
|
|
171
|
+
},
|
|
82
172
|
nullsSortLargest: true,
|
|
83
173
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
84
174
|
paramPlaceholder(index) {
|
|
@@ -256,6 +346,30 @@ function intTypeFromTags(tags) {
|
|
|
256
346
|
if (tags?.has("int64") || tags?.has("uint64")) return "BIGINT";
|
|
257
347
|
return "INTEGER";
|
|
258
348
|
}
|
|
349
|
+
/** Physical types `MIN` aggregates (base name, lower-cased): the streaming pick applies. */
|
|
350
|
+
const MIN_ORDERED_TYPE = /^(smallint|int2|integer|int|int4|bigint|int8|real|float4|double precision|float8|numeric|decimal|money|text|varchar|character varying|char|character|bpchar|date|time|timetz|timestamp|timestamptz|interval|time (with|without) time zone|timestamp (with|without) time zone)\b/;
|
|
351
|
+
/** Text key types: a value comes back in the form it was sent (modulo case for `citext`), so a returned key maps to its input row. */
|
|
352
|
+
const TEXT_KEY_TYPE = /^(text|varchar|character varying|char|character|bpchar|citext)\b/;
|
|
353
|
+
/** `true` when the physical column type `type` is a text type (see {@link TEXT_KEY_TYPE}). */
|
|
354
|
+
function isTextKeyType(type) {
|
|
355
|
+
return TEXT_KEY_TYPE.test(type.trim().toLowerCase());
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The "any value of the group" pick of a `first` / `last` derived column
|
|
359
|
+
* (constant within its group) over a PostgreSQL column: `MIN` (streaming — one
|
|
360
|
+
* accumulator per group) for the ordered types and for every array column
|
|
361
|
+
* (`MIN(anyarray)`; `ARRAY_AGG` over arrays builds a 2-D array and rejects NULL /
|
|
362
|
+
* empty ones), `BOOL_AND` for a boolean, and `(ARRAY_AGG(x))[1]` only where the
|
|
363
|
+
* physical type has no `MIN` (uuid, bytea, point, json / jsonb, citext,
|
|
364
|
+
* `@db.pg.type` overrides, …) or is unknown.
|
|
365
|
+
*/
|
|
366
|
+
function pgAnyValue(expr, field) {
|
|
367
|
+
const type = field ? pgTypeFromField(field).trim().toLowerCase() : "";
|
|
368
|
+
if (type.endsWith("[]")) return `MIN(${expr})`;
|
|
369
|
+
if (/^bool(ean)?\b/.test(type)) return `BOOL_AND(${expr})`;
|
|
370
|
+
if (MIN_ORDERED_TYPE.test(type)) return `MIN(${expr})`;
|
|
371
|
+
return `(ARRAY_AGG(${expr}))[1]`;
|
|
372
|
+
}
|
|
259
373
|
/**
|
|
260
374
|
* Maps an Atscript field descriptor to a PostgreSQL column type.
|
|
261
375
|
*
|
|
@@ -311,11 +425,11 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
311
425
|
continue;
|
|
312
426
|
}
|
|
313
427
|
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
314
|
-
if (options?.incrementFields?.has(field.
|
|
428
|
+
if (options?.incrementFields?.has(field.path)) {
|
|
315
429
|
const start = options.autoIncrementStart;
|
|
316
430
|
def += start !== void 0 ? ` GENERATED BY DEFAULT AS IDENTITY (START WITH ${start})` : " GENERATED BY DEFAULT AS IDENTITY";
|
|
317
431
|
}
|
|
318
|
-
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.
|
|
432
|
+
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.path)) def += " NOT NULL";
|
|
319
433
|
if (field.defaultValue?.kind === "value") def += ` DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`;
|
|
320
434
|
else if (field.defaultValue?.kind === "fn") {
|
|
321
435
|
if (field.defaultValue.fn === "uuid") def += " DEFAULT gen_random_uuid()";
|
|
@@ -385,6 +499,8 @@ function parseCount(value) {
|
|
|
385
499
|
* ```
|
|
386
500
|
*/
|
|
387
501
|
/** The suffix PostgreSQL gives an auto-named constraint of each `pg_constraint.contype`. */
|
|
502
|
+
/** Savepoint guarding one `insertManyIgnore` chunk (see {@link PostgresAdapter.insertManyIgnore}). */
|
|
503
|
+
const IGNORE_SAVEPOINT = "atscript_ignore_chunk";
|
|
388
504
|
const PG_CONSTRAINT_LABELS = {
|
|
389
505
|
p: "pkey",
|
|
390
506
|
u: "key",
|
|
@@ -527,10 +643,14 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
527
643
|
calendarBucketUnits() {
|
|
528
644
|
return ALL_BUCKET_UNITS;
|
|
529
645
|
}
|
|
530
|
-
/** Every aggregate function
|
|
646
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
531
647
|
aggregateFns() {
|
|
532
648
|
return ALL_AGGREGATE_FNS;
|
|
533
649
|
}
|
|
650
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
651
|
+
supportsAggregateExpressions() {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
534
654
|
/** Computed view columns and first-row joins. */
|
|
535
655
|
viewCapabilities() {
|
|
536
656
|
return ALL_VIEW_CAPABILITIES;
|
|
@@ -600,16 +720,20 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
600
720
|
try {
|
|
601
721
|
return await fn();
|
|
602
722
|
} catch (error) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
723
|
+
return this._mapConstraintError(error);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
727
|
+
_mapConstraintError(error) {
|
|
728
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
729
|
+
const err = error;
|
|
730
|
+
if (err.code === "23505") throw new DbError("CONFLICT", [{
|
|
731
|
+
path: this._extractFieldFromConstraint(err.constraint) ?? "",
|
|
732
|
+
message: err.detail ?? err.message
|
|
733
|
+
}]);
|
|
734
|
+
if (err.code === "23503") throw new DbError("FK_VIOLATION", this._mapFkError(err.detail ?? err.message, err.constraint));
|
|
612
735
|
}
|
|
736
|
+
throw error;
|
|
613
737
|
}
|
|
614
738
|
_extractFieldFromConstraint(constraint) {
|
|
615
739
|
if (!constraint) return;
|
|
@@ -652,11 +776,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
652
776
|
const tableName = this.resolveTableName();
|
|
653
777
|
const pkCols = this._pkColumns();
|
|
654
778
|
const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
|
|
655
|
-
const columns =
|
|
656
|
-
const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
|
|
779
|
+
const { columns, batches } = chunkInsertRows(data);
|
|
657
780
|
const allIds = [];
|
|
658
|
-
for (
|
|
659
|
-
const batch = data.slice(offset, offset + maxRowsPerBatch);
|
|
781
|
+
for (const batch of batches) {
|
|
660
782
|
const insert = buildInsertMany$1(tableName, batch, columns);
|
|
661
783
|
const sql = insert.sql + returningSuffix;
|
|
662
784
|
const params = insert.params;
|
|
@@ -673,6 +795,56 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
673
795
|
};
|
|
674
796
|
});
|
|
675
797
|
}
|
|
798
|
+
supportsInsertIgnore() {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Batched `INSERT … VALUES (…), (…) ON CONFLICT DO NOTHING RETURNING <pk +
|
|
803
|
+
* unique key columns>`: one statement per chunk (a conflict never raises, so
|
|
804
|
+
* the surrounding transaction survives). Skipped rows are the ones missing
|
|
805
|
+
* from RETURNING — mapped back by key values, see {@link mapIgnoredBatch}.
|
|
806
|
+
* Each chunk runs inside a SAVEPOINT: when the mapping is ambiguous (a key
|
|
807
|
+
* the server returns in another form than it was sent) the chunk is rolled
|
|
808
|
+
* back to the savepoint and redone row by row with the same
|
|
809
|
+
* `ON CONFLICT DO NOTHING`, so the result is always exact.
|
|
810
|
+
*/
|
|
811
|
+
async insertManyIgnore(data) {
|
|
812
|
+
if (data.length === 0) return [];
|
|
813
|
+
return this.withTransaction(async () => {
|
|
814
|
+
const tableName = this.resolveTableName();
|
|
815
|
+
const pkCols = this._pkColumns();
|
|
816
|
+
const keySets = this._table.uniqueKeySets;
|
|
817
|
+
const returning = [...new Set(keySets.flat())];
|
|
818
|
+
const returningCols = new Set(returning);
|
|
819
|
+
const nonTextKeyCols = new Set(this._table.fieldDescriptors.filter((f) => returningCols.has(f.physicalName) && !isTextKeyType(this.typeMapper(f))).map((f) => f.physicalName));
|
|
820
|
+
const returningSuffix = returning.length > 0 ? ` RETURNING ${returning.map((c) => qi(c)).join(", ")}` : "";
|
|
821
|
+
const insertedId = (row, returned) => ({ insertedId: this._resolveInsertedId(row, pkCols.length > 0 ? returned?.[pkCols[0]] : void 0) });
|
|
822
|
+
const { columns, batches } = chunkInsertRows(data);
|
|
823
|
+
const slots = [];
|
|
824
|
+
for (const batch of batches) {
|
|
825
|
+
const insert = buildInsertMany$1(tableName, batch, columns);
|
|
826
|
+
const sql = `${insert.sql} ON CONFLICT DO NOTHING${returningSuffix}`;
|
|
827
|
+
await this._exec().run(`SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
828
|
+
this._log(sql, insert.params);
|
|
829
|
+
const returned = (await this._wrapConstraintError(() => this._exec().run(sql, insert.params))).rows ?? [];
|
|
830
|
+
const mapping = returning.length === 0 ? batch.map((_, i) => i) : mapIgnoredBatch(batch, returned, keySets, nonTextKeyCols);
|
|
831
|
+
if (mapping) {
|
|
832
|
+
await this._exec().run(`RELEASE SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
833
|
+
mapping.forEach((hit, i) => slots.push(hit < 0 ? null : insertedId(batch[i], returned[hit])));
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
await this._exec().run(`ROLLBACK TO SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
837
|
+
for (const row of batch) {
|
|
838
|
+
const single = buildInsert$1(tableName, row);
|
|
839
|
+
const rowSql = `${single.sql} ON CONFLICT DO NOTHING${returningSuffix}`;
|
|
840
|
+
this._log(rowSql, single.params);
|
|
841
|
+
const rowResult = await this._wrapConstraintError(() => this._exec().run(rowSql, single.params));
|
|
842
|
+
slots.push(rowResult.rows?.length ? insertedId(row, rowResult.rows[0]) : null);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return slots;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
676
848
|
async findOne(query) {
|
|
677
849
|
const where = buildWhere(query.filter);
|
|
678
850
|
const controls = {
|
|
@@ -715,28 +887,11 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
715
887
|
if (query.controls.$count) {
|
|
716
888
|
const { sql, params } = buildAggregateCount$1(tableName, where, query.controls);
|
|
717
889
|
this._log(sql, params);
|
|
718
|
-
return [{ count: parseCount((await
|
|
890
|
+
return [{ count: parseCount((await mapQueryErrors(pgDialect, () => this._exec().get(sql, params), query.controls))?.count) }];
|
|
719
891
|
}
|
|
720
892
|
const { sql, params } = buildAggregateSelect$1(tableName, where, query.controls);
|
|
721
893
|
this._log(sql, params);
|
|
722
|
-
return
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
|
|
726
|
-
* (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
|
|
727
|
-
* calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
|
|
728
|
-
* zone passed the core's IANA validation, so the server's tz database is
|
|
729
|
-
* older than the runtime's. Other errors propagate unchanged.
|
|
730
|
-
*/
|
|
731
|
-
async _wrapBucketZoneError(fn) {
|
|
732
|
-
try {
|
|
733
|
-
return await fn();
|
|
734
|
-
} catch (error) {
|
|
735
|
-
const err = error;
|
|
736
|
-
const zone = err?.code === "22023" && typeof err.message === "string" ? /time zone "([^"]*)" not recognized/.exec(err.message)?.[1] : void 0;
|
|
737
|
-
if (zone !== void 0) throw bucketTimeZoneUnavailable(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
|
|
738
|
-
throw error;
|
|
739
|
-
}
|
|
894
|
+
return mapQueryErrors(pgDialect, () => this._exec().all(sql, params), query.controls);
|
|
740
895
|
}
|
|
741
896
|
/** Physical primary-key columns (`@db.column` renames applied). */
|
|
742
897
|
_pkColumns() {
|
|
@@ -1855,8 +2010,10 @@ var PgDriver = class {
|
|
|
1855
2010
|
}
|
|
1856
2011
|
};
|
|
1857
2012
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
2013
|
+
_closing;
|
|
2014
|
+
/** Idempotent: every call returns the first call's promise. */
|
|
2015
|
+
close() {
|
|
2016
|
+
return this._closing ??= Promise.resolve(this.getPool()).then((pool) => pool.end());
|
|
1860
2017
|
}
|
|
1861
2018
|
};
|
|
1862
2019
|
//#endregion
|
|
@@ -1873,7 +2030,7 @@ function createAdapter(uri, options) {
|
|
|
1873
2030
|
connectionString: uri,
|
|
1874
2031
|
...options
|
|
1875
2032
|
});
|
|
1876
|
-
return new DbSpace(() => new PostgresAdapter(driver));
|
|
2033
|
+
return new DbSpace(() => new PostgresAdapter(driver), { onClose: () => driver.close() });
|
|
1877
2034
|
}
|
|
1878
2035
|
//#endregion
|
|
1879
2036
|
export { PgDriver, PostgresAdapter, buildWhere, createAdapter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.148",
|
|
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.100",
|
|
47
|
+
"@atscript/typescript": "^0.1.100",
|
|
48
48
|
"@types/pg": "^8.11.0",
|
|
49
|
-
"@uniqu/core": "^0.1.
|
|
49
|
+
"@uniqu/core": "^0.1.13",
|
|
50
50
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.1.
|
|
51
|
+
"unplugin-atscript": "^0.1.100"
|
|
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.100",
|
|
55
|
+
"@atscript/typescript": "^0.1.100",
|
|
56
|
+
"@uniqu/core": "^0.1.13",
|
|
57
57
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db": "^0.1.
|
|
59
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
58
|
+
"@atscript/db": "^0.1.148",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.148"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|