@atscript/db-postgres 0.1.160 → 0.1.162
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 +101 -63
- package/dist/index.d.cts +25 -5
- package/dist/index.d.mts +25 -5
- package/dist/index.mjs +103 -65
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -152,6 +152,7 @@ const pgDialect = {
|
|
|
152
152
|
return quoteTableName(name);
|
|
153
153
|
},
|
|
154
154
|
unlimitedLimit: "ALL",
|
|
155
|
+
stringLiteral: _atscript_db_sql_tools.sqlStringLiteral,
|
|
155
156
|
toValue: toPgValue,
|
|
156
157
|
toParam(value) {
|
|
157
158
|
if (value === void 0) return null;
|
|
@@ -310,8 +311,8 @@ function buildInsert(table, data) {
|
|
|
310
311
|
function buildInsertMany(table, rows, columns) {
|
|
311
312
|
return (0, _atscript_db_sql_tools.buildInsertMany)(pgDialect, table, rows, columns);
|
|
312
313
|
}
|
|
313
|
-
function buildSelect(table, where, controls) {
|
|
314
|
-
return (0, _atscript_db_sql_tools.buildSelect)(pgDialect, table, where, controls);
|
|
314
|
+
function buildSelect(table, where, controls, lock) {
|
|
315
|
+
return (0, _atscript_db_sql_tools.buildSelect)(pgDialect, table, where, controls, lock);
|
|
315
316
|
}
|
|
316
317
|
function buildUpdate(table, data, where, limit, ops, versionColumn, expectedVersion) {
|
|
317
318
|
return (0, _atscript_db_sql_tools.buildUpdate)(pgDialect, table, data, where, limit, ops, versionColumn, expectedVersion);
|
|
@@ -613,6 +614,8 @@ function truncateBytes(s, bytes) {
|
|
|
613
614
|
while (byteLength(out) > bytes) out = out.slice(0, -1);
|
|
614
615
|
return out;
|
|
615
616
|
}
|
|
617
|
+
/** Name prefix of the pgvector HNSW indexes (managed outside the model's `indexes`). */
|
|
618
|
+
const PG_VECTOR_INDEX_PREFIX = "atscript__vec_";
|
|
616
619
|
/**
|
|
617
620
|
* The name PostgreSQL generates for an unnamed constraint (`makeObjectName`):
|
|
618
621
|
* `<table>_<columns joined by _>_<label>`, with the table and column parts
|
|
@@ -708,11 +711,12 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
708
711
|
_txConnection() {
|
|
709
712
|
return this._getTransactionState();
|
|
710
713
|
}
|
|
711
|
-
async _beginTransaction() {
|
|
714
|
+
async _beginTransaction(opts) {
|
|
712
715
|
const conn = await this.driver.getConnection();
|
|
713
716
|
try {
|
|
714
|
-
|
|
715
|
-
|
|
717
|
+
const sql = opts?.isolation ? `BEGIN ISOLATION LEVEL ${opts.isolation.toUpperCase()}` : "BEGIN";
|
|
718
|
+
await conn.exec(sql);
|
|
719
|
+
this._log(sql);
|
|
716
720
|
return conn;
|
|
717
721
|
} catch (err) {
|
|
718
722
|
conn.release();
|
|
@@ -819,6 +823,18 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
819
823
|
supportsNullsPlacement() {
|
|
820
824
|
return true;
|
|
821
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
|
|
828
|
+
* `SKIP LOCKED`]. Under REPEATABLE READ / SERIALIZABLE, locking a row that
|
|
829
|
+
* changed since the snapshot fails with `SERIALIZATION_FAILURE`. @since 0.1.162
|
|
830
|
+
*/
|
|
831
|
+
supportsReadLocks() {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
/** Every level, via `BEGIN ISOLATION LEVEL …`. @since 0.1.162 */
|
|
835
|
+
isolationLevels() {
|
|
836
|
+
return _atscript_db.ALL_ISOLATION_LEVELS;
|
|
837
|
+
}
|
|
822
838
|
/** Computed view columns and first-row joins. */
|
|
823
839
|
viewCapabilities() {
|
|
824
840
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -1086,7 +1102,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1086
1102
|
if (clauses.length === 0) continue;
|
|
1087
1103
|
const sql = `SELECT ${selectCols} FROM ${quoteTableName(this.resolveTableName())} WHERE ${clauses.join(" OR ")} ORDER BY ${order} FOR UPDATE`;
|
|
1088
1104
|
this._log(sql, params);
|
|
1089
|
-
const found = await this.
|
|
1105
|
+
const found = await this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
|
|
1090
1106
|
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1091
1107
|
const tuple = (0, _atscript_db.uniqueKeyTuple)(doc, fields);
|
|
1092
1108
|
if (tuple !== void 0) lockedTuples[k].add(tuple);
|
|
@@ -1128,15 +1144,15 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1128
1144
|
...query.controls,
|
|
1129
1145
|
$limit: 1
|
|
1130
1146
|
};
|
|
1131
|
-
const { sql, params } = buildSelect(this.resolveTableName(), where, controls);
|
|
1147
|
+
const { sql, params } = buildSelect(this.resolveTableName(), where, controls, query.lock);
|
|
1132
1148
|
this._log(sql, params);
|
|
1133
|
-
return this._exec().get(sql, params);
|
|
1149
|
+
return this._exec().get(sql, params).catch((error) => this._mapConstraintError(error));
|
|
1134
1150
|
}
|
|
1135
1151
|
async findMany(query) {
|
|
1136
1152
|
const where = buildWhere(query.filter);
|
|
1137
|
-
const { sql, params } = buildSelect(this.resolveTableName(), where, query.controls);
|
|
1153
|
+
const { sql, params } = buildSelect(this.resolveTableName(), where, query.controls, query.lock);
|
|
1138
1154
|
this._log(sql, params);
|
|
1139
|
-
return this._exec().all(sql, params);
|
|
1155
|
+
return this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
|
|
1140
1156
|
}
|
|
1141
1157
|
/**
|
|
1142
1158
|
* `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
|
|
@@ -1241,8 +1257,21 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1241
1257
|
modifiedCount: result.affectedRows
|
|
1242
1258
|
};
|
|
1243
1259
|
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Bounded writes (since 0.1.162): PostgreSQL has no `UPDATE/DELETE … LIMIT`,
|
|
1262
|
+
* so the statement is re-keyed on the primary key through an ordered,
|
|
1263
|
+
* limited subquery — the `deleteOne` / `updateOne` pattern with `n` rows.
|
|
1264
|
+
*/
|
|
1265
|
+
supportsWriteLimit() {
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
/** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
|
|
1269
|
+
_boundedWhere(where, bound) {
|
|
1270
|
+
if (!bound) return where;
|
|
1271
|
+
return (0, _atscript_db_sql_tools.writeLimitWhere)(pgDialect, quoteTableName(this.resolveTableName()), this._pk().cols.map((c) => qi(c)), where, bound);
|
|
1272
|
+
}
|
|
1244
1273
|
async updateMany(filter, data, ops, opts) {
|
|
1245
|
-
const where = buildWhere(filter);
|
|
1274
|
+
const where = this._boundedWhere(buildWhere(filter), opts?.limit);
|
|
1246
1275
|
const versionColumn = this._versionColumnFor(opts);
|
|
1247
1276
|
const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, ops, versionColumn);
|
|
1248
1277
|
this._log(sql, params);
|
|
@@ -1253,8 +1282,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1253
1282
|
};
|
|
1254
1283
|
}
|
|
1255
1284
|
async replaceOne(filter, data, expectedVersion) {
|
|
1256
|
-
const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, this._replaceColumns(), this._table.versionColumnPhysical);
|
|
1257
|
-
return this.updateOne(filter, full,
|
|
1285
|
+
const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, this._replaceColumns(), this._table.versionColumnPhysical, this._table.onUpdateIncPhysical);
|
|
1286
|
+
return this.updateOne(filter, full, this._onUpdateIncOps(), expectedVersion);
|
|
1258
1287
|
}
|
|
1259
1288
|
/** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
|
|
1260
1289
|
_replaceColumns() {
|
|
@@ -1267,7 +1296,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1267
1296
|
return memo.cols;
|
|
1268
1297
|
}
|
|
1269
1298
|
async replaceMany(filter, data) {
|
|
1270
|
-
return this.updateMany(filter, data);
|
|
1299
|
+
return this.updateMany(filter, data, this._onUpdateIncOps());
|
|
1271
1300
|
}
|
|
1272
1301
|
async deleteOne(filter) {
|
|
1273
1302
|
const where = buildWhere(filter);
|
|
@@ -1280,8 +1309,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1280
1309
|
this._log(sql, params);
|
|
1281
1310
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
1282
1311
|
}
|
|
1283
|
-
async deleteMany(filter) {
|
|
1284
|
-
return this._deleteWhere(buildWhere(filter));
|
|
1312
|
+
async deleteMany(filter, opts) {
|
|
1313
|
+
return this._deleteWhere(this._boundedWhere(buildWhere(filter), opts?.limit));
|
|
1285
1314
|
}
|
|
1286
1315
|
async _deleteWhere(where) {
|
|
1287
1316
|
const { sql, params } = buildDelete(this.resolveTableName(), where);
|
|
@@ -1773,13 +1802,23 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1773
1802
|
if (field.isGeoPoint) return this._supportsGeo ? "geography(Point,4326)" : pgTypeFromField(field);
|
|
1774
1803
|
return pgTypeFromField(field);
|
|
1775
1804
|
}
|
|
1776
|
-
async
|
|
1805
|
+
async _syncIndexes(dryRun) {
|
|
1777
1806
|
const tableName = this._table.tableName;
|
|
1778
1807
|
const schema = this._schema;
|
|
1779
1808
|
await this.prepareTypeMapper();
|
|
1780
|
-
|
|
1809
|
+
const dropIndex = async (name) => {
|
|
1810
|
+
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
1811
|
+
this._log(sql);
|
|
1812
|
+
await this._exec().exec(sql);
|
|
1813
|
+
};
|
|
1814
|
+
/** Every index name on the table (read by `listExisting`) — the vector pass checks it. */
|
|
1815
|
+
let liveNames = /* @__PURE__ */ new Set();
|
|
1816
|
+
const changes = await this.syncIndexesWithDiff({
|
|
1817
|
+
dryRun,
|
|
1818
|
+
isUnmanaged: (name) => name.startsWith(PG_VECTOR_INDEX_PREFIX),
|
|
1819
|
+
skipIndex: (index) => index.type === "geo" && !this._supportsGeo ? `[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped` : void 0,
|
|
1781
1820
|
listExisting: async () => {
|
|
1782
|
-
|
|
1821
|
+
const rows = await this._exec().all(`SELECT i.relname AS name,
|
|
1783
1822
|
(SELECT array_agg(a.attname::text ORDER BY k.ord)
|
|
1784
1823
|
FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1785
1824
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
@@ -1788,7 +1827,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1788
1827
|
JOIN pg_class t ON t.oid = x.indrelid
|
|
1789
1828
|
JOIN pg_class i ON i.oid = x.indexrelid
|
|
1790
1829
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
1791
|
-
WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [tableName, schema])
|
|
1830
|
+
WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [tableName, schema]);
|
|
1831
|
+
liveNames = new Set(rows.map((r) => r.name));
|
|
1832
|
+
return rows.map((r) => ({
|
|
1792
1833
|
name: r.name,
|
|
1793
1834
|
columns: r.columns ?? void 0
|
|
1794
1835
|
}));
|
|
@@ -1804,10 +1845,6 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1804
1845
|
return;
|
|
1805
1846
|
}
|
|
1806
1847
|
if (index.type === "geo") {
|
|
1807
|
-
if (!this._supportsGeo) {
|
|
1808
|
-
this.logger.warn(`[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped`);
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
1848
|
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gist (${qi(index.fields[0].name)})`;
|
|
1812
1849
|
this._log(sql);
|
|
1813
1850
|
await this._exec().exec(sql);
|
|
@@ -1819,54 +1856,57 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1819
1856
|
this._log(sql);
|
|
1820
1857
|
await this._exec().exec(sql);
|
|
1821
1858
|
},
|
|
1822
|
-
dropIndex
|
|
1823
|
-
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
1824
|
-
this._log(sql);
|
|
1825
|
-
await this._exec().exec(sql);
|
|
1826
|
-
}
|
|
1859
|
+
dropIndex
|
|
1827
1860
|
});
|
|
1861
|
+
const desiredVector = new Set(this._supportsVector ? [...this._vectorFields.values()].map((v) => `${PG_VECTOR_INDEX_PREFIX}${v.indexName}`) : []);
|
|
1862
|
+
const vector = (0, _atscript_db.createChangeRecorder)(dryRun);
|
|
1863
|
+
for (const name of liveNames) if (name.startsWith(PG_VECTOR_INDEX_PREFIX) && !desiredVector.has(name)) await vector.apply({
|
|
1864
|
+
name,
|
|
1865
|
+
action: "drop"
|
|
1866
|
+
}, name, () => dropIndex(name));
|
|
1828
1867
|
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
1829
|
-
const indexName =
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1868
|
+
const indexName = `${PG_VECTOR_INDEX_PREFIX}${vec.indexName}`;
|
|
1869
|
+
if (liveNames.has(indexName)) continue;
|
|
1870
|
+
await vector.apply({
|
|
1871
|
+
name: indexName,
|
|
1872
|
+
action: "add"
|
|
1873
|
+
}, indexName, async () => {
|
|
1874
|
+
const opsClass = similarityToPgOps(vec.similarity);
|
|
1875
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(indexName)} ON ${quoteTableName(this.resolveTableName())} USING hnsw (${qi(field)} ${opsClass})`;
|
|
1876
|
+
this._log(sql);
|
|
1877
|
+
await this._exec().exec(sql);
|
|
1878
|
+
});
|
|
1834
1879
|
}
|
|
1880
|
+
return [...changes, ...vector.changes];
|
|
1835
1881
|
}
|
|
1836
1882
|
async syncForeignKeys() {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
for (const fk of this._table.foreignKeys.values()) {
|
|
1850
|
-
const key = [...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(",");
|
|
1851
|
-
if (!existingKeys.has(key)) {
|
|
1883
|
+
return this._syncForeignKeys(false);
|
|
1884
|
+
}
|
|
1885
|
+
/** The FK changes {@link syncForeignKeys} would make — no DDL. */
|
|
1886
|
+
async planForeignKeys() {
|
|
1887
|
+
return this._syncForeignKeys(true);
|
|
1888
|
+
}
|
|
1889
|
+
_syncForeignKeys(dryRun) {
|
|
1890
|
+
return this.syncForeignKeysWithDiff({
|
|
1891
|
+
dryRun,
|
|
1892
|
+
listExisting: () => this._getExistingFkConstraints(),
|
|
1893
|
+
dropConstraint: (name) => this._dropFkConstraint(name),
|
|
1894
|
+
addConstraint: async (fk) => {
|
|
1852
1895
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
|
|
1853
1896
|
this._log(ddl);
|
|
1854
1897
|
await this._exec().exec(ddl);
|
|
1855
1898
|
}
|
|
1856
|
-
}
|
|
1899
|
+
});
|
|
1857
1900
|
}
|
|
1858
1901
|
async dropForeignKeys(fkFieldKeys) {
|
|
1859
1902
|
if (fkFieldKeys.length === 0) return;
|
|
1860
1903
|
const keySet = new Set(fkFieldKeys);
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
await this._exec().exec(ddl);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1904
|
+
for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has((0, _atscript_db.fkKey)(cols))) await this._dropFkConstraint(constraintName);
|
|
1905
|
+
}
|
|
1906
|
+
async _dropFkConstraint(name) {
|
|
1907
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP CONSTRAINT ${qi(name)}`;
|
|
1908
|
+
this._log(ddl);
|
|
1909
|
+
await this._exec().exec(ddl);
|
|
1870
1910
|
}
|
|
1871
1911
|
/** Queries information_schema for existing FK constraints. */
|
|
1872
1912
|
async _getExistingFkConstraints() {
|
|
@@ -2304,10 +2344,8 @@ function normalizePgDefault(value, isIdentity) {
|
|
|
2304
2344
|
if (lower.includes("extract") && lower.includes("epoch") && lower.includes("now()")) return "fn:now";
|
|
2305
2345
|
if (lower === "gen_random_uuid()") return "fn:uuid";
|
|
2306
2346
|
if (lower.startsWith("nextval(")) return "fn:increment";
|
|
2307
|
-
const castMatch = cleaned.match(/^'
|
|
2308
|
-
|
|
2309
|
-
if (cleaned.startsWith("'") && cleaned.endsWith("'")) return cleaned.slice(1, -1).replace(/''/g, "'");
|
|
2310
|
-
return cleaned;
|
|
2347
|
+
const castMatch = cleaned.match(/^('.*')::[\w\s]+$/);
|
|
2348
|
+
return (0, _atscript_db_sql_tools.unquoteSqlStringLiteral)(castMatch ? castMatch[1] : cleaned) ?? cleaned;
|
|
2311
2349
|
}
|
|
2312
2350
|
/**
|
|
2313
2351
|
* Converts a normalized similarity threshold (0-1) to a pgvector max distance.
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteManyOptions, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbIsolationLevel, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TForeignKeyChange, TGenericLogger, TIndexChange, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTransactionOptions, 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
|
|
|
@@ -134,7 +134,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
134
134
|
protected _transactionOwner(): unknown;
|
|
135
135
|
/** The dedicated connection of this pool's open transaction, if any. */
|
|
136
136
|
private _txConnection;
|
|
137
|
-
protected _beginTransaction(): Promise<TPgConnection>;
|
|
137
|
+
protected _beginTransaction(opts?: TTransactionOptions): Promise<TPgConnection>;
|
|
138
138
|
protected _commitTransaction(state: unknown): Promise<void>;
|
|
139
139
|
protected _rollbackTransaction(state: unknown): Promise<void>;
|
|
140
140
|
/**
|
|
@@ -178,6 +178,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
178
178
|
supportsAggregateExpressions(): boolean;
|
|
179
179
|
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
180
180
|
supportsNullsPlacement(): boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
|
|
183
|
+
* `SKIP LOCKED`]. Under REPEATABLE READ / SERIALIZABLE, locking a row that
|
|
184
|
+
* changed since the snapshot fails with `SERIALIZATION_FAILURE`. @since 0.1.162
|
|
185
|
+
*/
|
|
186
|
+
supportsReadLocks(): boolean;
|
|
187
|
+
/** Every level, via `BEGIN ISOLATION LEVEL …`. @since 0.1.162 */
|
|
188
|
+
isolationLevels(): ReadonlySet<TDbIsolationLevel>;
|
|
181
189
|
/** Computed view columns and first-row joins. */
|
|
182
190
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
183
191
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -274,13 +282,21 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
274
282
|
*/
|
|
275
283
|
private _limitOnePredicate;
|
|
276
284
|
updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
285
|
+
/**
|
|
286
|
+
* Bounded writes (since 0.1.162): PostgreSQL has no `UPDATE/DELETE … LIMIT`,
|
|
287
|
+
* so the statement is re-keyed on the primary key through an ordered,
|
|
288
|
+
* limited subquery — the `deleteOne` / `updateOne` pattern with `n` rows.
|
|
289
|
+
*/
|
|
290
|
+
supportsWriteLimit(): boolean;
|
|
291
|
+
/** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
|
|
292
|
+
private _boundedWhere;
|
|
277
293
|
updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
278
294
|
replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
|
|
279
295
|
/** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
|
|
280
296
|
private _replaceColumns;
|
|
281
297
|
replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
282
298
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
283
|
-
deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
299
|
+
deleteMany(filter: FilterExpr, opts?: TDbDeleteManyOptions): Promise<TDbDeleteResult>;
|
|
284
300
|
private _deleteWhere;
|
|
285
301
|
prepareTypeMapper(): Promise<void>;
|
|
286
302
|
/**
|
|
@@ -356,9 +372,13 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
356
372
|
dropViewByName(viewName: string): Promise<void>;
|
|
357
373
|
renameTable(oldName: string): Promise<void>;
|
|
358
374
|
typeMapper(field: TDbFieldMeta): string;
|
|
359
|
-
|
|
360
|
-
syncForeignKeys(): Promise<
|
|
375
|
+
protected _syncIndexes(dryRun: boolean): Promise<TIndexChange[]>;
|
|
376
|
+
syncForeignKeys(): Promise<TForeignKeyChange[]>;
|
|
377
|
+
/** The FK changes {@link syncForeignKeys} would make — no DDL. */
|
|
378
|
+
planForeignKeys(): Promise<TForeignKeyChange[]>;
|
|
379
|
+
private _syncForeignKeys;
|
|
361
380
|
dropForeignKeys(fkFieldKeys: string[]): Promise<void>;
|
|
381
|
+
private _dropFkConstraint;
|
|
362
382
|
/** Queries information_schema for existing FK constraints. */
|
|
363
383
|
private _getExistingFkConstraints;
|
|
364
384
|
getSearchIndexes(): TSearchIndexInfo[];
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteManyOptions, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbIsolationLevel, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TForeignKeyChange, TGenericLogger, TIndexChange, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTransactionOptions, 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
|
|
|
@@ -134,7 +134,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
134
134
|
protected _transactionOwner(): unknown;
|
|
135
135
|
/** The dedicated connection of this pool's open transaction, if any. */
|
|
136
136
|
private _txConnection;
|
|
137
|
-
protected _beginTransaction(): Promise<TPgConnection>;
|
|
137
|
+
protected _beginTransaction(opts?: TTransactionOptions): Promise<TPgConnection>;
|
|
138
138
|
protected _commitTransaction(state: unknown): Promise<void>;
|
|
139
139
|
protected _rollbackTransaction(state: unknown): Promise<void>;
|
|
140
140
|
/**
|
|
@@ -178,6 +178,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
178
178
|
supportsAggregateExpressions(): boolean;
|
|
179
179
|
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
180
180
|
supportsNullsPlacement(): boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
|
|
183
|
+
* `SKIP LOCKED`]. Under REPEATABLE READ / SERIALIZABLE, locking a row that
|
|
184
|
+
* changed since the snapshot fails with `SERIALIZATION_FAILURE`. @since 0.1.162
|
|
185
|
+
*/
|
|
186
|
+
supportsReadLocks(): boolean;
|
|
187
|
+
/** Every level, via `BEGIN ISOLATION LEVEL …`. @since 0.1.162 */
|
|
188
|
+
isolationLevels(): ReadonlySet<TDbIsolationLevel>;
|
|
181
189
|
/** Computed view columns and first-row joins. */
|
|
182
190
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
183
191
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -274,13 +282,21 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
274
282
|
*/
|
|
275
283
|
private _limitOnePredicate;
|
|
276
284
|
updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
285
|
+
/**
|
|
286
|
+
* Bounded writes (since 0.1.162): PostgreSQL has no `UPDATE/DELETE … LIMIT`,
|
|
287
|
+
* so the statement is re-keyed on the primary key through an ordered,
|
|
288
|
+
* limited subquery — the `deleteOne` / `updateOne` pattern with `n` rows.
|
|
289
|
+
*/
|
|
290
|
+
supportsWriteLimit(): boolean;
|
|
291
|
+
/** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
|
|
292
|
+
private _boundedWhere;
|
|
277
293
|
updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
278
294
|
replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
|
|
279
295
|
/** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
|
|
280
296
|
private _replaceColumns;
|
|
281
297
|
replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
282
298
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
283
|
-
deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
299
|
+
deleteMany(filter: FilterExpr, opts?: TDbDeleteManyOptions): Promise<TDbDeleteResult>;
|
|
284
300
|
private _deleteWhere;
|
|
285
301
|
prepareTypeMapper(): Promise<void>;
|
|
286
302
|
/**
|
|
@@ -356,9 +372,13 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
356
372
|
dropViewByName(viewName: string): Promise<void>;
|
|
357
373
|
renameTable(oldName: string): Promise<void>;
|
|
358
374
|
typeMapper(field: TDbFieldMeta): string;
|
|
359
|
-
|
|
360
|
-
syncForeignKeys(): Promise<
|
|
375
|
+
protected _syncIndexes(dryRun: boolean): Promise<TIndexChange[]>;
|
|
376
|
+
syncForeignKeys(): Promise<TForeignKeyChange[]>;
|
|
377
|
+
/** The FK changes {@link syncForeignKeys} would make — no DDL. */
|
|
378
|
+
planForeignKeys(): Promise<TForeignKeyChange[]>;
|
|
379
|
+
private _syncForeignKeys;
|
|
361
380
|
dropForeignKeys(fkFieldKeys: string[]): Promise<void>;
|
|
381
|
+
private _dropFkConstraint;
|
|
362
382
|
/** Queries information_schema for existing FK constraints. */
|
|
363
383
|
private _getExistingFkConstraints;
|
|
364
384
|
getSearchIndexes(): TSearchIndexInfo[];
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, defaultFulltextIndex, describeFulltext,
|
|
1
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, createChangeRecorder, defaultFulltextIndex, describeFulltext, fkKey, isColumnTypeChanged, searchTermInteger, splitFulltextFields, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
2
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
3
|
-
import { SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, parseRegexString, quotedJsonPathSegments, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, vectorDistanceSource } from "@atscript/db-sql-tools";
|
|
3
|
+
import { SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, parseRegexString, quotedJsonPathSegments, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, unquoteSqlStringLiteral, vectorDistanceSource, writeLimitWhere } from "@atscript/db-sql-tools";
|
|
4
4
|
import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
|
|
5
5
|
//#region src/insert-ignore.ts
|
|
6
6
|
/**
|
|
@@ -151,6 +151,7 @@ const pgDialect = {
|
|
|
151
151
|
return quoteTableName(name);
|
|
152
152
|
},
|
|
153
153
|
unlimitedLimit: "ALL",
|
|
154
|
+
stringLiteral: sqlStringLiteral,
|
|
154
155
|
toValue: toPgValue,
|
|
155
156
|
toParam(value) {
|
|
156
157
|
if (value === void 0) return null;
|
|
@@ -309,8 +310,8 @@ function buildInsert$1(table, data) {
|
|
|
309
310
|
function buildInsertMany$1(table, rows, columns) {
|
|
310
311
|
return buildInsertMany(pgDialect, table, rows, columns);
|
|
311
312
|
}
|
|
312
|
-
function buildSelect$1(table, where, controls) {
|
|
313
|
-
return buildSelect(pgDialect, table, where, controls);
|
|
313
|
+
function buildSelect$1(table, where, controls, lock) {
|
|
314
|
+
return buildSelect(pgDialect, table, where, controls, lock);
|
|
314
315
|
}
|
|
315
316
|
function buildUpdate$1(table, data, where, limit, ops, versionColumn, expectedVersion) {
|
|
316
317
|
return buildUpdate(pgDialect, table, data, where, limit, ops, versionColumn, expectedVersion);
|
|
@@ -612,6 +613,8 @@ function truncateBytes(s, bytes) {
|
|
|
612
613
|
while (byteLength(out) > bytes) out = out.slice(0, -1);
|
|
613
614
|
return out;
|
|
614
615
|
}
|
|
616
|
+
/** Name prefix of the pgvector HNSW indexes (managed outside the model's `indexes`). */
|
|
617
|
+
const PG_VECTOR_INDEX_PREFIX = "atscript__vec_";
|
|
615
618
|
/**
|
|
616
619
|
* The name PostgreSQL generates for an unnamed constraint (`makeObjectName`):
|
|
617
620
|
* `<table>_<columns joined by _>_<label>`, with the table and column parts
|
|
@@ -707,11 +710,12 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
707
710
|
_txConnection() {
|
|
708
711
|
return this._getTransactionState();
|
|
709
712
|
}
|
|
710
|
-
async _beginTransaction() {
|
|
713
|
+
async _beginTransaction(opts) {
|
|
711
714
|
const conn = await this.driver.getConnection();
|
|
712
715
|
try {
|
|
713
|
-
|
|
714
|
-
|
|
716
|
+
const sql = opts?.isolation ? `BEGIN ISOLATION LEVEL ${opts.isolation.toUpperCase()}` : "BEGIN";
|
|
717
|
+
await conn.exec(sql);
|
|
718
|
+
this._log(sql);
|
|
715
719
|
return conn;
|
|
716
720
|
} catch (err) {
|
|
717
721
|
conn.release();
|
|
@@ -818,6 +822,18 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
818
822
|
supportsNullsPlacement() {
|
|
819
823
|
return true;
|
|
820
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
|
|
827
|
+
* `SKIP LOCKED`]. Under REPEATABLE READ / SERIALIZABLE, locking a row that
|
|
828
|
+
* changed since the snapshot fails with `SERIALIZATION_FAILURE`. @since 0.1.162
|
|
829
|
+
*/
|
|
830
|
+
supportsReadLocks() {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
/** Every level, via `BEGIN ISOLATION LEVEL …`. @since 0.1.162 */
|
|
834
|
+
isolationLevels() {
|
|
835
|
+
return ALL_ISOLATION_LEVELS;
|
|
836
|
+
}
|
|
821
837
|
/** Computed view columns and first-row joins. */
|
|
822
838
|
viewCapabilities() {
|
|
823
839
|
return ALL_VIEW_CAPABILITIES;
|
|
@@ -1085,7 +1101,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1085
1101
|
if (clauses.length === 0) continue;
|
|
1086
1102
|
const sql = `SELECT ${selectCols} FROM ${quoteTableName(this.resolveTableName())} WHERE ${clauses.join(" OR ")} ORDER BY ${order} FOR UPDATE`;
|
|
1087
1103
|
this._log(sql, params);
|
|
1088
|
-
const found = await this.
|
|
1104
|
+
const found = await this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
|
|
1089
1105
|
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1090
1106
|
const tuple = uniqueKeyTuple(doc, fields);
|
|
1091
1107
|
if (tuple !== void 0) lockedTuples[k].add(tuple);
|
|
@@ -1127,15 +1143,15 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1127
1143
|
...query.controls,
|
|
1128
1144
|
$limit: 1
|
|
1129
1145
|
};
|
|
1130
|
-
const { sql, params } = buildSelect$1(this.resolveTableName(), where, controls);
|
|
1146
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), where, controls, query.lock);
|
|
1131
1147
|
this._log(sql, params);
|
|
1132
|
-
return this._exec().get(sql, params);
|
|
1148
|
+
return this._exec().get(sql, params).catch((error) => this._mapConstraintError(error));
|
|
1133
1149
|
}
|
|
1134
1150
|
async findMany(query) {
|
|
1135
1151
|
const where = buildWhere(query.filter);
|
|
1136
|
-
const { sql, params } = buildSelect$1(this.resolveTableName(), where, query.controls);
|
|
1152
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), where, query.controls, query.lock);
|
|
1137
1153
|
this._log(sql, params);
|
|
1138
|
-
return this._exec().all(sql, params);
|
|
1154
|
+
return this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
|
|
1139
1155
|
}
|
|
1140
1156
|
/**
|
|
1141
1157
|
* `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
|
|
@@ -1240,8 +1256,21 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1240
1256
|
modifiedCount: result.affectedRows
|
|
1241
1257
|
};
|
|
1242
1258
|
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Bounded writes (since 0.1.162): PostgreSQL has no `UPDATE/DELETE … LIMIT`,
|
|
1261
|
+
* so the statement is re-keyed on the primary key through an ordered,
|
|
1262
|
+
* limited subquery — the `deleteOne` / `updateOne` pattern with `n` rows.
|
|
1263
|
+
*/
|
|
1264
|
+
supportsWriteLimit() {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
/** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
|
|
1268
|
+
_boundedWhere(where, bound) {
|
|
1269
|
+
if (!bound) return where;
|
|
1270
|
+
return writeLimitWhere(pgDialect, quoteTableName(this.resolveTableName()), this._pk().cols.map((c) => qi(c)), where, bound);
|
|
1271
|
+
}
|
|
1243
1272
|
async updateMany(filter, data, ops, opts) {
|
|
1244
|
-
const where = buildWhere(filter);
|
|
1273
|
+
const where = this._boundedWhere(buildWhere(filter), opts?.limit);
|
|
1245
1274
|
const versionColumn = this._versionColumnFor(opts);
|
|
1246
1275
|
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, ops, versionColumn);
|
|
1247
1276
|
this._log(sql, params);
|
|
@@ -1252,8 +1281,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1252
1281
|
};
|
|
1253
1282
|
}
|
|
1254
1283
|
async replaceOne(filter, data, expectedVersion) {
|
|
1255
|
-
const full = fillReplacePayload(data, this._replaceColumns(), this._table.versionColumnPhysical);
|
|
1256
|
-
return this.updateOne(filter, full,
|
|
1284
|
+
const full = fillReplacePayload(data, this._replaceColumns(), this._table.versionColumnPhysical, this._table.onUpdateIncPhysical);
|
|
1285
|
+
return this.updateOne(filter, full, this._onUpdateIncOps(), expectedVersion);
|
|
1257
1286
|
}
|
|
1258
1287
|
/** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
|
|
1259
1288
|
_replaceColumns() {
|
|
@@ -1266,7 +1295,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1266
1295
|
return memo.cols;
|
|
1267
1296
|
}
|
|
1268
1297
|
async replaceMany(filter, data) {
|
|
1269
|
-
return this.updateMany(filter, data);
|
|
1298
|
+
return this.updateMany(filter, data, this._onUpdateIncOps());
|
|
1270
1299
|
}
|
|
1271
1300
|
async deleteOne(filter) {
|
|
1272
1301
|
const where = buildWhere(filter);
|
|
@@ -1279,8 +1308,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1279
1308
|
this._log(sql, params);
|
|
1280
1309
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
1281
1310
|
}
|
|
1282
|
-
async deleteMany(filter) {
|
|
1283
|
-
return this._deleteWhere(buildWhere(filter));
|
|
1311
|
+
async deleteMany(filter, opts) {
|
|
1312
|
+
return this._deleteWhere(this._boundedWhere(buildWhere(filter), opts?.limit));
|
|
1284
1313
|
}
|
|
1285
1314
|
async _deleteWhere(where) {
|
|
1286
1315
|
const { sql, params } = buildDelete$1(this.resolveTableName(), where);
|
|
@@ -1772,13 +1801,23 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1772
1801
|
if (field.isGeoPoint) return this._supportsGeo ? "geography(Point,4326)" : pgTypeFromField(field);
|
|
1773
1802
|
return pgTypeFromField(field);
|
|
1774
1803
|
}
|
|
1775
|
-
async
|
|
1804
|
+
async _syncIndexes(dryRun) {
|
|
1776
1805
|
const tableName = this._table.tableName;
|
|
1777
1806
|
const schema = this._schema;
|
|
1778
1807
|
await this.prepareTypeMapper();
|
|
1779
|
-
|
|
1808
|
+
const dropIndex = async (name) => {
|
|
1809
|
+
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
1810
|
+
this._log(sql);
|
|
1811
|
+
await this._exec().exec(sql);
|
|
1812
|
+
};
|
|
1813
|
+
/** Every index name on the table (read by `listExisting`) — the vector pass checks it. */
|
|
1814
|
+
let liveNames = /* @__PURE__ */ new Set();
|
|
1815
|
+
const changes = await this.syncIndexesWithDiff({
|
|
1816
|
+
dryRun,
|
|
1817
|
+
isUnmanaged: (name) => name.startsWith(PG_VECTOR_INDEX_PREFIX),
|
|
1818
|
+
skipIndex: (index) => index.type === "geo" && !this._supportsGeo ? `[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped` : void 0,
|
|
1780
1819
|
listExisting: async () => {
|
|
1781
|
-
|
|
1820
|
+
const rows = await this._exec().all(`SELECT i.relname AS name,
|
|
1782
1821
|
(SELECT array_agg(a.attname::text ORDER BY k.ord)
|
|
1783
1822
|
FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1784
1823
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
@@ -1787,7 +1826,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1787
1826
|
JOIN pg_class t ON t.oid = x.indrelid
|
|
1788
1827
|
JOIN pg_class i ON i.oid = x.indexrelid
|
|
1789
1828
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
1790
|
-
WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [tableName, schema])
|
|
1829
|
+
WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [tableName, schema]);
|
|
1830
|
+
liveNames = new Set(rows.map((r) => r.name));
|
|
1831
|
+
return rows.map((r) => ({
|
|
1791
1832
|
name: r.name,
|
|
1792
1833
|
columns: r.columns ?? void 0
|
|
1793
1834
|
}));
|
|
@@ -1803,10 +1844,6 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1803
1844
|
return;
|
|
1804
1845
|
}
|
|
1805
1846
|
if (index.type === "geo") {
|
|
1806
|
-
if (!this._supportsGeo) {
|
|
1807
|
-
this.logger.warn(`[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped`);
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
1847
|
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gist (${qi(index.fields[0].name)})`;
|
|
1811
1848
|
this._log(sql);
|
|
1812
1849
|
await this._exec().exec(sql);
|
|
@@ -1818,54 +1855,57 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1818
1855
|
this._log(sql);
|
|
1819
1856
|
await this._exec().exec(sql);
|
|
1820
1857
|
},
|
|
1821
|
-
dropIndex
|
|
1822
|
-
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
1823
|
-
this._log(sql);
|
|
1824
|
-
await this._exec().exec(sql);
|
|
1825
|
-
}
|
|
1858
|
+
dropIndex
|
|
1826
1859
|
});
|
|
1860
|
+
const desiredVector = new Set(this._supportsVector ? [...this._vectorFields.values()].map((v) => `${PG_VECTOR_INDEX_PREFIX}${v.indexName}`) : []);
|
|
1861
|
+
const vector = createChangeRecorder(dryRun);
|
|
1862
|
+
for (const name of liveNames) if (name.startsWith(PG_VECTOR_INDEX_PREFIX) && !desiredVector.has(name)) await vector.apply({
|
|
1863
|
+
name,
|
|
1864
|
+
action: "drop"
|
|
1865
|
+
}, name, () => dropIndex(name));
|
|
1827
1866
|
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
1828
|
-
const indexName =
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1867
|
+
const indexName = `${PG_VECTOR_INDEX_PREFIX}${vec.indexName}`;
|
|
1868
|
+
if (liveNames.has(indexName)) continue;
|
|
1869
|
+
await vector.apply({
|
|
1870
|
+
name: indexName,
|
|
1871
|
+
action: "add"
|
|
1872
|
+
}, indexName, async () => {
|
|
1873
|
+
const opsClass = similarityToPgOps(vec.similarity);
|
|
1874
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(indexName)} ON ${quoteTableName(this.resolveTableName())} USING hnsw (${qi(field)} ${opsClass})`;
|
|
1875
|
+
this._log(sql);
|
|
1876
|
+
await this._exec().exec(sql);
|
|
1877
|
+
});
|
|
1833
1878
|
}
|
|
1879
|
+
return [...changes, ...vector.changes];
|
|
1834
1880
|
}
|
|
1835
1881
|
async syncForeignKeys() {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
for (const fk of this._table.foreignKeys.values()) {
|
|
1849
|
-
const key = [...fkColumns(fk).fields].toSorted().join(",");
|
|
1850
|
-
if (!existingKeys.has(key)) {
|
|
1882
|
+
return this._syncForeignKeys(false);
|
|
1883
|
+
}
|
|
1884
|
+
/** The FK changes {@link syncForeignKeys} would make — no DDL. */
|
|
1885
|
+
async planForeignKeys() {
|
|
1886
|
+
return this._syncForeignKeys(true);
|
|
1887
|
+
}
|
|
1888
|
+
_syncForeignKeys(dryRun) {
|
|
1889
|
+
return this.syncForeignKeysWithDiff({
|
|
1890
|
+
dryRun,
|
|
1891
|
+
listExisting: () => this._getExistingFkConstraints(),
|
|
1892
|
+
dropConstraint: (name) => this._dropFkConstraint(name),
|
|
1893
|
+
addConstraint: async (fk) => {
|
|
1851
1894
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${foreignKeySql(qi, fk)}`;
|
|
1852
1895
|
this._log(ddl);
|
|
1853
1896
|
await this._exec().exec(ddl);
|
|
1854
1897
|
}
|
|
1855
|
-
}
|
|
1898
|
+
});
|
|
1856
1899
|
}
|
|
1857
1900
|
async dropForeignKeys(fkFieldKeys) {
|
|
1858
1901
|
if (fkFieldKeys.length === 0) return;
|
|
1859
1902
|
const keySet = new Set(fkFieldKeys);
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
await this._exec().exec(ddl);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1903
|
+
for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has(fkKey(cols))) await this._dropFkConstraint(constraintName);
|
|
1904
|
+
}
|
|
1905
|
+
async _dropFkConstraint(name) {
|
|
1906
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP CONSTRAINT ${qi(name)}`;
|
|
1907
|
+
this._log(ddl);
|
|
1908
|
+
await this._exec().exec(ddl);
|
|
1869
1909
|
}
|
|
1870
1910
|
/** Queries information_schema for existing FK constraints. */
|
|
1871
1911
|
async _getExistingFkConstraints() {
|
|
@@ -2303,10 +2343,8 @@ function normalizePgDefault(value, isIdentity) {
|
|
|
2303
2343
|
if (lower.includes("extract") && lower.includes("epoch") && lower.includes("now()")) return "fn:now";
|
|
2304
2344
|
if (lower === "gen_random_uuid()") return "fn:uuid";
|
|
2305
2345
|
if (lower.startsWith("nextval(")) return "fn:increment";
|
|
2306
|
-
const castMatch = cleaned.match(/^'
|
|
2307
|
-
|
|
2308
|
-
if (cleaned.startsWith("'") && cleaned.endsWith("'")) return cleaned.slice(1, -1).replace(/''/g, "'");
|
|
2309
|
-
return cleaned;
|
|
2346
|
+
const castMatch = cleaned.match(/^('.*')::[\w\s]+$/);
|
|
2347
|
+
return unquoteSqlStringLiteral(castMatch ? castMatch[1] : cleaned) ?? cleaned;
|
|
2310
2348
|
}
|
|
2311
2349
|
/**
|
|
2312
2350
|
* Converts a normalized similarity threshold (0-1) to a pgvector max distance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.162",
|
|
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.109",
|
|
47
|
+
"@atscript/typescript": "^0.1.109",
|
|
48
48
|
"@types/pg": "^8.11.0",
|
|
49
49
|
"@uniqu/core": "^0.1.15",
|
|
50
50
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.1.
|
|
51
|
+
"unplugin-atscript": "^0.1.109"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1.
|
|
55
|
-
"@atscript/typescript": "^0.1.
|
|
54
|
+
"@atscript/core": "^0.1.109",
|
|
55
|
+
"@atscript/typescript": "^0.1.109",
|
|
56
56
|
"@uniqu/core": "^0.1.15",
|
|
57
57
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db": "^0.1.
|
|
59
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
58
|
+
"@atscript/db": "^0.1.162",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.162"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|