@atscript/db-postgres 0.1.163 → 0.1.165
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 +78 -42
- package/dist/index.d.cts +17 -2
- package/dist/index.d.mts +17 -2
- package/dist/index.mjs +80 -44
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -311,8 +311,8 @@ function buildInsert(table, data) {
|
|
|
311
311
|
function buildInsertMany(table, rows, columns) {
|
|
312
312
|
return (0, _atscript_db_sql_tools.buildInsertMany)(pgDialect, table, rows, columns);
|
|
313
313
|
}
|
|
314
|
-
function buildSelect(table, where, controls, lock) {
|
|
315
|
-
return (0, _atscript_db_sql_tools.buildSelect)(pgDialect, table, where, controls, lock);
|
|
314
|
+
function buildSelect(table, where, controls, lock, defaultOrder) {
|
|
315
|
+
return (0, _atscript_db_sql_tools.buildSelect)(pgDialect, table, where, controls, lock, defaultOrder);
|
|
316
316
|
}
|
|
317
317
|
function buildUpdate(table, data, where, limit, ops, versionColumn, expectedVersion) {
|
|
318
318
|
return (0, _atscript_db_sql_tools.buildUpdate)(pgDialect, table, data, where, limit, ops, versionColumn, expectedVersion);
|
|
@@ -931,11 +931,19 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
931
931
|
_mapConstraintError(error) {
|
|
932
932
|
if (error && typeof error === "object" && "code" in error) {
|
|
933
933
|
const err = error;
|
|
934
|
-
if (err.code === "23505") throw
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
934
|
+
if (err.code === "23505") throw (0, _atscript_db.constraintViolation)("CONFLICT", {
|
|
935
|
+
name: err.constraint,
|
|
936
|
+
columns: keyColumns(err.detail),
|
|
937
|
+
primaryKey: err.constraint?.endsWith("_pkey")
|
|
938
|
+
}, err.detail ?? err.message);
|
|
939
|
+
if (err.code === "23503") {
|
|
940
|
+
const detail = err.detail ?? err.message;
|
|
941
|
+
const referencedBy = detail.match(/is still referenced from table "([^"]+)"/)?.[1];
|
|
942
|
+
throw (0, _atscript_db.constraintViolation)("FK_VIOLATION", referencedBy ? { referencedBy } : {
|
|
943
|
+
name: err.constraint,
|
|
944
|
+
columns: keyColumns(detail)
|
|
945
|
+
}, detail);
|
|
946
|
+
}
|
|
939
947
|
const contention = PG_CONTENTION_CODES[err.code];
|
|
940
948
|
if (contention) throw new _atscript_db.DbError(contention, [{
|
|
941
949
|
path: "",
|
|
@@ -944,30 +952,6 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
944
952
|
}
|
|
945
953
|
throw error;
|
|
946
954
|
}
|
|
947
|
-
_extractFieldFromConstraint(constraint) {
|
|
948
|
-
if (!constraint) return;
|
|
949
|
-
const tableName = this._table.tableName;
|
|
950
|
-
if (constraint.startsWith(`${tableName}_`) && constraint.endsWith("_key")) {
|
|
951
|
-
const fieldPart = constraint.slice(tableName.length + 1, -4);
|
|
952
|
-
const fd = this._table.fieldDescriptors.find((f) => f.physicalName === fieldPart);
|
|
953
|
-
if (fd) return fd.path;
|
|
954
|
-
}
|
|
955
|
-
return constraint;
|
|
956
|
-
}
|
|
957
|
-
_mapFkError(detail, constraint) {
|
|
958
|
-
const fkMatch = detail.match(/Key \(([^)]+)\)/);
|
|
959
|
-
if (fkMatch) {
|
|
960
|
-
const physicalCol = fkMatch[1].split(",")[0].trim();
|
|
961
|
-
return [{
|
|
962
|
-
path: this._table.fieldDescriptors.find((f) => f.physicalName === physicalCol)?.path ?? physicalCol,
|
|
963
|
-
message: detail
|
|
964
|
-
}];
|
|
965
|
-
}
|
|
966
|
-
return [{
|
|
967
|
-
path: constraint ?? "",
|
|
968
|
-
message: detail
|
|
969
|
-
}];
|
|
970
|
-
}
|
|
971
955
|
async insertOne(data) {
|
|
972
956
|
const insert = buildInsert(this.resolveTableName(), data);
|
|
973
957
|
const sql = insert.sql + this._pk().returning;
|
|
@@ -1942,7 +1926,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1942
1926
|
const indexes = [];
|
|
1943
1927
|
const { all: ftAll, def: ftDefault } = this._fulltextIndexes();
|
|
1944
1928
|
for (const index of ftAll) indexes.push({
|
|
1945
|
-
name: index.
|
|
1929
|
+
name: index.name,
|
|
1930
|
+
aliases: [index.key],
|
|
1946
1931
|
description: (0, _atscript_db.describeFulltext)(index, (names) => `GIN tsvector index on ${names}`),
|
|
1947
1932
|
type: "text",
|
|
1948
1933
|
fields: this._indexLogicalPaths(index),
|
|
@@ -1964,7 +1949,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1964
1949
|
async search(text, query, indexName) {
|
|
1965
1950
|
if (!text.trim()) return [];
|
|
1966
1951
|
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
1967
|
-
const { sql, params } = buildSelect(this.resolveTableName(), combinedWhere, query.controls);
|
|
1952
|
+
const { sql, params } = buildSelect(this.resolveTableName(), combinedWhere, query.controls, void 0, combinedWhere.rank);
|
|
1968
1953
|
this._log(sql, params);
|
|
1969
1954
|
return this._exec().all(sql, params);
|
|
1970
1955
|
}
|
|
@@ -1976,7 +1961,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1976
1961
|
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
1977
1962
|
const tableName = this.resolveTableName();
|
|
1978
1963
|
const selectPromise = (async () => {
|
|
1979
|
-
const { sql, params } = buildSelect(tableName, combinedWhere, query.controls);
|
|
1964
|
+
const { sql, params } = buildSelect(tableName, combinedWhere, query.controls, void 0, combinedWhere.rank);
|
|
1980
1965
|
this._log(sql, params);
|
|
1981
1966
|
return this._exec().all(sql, params);
|
|
1982
1967
|
})();
|
|
@@ -1994,37 +1979,80 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1994
1979
|
count
|
|
1995
1980
|
};
|
|
1996
1981
|
}
|
|
1982
|
+
/**
|
|
1983
|
+
* The search predicate (AND the request filter) and its relevance order
|
|
1984
|
+
* (`rank`, since 0.1.165) — used as the ORDER BY when the read has no
|
|
1985
|
+
* `$sort`: exact-number hits first, then `ts_rank` over the text members
|
|
1986
|
+
* (per-field weights through `setweight` buckets), then the primary key.
|
|
1987
|
+
* Grouped searches use only the predicate.
|
|
1988
|
+
*/
|
|
1997
1989
|
_buildSearchWhere(text, query, indexName) {
|
|
1998
1990
|
const fulltextIndex = this._getFulltextIndex(indexName);
|
|
1999
|
-
if (!fulltextIndex) throw new Error(
|
|
1991
|
+
if (!fulltextIndex) throw new Error((0, _atscript_db.searchIndexNotFoundMessage)(indexName));
|
|
2000
1992
|
const { text: textFields, integer: integerFields } = (0, _atscript_db.splitFulltextFields)(fulltextIndex);
|
|
2001
1993
|
const where = buildWhere(query.filter);
|
|
2002
1994
|
const n = integerFields.length > 0 ? (0, _atscript_db.searchTermInteger)(text) : void 0;
|
|
2003
1995
|
const parts = [];
|
|
2004
1996
|
const params = [];
|
|
1997
|
+
const equalities = n === void 0 ? [] : integerFields.map((f) => `${qi(f.name)} = CAST(? AS BIGINT)`);
|
|
1998
|
+
let textRank;
|
|
2005
1999
|
if (textFields.length > 0) {
|
|
2006
|
-
const
|
|
2007
|
-
parts.push(
|
|
2000
|
+
const tsvector = `to_tsvector('english', ${this._buildTsvectorExpr(textFields)})`;
|
|
2001
|
+
parts.push(`${tsvector} @@ plainto_tsquery('english', ?)`);
|
|
2008
2002
|
params.push(text);
|
|
2003
|
+
textRank = {
|
|
2004
|
+
sql: `${this._buildTsRankExpr(textFields, tsvector)} DESC`,
|
|
2005
|
+
params: [text]
|
|
2006
|
+
};
|
|
2009
2007
|
}
|
|
2010
|
-
|
|
2011
|
-
parts.push(
|
|
2008
|
+
for (const eq of equalities) {
|
|
2009
|
+
parts.push(eq);
|
|
2012
2010
|
params.push(n);
|
|
2013
2011
|
}
|
|
2014
2012
|
const clause = (0, _atscript_db_sql_tools.orFragment)(parts, params);
|
|
2015
2013
|
return {
|
|
2016
2014
|
sql: where.sql === "1=1" ? clause.sql : `${where.sql} AND ${clause.sql}`,
|
|
2017
|
-
params: [...where.params, ...clause.params]
|
|
2015
|
+
params: [...where.params, ...clause.params],
|
|
2016
|
+
rank: (0, _atscript_db_sql_tools.searchRankOrder)(pgDialect, {
|
|
2017
|
+
exactEqualities: equalities,
|
|
2018
|
+
n,
|
|
2019
|
+
textRank,
|
|
2020
|
+
pkColumns: this._table.primaryKeys.map((key) => this._table.physicalPath(key))
|
|
2021
|
+
})
|
|
2018
2022
|
};
|
|
2019
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* `ts_rank(…)` over the text members (one `?` — the search text). Without
|
|
2026
|
+
* per-field weights it ranks `tsvector` — the indexed expression — itself; with them each
|
|
2027
|
+
* member's vector is put in a `setweight` bucket (the distinct weights,
|
|
2028
|
+
* highest first, map to A, B, C, then D for every lower one) and the bucket
|
|
2029
|
+
* weights are passed relative to the highest (`{D, C, B, A}`). The match
|
|
2030
|
+
* predicate keeps the unweighted indexed expression, so the GIN index serves
|
|
2031
|
+
* it unchanged.
|
|
2032
|
+
*/
|
|
2033
|
+
_buildTsRankExpr(fields, tsvector) {
|
|
2034
|
+
const query = `plainto_tsquery('english', ?)`;
|
|
2035
|
+
const distinct = [...new Set(fields.map(weightOf))].toSorted((a, b) => b - a);
|
|
2036
|
+
if (distinct.length <= 1) return `ts_rank(${tsvector}, ${query})`;
|
|
2037
|
+
const letters = [
|
|
2038
|
+
"A",
|
|
2039
|
+
"B",
|
|
2040
|
+
"C",
|
|
2041
|
+
"D"
|
|
2042
|
+
];
|
|
2043
|
+
const bucketOf = (w) => Math.min(distinct.indexOf(w), 3);
|
|
2044
|
+
const top = distinct[0];
|
|
2045
|
+
const bucketWeights = letters.map((_, i) => distinct[Math.min(i, distinct.length - 1)] / top);
|
|
2046
|
+
const vector = fields.map((f) => `setweight(to_tsvector('english', coalesce(${qi(f.name)}, '')), '${letters[bucketOf(weightOf(f))]}')`).join(" || ");
|
|
2047
|
+
return `ts_rank('{${[...bucketWeights].toReversed().map((w) => Number(w.toFixed(6))).join(", ")}}', ${vector}, ${query})`;
|
|
2048
|
+
}
|
|
2020
2049
|
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
2021
2050
|
_buildTsvectorExpr(fields) {
|
|
2022
2051
|
return fields.map((f) => `coalesce(${qi(f.name)}, '')`).join(" || ' ' || ");
|
|
2023
2052
|
}
|
|
2024
2053
|
_getFulltextIndex(indexName) {
|
|
2025
2054
|
const { all, def } = this._fulltextIndexes();
|
|
2026
|
-
|
|
2027
|
-
return all.find((index) => index.key === indexName);
|
|
2055
|
+
return indexName ? (0, _atscript_db.resolveFulltextIndex)(all, indexName) : def;
|
|
2028
2056
|
}
|
|
2029
2057
|
/** The table's fulltext indexes and the default one, built once per table. */
|
|
2030
2058
|
_fulltextIndexes() {
|
|
@@ -2383,6 +2411,14 @@ function similarityToPgOps(similarity) {
|
|
|
2383
2411
|
function vectorToString(vector) {
|
|
2384
2412
|
return `[${vector.join(",")}]`;
|
|
2385
2413
|
}
|
|
2414
|
+
/** The columns of a PostgreSQL `Key (a, b)=(…)` detail. */
|
|
2415
|
+
function keyColumns(detail) {
|
|
2416
|
+
return (detail?.match(/Key \(([^)]+)\)=/)?.[1])?.split(",").map((c) => c.trim().replace(/^"|"$/g, ""));
|
|
2417
|
+
}
|
|
2418
|
+
/** A fulltext member's weight (`@db.index.fulltext 'idx', w`), `1` when unset. */
|
|
2419
|
+
function weightOf(f) {
|
|
2420
|
+
return f.weight ?? 1;
|
|
2421
|
+
}
|
|
2386
2422
|
//#endregion
|
|
2387
2423
|
//#region src/pg-driver.ts
|
|
2388
2424
|
const NO_PARAMS = [];
|
package/dist/index.d.cts
CHANGED
|
@@ -213,8 +213,6 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
213
213
|
private _wrapConstraintError;
|
|
214
214
|
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock timeout, else as is. */
|
|
215
215
|
private _mapConstraintError;
|
|
216
|
-
private _extractFieldFromConstraint;
|
|
217
|
-
private _mapFkError;
|
|
218
216
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
219
217
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
220
218
|
supportsInsertIgnore(): boolean;
|
|
@@ -388,7 +386,24 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
388
386
|
data: Array<Record<string, unknown>>;
|
|
389
387
|
count: number;
|
|
390
388
|
}>;
|
|
389
|
+
/**
|
|
390
|
+
* The search predicate (AND the request filter) and its relevance order
|
|
391
|
+
* (`rank`, since 0.1.165) — used as the ORDER BY when the read has no
|
|
392
|
+
* `$sort`: exact-number hits first, then `ts_rank` over the text members
|
|
393
|
+
* (per-field weights through `setweight` buckets), then the primary key.
|
|
394
|
+
* Grouped searches use only the predicate.
|
|
395
|
+
*/
|
|
391
396
|
private _buildSearchWhere;
|
|
397
|
+
/**
|
|
398
|
+
* `ts_rank(…)` over the text members (one `?` — the search text). Without
|
|
399
|
+
* per-field weights it ranks `tsvector` — the indexed expression — itself; with them each
|
|
400
|
+
* member's vector is put in a `setweight` bucket (the distinct weights,
|
|
401
|
+
* highest first, map to A, B, C, then D for every lower one) and the bucket
|
|
402
|
+
* weights are passed relative to the highest (`{D, C, B, A}`). The match
|
|
403
|
+
* predicate keeps the unweighted indexed expression, so the GIN index serves
|
|
404
|
+
* it unchanged.
|
|
405
|
+
*/
|
|
406
|
+
private _buildTsRankExpr;
|
|
392
407
|
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
393
408
|
private _buildTsvectorExpr;
|
|
394
409
|
private _getFulltextIndex;
|
package/dist/index.d.mts
CHANGED
|
@@ -213,8 +213,6 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
213
213
|
private _wrapConstraintError;
|
|
214
214
|
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock timeout, else as is. */
|
|
215
215
|
private _mapConstraintError;
|
|
216
|
-
private _extractFieldFromConstraint;
|
|
217
|
-
private _mapFkError;
|
|
218
216
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
219
217
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
220
218
|
supportsInsertIgnore(): boolean;
|
|
@@ -388,7 +386,24 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
388
386
|
data: Array<Record<string, unknown>>;
|
|
389
387
|
count: number;
|
|
390
388
|
}>;
|
|
389
|
+
/**
|
|
390
|
+
* The search predicate (AND the request filter) and its relevance order
|
|
391
|
+
* (`rank`, since 0.1.165) — used as the ORDER BY when the read has no
|
|
392
|
+
* `$sort`: exact-number hits first, then `ts_rank` over the text members
|
|
393
|
+
* (per-field weights through `setweight` buckets), then the primary key.
|
|
394
|
+
* Grouped searches use only the predicate.
|
|
395
|
+
*/
|
|
391
396
|
private _buildSearchWhere;
|
|
397
|
+
/**
|
|
398
|
+
* `ts_rank(…)` over the text members (one `?` — the search text). Without
|
|
399
|
+
* per-field weights it ranks `tsvector` — the indexed expression — itself; with them each
|
|
400
|
+
* member's vector is put in a `setweight` bucket (the distinct weights,
|
|
401
|
+
* highest first, map to A, B, C, then D for every lower one) and the bucket
|
|
402
|
+
* weights are passed relative to the highest (`{D, C, B, A}`). The match
|
|
403
|
+
* predicate keeps the unweighted indexed expression, so the GIN index serves
|
|
404
|
+
* it unchanged.
|
|
405
|
+
*/
|
|
406
|
+
private _buildTsRankExpr;
|
|
392
407
|
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
393
408
|
private _buildTsvectorExpr;
|
|
394
409
|
private _getFulltextIndex;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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";
|
|
1
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, constraintViolation, createChangeRecorder, defaultFulltextIndex, describeFulltext, fkKey, isColumnTypeChanged, resolveFulltextIndex, searchIndexNotFoundMessage, 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, unquoteSqlStringLiteral, vectorDistanceSource, writeLimitWhere } 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, searchRankOrder, 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
|
/**
|
|
@@ -310,8 +310,8 @@ function buildInsert$1(table, data) {
|
|
|
310
310
|
function buildInsertMany$1(table, rows, columns) {
|
|
311
311
|
return buildInsertMany(pgDialect, table, rows, columns);
|
|
312
312
|
}
|
|
313
|
-
function buildSelect$1(table, where, controls, lock) {
|
|
314
|
-
return buildSelect(pgDialect, table, where, controls, lock);
|
|
313
|
+
function buildSelect$1(table, where, controls, lock, defaultOrder) {
|
|
314
|
+
return buildSelect(pgDialect, table, where, controls, lock, defaultOrder);
|
|
315
315
|
}
|
|
316
316
|
function buildUpdate$1(table, data, where, limit, ops, versionColumn, expectedVersion) {
|
|
317
317
|
return buildUpdate(pgDialect, table, data, where, limit, ops, versionColumn, expectedVersion);
|
|
@@ -930,11 +930,19 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
930
930
|
_mapConstraintError(error) {
|
|
931
931
|
if (error && typeof error === "object" && "code" in error) {
|
|
932
932
|
const err = error;
|
|
933
|
-
if (err.code === "23505") throw
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
933
|
+
if (err.code === "23505") throw constraintViolation("CONFLICT", {
|
|
934
|
+
name: err.constraint,
|
|
935
|
+
columns: keyColumns(err.detail),
|
|
936
|
+
primaryKey: err.constraint?.endsWith("_pkey")
|
|
937
|
+
}, err.detail ?? err.message);
|
|
938
|
+
if (err.code === "23503") {
|
|
939
|
+
const detail = err.detail ?? err.message;
|
|
940
|
+
const referencedBy = detail.match(/is still referenced from table "([^"]+)"/)?.[1];
|
|
941
|
+
throw constraintViolation("FK_VIOLATION", referencedBy ? { referencedBy } : {
|
|
942
|
+
name: err.constraint,
|
|
943
|
+
columns: keyColumns(detail)
|
|
944
|
+
}, detail);
|
|
945
|
+
}
|
|
938
946
|
const contention = PG_CONTENTION_CODES[err.code];
|
|
939
947
|
if (contention) throw new DbError(contention, [{
|
|
940
948
|
path: "",
|
|
@@ -943,30 +951,6 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
943
951
|
}
|
|
944
952
|
throw error;
|
|
945
953
|
}
|
|
946
|
-
_extractFieldFromConstraint(constraint) {
|
|
947
|
-
if (!constraint) return;
|
|
948
|
-
const tableName = this._table.tableName;
|
|
949
|
-
if (constraint.startsWith(`${tableName}_`) && constraint.endsWith("_key")) {
|
|
950
|
-
const fieldPart = constraint.slice(tableName.length + 1, -4);
|
|
951
|
-
const fd = this._table.fieldDescriptors.find((f) => f.physicalName === fieldPart);
|
|
952
|
-
if (fd) return fd.path;
|
|
953
|
-
}
|
|
954
|
-
return constraint;
|
|
955
|
-
}
|
|
956
|
-
_mapFkError(detail, constraint) {
|
|
957
|
-
const fkMatch = detail.match(/Key \(([^)]+)\)/);
|
|
958
|
-
if (fkMatch) {
|
|
959
|
-
const physicalCol = fkMatch[1].split(",")[0].trim();
|
|
960
|
-
return [{
|
|
961
|
-
path: this._table.fieldDescriptors.find((f) => f.physicalName === physicalCol)?.path ?? physicalCol,
|
|
962
|
-
message: detail
|
|
963
|
-
}];
|
|
964
|
-
}
|
|
965
|
-
return [{
|
|
966
|
-
path: constraint ?? "",
|
|
967
|
-
message: detail
|
|
968
|
-
}];
|
|
969
|
-
}
|
|
970
954
|
async insertOne(data) {
|
|
971
955
|
const insert = buildInsert$1(this.resolveTableName(), data);
|
|
972
956
|
const sql = insert.sql + this._pk().returning;
|
|
@@ -1941,7 +1925,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1941
1925
|
const indexes = [];
|
|
1942
1926
|
const { all: ftAll, def: ftDefault } = this._fulltextIndexes();
|
|
1943
1927
|
for (const index of ftAll) indexes.push({
|
|
1944
|
-
name: index.
|
|
1928
|
+
name: index.name,
|
|
1929
|
+
aliases: [index.key],
|
|
1945
1930
|
description: describeFulltext(index, (names) => `GIN tsvector index on ${names}`),
|
|
1946
1931
|
type: "text",
|
|
1947
1932
|
fields: this._indexLogicalPaths(index),
|
|
@@ -1963,7 +1948,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1963
1948
|
async search(text, query, indexName) {
|
|
1964
1949
|
if (!text.trim()) return [];
|
|
1965
1950
|
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
1966
|
-
const { sql, params } = buildSelect$1(this.resolveTableName(), combinedWhere, query.controls);
|
|
1951
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), combinedWhere, query.controls, void 0, combinedWhere.rank);
|
|
1967
1952
|
this._log(sql, params);
|
|
1968
1953
|
return this._exec().all(sql, params);
|
|
1969
1954
|
}
|
|
@@ -1975,7 +1960,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1975
1960
|
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
1976
1961
|
const tableName = this.resolveTableName();
|
|
1977
1962
|
const selectPromise = (async () => {
|
|
1978
|
-
const { sql, params } = buildSelect$1(tableName, combinedWhere, query.controls);
|
|
1963
|
+
const { sql, params } = buildSelect$1(tableName, combinedWhere, query.controls, void 0, combinedWhere.rank);
|
|
1979
1964
|
this._log(sql, params);
|
|
1980
1965
|
return this._exec().all(sql, params);
|
|
1981
1966
|
})();
|
|
@@ -1993,37 +1978,80 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1993
1978
|
count
|
|
1994
1979
|
};
|
|
1995
1980
|
}
|
|
1981
|
+
/**
|
|
1982
|
+
* The search predicate (AND the request filter) and its relevance order
|
|
1983
|
+
* (`rank`, since 0.1.165) — used as the ORDER BY when the read has no
|
|
1984
|
+
* `$sort`: exact-number hits first, then `ts_rank` over the text members
|
|
1985
|
+
* (per-field weights through `setweight` buckets), then the primary key.
|
|
1986
|
+
* Grouped searches use only the predicate.
|
|
1987
|
+
*/
|
|
1996
1988
|
_buildSearchWhere(text, query, indexName) {
|
|
1997
1989
|
const fulltextIndex = this._getFulltextIndex(indexName);
|
|
1998
|
-
if (!fulltextIndex) throw new Error(
|
|
1990
|
+
if (!fulltextIndex) throw new Error(searchIndexNotFoundMessage(indexName));
|
|
1999
1991
|
const { text: textFields, integer: integerFields } = splitFulltextFields(fulltextIndex);
|
|
2000
1992
|
const where = buildWhere(query.filter);
|
|
2001
1993
|
const n = integerFields.length > 0 ? searchTermInteger(text) : void 0;
|
|
2002
1994
|
const parts = [];
|
|
2003
1995
|
const params = [];
|
|
1996
|
+
const equalities = n === void 0 ? [] : integerFields.map((f) => `${qi(f.name)} = CAST(? AS BIGINT)`);
|
|
1997
|
+
let textRank;
|
|
2004
1998
|
if (textFields.length > 0) {
|
|
2005
|
-
const
|
|
2006
|
-
parts.push(
|
|
1999
|
+
const tsvector = `to_tsvector('english', ${this._buildTsvectorExpr(textFields)})`;
|
|
2000
|
+
parts.push(`${tsvector} @@ plainto_tsquery('english', ?)`);
|
|
2007
2001
|
params.push(text);
|
|
2002
|
+
textRank = {
|
|
2003
|
+
sql: `${this._buildTsRankExpr(textFields, tsvector)} DESC`,
|
|
2004
|
+
params: [text]
|
|
2005
|
+
};
|
|
2008
2006
|
}
|
|
2009
|
-
|
|
2010
|
-
parts.push(
|
|
2007
|
+
for (const eq of equalities) {
|
|
2008
|
+
parts.push(eq);
|
|
2011
2009
|
params.push(n);
|
|
2012
2010
|
}
|
|
2013
2011
|
const clause = orFragment(parts, params);
|
|
2014
2012
|
return {
|
|
2015
2013
|
sql: where.sql === "1=1" ? clause.sql : `${where.sql} AND ${clause.sql}`,
|
|
2016
|
-
params: [...where.params, ...clause.params]
|
|
2014
|
+
params: [...where.params, ...clause.params],
|
|
2015
|
+
rank: searchRankOrder(pgDialect, {
|
|
2016
|
+
exactEqualities: equalities,
|
|
2017
|
+
n,
|
|
2018
|
+
textRank,
|
|
2019
|
+
pkColumns: this._table.primaryKeys.map((key) => this._table.physicalPath(key))
|
|
2020
|
+
})
|
|
2017
2021
|
};
|
|
2018
2022
|
}
|
|
2023
|
+
/**
|
|
2024
|
+
* `ts_rank(…)` over the text members (one `?` — the search text). Without
|
|
2025
|
+
* per-field weights it ranks `tsvector` — the indexed expression — itself; with them each
|
|
2026
|
+
* member's vector is put in a `setweight` bucket (the distinct weights,
|
|
2027
|
+
* highest first, map to A, B, C, then D for every lower one) and the bucket
|
|
2028
|
+
* weights are passed relative to the highest (`{D, C, B, A}`). The match
|
|
2029
|
+
* predicate keeps the unweighted indexed expression, so the GIN index serves
|
|
2030
|
+
* it unchanged.
|
|
2031
|
+
*/
|
|
2032
|
+
_buildTsRankExpr(fields, tsvector) {
|
|
2033
|
+
const query = `plainto_tsquery('english', ?)`;
|
|
2034
|
+
const distinct = [...new Set(fields.map(weightOf))].toSorted((a, b) => b - a);
|
|
2035
|
+
if (distinct.length <= 1) return `ts_rank(${tsvector}, ${query})`;
|
|
2036
|
+
const letters = [
|
|
2037
|
+
"A",
|
|
2038
|
+
"B",
|
|
2039
|
+
"C",
|
|
2040
|
+
"D"
|
|
2041
|
+
];
|
|
2042
|
+
const bucketOf = (w) => Math.min(distinct.indexOf(w), 3);
|
|
2043
|
+
const top = distinct[0];
|
|
2044
|
+
const bucketWeights = letters.map((_, i) => distinct[Math.min(i, distinct.length - 1)] / top);
|
|
2045
|
+
const vector = fields.map((f) => `setweight(to_tsvector('english', coalesce(${qi(f.name)}, '')), '${letters[bucketOf(weightOf(f))]}')`).join(" || ");
|
|
2046
|
+
return `ts_rank('{${[...bucketWeights].toReversed().map((w) => Number(w.toFixed(6))).join(", ")}}', ${vector}, ${query})`;
|
|
2047
|
+
}
|
|
2019
2048
|
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
2020
2049
|
_buildTsvectorExpr(fields) {
|
|
2021
2050
|
return fields.map((f) => `coalesce(${qi(f.name)}, '')`).join(" || ' ' || ");
|
|
2022
2051
|
}
|
|
2023
2052
|
_getFulltextIndex(indexName) {
|
|
2024
2053
|
const { all, def } = this._fulltextIndexes();
|
|
2025
|
-
|
|
2026
|
-
return all.find((index) => index.key === indexName);
|
|
2054
|
+
return indexName ? resolveFulltextIndex(all, indexName) : def;
|
|
2027
2055
|
}
|
|
2028
2056
|
/** The table's fulltext indexes and the default one, built once per table. */
|
|
2029
2057
|
_fulltextIndexes() {
|
|
@@ -2382,6 +2410,14 @@ function similarityToPgOps(similarity) {
|
|
|
2382
2410
|
function vectorToString(vector) {
|
|
2383
2411
|
return `[${vector.join(",")}]`;
|
|
2384
2412
|
}
|
|
2413
|
+
/** The columns of a PostgreSQL `Key (a, b)=(…)` detail. */
|
|
2414
|
+
function keyColumns(detail) {
|
|
2415
|
+
return (detail?.match(/Key \(([^)]+)\)=/)?.[1])?.split(",").map((c) => c.trim().replace(/^"|"$/g, ""));
|
|
2416
|
+
}
|
|
2417
|
+
/** A fulltext member's weight (`@db.index.fulltext 'idx', w`), `1` when unset. */
|
|
2418
|
+
function weightOf(f) {
|
|
2419
|
+
return f.weight ?? 1;
|
|
2420
|
+
}
|
|
2385
2421
|
//#endregion
|
|
2386
2422
|
//#region src/pg-driver.ts
|
|
2387
2423
|
const NO_PARAMS = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.165",
|
|
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.2.1",
|
|
47
|
+
"@atscript/typescript": "^0.2.1",
|
|
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.2.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1
|
|
55
|
-
"@atscript/typescript": "^0.1
|
|
54
|
+
"@atscript/core": "^0.2.1",
|
|
55
|
+
"@atscript/typescript": "^0.2.1",
|
|
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.165",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.165"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|