uql-orm 0.70.0 → 0.72.0
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/README.md +2 -0
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
- package/dist/cockroachdb/cockroachDialect.js +8 -0
- package/dist/d1/d1SqliteDialect.d.ts +3 -0
- package/dist/d1/d1SqliteDialect.js +3 -1
- package/dist/dialect/abstractSqlDialect.d.ts +14 -2
- package/dist/dialect/abstractSqlDialect.js +48 -16
- package/dist/dialect/aliases.d.ts +2 -0
- package/dist/dialect/aliases.js +2 -0
- package/dist/dialect/hydrateColumn.d.ts +3 -2
- package/dist/dialect/hydrateColumn.js +10 -1
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +6 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +21 -5
- package/dist/dialect/pgLikeSqlDialect.js +48 -15
- package/dist/dialect/queryJoins.d.ts +14 -4
- package/dist/dialect/queryJoins.js +31 -11
- package/dist/dialect/vectorCast.d.ts +2 -0
- package/dist/dialect/vectorCast.js +7 -0
- package/dist/dialect/vectorSqlDialect.d.ts +7 -3
- package/dist/dialect/vectorSqlDialect.js +15 -10
- package/dist/libsql/libsqlDialect.d.ts +1 -1
- package/dist/libsql/libsqlDialect.js +3 -3
- package/dist/maria/mariaDialect.d.ts +3 -9
- package/dist/maria/mariaDialect.js +5 -15
- package/dist/maria/mariadbQuerier.js +9 -3
- package/dist/migrate/ddl/index.d.ts +1 -0
- package/dist/migrate/ddl/index.js +4 -2
- package/dist/migrate/ddl/indexDdl.d.ts +2 -0
- package/dist/migrate/ddl/indexDdl.js +10 -2
- package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
- package/dist/migrate/ddl/pgIndexDdl.js +11 -11
- package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
- package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
- package/dist/migrate/generator/mongoCommand.d.ts +28 -0
- package/dist/migrate/generator/mongoCommand.js +8 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
- package/dist/migrate/introspection/mongoIntrospector.js +33 -7
- package/dist/migrate/introspection/postgresIntrospector.js +19 -0
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
- package/dist/mongo/mongoDialect.d.ts +5 -3
- package/dist/mongo/mongoDialect.js +40 -17
- package/dist/mongo/mongodbQuerier.js +4 -5
- package/dist/mssql/mssqlDialect.js +1 -0
- package/dist/schema/canonicalType.js +10 -4
- package/dist/schema/indexDifferences.d.ts +5 -2
- package/dist/schema/indexDifferences.js +5 -0
- package/dist/schema/schemaASTBuilder.js +3 -2
- package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
- package/dist/sqlite/localSqliteQuerierPool.js +8 -11
- package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
- package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
- package/dist/sqlite/sqliteDialect.d.ts +11 -1
- package/dist/sqlite/sqliteDialect.js +46 -3
- package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
- package/dist/sqlite/sqliteQuerierPool.js +10 -7
- package/dist/turso/tursoLocalDialect.d.ts +1 -1
- package/dist/turso/tursoLocalDialect.js +1 -1
- package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
- package/dist/turso/tursoLocalQuerierPool.js +3 -10
- package/dist/type/dialect.d.ts +5 -0
- package/dist/type/entity.d.ts +27 -13
- package/dist/type/migration.d.ts +7 -9
- package/dist/type/query.d.ts +11 -4
- package/dist/type/queryAggregate.d.ts +5 -16
- package/dist/type/utility.d.ts +13 -0
- package/dist/type/vector.d.ts +17 -0
- package/dist/type/vector.js +6 -0
- package/dist/util/ddlExpression.util.d.ts +2 -0
- package/dist/util/ddlExpression.util.js +5 -0
- package/dist/util/dialect.util.d.ts +21 -2
- package/dist/util/dialect.util.js +42 -2
- package/package.json +4 -3
- package/skills/uql-orm/SKILL.md +146 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IndexType } from '../schema/types.js';
|
|
1
2
|
import { type DriverCapabilities, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QueryTextSearchOptions, type SqlDialectFeatures, type Type, type VectorDistance, type VectorMetric } from '../type/index.js';
|
|
2
3
|
import type { DialectOptions } from './abstractDialect.js';
|
|
3
4
|
import { AbstractSqlDialect, type RelationRows } from './abstractSqlDialect.js';
|
|
@@ -45,11 +46,11 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
45
46
|
readonly maxBindValues: number;
|
|
46
47
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
47
48
|
/**
|
|
48
|
-
* The
|
|
49
|
+
* The setting each vector index type reads for "how much of the index to explore". They are not the
|
|
49
50
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
50
51
|
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
+
protected readonly annSettings: ReadonlyMap<IndexType, string>;
|
|
53
54
|
/**
|
|
54
55
|
* `SET LOCAL hnsw.ef_search = N`, plus `hnsw.iterative_scan = strict_order` where the query also filters
|
|
55
56
|
* by distance, which would otherwise drop rows the candidate list missed.
|
|
@@ -58,11 +59,26 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
58
59
|
normalizeValue(value: unknown): unknown;
|
|
59
60
|
placeholder(index: number): string;
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* `<document> @@ WEBSEARCH_TO_TSQUERY(...)`, under the `$config` asked for, else that of the fulltext
|
|
63
|
+
* index over these fields, which the planner serves it from. `WEBSEARCH_TO_TSQUERY` takes free-form
|
|
64
|
+
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
64
65
|
*/
|
|
65
66
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
67
|
+
/** `TS_RANK` of the document the predicate matches, against the same search. */
|
|
68
|
+
protected appendTextRank<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
69
|
+
/** The document a search reads and the search itself, open for its value. */
|
|
70
|
+
private textSearchParts;
|
|
71
|
+
/**
|
|
72
|
+
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
73
|
+
* `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
|
|
74
|
+
* over one, and a bound one reaches it only where the driver leaves the parameter untyped.
|
|
75
|
+
*/
|
|
76
|
+
textSearchTarget(columns: readonly string[], config?: string): string;
|
|
77
|
+
/** A config as the text-search functions' first argument, or nothing, for the server's default. */
|
|
78
|
+
private textConfigArg;
|
|
79
|
+
/** How a config literal is typed, and the function reading a search's text: CockroachDB lacks both of these. */
|
|
80
|
+
protected readonly textConfigCast: string;
|
|
81
|
+
protected readonly textQueryFn: string;
|
|
66
82
|
protected jsonContains(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
|
|
67
83
|
protected jsonLength(slot: JsonSlot): string;
|
|
68
84
|
/** Each element stays `jsonb`, so it is read as any path is: `->` for the value, `->>` for its text. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryRaw, } from '../type/index.js';
|
|
2
|
-
import { hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
2
|
+
import { fulltextConfig, fulltextIndexOver, hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
3
3
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
5
|
import { JSON_PULL_ALIAS, RELATION_ROW_ALIAS } from './aliases.js';
|
|
@@ -13,6 +13,8 @@ export const PG_VECTOR_METRICS = new Map([
|
|
|
13
13
|
['inner', { op: '<#>', index: 'ip' }],
|
|
14
14
|
['l1', { op: '<+>', index: 'l1' }],
|
|
15
15
|
]);
|
|
16
|
+
/** pgvector's HNSW candidate list, the one setting an iterative scan goes with. */
|
|
17
|
+
const HNSW_EF_SEARCH = 'hnsw.ef_search';
|
|
16
18
|
/** What the Postgres-wire engines have. */
|
|
17
19
|
export const PG_FEATURES = {
|
|
18
20
|
ifNotExists: true,
|
|
@@ -25,6 +27,7 @@ export const PG_FEATURES = {
|
|
|
25
27
|
commentSyntax: 'statement',
|
|
26
28
|
vectorIndexRequiresNotNull: false,
|
|
27
29
|
vectorSupportsLength: true,
|
|
30
|
+
vectorBytes: false,
|
|
28
31
|
supportsTimestamptz: true,
|
|
29
32
|
stringSizing: 'bounded-text',
|
|
30
33
|
supportsUnsigned: false,
|
|
@@ -87,12 +90,12 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
87
90
|
maxBindValues = 65535;
|
|
88
91
|
vectorMetrics = PG_VECTOR_METRICS;
|
|
89
92
|
/**
|
|
90
|
-
* The
|
|
93
|
+
* The setting each vector index type reads for "how much of the index to explore". They are not the
|
|
91
94
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
92
95
|
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
93
96
|
*/
|
|
94
|
-
|
|
95
|
-
['hnsw',
|
|
97
|
+
annSettings = new Map([
|
|
98
|
+
['hnsw', HNSW_EF_SEARCH],
|
|
96
99
|
['ivfflat', 'ivfflat.probes'],
|
|
97
100
|
]);
|
|
98
101
|
/**
|
|
@@ -101,12 +104,12 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
101
104
|
*/
|
|
102
105
|
vectorTuningStatements(meta, q) {
|
|
103
106
|
const indexType = this.tunedVectorIndex(meta, q)?.type;
|
|
104
|
-
const setting = indexType ?
|
|
107
|
+
const setting = indexType ? this.annSettings.get(indexType) : undefined;
|
|
105
108
|
if (!setting) {
|
|
106
109
|
return [];
|
|
107
110
|
}
|
|
108
111
|
const statements = [`SET LOCAL ${setting} = ${q.$candidates}`];
|
|
109
|
-
if (
|
|
112
|
+
if (setting === HNSW_EF_SEARCH && hasVectorNear(q.$where)) {
|
|
110
113
|
statements.push('SET LOCAL hnsw.iterative_scan = strict_order');
|
|
111
114
|
}
|
|
112
115
|
return statements;
|
|
@@ -121,20 +124,50 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
121
124
|
return `$${index}`;
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
+
* `<document> @@ WEBSEARCH_TO_TSQUERY(...)`, under the `$config` asked for, else that of the fulltext
|
|
128
|
+
* index over these fields, which the planner serves it from. `WEBSEARCH_TO_TSQUERY` takes free-form
|
|
129
|
+
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
127
130
|
*/
|
|
128
131
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
.join(` || ' ' || `);
|
|
132
|
-
// The config is bound once and its numbered placeholder reused by both calls.
|
|
133
|
-
const config = search.$config ? `${this.addValue(ctx, search.$config)}::regconfig, ` : '';
|
|
134
|
-
ctx.append(`TO_TSVECTOR(${config}${fields}) @@ WEBSEARCH_TO_TSQUERY(${config}`);
|
|
132
|
+
const { document, query } = this.textSearchParts(meta, search);
|
|
133
|
+
ctx.append(`${document} @@ ${query}`);
|
|
135
134
|
ctx.addValue(search.$value);
|
|
136
135
|
ctx.append(')');
|
|
137
136
|
}
|
|
137
|
+
/** `TS_RANK` of the document the predicate matches, against the same search. */
|
|
138
|
+
appendTextRank(ctx, meta, search) {
|
|
139
|
+
const { document, query } = this.textSearchParts(meta, search);
|
|
140
|
+
ctx.append(`TS_RANK(${document}, ${query}`);
|
|
141
|
+
ctx.addValue(search.$value);
|
|
142
|
+
ctx.append('))');
|
|
143
|
+
}
|
|
144
|
+
/** The document a search reads and the search itself, open for its value. */
|
|
145
|
+
textSearchParts(meta, search) {
|
|
146
|
+
const keys = textSearchFields(meta, search);
|
|
147
|
+
const index = fulltextIndexOver(meta, keys);
|
|
148
|
+
const config = search.$config ?? (index && fulltextConfig(index));
|
|
149
|
+
const columns = keys.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
150
|
+
return {
|
|
151
|
+
document: this.textSearchTarget(columns, config),
|
|
152
|
+
query: `${this.textQueryFn}(${this.textConfigArg(config)}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
157
|
+
* `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
|
|
158
|
+
* over one, and a bound one reaches it only where the driver leaves the parameter untyped.
|
|
159
|
+
*/
|
|
160
|
+
textSearchTarget(columns, config) {
|
|
161
|
+
const document = columns.map((column) => `COALESCE(${column}, '')`).join(` || ' ' || `);
|
|
162
|
+
return `TO_TSVECTOR(${this.textConfigArg(config)}${document})`;
|
|
163
|
+
}
|
|
164
|
+
/** A config as the text-search functions' first argument, or nothing, for the server's default. */
|
|
165
|
+
textConfigArg(config) {
|
|
166
|
+
return config === undefined ? '' : `${this.escape(config)}${this.textConfigCast}, `;
|
|
167
|
+
}
|
|
168
|
+
/** How a config literal is typed, and the function reading a search's text: CockroachDB lacks both of these. */
|
|
169
|
+
textConfigCast = '::regconfig';
|
|
170
|
+
textQueryFn = 'WEBSEARCH_TO_TSQUERY';
|
|
138
171
|
jsonContains(ctx, slot, values) {
|
|
139
172
|
return `${this.jsonValue(slot)} @> ${this.jsonVal(ctx, values)}`;
|
|
140
173
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, QueryWhere, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
|
|
4
4
|
* `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
|
|
@@ -32,15 +32,25 @@ export type QuerySortOptions = {
|
|
|
32
32
|
/** Alias the queried entity's own columns are qualified by, when the statement qualifies them. */
|
|
33
33
|
readonly prefix?: string;
|
|
34
34
|
readonly joins?: QueryJoins;
|
|
35
|
-
readonly distinct?: boolean;
|
|
36
35
|
};
|
|
37
36
|
/**
|
|
38
37
|
* What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
|
|
39
38
|
* columns, the `ORDER BY` and the lock agree. `claimAlias` names each join's table, parents first.
|
|
40
39
|
*/
|
|
41
40
|
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
|
|
42
|
-
/**
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* What an aggregate joins, and the `$where` left to it. Each to-one relation a `$group` path passes through
|
|
43
|
+
* is an `INNER` join, since a group of a path names a related row; a filter on one of them, keyed at the top
|
|
44
|
+
* of the `$where` where an `AND` joins it, moves into that join rather than reading its table again. Under a
|
|
45
|
+
* `$not` it could not: the join would drop the rows the negation keeps.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveGroupJoins<E>(meta: EntityMeta<E>, q: {
|
|
48
|
+
readonly $group?: QueryGroupMap<E>;
|
|
49
|
+
readonly $where?: QueryWhere<E>;
|
|
50
|
+
}, claimAlias?: (path: string) => string): {
|
|
51
|
+
readonly joins: QueryJoins;
|
|
52
|
+
readonly where: QueryWhere<E> | undefined;
|
|
53
|
+
};
|
|
44
54
|
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
45
55
|
export declare function groupPathField(joins: QueryJoins, path: readonly string[]): {
|
|
46
56
|
readonly key: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMeta, relationOf } from '../entity/index.js';
|
|
2
|
-
import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
2
|
+
import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey, parseRelationSize, } from '../util/index.js';
|
|
3
3
|
export const NO_JOINS = new Map();
|
|
4
4
|
/**
|
|
5
5
|
* What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
|
|
@@ -11,18 +11,35 @@ export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
|
|
|
11
11
|
}
|
|
12
12
|
const joins = new Map();
|
|
13
13
|
addPopulateJoins(joins, claimAlias, meta, q.$populate);
|
|
14
|
-
addPathJoins(joins, claimAlias, meta, q.$sort);
|
|
14
|
+
addPathJoins(joins, claimAlias, meta, q.$sort, false);
|
|
15
15
|
return joins;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* What an aggregate joins, and the `$where` left to it. Each to-one relation a `$group` path passes through
|
|
19
|
+
* is an `INNER` join, since a group of a path names a related row; a filter on one of them, keyed at the top
|
|
20
|
+
* of the `$where` where an `AND` joins it, moves into that join rather than reading its table again. Under a
|
|
21
|
+
* `$not` it could not: the join would drop the rows the negation keeps.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveGroupJoins(meta, q, claimAlias = (path) => path) {
|
|
19
24
|
const joins = new Map();
|
|
20
|
-
for (const ref of Object.values(group ?? {})) {
|
|
25
|
+
for (const ref of Object.values(q.$group ?? {})) {
|
|
21
26
|
if (isRecord(ref)) {
|
|
22
|
-
addPathJoins(joins, claimAlias, meta, ref);
|
|
27
|
+
addPathJoins(joins, claimAlias, meta, ref, true);
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
|
|
30
|
+
if (!q.$where) {
|
|
31
|
+
return { joins, where: q.$where };
|
|
32
|
+
}
|
|
33
|
+
const where = { ...q.$where };
|
|
34
|
+
for (const key of getKeys(q.$where)) {
|
|
35
|
+
const join = joins.get(key);
|
|
36
|
+
const filter = q.$where[key];
|
|
37
|
+
if (join && isRecord(filter) && parseRelationSize(filter) === undefined) {
|
|
38
|
+
joins.set(key, { ...join, query: { $where: filter } });
|
|
39
|
+
delete where[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { joins, where };
|
|
26
43
|
}
|
|
27
44
|
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
28
45
|
export function groupPathField(joins, path) {
|
|
@@ -93,8 +110,11 @@ function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
|
|
|
93
110
|
addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The to-one relations a nested map of fields passes through, a `$sort` or a `$group` path, as joins adding
|
|
115
|
+
* no columns: `required` where the path names a related row, as a group's does, and a sort's does not.
|
|
116
|
+
*/
|
|
117
|
+
function addPathJoins(joins, claimAlias, meta, map, required, parent) {
|
|
98
118
|
if (!map) {
|
|
99
119
|
return;
|
|
100
120
|
}
|
|
@@ -106,9 +126,9 @@ function addPathJoins(joins, claimAlias, meta, map, parent) {
|
|
|
106
126
|
if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
|
|
107
127
|
continue;
|
|
108
128
|
}
|
|
109
|
-
const join = addJoin(joins, claimAlias, parent, key, relation, {},
|
|
129
|
+
const join = addJoin(joins, claimAlias, parent, key, relation, {}, required, false);
|
|
110
130
|
// `E` stated: inferred from the nested map, it lands on the nested relation's target.
|
|
111
|
-
addPathJoins(joins, claimAlias, join.meta, value, join);
|
|
131
|
+
addPathJoins(joins, claimAlias, join.meta, value, required, join);
|
|
112
132
|
}
|
|
113
133
|
}
|
|
114
134
|
/** The join a sort may address at `path` with the relation's own sort map, or why it may not; `unjoinable` is the dialect's remedy. */
|
|
@@ -20,5 +20,7 @@ export declare function toSparsevecLiteral(values: readonly unknown[]): string;
|
|
|
20
20
|
* as it was written, `{1:1,3:2}/3` or `[1,2,3]`; `undefined` where the text matches neither.
|
|
21
21
|
*/
|
|
22
22
|
export declare function parseVectorLiteral(raw: string, cast: VectorCast): number[] | undefined;
|
|
23
|
+
/** The packed little-endian float32s a blob vector column holds, and {@link decodeFloat32s} reads back. */
|
|
24
|
+
export declare function encodeFloat32s(values: readonly unknown[]): Uint8Array;
|
|
23
25
|
/** Packed little-endian float32s, each read as {@link shortestFloat32}. */
|
|
24
26
|
export declare function decodeFloat32s(bytes: Uint8Array): number[];
|
|
@@ -62,6 +62,13 @@ function parseDense(text) {
|
|
|
62
62
|
return undefined;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/** The packed little-endian float32s a blob vector column holds, and {@link decodeFloat32s} reads back. */
|
|
66
|
+
export function encodeFloat32s(values) {
|
|
67
|
+
const bytes = new Uint8Array(values.length * 4);
|
|
68
|
+
const view = new DataView(bytes.buffer);
|
|
69
|
+
values.forEach((value, at) => view.setFloat32(at * 4, Number(value), true));
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
65
72
|
/** Packed little-endian float32s, each read as {@link shortestFloat32}. */
|
|
66
73
|
export function decodeFloat32s(bytes) {
|
|
67
74
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorSearch, SqlDialectFeatures, VectorDistance, VectorMetric } from '../type/index.js';
|
|
1
|
+
import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorSearch, QueryWhere, SqlDialectFeatures, VectorDistance, VectorMetric } from '../type/index.js';
|
|
2
2
|
import { AbstractDialect } from './abstractDialect.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type VectorCast } from './vectorCast.js';
|
|
4
4
|
/**
|
|
5
5
|
* Vector search for the SQL dialects: the distance a `$sort` ranks by and projects, and the ANN tuning.
|
|
6
6
|
* Each dialect lists its metrics in {@link vectorMetrics}, an operator or a function; empty means no search.
|
|
@@ -20,12 +20,16 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
20
20
|
* spelled into a `SET` rather than bound, and `/http` input is untyped.
|
|
21
21
|
*/
|
|
22
22
|
protected tunedVectorIndex<E>(meta: EntityMeta<E>, q: Query<E>): EntityIndexMeta | undefined;
|
|
23
|
+
/** The `$where` a read runs: the query's own, which an engine reading its vector index as a table narrows. */
|
|
24
|
+
protected rankedWhere<E>(_meta: EntityMeta<E>, q: Query<E>, _prefix: string | undefined): QueryWhere<E> | undefined;
|
|
23
25
|
/**
|
|
24
26
|
* Every distance metric this dialect has, and how a search and an index spell each. Empty means no
|
|
25
27
|
* vector search at all, which is what MySQL and D1 are. The key set is the single answer to "is this
|
|
26
28
|
* metric supported here", for a query and an index alike.
|
|
27
29
|
*/
|
|
28
30
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
31
|
+
/** Whether this engine has a vector index: the one a metric's `index` names. */
|
|
32
|
+
hasVectorIndex(): boolean;
|
|
29
33
|
/** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
|
|
30
34
|
abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
31
35
|
/**
|
|
@@ -39,7 +43,7 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
39
43
|
};
|
|
40
44
|
/**
|
|
41
45
|
* Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
|
|
42
|
-
* dialect needing a conversion around it (`$1::vector`, `
|
|
46
|
+
* dialect needing a conversion around it (`$1::vector`, `CAST(? AS VECTOR(n))`) declares it once.
|
|
43
47
|
*/
|
|
44
48
|
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[], _field?: FieldOptions): void;
|
|
45
49
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { unsupportedVectorMetric } from '../type/vector.js';
|
|
2
|
-
import { findVectorIndex, findVectorSort } from '../util/dialect.util.js';
|
|
1
|
+
import { DEFAULT_VECTOR_DISTANCE, unsupportedVectorMetric } from '../type/vector.js';
|
|
2
|
+
import { findVectorIndex, findVectorSort, vectorCandidates } from '../util/dialect.util.js';
|
|
3
3
|
import { entityName } from '../util/object.util.js';
|
|
4
4
|
import { AbstractDialect } from './abstractDialect.js';
|
|
5
|
+
import { encodeFloat32s } from './vectorCast.js';
|
|
5
6
|
/**
|
|
6
7
|
* Vector search for the SQL dialects: the distance a `$sort` ranks by and projects, and the ANN tuning.
|
|
7
8
|
* Each dialect lists its metrics in {@link vectorMetrics}, an operator or a function; empty means no search.
|
|
@@ -24,22 +25,26 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
24
25
|
* spelled into a `SET` rather than bound, and `/http` input is untyped.
|
|
25
26
|
*/
|
|
26
27
|
tunedVectorIndex(meta, q) {
|
|
27
|
-
|
|
28
|
-
if (candidates === undefined) {
|
|
28
|
+
if (vectorCandidates(q) === undefined) {
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
|
-
if (!Number.isInteger(candidates) || candidates < 1) {
|
|
32
|
-
throw new TypeError(`$candidates must be a positive integer, got ${JSON.stringify(candidates)}`);
|
|
33
|
-
}
|
|
34
31
|
const key = this.vectorSortKey(q);
|
|
35
32
|
return key ? findVectorIndex(meta, key) : undefined;
|
|
36
33
|
}
|
|
34
|
+
/** The `$where` a read runs: the query's own, which an engine reading its vector index as a table narrows. */
|
|
35
|
+
rankedWhere(_meta, q, _prefix) {
|
|
36
|
+
return q.$where;
|
|
37
|
+
}
|
|
37
38
|
/**
|
|
38
39
|
* Every distance metric this dialect has, and how a search and an index spell each. Empty means no
|
|
39
40
|
* vector search at all, which is what MySQL and D1 are. The key set is the single answer to "is this
|
|
40
41
|
* metric supported here", for a query and an index alike.
|
|
41
42
|
*/
|
|
42
43
|
vectorMetrics = new Map();
|
|
44
|
+
/** Whether this engine has a vector index: the one a metric's `index` names. */
|
|
45
|
+
hasVectorIndex() {
|
|
46
|
+
return [...this.vectorMetrics.values()].some((metric) => metric.index);
|
|
47
|
+
}
|
|
43
48
|
/**
|
|
44
49
|
* What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
|
|
45
50
|
* field's, then its index's, which serves no other, then cosine.
|
|
@@ -47,15 +52,15 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
47
52
|
resolveVectorDistance(meta, key, search) {
|
|
48
53
|
const field = meta.fields[key];
|
|
49
54
|
const colName = this.resolveColumnName(key, field);
|
|
50
|
-
const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ??
|
|
55
|
+
const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE;
|
|
51
56
|
return { colName, distance, field };
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
59
|
* Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
|
|
55
|
-
* dialect needing a conversion around it (`$1::vector`, `
|
|
60
|
+
* dialect needing a conversion around it (`$1::vector`, `CAST(? AS VECTOR(n))`) declares it once.
|
|
56
61
|
*/
|
|
57
62
|
appendVectorValue(ctx, value, _field) {
|
|
58
|
-
ctx.addValue(`[${value.join(',')}]`);
|
|
63
|
+
ctx.addValue(this.features.vectorBytes ? encodeFloat32s(value) : `[${value.join(',')}]`);
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* The vector type this dialect actually has for a declared one, so the cast follows the column
|
|
@@ -7,6 +7,6 @@ import type { VectorDistance, VectorMetric } from '../type/index.js';
|
|
|
7
7
|
* functions, which `TursoDialect` inherits.
|
|
8
8
|
*/
|
|
9
9
|
export declare class LibsqlDialect extends SqliteDialect {
|
|
10
|
-
/** libSQL's built-in vector functions; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
10
|
+
/** libSQL's built-in vector functions, and the metric its DiskANN index names; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
11
11
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
12
12
|
}
|
|
@@ -6,9 +6,9 @@ import { SqliteDialect } from '../sqlite/sqliteDialect.js';
|
|
|
6
6
|
* functions, which `TursoDialect` inherits.
|
|
7
7
|
*/
|
|
8
8
|
export class LibsqlDialect extends SqliteDialect {
|
|
9
|
-
/** libSQL's built-in vector functions; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
9
|
+
/** libSQL's built-in vector functions, and the metric its DiskANN index names; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
10
10
|
vectorMetrics = new Map([
|
|
11
|
-
['cosine', { fn: 'vector_distance_cos' }],
|
|
12
|
-
['l2', { fn: 'vector_distance_l2' }],
|
|
11
|
+
['cosine', { fn: 'vector_distance_cos', index: 'cosine' }],
|
|
12
|
+
['l2', { fn: 'vector_distance_l2', index: 'l2' }],
|
|
13
13
|
]);
|
|
14
14
|
}
|
|
@@ -6,8 +6,9 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
6
6
|
readonly dialectName = "mariadb";
|
|
7
7
|
readonly insertIdSource = "returning";
|
|
8
8
|
/**
|
|
9
|
-
* Unlike MySQL: `VECTOR(n)` takes its dimension
|
|
10
|
-
* `CREATE INDEX` takes `IF NOT EXISTS`, and a lock cannot be narrowed
|
|
9
|
+
* Unlike MySQL: `VECTOR(n)` takes its dimension and binds as packed float32 bytes, every column of a
|
|
10
|
+
* vector index has to be NOT NULL, `CREATE INDEX` takes `IF NOT EXISTS`, and a lock cannot be narrowed
|
|
11
|
+
* to one table of a join.
|
|
11
12
|
*/
|
|
12
13
|
readonly features: SqlDialectFeatures;
|
|
13
14
|
/**
|
|
@@ -34,13 +35,6 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
34
35
|
protected jsonDiffers(elem: string, operand: string): string;
|
|
35
36
|
/** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+, which the index's `DISTANCE=` names alike. */
|
|
36
37
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
37
|
-
/**
|
|
38
|
-
* A `VECTOR` column holds a packed little-endian float32 blob, and MariaDB refuses text where one
|
|
39
|
-
* belongs: inserting `'[1,2,3]'` fails with `Incorrect vector value`, and passing it to
|
|
40
|
-
* `VEC_DISTANCE_COSINE` with `Illegal parameter data type varchar`. `VEC_FromText` is the
|
|
41
|
-
* conversion, needed on both paths.
|
|
42
|
-
*/
|
|
43
|
-
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
|
|
44
38
|
/**
|
|
45
39
|
* `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
|
|
46
40
|
* neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
|
|
@@ -8,12 +8,14 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
8
8
|
// MariaDB 10.5+ has `INSERT ... RETURNING`, so ids come back exact per row - the upsert's too.
|
|
9
9
|
insertIdSource = 'returning';
|
|
10
10
|
/**
|
|
11
|
-
* Unlike MySQL: `VECTOR(n)` takes its dimension
|
|
12
|
-
* `CREATE INDEX` takes `IF NOT EXISTS`, and a lock cannot be narrowed
|
|
11
|
+
* Unlike MySQL: `VECTOR(n)` takes its dimension and binds as packed float32 bytes, every column of a
|
|
12
|
+
* vector index has to be NOT NULL, `CREATE INDEX` takes `IF NOT EXISTS`, and a lock cannot be narrowed
|
|
13
|
+
* to one table of a join.
|
|
13
14
|
*/
|
|
14
15
|
features = {
|
|
15
16
|
...MYSQL_FEATURES,
|
|
16
17
|
vectorSupportsLength: true,
|
|
18
|
+
vectorBytes: true,
|
|
17
19
|
vectorIndexRequiresNotNull: true,
|
|
18
20
|
indexIfNotExists: true,
|
|
19
21
|
rowLockOf: false,
|
|
@@ -25,8 +27,7 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
25
27
|
appendRelationArray(ctx, { entity, query, alias, joins }) {
|
|
26
28
|
const meta = getMeta(entity);
|
|
27
29
|
const terms = this.projection(ctx, entity, query, { prefix: alias, json: true }, joins);
|
|
28
|
-
const
|
|
29
|
-
const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query.$sort, sortOpts));
|
|
30
|
+
const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query, { prefix: alias, joins }));
|
|
30
31
|
const page = this.buildFragment(ctx, (fragmentCtx) => this.pager(fragmentCtx, query));
|
|
31
32
|
const from = this.buildFragment(ctx, (fragmentCtx) => {
|
|
32
33
|
this.selectRelationJoins(fragmentCtx, meta, alias, joins);
|
|
@@ -69,17 +70,6 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
69
70
|
['cosine', { fn: 'VEC_DISTANCE_COSINE', index: 'cosine' }],
|
|
70
71
|
['l2', { fn: 'VEC_DISTANCE_EUCLIDEAN', index: 'euclidean' }],
|
|
71
72
|
]);
|
|
72
|
-
/**
|
|
73
|
-
* A `VECTOR` column holds a packed little-endian float32 blob, and MariaDB refuses text where one
|
|
74
|
-
* belongs: inserting `'[1,2,3]'` fails with `Incorrect vector value`, and passing it to
|
|
75
|
-
* `VEC_DISTANCE_COSINE` with `Illegal parameter data type varchar`. `VEC_FromText` is the
|
|
76
|
-
* conversion, needed on both paths.
|
|
77
|
-
*/
|
|
78
|
-
appendVectorValue(ctx, value) {
|
|
79
|
-
ctx.append('VEC_FromText(');
|
|
80
|
-
super.appendVectorValue(ctx, value);
|
|
81
|
-
ctx.append(')');
|
|
82
|
-
}
|
|
83
73
|
/**
|
|
84
74
|
* `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
|
|
85
75
|
* neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
2
2
|
import { decodeBigInts } from '../util/wideNumber.js';
|
|
3
|
+
/** This driver binds only a `Buffer` as bytes: any other `Uint8Array` goes as the JSON of its indices. */
|
|
4
|
+
function toBindValues(values) {
|
|
5
|
+
return values?.map((value) => value instanceof Uint8Array && !Buffer.isBuffer(value)
|
|
6
|
+
? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
7
|
+
: value);
|
|
8
|
+
}
|
|
3
9
|
export class MariadbQuerier extends AbstractPoolQuerier {
|
|
4
10
|
async internalAll(query, values) {
|
|
5
|
-
const rows = await this.getConn().query(query, values);
|
|
11
|
+
const rows = await this.getConn().query(query, toBindValues(values));
|
|
6
12
|
return Array.from(rows, decodeBigInts);
|
|
7
13
|
}
|
|
8
14
|
async internalRun(query, values) {
|
|
9
|
-
const res = await this.getConn().query(query, values);
|
|
15
|
+
const res = await this.getConn().query(query, toBindValues(values));
|
|
10
16
|
// An OK packet reports `affectedRows`; a `RETURNING` statement answers rows instead, and counts by them.
|
|
11
17
|
const changes = res.affectedRows ?? res.length;
|
|
12
18
|
const rows = res.length ? Array.from(res, decodeBigInts) : [];
|
|
13
19
|
return this.buildUpdateResult({ rows, changes, upsertStatus: res.affectedRows });
|
|
14
20
|
}
|
|
15
21
|
async *internalStream(query, values) {
|
|
16
|
-
const stream = this.getConn().queryStream(query, values);
|
|
22
|
+
const stream = this.getConn().queryStream(query, toBindValues(values));
|
|
17
23
|
try {
|
|
18
24
|
for await (const row of stream) {
|
|
19
25
|
yield decodeBigInts(row);
|
|
@@ -6,6 +6,7 @@ export { MsSqlIndexDdl } from './mssqlIndexDdl.js';
|
|
|
6
6
|
export { MsSqlTableDdl } from './mssqlTableDdl.js';
|
|
7
7
|
export { MariaIndexDdl, MySqlIndexDdl, MysqlLikeIndexDdl } from './mysqlIndexDdl.js';
|
|
8
8
|
export { CockroachIndexDdl, PgIndexDdl } from './pgIndexDdl.js';
|
|
9
|
+
export { SqliteIndexDdl } from './sqliteIndexDdl.js';
|
|
9
10
|
export { TableDdl } from './tableDdl.js';
|
|
10
11
|
export declare function indexDdlFor(dialect: AbstractSqlDialect): IndexDdl;
|
|
11
12
|
/**
|
|
@@ -3,16 +3,18 @@ import { MsSqlIndexDdl } from './mssqlIndexDdl.js';
|
|
|
3
3
|
import { MsSqlTableDdl } from './mssqlTableDdl.js';
|
|
4
4
|
import { MariaIndexDdl, MySqlIndexDdl } from './mysqlIndexDdl.js';
|
|
5
5
|
import { CockroachIndexDdl, PgIndexDdl } from './pgIndexDdl.js';
|
|
6
|
+
import { SqliteIndexDdl } from './sqliteIndexDdl.js';
|
|
6
7
|
import { TableDdl } from './tableDdl.js';
|
|
7
8
|
export { IndexDdl } from './indexDdl.js';
|
|
8
9
|
export { MsSqlIndexDdl } from './mssqlIndexDdl.js';
|
|
9
10
|
export { MsSqlTableDdl } from './mssqlTableDdl.js';
|
|
10
11
|
export { MariaIndexDdl, MySqlIndexDdl, MysqlLikeIndexDdl } from './mysqlIndexDdl.js';
|
|
11
12
|
export { CockroachIndexDdl, PgIndexDdl } from './pgIndexDdl.js';
|
|
13
|
+
export { SqliteIndexDdl } from './sqliteIndexDdl.js';
|
|
12
14
|
export { TableDdl } from './tableDdl.js';
|
|
13
15
|
/**
|
|
14
16
|
* Each engine's index DDL, by the `dialectName` a subclass inherits: by name, so this entry carries no
|
|
15
|
-
* dialect, and exhaustive, so a new engine has to name its own.
|
|
17
|
+
* dialect, and exhaustive, so a new engine has to name its own.
|
|
16
18
|
*/
|
|
17
19
|
const INDEX_DDL = {
|
|
18
20
|
postgres: PgIndexDdl,
|
|
@@ -20,7 +22,7 @@ const INDEX_DDL = {
|
|
|
20
22
|
mysql: MySqlIndexDdl,
|
|
21
23
|
mariadb: MariaIndexDdl,
|
|
22
24
|
mssql: MsSqlIndexDdl,
|
|
23
|
-
sqlite:
|
|
25
|
+
sqlite: SqliteIndexDdl,
|
|
24
26
|
};
|
|
25
27
|
export function indexDdlFor(dialect) {
|
|
26
28
|
return new INDEX_DDL[dialect.dialectName](dialect);
|
|
@@ -37,6 +37,8 @@ export declare class IndexDdl<D extends AbstractSqlDialect = AbstractSqlDialect>
|
|
|
37
37
|
protected readonly indexTypeKeywords: ReadonlyMap<IndexType, string>;
|
|
38
38
|
/** The keyword an index type replaces `INDEX` with, or `INDEX` for the types that do not. */
|
|
39
39
|
protected indexKeyword(index: IndexSchema): string;
|
|
40
|
+
/** What the index is over, between the parentheses: a fulltext one's columns as a search matches them, else its entries. */
|
|
41
|
+
protected indexTarget(index: IndexSchema): string;
|
|
40
42
|
/** One index entry: what is indexed, its operator class if any, then its stored order. */
|
|
41
43
|
protected indexColumn(entry: IndexColumnSchema, index: IndexSchema): string;
|
|
42
44
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsonTypeMode } from '../../dialect/jsonSql.js';
|
|
2
2
|
import { INDEX_TYPES } from '../../schema/types.js';
|
|
3
3
|
import { INDEX_FEATURE_LABELS, } from '../../type/index.js';
|
|
4
|
-
import { getKeys } from '../../util/index.js';
|
|
4
|
+
import { fulltextConfig, getKeys } from '../../util/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* What in an index asks for each feature. A `Record` over the feature union rather than a list, so a
|
|
7
7
|
* feature added to {@link INDEX_FEATURE_LABELS} cannot reach a dialect without the test that decides
|
|
@@ -45,7 +45,7 @@ export class IndexDdl {
|
|
|
45
45
|
assertIndexFeatures(index, this.indexFeatures, this.dialect.dialectName);
|
|
46
46
|
const unique = index.unique ? 'UNIQUE ' : '';
|
|
47
47
|
const ifNotExists = (opts.ifNotExists ?? this.dialect.features.indexIfNotExists) ? 'IF NOT EXISTS ' : '';
|
|
48
|
-
const columns =
|
|
48
|
+
const columns = this.indexTarget(index);
|
|
49
49
|
return (`CREATE ${unique}${this.indexKeyword(index)} ${ifNotExists}${this.dialect.escapeId(index.name)} ` +
|
|
50
50
|
`ON ${this.dialect.escapeId(tableName)}${this.indexAccessMethod(index)} (${columns})` +
|
|
51
51
|
`${this.indexInclude(index)}${this.indexTuning(index)}${this.indexPredicate(index)};`);
|
|
@@ -78,6 +78,14 @@ export class IndexDdl {
|
|
|
78
78
|
indexKeyword(index) {
|
|
79
79
|
return (index.type && this.indexTypeKeywords.get(index.type)) || 'INDEX';
|
|
80
80
|
}
|
|
81
|
+
/** What the index is over, between the parentheses: a fulltext one's columns as a search matches them, else its entries. */
|
|
82
|
+
indexTarget(index) {
|
|
83
|
+
if (index.type === 'fulltext' && !index.entries.some((entry) => entry.expression)) {
|
|
84
|
+
const columns = index.entries.map((entry) => this.dialect.escapeId(entry.column));
|
|
85
|
+
return this.dialect.textSearchTarget(columns, fulltextConfig(index));
|
|
86
|
+
}
|
|
87
|
+
return index.entries.map((entry) => this.indexColumn(entry, index)).join(', ');
|
|
88
|
+
}
|
|
81
89
|
/** One index entry: what is indexed, its operator class if any, then its stored order. */
|
|
82
90
|
indexColumn(entry, index) {
|
|
83
91
|
return `${this.indexColumnTarget(entry)}${this.indexColumnOpsClass(entry, index)}${this.indexColumnOrder(entry)}`;
|
|
@@ -2,9 +2,8 @@ import type { IndexColumnSchema, IndexSchema } from '../../type/index.js';
|
|
|
2
2
|
import { IndexDdl } from './indexDdl.js';
|
|
3
3
|
/** `CREATE INDEX ... USING hnsw ("embedding" vector_cosine_ops) WITH (m = ...)`, pgvector's form. */
|
|
4
4
|
export declare class PgIndexDdl extends IndexDdl {
|
|
5
|
-
/** Postgres 18's `pg_am`, with pgvector's two. */
|
|
5
|
+
/** Postgres 18's `pg_am`, with pgvector's two, and `fulltext`, which builds a `gin` one. */
|
|
6
6
|
protected readonly indexTypes: Set<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch">;
|
|
7
|
-
protected readonly indexTypeHints: ReadonlyMap<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch", string>;
|
|
8
7
|
protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
|
|
9
8
|
/** pgvector's own index types; CockroachDB's native one widens this. */
|
|
10
9
|
protected isVectorIndex(index: IndexSchema): boolean;
|
|
@@ -27,6 +26,6 @@ export declare class CockroachIndexDdl extends PgIndexDdl {
|
|
|
27
26
|
protected isVectorIndex(index: IndexSchema): boolean;
|
|
28
27
|
protected indexKeyword(index: IndexSchema): string;
|
|
29
28
|
protected indexAccessMethod(index: IndexSchema): string;
|
|
30
|
-
/**
|
|
31
|
-
protected indexTuning(): string;
|
|
29
|
+
/** Its build-time candidate list alone: pgvector's `WITH (m = 16)` answers "invalid storage parameter", `hnsw` included. */
|
|
30
|
+
protected indexTuning(index: IndexSchema): string;
|
|
32
31
|
}
|