@atscript/db-postgres 0.1.150 → 0.1.152
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 +431 -79
- package/dist/index.d.cts +123 -10
- package/dist/index.d.mts +123 -10
- package/dist/index.mjs +431 -79
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -110,10 +110,30 @@ function quoteTableName(name) {
|
|
|
110
110
|
if (dot >= 0) return `${qi(name.slice(0, dot))}.${qi(name.slice(dot + 1))}`;
|
|
111
111
|
return qi(name);
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* A `db.geoPoint` value written or filtered on before the adapter knows the
|
|
115
|
+
* physical type of its column — a PostGIS `geography` / `geometry` column
|
|
116
|
+
* (EWKT) or the JSONB fallback (`raw`, what the JSONB path binds). Whichever
|
|
117
|
+
* adapter executes the statement resolves it right before it runs (the
|
|
118
|
+
* marker may come from another table's formatter, e.g. a relational filter):
|
|
119
|
+
* `native` answers for the column the marker was formatted for, probing the
|
|
120
|
+
* catalog on `exec` when not known yet.
|
|
121
|
+
*/
|
|
122
|
+
var PendingGeoPoint = class {
|
|
123
|
+
point;
|
|
124
|
+
raw;
|
|
125
|
+
native;
|
|
126
|
+
constructor(point, raw, native) {
|
|
127
|
+
this.point = point;
|
|
128
|
+
this.raw = raw;
|
|
129
|
+
this.native = native;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
113
132
|
/** Converts JS values to SQL-bindable params, keeping booleans native. */
|
|
114
133
|
function toPgValue(value) {
|
|
115
134
|
if (value === void 0) return null;
|
|
116
135
|
if (value === null) return null;
|
|
136
|
+
if (value instanceof PendingGeoPoint) return new PendingGeoPoint(value.point, toPgValue(value.raw), value.native);
|
|
117
137
|
if (value instanceof Date) return value.toISOString();
|
|
118
138
|
if (typeof value === "object") return JSON.stringify(value);
|
|
119
139
|
return value;
|
|
@@ -479,6 +499,53 @@ function buildWhere(filter, opts) {
|
|
|
479
499
|
}
|
|
480
500
|
//#endregion
|
|
481
501
|
//#region src/postgres-adapter.ts
|
|
502
|
+
/**
|
|
503
|
+
* Read-only geo facts of one table, in one round trip: PostGIS presence and
|
|
504
|
+
* the physical type of the named columns (`{ column: type }`, a domain
|
|
505
|
+
* reported as its base type; `null` when the relation does not exist). Catalog
|
|
506
|
+
* reads only — no privileges needed, never aborts a transaction.
|
|
507
|
+
*/
|
|
508
|
+
const GEO_PROBE_SQL = `SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis') AS "postgis",
|
|
509
|
+
(SELECT json_object_agg(a.attname, COALESCE(b.typname, t.typname))
|
|
510
|
+
FROM pg_attribute a
|
|
511
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
512
|
+
LEFT JOIN pg_type b ON b.oid = NULLIF(t.typbasetype, 0)
|
|
513
|
+
WHERE a.attrelid = to_regclass($1::text) AND a.attnum > 0 AND NOT a.attisdropped
|
|
514
|
+
AND a.attname = ANY($2::text[])) AS "columns"`;
|
|
515
|
+
/** A PostGIS column type — a geo value binds as EWKT. Anything else (JSONB, TEXT) takes the JSON form. */
|
|
516
|
+
const POSTGIS_TYPES = new Set(["geography", "geometry"]);
|
|
517
|
+
/** The SQL `$geoWithin` renders (`pgDialect.geoWithin`) — PostGIS only. */
|
|
518
|
+
const GEO_WITHIN_SQL = "ST_DWithin(";
|
|
519
|
+
/** Index of the first {@link PendingGeoPoint} in `params`, or -1. */
|
|
520
|
+
function firstPendingGeo(params) {
|
|
521
|
+
if (params) {
|
|
522
|
+
for (let i = 0; i < params.length; i++) if (params[i] instanceof PendingGeoPoint) return i;
|
|
523
|
+
}
|
|
524
|
+
return -1;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* A copy of `params` with every {@link PendingGeoPoint} (from `from` on)
|
|
528
|
+
* resolved for its own column — EWKT for a PostGIS column, the JSON form
|
|
529
|
+
* otherwise — each marker's column probe running on `exec`, the statement's.
|
|
530
|
+
*/
|
|
531
|
+
async function resolvePendingGeo(params, from, exec) {
|
|
532
|
+
const out = params.slice();
|
|
533
|
+
for (let i = from; i < out.length; i++) {
|
|
534
|
+
const value = out[i];
|
|
535
|
+
if (value instanceof PendingGeoPoint) {
|
|
536
|
+
const native = value.native(exec);
|
|
537
|
+
out[i] = (typeof native === "boolean" ? native : await native) ? geoPointToEwkt(value.point) : value.raw;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
/** `GEO_NOT_SUPPORTED` — geo search / `$geoWithin` without PostGIS. */
|
|
543
|
+
function geoNotSupported(path, message) {
|
|
544
|
+
return new _atscript_db.DbError("GEO_NOT_SUPPORTED", [{
|
|
545
|
+
path,
|
|
546
|
+
message
|
|
547
|
+
}]);
|
|
548
|
+
}
|
|
482
549
|
/** PostgreSQL COUNT() may return string (bigint) — parse to number. */
|
|
483
550
|
function parseCount(value) {
|
|
484
551
|
if (typeof value === "string") return Number.parseInt(value, 10);
|
|
@@ -550,8 +617,42 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
550
617
|
_nocaseColumns = /* @__PURE__ */ new Set();
|
|
551
618
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
552
619
|
_citextProvisioned = false;
|
|
553
|
-
/**
|
|
620
|
+
/**
|
|
621
|
+
* Whether the connected PostgreSQL instance has the PostGIS extension —
|
|
622
|
+
* `undefined` until known: set by schema sync ({@link prepareTypeMapper},
|
|
623
|
+
* which installs it), or by the read-only geo probe ({@link _probeGeo}).
|
|
624
|
+
* Decides DDL and geo search; how a geo VALUE binds is decided per column
|
|
625
|
+
* ({@link _geoNative}).
|
|
626
|
+
*/
|
|
554
627
|
_supportsGeo;
|
|
628
|
+
/** Whether {@link _detectGeoSupport} (the installing detection) already ran. */
|
|
629
|
+
_geoInstallTried = false;
|
|
630
|
+
/** Memo of {@link _hasGeoPointFields}. */
|
|
631
|
+
_geoFields;
|
|
632
|
+
/** Physical names of the unencrypted `db.geoPoint` columns (set with the metadata). */
|
|
633
|
+
_geoColumnNames = [];
|
|
634
|
+
/**
|
|
635
|
+
* Physical geo column → whether it is a PostGIS column (`geography` /
|
|
636
|
+
* `geometry`: EWKT) or not (JSONB: the JSON form). Learned from the catalog
|
|
637
|
+
* — after schema sync ({@link afterSyncTable}) or before the first statement
|
|
638
|
+
* binding a geo value — never from the extension alone: a JSONB column
|
|
639
|
+
* created before PostGIS was installed stays JSONB until a sync migrates
|
|
640
|
+
* it. Cleared by every DDL path ({@link _geoSchemaChanged}).
|
|
641
|
+
*/
|
|
642
|
+
_geoNative = /* @__PURE__ */ new Map();
|
|
643
|
+
/** The in-flight shared geo probe and the executor it runs on. */
|
|
644
|
+
_geoProbe;
|
|
645
|
+
/** Bumped by {@link _geoSchemaChanged}: a probe started before a DDL caches nothing. */
|
|
646
|
+
_geoSchemaGen = 0;
|
|
647
|
+
/** The pool executor with geo-marker resolution (built once). */
|
|
648
|
+
_poolExec;
|
|
649
|
+
/** The last transaction connection's wrapped executor. */
|
|
650
|
+
_txExec;
|
|
651
|
+
_pkColumnsMemo;
|
|
652
|
+
_replaceColumnsMemo;
|
|
653
|
+
_ignorePlanMemo;
|
|
654
|
+
_fulltextMemo;
|
|
655
|
+
_searchIndexesMemo;
|
|
555
656
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
556
657
|
_supportsVector;
|
|
557
658
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -614,7 +715,41 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
614
715
|
* otherwise the pool-based driver.
|
|
615
716
|
*/
|
|
616
717
|
_exec() {
|
|
617
|
-
|
|
718
|
+
const conn = this._txConnection();
|
|
719
|
+
if (!conn) return this._poolExec ??= this._geoResolvingExec(this.driver);
|
|
720
|
+
if (this._txExec?.conn !== conn) this._txExec = {
|
|
721
|
+
conn,
|
|
722
|
+
exec: this._geoResolvingExec(conn)
|
|
723
|
+
};
|
|
724
|
+
return this._txExec.exec;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* `exec` resolving `PendingGeoPoint` params before the statement runs. Every
|
|
728
|
+
* adapter wraps — not only geo tables: a relational filter on another
|
|
729
|
+
* table's geo column carries that table's markers into this one's
|
|
730
|
+
* statement. A statement without markers runs as is (one param scan).
|
|
731
|
+
*/
|
|
732
|
+
_geoResolvingExec(exec) {
|
|
733
|
+
const prep = (sql, params, run) => {
|
|
734
|
+
if (this._geoFields && this._supportsGeo !== true && sql.includes(GEO_WITHIN_SQL)) return this._assertGeoWithin(exec).then(() => prep(sql, params, run));
|
|
735
|
+
const at = firstPendingGeo(params);
|
|
736
|
+
return at < 0 ? run(params) : resolvePendingGeo(params, at, exec).then(run);
|
|
737
|
+
};
|
|
738
|
+
return {
|
|
739
|
+
run: (sql, params) => prep(sql, params, (p) => exec.run(sql, p)),
|
|
740
|
+
all: (sql, params) => prep(sql, params, (p) => exec.all(sql, p)),
|
|
741
|
+
get: (sql, params) => prep(sql, params, (p) => exec.get(sql, p)),
|
|
742
|
+
exec: (sql) => exec.exec(sql).finally(() => this._geoSchemaChanged())
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* `$geoWithin` passed the core guard while PostGIS presence was unknown
|
|
747
|
+
* ({@link isGeoSearchable} answers optimistically then): learn it, and
|
|
748
|
+
* refuse like the guard would have when it is absent.
|
|
749
|
+
*/
|
|
750
|
+
async _assertGeoWithin(exec) {
|
|
751
|
+
if (this._supportsGeo === void 0) await this._probeGeo(exec);
|
|
752
|
+
if (this._supportsGeo !== true) throw geoNotSupported("", "$geoWithin requires the PostGIS extension");
|
|
618
753
|
}
|
|
619
754
|
/**
|
|
620
755
|
* Relational predicates (`$some` / `$none`) render as correlated
|
|
@@ -659,7 +794,13 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
659
794
|
}
|
|
660
795
|
onBeforeFlatten(_type) {}
|
|
661
796
|
onAfterFlatten() {
|
|
662
|
-
|
|
797
|
+
const geo = [];
|
|
798
|
+
for (const fd of this._table.fieldDescriptors) {
|
|
799
|
+
if (fd.collate === "nocase") this._nocaseColumns.add(fd.physicalName);
|
|
800
|
+
if (fd.isGeoPoint === true && !fd.encrypted) geo.push(fd.physicalName);
|
|
801
|
+
}
|
|
802
|
+
this._geoColumnNames = geo;
|
|
803
|
+
this._geoFields = geo.length > 0;
|
|
663
804
|
}
|
|
664
805
|
onFieldScanned(field, _type, metadata) {
|
|
665
806
|
if (metadata.has("db.default.increment")) {
|
|
@@ -695,20 +836,27 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
695
836
|
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
696
837
|
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
697
838
|
};
|
|
698
|
-
if (field.isGeoPoint && !field.encrypted)
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
if (
|
|
839
|
+
if (field.isGeoPoint && !field.encrypted) {
|
|
840
|
+
const column = field.physicalName;
|
|
841
|
+
const native = (exec) => this._geoColumnNative(column, exec);
|
|
842
|
+
return {
|
|
843
|
+
toStorage: (value) => {
|
|
844
|
+
if (this._supportsGeo === false) return value;
|
|
845
|
+
const point = (0, _atscript_db_sql_tools.normalizeGeoPointValue)(value);
|
|
846
|
+
if (!point) return value;
|
|
847
|
+
const known = this._geoNative.get(column);
|
|
848
|
+
if (known === void 0) return new PendingGeoPoint(point, value, native);
|
|
849
|
+
return known ? geoPointToEwkt(point) : value;
|
|
850
|
+
},
|
|
851
|
+
fromStorage: (value) => {
|
|
852
|
+
if (typeof value === "string") {
|
|
853
|
+
const point = parseEwkbPointHex(value);
|
|
854
|
+
if (point) return point;
|
|
855
|
+
}
|
|
856
|
+
return value;
|
|
708
857
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
};
|
|
858
|
+
};
|
|
859
|
+
}
|
|
712
860
|
}
|
|
713
861
|
/**
|
|
714
862
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -762,9 +910,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
762
910
|
}];
|
|
763
911
|
}
|
|
764
912
|
async insertOne(data) {
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
913
|
+
const insert = buildInsert(this.resolveTableName(), data);
|
|
914
|
+
const sql = insert.sql + this._pk().returning;
|
|
915
|
+
const params = insert.params;
|
|
768
916
|
this._log(sql, params);
|
|
769
917
|
const returned = (await this._wrapConstraintError(() => this._exec().run(sql, params))).rows?.[0];
|
|
770
918
|
return { insertedId: this._resolveInsertedId(data, returned ? Object.values(returned)[0] : void 0) };
|
|
@@ -774,11 +922,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
774
922
|
insertedCount: 0,
|
|
775
923
|
insertedIds: []
|
|
776
924
|
};
|
|
777
|
-
|
|
925
|
+
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
926
|
+
const run = async () => {
|
|
778
927
|
const tableName = this.resolveTableName();
|
|
779
|
-
const
|
|
780
|
-
const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
|
|
781
|
-
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
928
|
+
const returningSuffix = this._pk().returning;
|
|
782
929
|
const allIds = [];
|
|
783
930
|
for (const batch of batches) {
|
|
784
931
|
const insert = buildInsertMany(tableName, batch, columns);
|
|
@@ -795,7 +942,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
795
942
|
insertedCount: allIds.length,
|
|
796
943
|
insertedIds: allIds
|
|
797
944
|
};
|
|
798
|
-
}
|
|
945
|
+
};
|
|
946
|
+
return batches.length === 1 ? run() : this.withTransaction(run);
|
|
799
947
|
}
|
|
800
948
|
supportsInsertIgnore() {
|
|
801
949
|
return true;
|
|
@@ -805,33 +953,34 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
805
953
|
* unique key columns>`: one statement per chunk (a conflict never raises, so
|
|
806
954
|
* the surrounding transaction survives). Skipped rows are the ones missing
|
|
807
955
|
* from RETURNING — mapped back by key values, see {@link mapIgnoredBatch}.
|
|
808
|
-
*
|
|
809
|
-
* the server returns in another form than it
|
|
810
|
-
* back to the savepoint and redone row by row
|
|
811
|
-
* `ON CONFLICT DO NOTHING`, so the result is always exact.
|
|
956
|
+
* A multi-row chunk of a keyed table runs inside a SAVEPOINT: when the
|
|
957
|
+
* mapping is ambiguous (a key the server returns in another form than it
|
|
958
|
+
* was sent) the chunk is rolled back to the savepoint and redone row by row
|
|
959
|
+
* with the same `ON CONFLICT DO NOTHING`, so the result is always exact. A
|
|
960
|
+
* one-row chunk (or a table without keys) maps exactly by construction, so
|
|
961
|
+
* it needs no savepoint — and a single such chunk no transaction either.
|
|
812
962
|
*/
|
|
813
963
|
async insertManyIgnore(data) {
|
|
814
964
|
if (data.length === 0) return [];
|
|
815
|
-
|
|
965
|
+
const plan = this._ignorePlan();
|
|
966
|
+
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
967
|
+
const needsSavepoint = (batch) => batch.length > 1 && plan.returning.length > 0;
|
|
968
|
+
const run = async () => {
|
|
816
969
|
const tableName = this.resolveTableName();
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
const returning = [...new Set(keySets.flat())];
|
|
820
|
-
const returningCols = new Set(returning);
|
|
821
|
-
const nonTextKeyCols = new Set(this._table.fieldDescriptors.filter((f) => returningCols.has(f.physicalName) && !isTextKeyType(this.typeMapper(f))).map((f) => f.physicalName));
|
|
822
|
-
const returningSuffix = returning.length > 0 ? ` RETURNING ${returning.map((c) => qi(c)).join(", ")}` : "";
|
|
970
|
+
const { keySets, returning, nonTextKeyCols, returningSuffix } = plan;
|
|
971
|
+
const pkCols = this._pk().cols;
|
|
823
972
|
const insertedId = (row, returned) => ({ insertedId: this._resolveInsertedId(row, pkCols.length > 0 ? returned?.[pkCols[0]] : void 0) });
|
|
824
|
-
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
825
973
|
const slots = [];
|
|
826
974
|
for (const batch of batches) {
|
|
827
975
|
const insert = buildInsertMany(tableName, batch, columns);
|
|
828
976
|
const sql = `${insert.sql} ON CONFLICT DO NOTHING${returningSuffix}`;
|
|
829
|
-
|
|
977
|
+
const savepoint = needsSavepoint(batch);
|
|
978
|
+
if (savepoint) await this._exec().run(`SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
830
979
|
this._log(sql, insert.params);
|
|
831
980
|
const returned = (await this._wrapConstraintError(() => this._exec().run(sql, insert.params))).rows ?? [];
|
|
832
981
|
const mapping = returning.length === 0 ? batch.map((_, i) => i) : mapIgnoredBatch(batch, returned, keySets, nonTextKeyCols);
|
|
833
982
|
if (mapping) {
|
|
834
|
-
await this._exec().run(`RELEASE SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
983
|
+
if (savepoint) await this._exec().run(`RELEASE SAVEPOINT ${IGNORE_SAVEPOINT}`);
|
|
835
984
|
mapping.forEach((hit, i) => slots.push(hit < 0 ? null : insertedId(batch[i], returned[hit])));
|
|
836
985
|
continue;
|
|
837
986
|
}
|
|
@@ -845,7 +994,32 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
845
994
|
}
|
|
846
995
|
}
|
|
847
996
|
return slots;
|
|
848
|
-
}
|
|
997
|
+
};
|
|
998
|
+
return batches.length === 1 && !needsSavepoint(batches[0]) ? run() : this.withTransaction(run);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* The key columns of a conflict-ignoring insert (built once per table):
|
|
1002
|
+
* every primary / unique key set, their RETURNING list, and the key columns
|
|
1003
|
+
* whose physical type is not text (see {@link mapIgnoredBatch}). Keyed on the
|
|
1004
|
+
* detected extension state the type mapper reads.
|
|
1005
|
+
*/
|
|
1006
|
+
_ignorePlan() {
|
|
1007
|
+
const key = `${this._supportsGeo}|${this._supportsVector}`;
|
|
1008
|
+
const memo = this._ignorePlanMemo;
|
|
1009
|
+
if (memo?.key === key) return memo;
|
|
1010
|
+
const keySets = this._table.uniqueKeySets;
|
|
1011
|
+
const returning = [...new Set(keySets.flat())];
|
|
1012
|
+
const returningCols = new Set(returning);
|
|
1013
|
+
const nonTextKeyCols = new Set(this._table.fieldDescriptors.filter((f) => returningCols.has(f.physicalName) && !isTextKeyType(this.typeMapper(f))).map((f) => f.physicalName));
|
|
1014
|
+
const returningSuffix = returning.length > 0 ? ` RETURNING ${returning.map((c) => qi(c)).join(", ")}` : "";
|
|
1015
|
+
this._ignorePlanMemo = {
|
|
1016
|
+
key,
|
|
1017
|
+
keySets,
|
|
1018
|
+
returning,
|
|
1019
|
+
nonTextKeyCols,
|
|
1020
|
+
returningSuffix
|
|
1021
|
+
};
|
|
1022
|
+
return this._ignorePlanMemo;
|
|
849
1023
|
}
|
|
850
1024
|
async findOne(query) {
|
|
851
1025
|
const where = buildWhere(query.filter);
|
|
@@ -895,9 +1069,48 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
895
1069
|
this._log(sql, params);
|
|
896
1070
|
return (0, _atscript_db_sql_tools.mapQueryErrors)(pgDialect, () => this._exec().all(sql, params), query.controls);
|
|
897
1071
|
}
|
|
898
|
-
/**
|
|
899
|
-
|
|
900
|
-
|
|
1072
|
+
/**
|
|
1073
|
+
* Physical primary-key columns (`@db.column` renames applied) and their
|
|
1074
|
+
* ` RETURNING …` clause (empty without a PK) — built once per table.
|
|
1075
|
+
*/
|
|
1076
|
+
_pk() {
|
|
1077
|
+
const src = this._table.primaryKeys;
|
|
1078
|
+
let memo = this._pkColumnsMemo;
|
|
1079
|
+
if (memo?.src !== src) {
|
|
1080
|
+
const cols = src.map((key) => this._table.physicalPath(key));
|
|
1081
|
+
const returning = cols.length > 0 ? ` RETURNING ${cols.map((c) => qi(c)).join(", ")}` : "";
|
|
1082
|
+
memo = this._pkColumnsMemo = {
|
|
1083
|
+
src,
|
|
1084
|
+
cols,
|
|
1085
|
+
returning
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return memo;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Whether `filter` pins at most one row by itself: a plain object holding
|
|
1092
|
+
* exactly the primary-key columns, each equal to a scalar (`{ id: 5 }` or
|
|
1093
|
+
* `{ id: { $eq: 5 } }`) — what the core sends for a by-id write. Such a
|
|
1094
|
+
* filter needs no `LIMIT 1` re-keying subquery: the outer predicate is the
|
|
1095
|
+
* PK equality either way.
|
|
1096
|
+
*/
|
|
1097
|
+
_isExactPkFilter(filter) {
|
|
1098
|
+
const pk = this._pk().cols;
|
|
1099
|
+
if (pk.length === 0 || typeof filter !== "object" || filter === null || Array.isArray(filter)) return false;
|
|
1100
|
+
const f = filter;
|
|
1101
|
+
if (Object.keys(f).length !== pk.length) return false;
|
|
1102
|
+
for (const col of pk) {
|
|
1103
|
+
if (!Object.hasOwn(f, col)) return false;
|
|
1104
|
+
let value = f[col];
|
|
1105
|
+
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
|
|
1106
|
+
const ops = Object.keys(value);
|
|
1107
|
+
if (ops.length !== 1 || ops[0] !== "$eq") return false;
|
|
1108
|
+
value = value.$eq;
|
|
1109
|
+
}
|
|
1110
|
+
const t = typeof value;
|
|
1111
|
+
if (t !== "string" && t !== "number" && t !== "bigint" && t !== "boolean") return false;
|
|
1112
|
+
}
|
|
1113
|
+
return true;
|
|
901
1114
|
}
|
|
902
1115
|
/**
|
|
903
1116
|
* The predicate that narrows a single-row UPDATE/DELETE to the first row
|
|
@@ -908,7 +1121,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
908
1121
|
* existing behavior rather than guess.
|
|
909
1122
|
*/
|
|
910
1123
|
_limitOnePredicate(quotedTable, whereSql) {
|
|
911
|
-
const pkCols = this.
|
|
1124
|
+
const pkCols = this._pk().cols;
|
|
912
1125
|
const quotedKeys = pkCols.length > 0 ? pkCols.map((c) => qi(c)) : ["ctid"];
|
|
913
1126
|
const colList = quotedKeys.join(", ");
|
|
914
1127
|
return `${quotedKeys.length === 1 ? `${colList} =` : `(${colList}) IN`} (SELECT ${colList} FROM ${quotedTable} WHERE ${whereSql} LIMIT 1)`;
|
|
@@ -916,9 +1129,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
916
1129
|
async updateOne(filter, data, ops, expectedVersion, opts) {
|
|
917
1130
|
const where = buildWhere(filter);
|
|
918
1131
|
const tableName = this.resolveTableName();
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
sql: this._limitOnePredicate(quotedTable, where.sql),
|
|
1132
|
+
const { sql, params } = buildUpdate(tableName, data, this._isExactPkFilter(filter) ? where : {
|
|
1133
|
+
sql: this._limitOnePredicate(quoteTableName(tableName), where.sql),
|
|
922
1134
|
params: where.params
|
|
923
1135
|
}, void 0, ops, this._versionColumnFor(opts, expectedVersion), expectedVersion);
|
|
924
1136
|
this._log(sql, params);
|
|
@@ -940,14 +1152,25 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
940
1152
|
};
|
|
941
1153
|
}
|
|
942
1154
|
async replaceOne(filter, data, expectedVersion) {
|
|
943
|
-
const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data,
|
|
1155
|
+
const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, this._replaceColumns(), this._table.versionColumnPhysical);
|
|
944
1156
|
return this.updateOne(filter, full, void 0, expectedVersion);
|
|
945
1157
|
}
|
|
1158
|
+
/** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
|
|
1159
|
+
_replaceColumns() {
|
|
1160
|
+
const src = this._table.fieldDescriptors;
|
|
1161
|
+
let memo = this._replaceColumnsMemo;
|
|
1162
|
+
if (memo?.src !== src) memo = this._replaceColumnsMemo = {
|
|
1163
|
+
src,
|
|
1164
|
+
cols: (0, _atscript_db_sql_tools.replaceColumnsFor)(src, this.nativeDefaultFns())
|
|
1165
|
+
};
|
|
1166
|
+
return memo.cols;
|
|
1167
|
+
}
|
|
946
1168
|
async replaceMany(filter, data) {
|
|
947
1169
|
return this.updateMany(filter, data);
|
|
948
1170
|
}
|
|
949
1171
|
async deleteOne(filter) {
|
|
950
1172
|
const where = buildWhere(filter);
|
|
1173
|
+
if (this._isExactPkFilter(filter)) return this._deleteWhere(where);
|
|
951
1174
|
const quotedTable = quoteTableName(this.resolveTableName());
|
|
952
1175
|
const { sql, params } = (0, _atscript_db_sql_tools.finalizeParams)(pgDialect, {
|
|
953
1176
|
sql: `DELETE FROM ${quotedTable} WHERE ${where.sql} AND ${this._limitOnePredicate(quotedTable, where.sql)}`,
|
|
@@ -957,18 +1180,24 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
957
1180
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
958
1181
|
}
|
|
959
1182
|
async deleteMany(filter) {
|
|
960
|
-
|
|
1183
|
+
return this._deleteWhere(buildWhere(filter));
|
|
1184
|
+
}
|
|
1185
|
+
async _deleteWhere(where) {
|
|
961
1186
|
const { sql, params } = buildDelete(this.resolveTableName(), where);
|
|
962
1187
|
this._log(sql, params);
|
|
963
1188
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
964
1189
|
}
|
|
965
1190
|
async prepareTypeMapper() {
|
|
966
1191
|
if (this._supportsVector === void 0 && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
967
|
-
if (this.
|
|
1192
|
+
if (!this._geoInstallTried && this._hasGeoPointFields()) await this._detectGeoSupport();
|
|
968
1193
|
}
|
|
969
|
-
/**
|
|
1194
|
+
/**
|
|
1195
|
+
* Whether the table declares any `db.geoPoint` fields (unencrypted) — set
|
|
1196
|
+
* when the metadata is built (`false` before, and on an administrative
|
|
1197
|
+
* adapter: no geo value can be formatted without built metadata).
|
|
1198
|
+
*/
|
|
970
1199
|
_hasGeoPointFields() {
|
|
971
|
-
return this.
|
|
1200
|
+
return this._geoFields === true;
|
|
972
1201
|
}
|
|
973
1202
|
async ensureTable(opts) {
|
|
974
1203
|
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
@@ -1275,9 +1504,11 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1275
1504
|
await conn.exec(ddl);
|
|
1276
1505
|
}
|
|
1277
1506
|
await conn.exec("COMMIT");
|
|
1507
|
+
this._geoSchemaChanged();
|
|
1278
1508
|
await this._resetIdentitySequences();
|
|
1279
1509
|
} catch (err) {
|
|
1280
1510
|
await conn.exec("ROLLBACK").catch(() => {});
|
|
1511
|
+
this._geoSchemaChanged();
|
|
1281
1512
|
const detail = err.detail;
|
|
1282
1513
|
if (err instanceof Error && typeof detail === "string" && detail.length > 0) err.message = `${err.message} — ${detail}`;
|
|
1283
1514
|
throw err;
|
|
@@ -1333,6 +1564,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1333
1564
|
}
|
|
1334
1565
|
async afterSyncTable() {
|
|
1335
1566
|
await this._resetIdentitySequences();
|
|
1567
|
+
if (this._geoFields && this._supportsGeo === true && this._geoNative.size === 0) await this._probeGeo(this._txConnection() ?? this.driver).catch(() => void 0);
|
|
1336
1568
|
}
|
|
1337
1569
|
/**
|
|
1338
1570
|
* Resets IDENTITY sequences to MAX(column) so that the next auto-generated
|
|
@@ -1535,9 +1767,17 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1535
1767
|
return byName;
|
|
1536
1768
|
}
|
|
1537
1769
|
getSearchIndexes() {
|
|
1770
|
+
const src = this._table.indexes;
|
|
1771
|
+
let memo = this._searchIndexesMemo;
|
|
1772
|
+
if (memo?.src !== src) memo = this._searchIndexesMemo = {
|
|
1773
|
+
src,
|
|
1774
|
+
list: this._buildSearchIndexes()
|
|
1775
|
+
};
|
|
1776
|
+
return [...memo.list];
|
|
1777
|
+
}
|
|
1778
|
+
_buildSearchIndexes() {
|
|
1538
1779
|
const indexes = [];
|
|
1539
|
-
const ftAll =
|
|
1540
|
-
const ftDefault = (0, _atscript_db.defaultFulltextIndex)(ftAll);
|
|
1780
|
+
const { all: ftAll, def: ftDefault } = this._fulltextIndexes();
|
|
1541
1781
|
for (const index of ftAll) indexes.push({
|
|
1542
1782
|
name: index.key,
|
|
1543
1783
|
description: (0, _atscript_db.describeFulltext)(index, (names) => `GIN tsvector index on ${names}`),
|
|
@@ -1619,9 +1859,23 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1619
1859
|
return fields.map((f) => `coalesce(${qi(f.name)}, '')`).join(" || ' ' || ");
|
|
1620
1860
|
}
|
|
1621
1861
|
_getFulltextIndex(indexName) {
|
|
1622
|
-
const
|
|
1623
|
-
if (!indexName) return
|
|
1624
|
-
return
|
|
1862
|
+
const { all, def } = this._fulltextIndexes();
|
|
1863
|
+
if (!indexName) return def;
|
|
1864
|
+
return all.find((index) => index.key === indexName);
|
|
1865
|
+
}
|
|
1866
|
+
/** The table's fulltext indexes and the default one, built once per table. */
|
|
1867
|
+
_fulltextIndexes() {
|
|
1868
|
+
const src = this._table.indexes;
|
|
1869
|
+
let memo = this._fulltextMemo;
|
|
1870
|
+
if (memo?.src !== src) {
|
|
1871
|
+
const all = [...src.values()].filter((i) => i.type === "fulltext");
|
|
1872
|
+
memo = this._fulltextMemo = {
|
|
1873
|
+
src,
|
|
1874
|
+
all,
|
|
1875
|
+
def: (0, _atscript_db.defaultFulltextIndex)(all)
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
return memo;
|
|
1625
1879
|
}
|
|
1626
1880
|
/**
|
|
1627
1881
|
* Detects pgvector support by attempting to enable the extension.
|
|
@@ -1643,15 +1897,16 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1643
1897
|
async vectorSearch(vector, query, indexName) {
|
|
1644
1898
|
await this._detectVectorSupport();
|
|
1645
1899
|
if (!this._supportsVector) throw new Error("Vector search requires the pgvector extension");
|
|
1646
|
-
const { sql, params } = this._buildVectorSearchQuery(vector, query, indexName);
|
|
1900
|
+
const { sql, params } = this._buildVectorSearchQuery(this._prepareVectorSearch(vector, query, indexName));
|
|
1647
1901
|
this._log(sql, params);
|
|
1648
1902
|
return this._exec().all(sql, params);
|
|
1649
1903
|
}
|
|
1650
1904
|
async vectorSearchWithCount(vector, query, indexName) {
|
|
1651
1905
|
await this._detectVectorSupport();
|
|
1652
1906
|
if (!this._supportsVector) throw new Error("Vector search requires the pgvector extension");
|
|
1653
|
-
const
|
|
1654
|
-
const { sql
|
|
1907
|
+
const ctx = this._prepareVectorSearch(vector, query, indexName);
|
|
1908
|
+
const { sql, params } = this._buildVectorSearchQuery(ctx);
|
|
1909
|
+
const { sql: countSql, params: countParams } = this._buildVectorSearchCountQuery(ctx);
|
|
1655
1910
|
this._log(sql, params);
|
|
1656
1911
|
this._log(countSql, countParams);
|
|
1657
1912
|
const [data, countRow] = await Promise.all([this._exec().all(sql, params), this._exec().get(countSql, countParams)]);
|
|
@@ -1705,8 +1960,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1705
1960
|
maxDistance: ctx.threshold === void 0 ? void 0 : thresholdToDistance(ctx.threshold, ctx.vec.similarity)
|
|
1706
1961
|
};
|
|
1707
1962
|
}
|
|
1708
|
-
_buildVectorSearchQuery(
|
|
1709
|
-
const ctx = this._prepareVectorSearch(vector, query, indexName);
|
|
1963
|
+
_buildVectorSearchQuery(ctx) {
|
|
1710
1964
|
const { source, maxDistance } = this._vectorSearchSource(ctx, true);
|
|
1711
1965
|
const skip = Number(ctx.controls.$skip) || 0;
|
|
1712
1966
|
return (0, _atscript_db_sql_tools.buildVectorSearchSelect)(pgDialect, source, {
|
|
@@ -1716,8 +1970,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1716
1970
|
maxDistance
|
|
1717
1971
|
});
|
|
1718
1972
|
}
|
|
1719
|
-
_buildVectorSearchCountQuery(
|
|
1720
|
-
const { source, maxDistance } = this._vectorSearchSource(
|
|
1973
|
+
_buildVectorSearchCountQuery(ctx) {
|
|
1974
|
+
const { source, maxDistance } = this._vectorSearchSource(ctx, false);
|
|
1721
1975
|
return (0, _atscript_db_sql_tools.buildVectorSearchCount)(pgDialect, source, { maxDistance });
|
|
1722
1976
|
}
|
|
1723
1977
|
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
@@ -1727,11 +1981,13 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1727
1981
|
return this._vectorThresholds.get(indexName);
|
|
1728
1982
|
}
|
|
1729
1983
|
/**
|
|
1730
|
-
* Detects PostGIS support by attempting to enable the extension
|
|
1731
|
-
*
|
|
1984
|
+
* Detects PostGIS support by attempting to enable the extension — schema
|
|
1985
|
+
* sync's detection ({@link prepareTypeMapper}). Runs once; a probe that
|
|
1986
|
+
* already found PostGIS skips it.
|
|
1732
1987
|
*/
|
|
1733
1988
|
async _detectGeoSupport() {
|
|
1734
|
-
if (this._supportsGeo
|
|
1989
|
+
if (this._geoInstallTried || this._supportsGeo === true) return this._supportsGeo === true;
|
|
1990
|
+
this._geoInstallTried = true;
|
|
1735
1991
|
try {
|
|
1736
1992
|
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS postgis");
|
|
1737
1993
|
this._supportsGeo = true;
|
|
@@ -1741,17 +1997,92 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1741
1997
|
}
|
|
1742
1998
|
return this._supportsGeo;
|
|
1743
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Whether geo column `column` is a PostGIS column (EWKT) — synchronously
|
|
2002
|
+
* when known, else after {@link _probeGeo} on `exec` (the statement's own
|
|
2003
|
+
* executor). A column the catalog does not show (table not created yet)
|
|
2004
|
+
* follows PostGIS presence — what sync would create — and is probed again
|
|
2005
|
+
* next time.
|
|
2006
|
+
*/
|
|
2007
|
+
_geoColumnNative(column, exec) {
|
|
2008
|
+
if (this._supportsGeo === false) return false;
|
|
2009
|
+
const known = this._geoNative.get(column);
|
|
2010
|
+
if (known !== void 0) return known;
|
|
2011
|
+
return this._probeGeo(exec).then((probe) => probe.columns.get(column) ?? this._supportsGeo ?? probe.postgis);
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* The read-only geo probe ({@link GEO_PROBE_SQL}) of this table on `exec`:
|
|
2015
|
+
* records PostGIS presence when not known yet, and caches the type of every
|
|
2016
|
+
* geo column the catalog shows (unless a DDL path ran meanwhile).
|
|
2017
|
+
*
|
|
2018
|
+
* Concurrent callers on the SAME executor share one probe; a caller on
|
|
2019
|
+
* another executor runs its own — waiting on a probe queued for a pool
|
|
2020
|
+
* connection while holding a transaction connection could deadlock a
|
|
2021
|
+
* small pool, and a transaction's failure (an aborted one) never reaches
|
|
2022
|
+
* callers outside it. A failed probe is not cached, and a caller that
|
|
2023
|
+
* joined it retries once (a pool probe may have hit a broken connection).
|
|
2024
|
+
*/
|
|
2025
|
+
_probeGeo(exec) {
|
|
2026
|
+
const shared = this._geoProbe;
|
|
2027
|
+
if (shared?.exec === exec) return shared.promise.catch(() => this._runGeoProbe(exec));
|
|
2028
|
+
const promise = this._runGeoProbe(exec);
|
|
2029
|
+
const entry = {
|
|
2030
|
+
exec,
|
|
2031
|
+
promise
|
|
2032
|
+
};
|
|
2033
|
+
this._geoProbe = entry;
|
|
2034
|
+
const clear = () => {
|
|
2035
|
+
if (this._geoProbe === entry) this._geoProbe = void 0;
|
|
2036
|
+
};
|
|
2037
|
+
promise.then(clear, clear);
|
|
2038
|
+
return promise;
|
|
2039
|
+
}
|
|
2040
|
+
async _runGeoProbe(exec) {
|
|
2041
|
+
const gen = this._geoSchemaGen;
|
|
2042
|
+
const row = await exec.get(GEO_PROBE_SQL, [quoteTableName(this.resolveTableName()), this._geoColumnNames]);
|
|
2043
|
+
const postgis = row?.postgis === true;
|
|
2044
|
+
const raw = typeof row?.columns === "string" ? JSON.parse(row.columns) : row?.columns;
|
|
2045
|
+
const columns = /* @__PURE__ */ new Map();
|
|
2046
|
+
if (raw && typeof raw === "object") for (const [name, type] of Object.entries(raw)) columns.set(name, POSTGIS_TYPES.has(String(type)));
|
|
2047
|
+
this._supportsGeo ??= postgis;
|
|
2048
|
+
if (gen === this._geoSchemaGen) for (const [name, native] of columns) this._geoNative.set(name, native);
|
|
2049
|
+
return {
|
|
2050
|
+
postgis,
|
|
2051
|
+
columns
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* A DDL path ran (create / alter / recreate / rename / drop): the geo
|
|
2056
|
+
* column types are learned anew, and a probe in flight caches nothing.
|
|
2057
|
+
*/
|
|
2058
|
+
_geoSchemaChanged() {
|
|
2059
|
+
if (this._geoFields) {
|
|
2060
|
+
this._geoNative.clear();
|
|
2061
|
+
this._geoSchemaGen++;
|
|
2062
|
+
this._geoProbe = void 0;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* PostGIS presence when known; while unknown (no sync, no statement yet),
|
|
2067
|
+
* a table with geo columns answers `true` — the geo paths learn presence
|
|
2068
|
+
* before they run and refuse with `GEO_NOT_SUPPORTED` then, so the first
|
|
2069
|
+
* geo search of a process that never syncs is not refused up front.
|
|
2070
|
+
*/
|
|
1744
2071
|
isGeoSearchable() {
|
|
1745
|
-
return this._supportsGeo
|
|
2072
|
+
return this._supportsGeo ?? this._hasGeoPointFields();
|
|
2073
|
+
}
|
|
2074
|
+
/** PostGIS presence, probed when not known yet (geo search entry points). */
|
|
2075
|
+
async _ensureGeoKnown() {
|
|
2076
|
+
if (this._supportsGeo === void 0) await this._probeGeo(this._txConnection() ?? this.driver);
|
|
1746
2077
|
}
|
|
1747
2078
|
async geoSearch(point, query, indexName) {
|
|
1748
|
-
await this.
|
|
2079
|
+
await this._ensureGeoKnown();
|
|
1749
2080
|
const { sql, params } = this._buildGeoSearchSelect(this._prepareGeoSearch(point, query, indexName));
|
|
1750
2081
|
this._log(sql, params);
|
|
1751
2082
|
return (await this._exec().all(sql, params)).map((row) => (0, _atscript_db_sql_tools.renameGeoDistance)(row));
|
|
1752
2083
|
}
|
|
1753
2084
|
async geoSearchWithCount(point, query, indexName) {
|
|
1754
|
-
await this.
|
|
2085
|
+
await this._ensureGeoKnown();
|
|
1755
2086
|
const ctx = this._prepareGeoSearch(point, query, indexName);
|
|
1756
2087
|
const { sql, params } = this._buildGeoSearchSelect(ctx);
|
|
1757
2088
|
const countFrag = (0, _atscript_db_sql_tools.buildGeoSearchCount)(pgDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window);
|
|
@@ -1765,10 +2096,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1765
2096
|
}
|
|
1766
2097
|
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
1767
2098
|
_prepareGeoSearch(point, query, indexName) {
|
|
1768
|
-
if (!this._supportsGeo) throw
|
|
1769
|
-
path: "",
|
|
1770
|
-
message: "Geo search requires the PostGIS extension"
|
|
1771
|
-
}]);
|
|
2099
|
+
if (!this._supportsGeo) throw geoNotSupported("", "Geo search requires the PostGIS extension");
|
|
1772
2100
|
const column = this._resolveGeoColumn(indexName);
|
|
1773
2101
|
const controls = query.controls ?? {};
|
|
1774
2102
|
return {
|
|
@@ -1886,16 +2214,39 @@ function vectorToString(vector) {
|
|
|
1886
2214
|
}
|
|
1887
2215
|
//#endregion
|
|
1888
2216
|
//#region src/pg-driver.ts
|
|
1889
|
-
|
|
2217
|
+
const NO_PARAMS = [];
|
|
2218
|
+
/**
|
|
2219
|
+
* pg rejects `undefined` in bind arrays — coerce to `null`. Copies only when
|
|
2220
|
+
* there is one (an array hole counts): the common case binds `params` as is
|
|
2221
|
+
* (pg never mutates the bind array).
|
|
2222
|
+
*/
|
|
1890
2223
|
function sanitizeParams(params) {
|
|
1891
|
-
if (!params) return
|
|
1892
|
-
return params.
|
|
2224
|
+
if (!params) return NO_PARAMS;
|
|
2225
|
+
return params.includes(void 0) ? Array.from(params, (v) => v === void 0 ? null : v) : params;
|
|
1893
2226
|
}
|
|
1894
|
-
/** Parses
|
|
2227
|
+
/** Parses TIMESTAMPTZ (its text carries the UTC offset) to epoch milliseconds. */
|
|
1895
2228
|
function parseTimestamp(val) {
|
|
1896
2229
|
const ms = new Date(val).getTime();
|
|
1897
2230
|
return Number.isNaN(ms) ? val : ms;
|
|
1898
2231
|
}
|
|
2232
|
+
/** PostgreSQL's text form of a `timestamp without time zone` (AD, finite): `YYYY-MM-DD HH:MM:SS[.ffffff]`. */
|
|
2233
|
+
const TIMESTAMP_TEXT = /^(\d{4,})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d{1,6}))?$/;
|
|
2234
|
+
/**
|
|
2235
|
+
* Parses TIMESTAMP (without time zone) to epoch milliseconds, reading the
|
|
2236
|
+
* wall time as UTC — the same instant whatever the process time zone (since
|
|
2237
|
+
* 0.1.151; it used to be the process's local time). Matches the MySQL
|
|
2238
|
+
* adapter's DATETIME reading. Sub-millisecond digits are truncated; other
|
|
2239
|
+
* forms (`infinity`, BC dates) keep the previous parse.
|
|
2240
|
+
*/
|
|
2241
|
+
function parseTimestampUtc(val) {
|
|
2242
|
+
const m = TIMESTAMP_TEXT.exec(val);
|
|
2243
|
+
if (!m) return parseTimestamp(val);
|
|
2244
|
+
const d = /* @__PURE__ */ new Date(0);
|
|
2245
|
+
d.setUTCFullYear(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
2246
|
+
d.setUTCHours(Number(m[4]), Number(m[5]), Number(m[6]), m[7] ? Number(m[7].padEnd(3, "0").slice(0, 3)) : 0);
|
|
2247
|
+
const ms = d.getTime();
|
|
2248
|
+
return Number.isNaN(ms) ? val : ms;
|
|
2249
|
+
}
|
|
1899
2250
|
/** Parses NUMERIC to number. */
|
|
1900
2251
|
function parseNumeric(val) {
|
|
1901
2252
|
const n = Number.parseFloat(val);
|
|
@@ -1915,13 +2266,14 @@ const INT8_OID = 20;
|
|
|
1915
2266
|
* Creates a per-pool custom types config that overrides specific parsers
|
|
1916
2267
|
* without mutating the global `pg.types`.
|
|
1917
2268
|
*
|
|
1918
|
-
* -
|
|
2269
|
+
* - TIMESTAMPTZ → epoch milliseconds (number); TIMESTAMP → epoch ms of its
|
|
2270
|
+
* wall time read as UTC (independent of the process time zone)
|
|
1919
2271
|
* - NUMERIC → number (not string)
|
|
1920
2272
|
* - INT8/BIGINT → number (for JS-safe range)
|
|
1921
2273
|
*/
|
|
1922
2274
|
function createCustomTypes(pgTypes) {
|
|
1923
2275
|
const overrides = new Map([
|
|
1924
|
-
[TIMESTAMP_OID,
|
|
2276
|
+
[TIMESTAMP_OID, parseTimestampUtc],
|
|
1925
2277
|
[TIMESTAMPTZ_OID, parseTimestamp],
|
|
1926
2278
|
[NUMERIC_OID, parseNumeric],
|
|
1927
2279
|
[INT8_OID, parseBigInt]
|