@atscript/db-postgres 0.1.166 → 0.1.168
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 +65 -11
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +66 -12
- package/package.json +12 -10
package/dist/index.cjs
CHANGED
|
@@ -187,6 +187,8 @@ const pgDialect = {
|
|
|
187
187
|
min: "BOOL_AND",
|
|
188
188
|
max: "BOOL_OR"
|
|
189
189
|
},
|
|
190
|
+
conditionalAggregate: "filter",
|
|
191
|
+
countDistinctColumns: (columns) => `COUNT(DISTINCT CASE WHEN ${columns.map((c) => `${c} IS NOT NULL`).join(" AND ")} THEN (${columns.join(", ")}) END)`,
|
|
190
192
|
/**
|
|
191
193
|
* SQLSTATE 22003 (`numeric_value_out_of_range`), a double overflow in
|
|
192
194
|
* aggregate arithmetic, is `INVALID_QUERY`; SQLSTATE 22023
|
|
@@ -819,6 +821,18 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
819
821
|
supportsAggregateExpressions() {
|
|
820
822
|
return true;
|
|
821
823
|
}
|
|
824
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
825
|
+
supportsConditionalAggregates() {
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
/** `countDistinct` over several fields. */
|
|
829
|
+
supportsDistinctFieldLists() {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
833
|
+
supportsNestedAggregates() {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
822
836
|
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
823
837
|
supportsNullsPlacement() {
|
|
824
838
|
return true;
|
|
@@ -1773,6 +1787,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1773
1787
|
}
|
|
1774
1788
|
async renameTable(oldName) {
|
|
1775
1789
|
const newName = this._table.tableName;
|
|
1790
|
+
if (!await this.getObjectKind(oldName) && await this.getObjectKind(newName)) return;
|
|
1776
1791
|
const ddl = `ALTER TABLE ${quoteTableName(oldName)} RENAME TO ${qi(newName)}`;
|
|
1777
1792
|
this._log(ddl);
|
|
1778
1793
|
await this._exec().exec(ddl);
|
|
@@ -1802,7 +1817,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1802
1817
|
isUnmanaged: (name) => name.startsWith(PG_VECTOR_INDEX_PREFIX),
|
|
1803
1818
|
skipIndex: (index) => index.type === "geo" && !this._supportsGeo ? `[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped` : void 0,
|
|
1804
1819
|
listExisting: async () => {
|
|
1805
|
-
const rows = await this._exec().all(`SELECT i.relname AS name,
|
|
1820
|
+
const rows = await this._exec().all(`SELECT i.relname AS name, pg_get_indexdef(x.indexrelid) AS def,
|
|
1806
1821
|
(SELECT array_agg(a.attname::text ORDER BY k.ord)
|
|
1807
1822
|
FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1808
1823
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
@@ -1815,15 +1830,17 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1815
1830
|
liveNames = new Set(rows.map((r) => r.name));
|
|
1816
1831
|
return rows.map((r) => ({
|
|
1817
1832
|
name: r.name,
|
|
1818
|
-
columns: r.columns ?? void 0
|
|
1833
|
+
columns: r.columns ?? void 0,
|
|
1834
|
+
definition: tsConfigOfIndexDef(r.def)
|
|
1819
1835
|
}));
|
|
1820
1836
|
},
|
|
1837
|
+
renderDesiredDefinition: (index) => index.type === "fulltext" ? pgTextSearchConfig(index) : void 0,
|
|
1821
1838
|
createIndex: async (index) => {
|
|
1822
1839
|
if (index.type === "fulltext") {
|
|
1823
1840
|
const textFields = (0, _atscript_db.splitFulltextFields)(index).text;
|
|
1824
1841
|
if (textFields.length === 0) return;
|
|
1825
1842
|
const tsvectorExpr = this._buildTsvectorExpr(textFields);
|
|
1826
|
-
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gin(to_tsvector('
|
|
1843
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gin(to_tsvector('${pgTextSearchConfig(index)}', ${tsvectorExpr}))`;
|
|
1827
1844
|
this._log(sql);
|
|
1828
1845
|
await this._exec().exec(sql);
|
|
1829
1846
|
return;
|
|
@@ -1997,12 +2014,15 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1997
2014
|
const equalities = n === void 0 ? [] : integerFields.map((f) => `${qi(f.name)} = CAST(? AS BIGINT)`);
|
|
1998
2015
|
let textRank;
|
|
1999
2016
|
if (textFields.length > 0) {
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2017
|
+
const config = pgTextSearchConfig(fulltextIndex);
|
|
2018
|
+
const tsvector = `to_tsvector('${config}', ${this._buildTsvectorExpr(textFields)})`;
|
|
2019
|
+
const { fn, term } = pgTsQuery(fulltextIndex, text);
|
|
2020
|
+
const tsquery = `${fn}('${config}', ?)`;
|
|
2021
|
+
parts.push(`${tsvector} @@ ${tsquery}`);
|
|
2022
|
+
params.push(term);
|
|
2003
2023
|
textRank = {
|
|
2004
|
-
sql: `${this._buildTsRankExpr(textFields, tsvector)} DESC`,
|
|
2005
|
-
params: [
|
|
2024
|
+
sql: `${this._buildTsRankExpr(textFields, tsvector, config, tsquery)} DESC`,
|
|
2025
|
+
params: [term]
|
|
2006
2026
|
};
|
|
2007
2027
|
}
|
|
2008
2028
|
for (const eq of equalities) {
|
|
@@ -2030,8 +2050,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2030
2050
|
* predicate keeps the unweighted indexed expression, so the GIN index serves
|
|
2031
2051
|
* it unchanged.
|
|
2032
2052
|
*/
|
|
2033
|
-
_buildTsRankExpr(fields, tsvector) {
|
|
2034
|
-
const query = `plainto_tsquery('english', ?)`;
|
|
2053
|
+
_buildTsRankExpr(fields, tsvector, config, query) {
|
|
2035
2054
|
const distinct = [...new Set(fields.map(weightOf))].toSorted((a, b) => b - a);
|
|
2036
2055
|
if (distinct.length <= 1) return `ts_rank(${tsvector}, ${query})`;
|
|
2037
2056
|
const letters = [
|
|
@@ -2043,7 +2062,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2043
2062
|
const bucketOf = (w) => Math.min(distinct.indexOf(w), 3);
|
|
2044
2063
|
const top = distinct[0];
|
|
2045
2064
|
const bucketWeights = letters.map((_, i) => distinct[Math.min(i, distinct.length - 1)] / top);
|
|
2046
|
-
const vector = fields.map((f) => `setweight(to_tsvector('
|
|
2065
|
+
const vector = fields.map((f) => `setweight(to_tsvector('${config}', coalesce(${qi(f.name)}, '')), '${letters[bucketOf(weightOf(f))]}')`).join(" || ");
|
|
2047
2066
|
return `ts_rank('{${[...bucketWeights].toReversed().map((w) => Number(w.toFixed(6))).join(", ")}}', ${vector}, ${query})`;
|
|
2048
2067
|
}
|
|
2049
2068
|
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
@@ -2415,6 +2434,41 @@ function vectorToString(vector) {
|
|
|
2415
2434
|
function keyColumns(detail) {
|
|
2416
2435
|
return (detail?.match(/Key \(([^)]+)\)=/)?.[1])?.split(",").map((c) => c.trim().replace(/^"|"$/g, ""));
|
|
2417
2436
|
}
|
|
2437
|
+
/**
|
|
2438
|
+
* The text search configuration of a fulltext index: its
|
|
2439
|
+
* `@db.index.fulltext.language` (`none` → `simple`), `english` when unset.
|
|
2440
|
+
* Inlined into DDL and queries, so only the closed language list passes.
|
|
2441
|
+
*/
|
|
2442
|
+
function pgTextSearchConfig(index) {
|
|
2443
|
+
const language = index.language;
|
|
2444
|
+
if (language === void 0) return "english";
|
|
2445
|
+
if (!_atscript_db.DB_FULLTEXT_LANGUAGES.includes(language)) throw new Error(`Unsupported fulltext language "${language}" on index "${index.name}"`);
|
|
2446
|
+
return language === "none" ? "simple" : language;
|
|
2447
|
+
}
|
|
2448
|
+
/** The configuration of a `to_tsvector('<config>'::regconfig, …)` index definition, else `undefined`. */
|
|
2449
|
+
function tsConfigOfIndexDef(def) {
|
|
2450
|
+
return def ? /to_tsvector\('([a-z_]+)'::regconfig/i.exec(def)?.[1] : void 0;
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* The tsquery function and its text for a search: `plainto_tsquery` (every
|
|
2454
|
+
* word) by default; `websearch_to_tsquery` under `@db.index.fulltext.match`
|
|
2455
|
+
* (since 0.1.168) — `"any"` OR-s the terms (quoted phrases kept whole, no
|
|
2456
|
+
* `-` exclusions).
|
|
2457
|
+
*/
|
|
2458
|
+
function pgTsQuery(index, text) {
|
|
2459
|
+
if (index.match === void 0) return {
|
|
2460
|
+
fn: "plainto_tsquery",
|
|
2461
|
+
term: text
|
|
2462
|
+
};
|
|
2463
|
+
if (index.match === "all") return {
|
|
2464
|
+
fn: "websearch_to_tsquery",
|
|
2465
|
+
term: text
|
|
2466
|
+
};
|
|
2467
|
+
return {
|
|
2468
|
+
fn: "websearch_to_tsquery",
|
|
2469
|
+
term: [...text.matchAll(/"[^"]*"|\S+/g)].map((m) => m[0].replace(/^-+/, "")).filter(Boolean).join(" or ")
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2418
2472
|
/** A fulltext member's weight (`@db.index.fulltext 'idx', w`), `1` when unset. */
|
|
2419
2473
|
function weightOf(f) {
|
|
2420
2474
|
return f.weight ?? 1;
|
package/dist/index.d.cts
CHANGED
|
@@ -176,6 +176,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
176
176
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
177
177
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
178
178
|
supportsAggregateExpressions(): boolean;
|
|
179
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
180
|
+
supportsConditionalAggregates(): boolean;
|
|
181
|
+
/** `countDistinct` over several fields. */
|
|
182
|
+
supportsDistinctFieldLists(): boolean;
|
|
183
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
184
|
+
supportsNestedAggregates(): boolean;
|
|
179
185
|
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
180
186
|
supportsNullsPlacement(): boolean;
|
|
181
187
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -176,6 +176,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
176
176
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
177
177
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
178
178
|
supportsAggregateExpressions(): boolean;
|
|
179
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
180
|
+
supportsConditionalAggregates(): boolean;
|
|
181
|
+
/** `countDistinct` over several fields. */
|
|
182
|
+
supportsDistinctFieldLists(): boolean;
|
|
183
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
184
|
+
supportsNestedAggregates(): boolean;
|
|
179
185
|
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
180
186
|
supportsNullsPlacement(): boolean;
|
|
181
187
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DB_FULLTEXT_LANGUAGES, 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
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";
|
|
@@ -186,6 +186,8 @@ const pgDialect = {
|
|
|
186
186
|
min: "BOOL_AND",
|
|
187
187
|
max: "BOOL_OR"
|
|
188
188
|
},
|
|
189
|
+
conditionalAggregate: "filter",
|
|
190
|
+
countDistinctColumns: (columns) => `COUNT(DISTINCT CASE WHEN ${columns.map((c) => `${c} IS NOT NULL`).join(" AND ")} THEN (${columns.join(", ")}) END)`,
|
|
189
191
|
/**
|
|
190
192
|
* SQLSTATE 22003 (`numeric_value_out_of_range`), a double overflow in
|
|
191
193
|
* aggregate arithmetic, is `INVALID_QUERY`; SQLSTATE 22023
|
|
@@ -818,6 +820,18 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
818
820
|
supportsAggregateExpressions() {
|
|
819
821
|
return true;
|
|
820
822
|
}
|
|
823
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
824
|
+
supportsConditionalAggregates() {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
/** `countDistinct` over several fields. */
|
|
828
|
+
supportsDistinctFieldLists() {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
832
|
+
supportsNestedAggregates() {
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
821
835
|
/** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
|
|
822
836
|
supportsNullsPlacement() {
|
|
823
837
|
return true;
|
|
@@ -1772,6 +1786,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1772
1786
|
}
|
|
1773
1787
|
async renameTable(oldName) {
|
|
1774
1788
|
const newName = this._table.tableName;
|
|
1789
|
+
if (!await this.getObjectKind(oldName) && await this.getObjectKind(newName)) return;
|
|
1775
1790
|
const ddl = `ALTER TABLE ${quoteTableName(oldName)} RENAME TO ${qi(newName)}`;
|
|
1776
1791
|
this._log(ddl);
|
|
1777
1792
|
await this._exec().exec(ddl);
|
|
@@ -1801,7 +1816,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1801
1816
|
isUnmanaged: (name) => name.startsWith(PG_VECTOR_INDEX_PREFIX),
|
|
1802
1817
|
skipIndex: (index) => index.type === "geo" && !this._supportsGeo ? `[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped` : void 0,
|
|
1803
1818
|
listExisting: async () => {
|
|
1804
|
-
const rows = await this._exec().all(`SELECT i.relname AS name,
|
|
1819
|
+
const rows = await this._exec().all(`SELECT i.relname AS name, pg_get_indexdef(x.indexrelid) AS def,
|
|
1805
1820
|
(SELECT array_agg(a.attname::text ORDER BY k.ord)
|
|
1806
1821
|
FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1807
1822
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
@@ -1814,15 +1829,17 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1814
1829
|
liveNames = new Set(rows.map((r) => r.name));
|
|
1815
1830
|
return rows.map((r) => ({
|
|
1816
1831
|
name: r.name,
|
|
1817
|
-
columns: r.columns ?? void 0
|
|
1832
|
+
columns: r.columns ?? void 0,
|
|
1833
|
+
definition: tsConfigOfIndexDef(r.def)
|
|
1818
1834
|
}));
|
|
1819
1835
|
},
|
|
1836
|
+
renderDesiredDefinition: (index) => index.type === "fulltext" ? pgTextSearchConfig(index) : void 0,
|
|
1820
1837
|
createIndex: async (index) => {
|
|
1821
1838
|
if (index.type === "fulltext") {
|
|
1822
1839
|
const textFields = splitFulltextFields(index).text;
|
|
1823
1840
|
if (textFields.length === 0) return;
|
|
1824
1841
|
const tsvectorExpr = this._buildTsvectorExpr(textFields);
|
|
1825
|
-
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gin(to_tsvector('
|
|
1842
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gin(to_tsvector('${pgTextSearchConfig(index)}', ${tsvectorExpr}))`;
|
|
1826
1843
|
this._log(sql);
|
|
1827
1844
|
await this._exec().exec(sql);
|
|
1828
1845
|
return;
|
|
@@ -1996,12 +2013,15 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1996
2013
|
const equalities = n === void 0 ? [] : integerFields.map((f) => `${qi(f.name)} = CAST(? AS BIGINT)`);
|
|
1997
2014
|
let textRank;
|
|
1998
2015
|
if (textFields.length > 0) {
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2016
|
+
const config = pgTextSearchConfig(fulltextIndex);
|
|
2017
|
+
const tsvector = `to_tsvector('${config}', ${this._buildTsvectorExpr(textFields)})`;
|
|
2018
|
+
const { fn, term } = pgTsQuery(fulltextIndex, text);
|
|
2019
|
+
const tsquery = `${fn}('${config}', ?)`;
|
|
2020
|
+
parts.push(`${tsvector} @@ ${tsquery}`);
|
|
2021
|
+
params.push(term);
|
|
2002
2022
|
textRank = {
|
|
2003
|
-
sql: `${this._buildTsRankExpr(textFields, tsvector)} DESC`,
|
|
2004
|
-
params: [
|
|
2023
|
+
sql: `${this._buildTsRankExpr(textFields, tsvector, config, tsquery)} DESC`,
|
|
2024
|
+
params: [term]
|
|
2005
2025
|
};
|
|
2006
2026
|
}
|
|
2007
2027
|
for (const eq of equalities) {
|
|
@@ -2029,8 +2049,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
2029
2049
|
* predicate keeps the unweighted indexed expression, so the GIN index serves
|
|
2030
2050
|
* it unchanged.
|
|
2031
2051
|
*/
|
|
2032
|
-
_buildTsRankExpr(fields, tsvector) {
|
|
2033
|
-
const query = `plainto_tsquery('english', ?)`;
|
|
2052
|
+
_buildTsRankExpr(fields, tsvector, config, query) {
|
|
2034
2053
|
const distinct = [...new Set(fields.map(weightOf))].toSorted((a, b) => b - a);
|
|
2035
2054
|
if (distinct.length <= 1) return `ts_rank(${tsvector}, ${query})`;
|
|
2036
2055
|
const letters = [
|
|
@@ -2042,7 +2061,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
2042
2061
|
const bucketOf = (w) => Math.min(distinct.indexOf(w), 3);
|
|
2043
2062
|
const top = distinct[0];
|
|
2044
2063
|
const bucketWeights = letters.map((_, i) => distinct[Math.min(i, distinct.length - 1)] / top);
|
|
2045
|
-
const vector = fields.map((f) => `setweight(to_tsvector('
|
|
2064
|
+
const vector = fields.map((f) => `setweight(to_tsvector('${config}', coalesce(${qi(f.name)}, '')), '${letters[bucketOf(weightOf(f))]}')`).join(" || ");
|
|
2046
2065
|
return `ts_rank('{${[...bucketWeights].toReversed().map((w) => Number(w.toFixed(6))).join(", ")}}', ${vector}, ${query})`;
|
|
2047
2066
|
}
|
|
2048
2067
|
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
@@ -2414,6 +2433,41 @@ function vectorToString(vector) {
|
|
|
2414
2433
|
function keyColumns(detail) {
|
|
2415
2434
|
return (detail?.match(/Key \(([^)]+)\)=/)?.[1])?.split(",").map((c) => c.trim().replace(/^"|"$/g, ""));
|
|
2416
2435
|
}
|
|
2436
|
+
/**
|
|
2437
|
+
* The text search configuration of a fulltext index: its
|
|
2438
|
+
* `@db.index.fulltext.language` (`none` → `simple`), `english` when unset.
|
|
2439
|
+
* Inlined into DDL and queries, so only the closed language list passes.
|
|
2440
|
+
*/
|
|
2441
|
+
function pgTextSearchConfig(index) {
|
|
2442
|
+
const language = index.language;
|
|
2443
|
+
if (language === void 0) return "english";
|
|
2444
|
+
if (!DB_FULLTEXT_LANGUAGES.includes(language)) throw new Error(`Unsupported fulltext language "${language}" on index "${index.name}"`);
|
|
2445
|
+
return language === "none" ? "simple" : language;
|
|
2446
|
+
}
|
|
2447
|
+
/** The configuration of a `to_tsvector('<config>'::regconfig, …)` index definition, else `undefined`. */
|
|
2448
|
+
function tsConfigOfIndexDef(def) {
|
|
2449
|
+
return def ? /to_tsvector\('([a-z_]+)'::regconfig/i.exec(def)?.[1] : void 0;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* The tsquery function and its text for a search: `plainto_tsquery` (every
|
|
2453
|
+
* word) by default; `websearch_to_tsquery` under `@db.index.fulltext.match`
|
|
2454
|
+
* (since 0.1.168) — `"any"` OR-s the terms (quoted phrases kept whole, no
|
|
2455
|
+
* `-` exclusions).
|
|
2456
|
+
*/
|
|
2457
|
+
function pgTsQuery(index, text) {
|
|
2458
|
+
if (index.match === void 0) return {
|
|
2459
|
+
fn: "plainto_tsquery",
|
|
2460
|
+
term: text
|
|
2461
|
+
};
|
|
2462
|
+
if (index.match === "all") return {
|
|
2463
|
+
fn: "websearch_to_tsquery",
|
|
2464
|
+
term: text
|
|
2465
|
+
};
|
|
2466
|
+
return {
|
|
2467
|
+
fn: "websearch_to_tsquery",
|
|
2468
|
+
term: [...text.matchAll(/"[^"]*"|\S+/g)].map((m) => m[0].replace(/^-+/, "")).filter(Boolean).join(" or ")
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2417
2471
|
/** A fulltext member's weight (`@db.index.fulltext 'idx', w`), `1` when unset. */
|
|
2418
2472
|
function weightOf(f) {
|
|
2419
2473
|
return f.weight ?? 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.168",
|
|
4
4
|
"description": "PostgreSQL adapter for @atscript/db with pg (node-postgres) driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -42,21 +42,23 @@
|
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@atscript/db-sql-tools": "^0.1.168"
|
|
47
|
+
},
|
|
45
48
|
"devDependencies": {
|
|
46
|
-
"@atscript/core": "^0.2.
|
|
47
|
-
"@atscript/typescript": "^0.2.
|
|
49
|
+
"@atscript/core": "^0.2.4",
|
|
50
|
+
"@atscript/typescript": "^0.2.4",
|
|
48
51
|
"@types/pg": "^8.11.0",
|
|
49
|
-
"@uniqu/core": "^0.
|
|
52
|
+
"@uniqu/core": "^0.2.0",
|
|
50
53
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.2.
|
|
54
|
+
"unplugin-atscript": "^0.2.4"
|
|
52
55
|
},
|
|
53
56
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.2.
|
|
55
|
-
"@atscript/typescript": "^0.2.
|
|
56
|
-
"@uniqu/core": "^0.
|
|
57
|
+
"@atscript/core": "^0.2.4",
|
|
58
|
+
"@atscript/typescript": "^0.2.4",
|
|
59
|
+
"@uniqu/core": "^0.2.0",
|
|
57
60
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db": "^0.1.
|
|
59
|
-
"@atscript/db-sql-tools": "^0.1.166"
|
|
61
|
+
"@atscript/db": "^0.1.168"
|
|
60
62
|
},
|
|
61
63
|
"peerDependenciesMeta": {
|
|
62
64
|
"pg": {
|