uql-orm 0.37.1 → 0.39.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/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +5 -5
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -24
- package/dist/cockroachdb/cockroachDialect.js +3 -29
- package/dist/dialect/abstractSqlDialect.d.ts +42 -12
- package/dist/dialect/abstractSqlDialect.js +117 -48
- package/dist/dialect/aliases.d.ts +6 -0
- package/dist/dialect/aliases.js +6 -0
- package/dist/dialect/index.d.ts +0 -5
- package/dist/dialect/index.js +2 -5
- package/dist/dialect/jsonSql.d.ts +17 -2
- package/dist/dialect/jsonSql.js +15 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +13 -14
- package/dist/dialect/mysqlLikeSqlDialect.js +16 -20
- package/dist/dialect/pgLikeSqlDialect.d.ts +17 -20
- package/dist/dialect/pgLikeSqlDialect.js +30 -62
- package/dist/dialect/vectorCast.d.ts +0 -9
- package/dist/dialect/vectorCast.js +0 -8
- package/dist/dialect/vectorSqlDialect.d.ts +47 -17
- package/dist/dialect/vectorSqlDialect.js +78 -30
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/metadata/definition.d.ts +1 -1
- package/dist/entity/metadata/definition.js +4 -3
- package/dist/http/query.js +3 -2
- package/dist/libsql/libsqlDialect.d.ts +2 -2
- package/dist/libsql/libsqlDialect.js +3 -3
- package/dist/maria/mariaDialect.d.ts +12 -17
- package/dist/maria/mariaDialect.js +21 -36
- package/dist/maria/mariaVectorMetrics.d.ts +8 -0
- package/dist/maria/mariaVectorMetrics.js +10 -0
- package/dist/maria/mariadbQuerier.d.ts +5 -0
- package/dist/maria/mariadbQuerier.js +5 -0
- package/dist/migrate/builder/migrationBuilder.d.ts +8 -17
- package/dist/migrate/builder/migrationBuilder.js +48 -136
- package/dist/migrate/builder/types.d.ts +0 -2
- package/dist/migrate/ddl/index.d.ts +11 -0
- package/dist/migrate/ddl/index.js +34 -0
- package/dist/{dialect/indexSqlDialect.d.ts → migrate/ddl/indexDdl.d.ts} +23 -19
- package/dist/migrate/ddl/indexDdl.js +126 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +52 -0
- package/dist/migrate/ddl/mysqlIndexDdl.js +125 -0
- package/dist/migrate/ddl/pgIndexDdl.d.ts +36 -0
- package/dist/migrate/ddl/pgIndexDdl.js +87 -0
- package/dist/migrate/drift/driftDetector.js +6 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
- package/dist/migrate/index.d.ts +1 -0
- package/dist/migrate/index.js +2 -0
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +9 -3
- package/dist/migrate/introspection/mysqlIntrospector.js +27 -5
- package/dist/migrate/migrator.js +3 -2
- package/dist/migrate/schemaGenerator.d.ts +6 -6
- package/dist/migrate/schemaGenerator.js +13 -22
- package/dist/mongo/mongoDialect.d.ts +1 -2
- package/dist/mongo/mongoDialect.js +29 -22
- package/dist/mongo/mongodbQuerier.js +1 -1
- package/dist/mysql/mysqlDialect.d.ts +3 -6
- package/dist/mysql/mysqlDialect.js +4 -11
- package/dist/postgres/postgresDialect.d.ts +2 -0
- package/dist/postgres/postgresDialect.js +2 -0
- package/dist/querier/abstractSqlQuerier.d.ts +11 -0
- package/dist/querier/abstractSqlQuerier.js +35 -0
- package/dist/schema/canonicalType.js +35 -36
- package/dist/schema/indexDifferences.d.ts +2 -1
- package/dist/schema/indexDifferences.js +3 -2
- package/dist/sqlite/sqliteDialect.d.ts +2 -2
- package/dist/sqlite/sqliteDialect.js +5 -5
- package/dist/turso/tursoDialect.d.ts +2 -2
- package/dist/turso/tursoDialect.js +4 -4
- package/dist/type/dialect.d.ts +21 -7
- package/dist/type/dialect.js +17 -0
- package/dist/type/entity.d.ts +117 -8
- package/dist/type/entity.js +7 -0
- package/dist/type/query.d.ts +18 -0
- package/dist/type/query.js +6 -0
- package/dist/type/queryWhere.d.ts +46 -2
- package/dist/type/vector.d.ts +45 -7
- package/dist/type/vector.js +16 -1
- package/dist/util/dialect.util.d.ts +20 -1
- package/dist/util/dialect.util.js +49 -4
- package/dist/util/object.util.d.ts +7 -1
- package/dist/util/object.util.js +8 -0
- package/dist/util/relationQuery.util.d.ts +3 -1
- package/dist/util/relationQuery.util.js +8 -3
- package/package.json +1 -1
- package/dist/dialect/indexSqlDialect.js +0 -103
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryRaw, } from '../type/index.js';
|
|
2
|
+
import { hasVectorNear } from '../util/dialect.util.js';
|
|
2
3
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
3
4
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
4
5
|
import { JSON_PULL_ALIAS } from './aliases.js';
|
|
@@ -28,7 +29,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
28
29
|
renameColumn: true,
|
|
29
30
|
foreignKeyAlter: true,
|
|
30
31
|
columnComment: false,
|
|
31
|
-
|
|
32
|
+
vectorIndexRequiresNotNull: false,
|
|
32
33
|
vectorSupportsLength: true,
|
|
33
34
|
supportsTimestamptz: true,
|
|
34
35
|
defaultStringAsText: true,
|
|
@@ -59,64 +60,41 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
59
60
|
['inner', { op: '<#>', opsSuffix: 'ip' }],
|
|
60
61
|
['l1', { op: '<+>', opsSuffix: 'l1' }],
|
|
61
62
|
]);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
indexAccessMethod(index) {
|
|
73
|
-
return index.type ? ` USING ${index.type}` : '';
|
|
74
|
-
}
|
|
75
|
-
indexFeatures = new Set([
|
|
76
|
-
'expression',
|
|
77
|
-
'partial',
|
|
78
|
-
'nullsOrder',
|
|
79
|
-
'opsClass',
|
|
80
|
-
'include',
|
|
63
|
+
/** `SET LOCAL` applies to the enclosing transaction and to nothing at all without one. */
|
|
64
|
+
vectorTuningNeedsTransaction = true;
|
|
65
|
+
/**
|
|
66
|
+
* The GUC each pgvector index type reads for "how much of the index to explore". They are not the
|
|
67
|
+
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
68
|
+
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
69
|
+
*/
|
|
70
|
+
static ANN_SETTINGS = new Map([
|
|
71
|
+
['hnsw', 'hnsw.ef_search'],
|
|
72
|
+
['ivfflat', 'ivfflat.probes'],
|
|
81
73
|
]);
|
|
82
74
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
75
|
+
* `SET LOCAL hnsw.ef_search = N`, plus `hnsw.iterative_scan` when the query also filters by
|
|
76
|
+
* distance. Without iterative scan, HNSW returns its candidate list and the predicate then removes
|
|
77
|
+
* from it, so a `$near` can hand back fewer rows than qualify - the recall bug `$candidates`
|
|
78
|
+
* exists to answer. `strict_order`, never `relaxed_order`: the latter returns rows out of distance
|
|
79
|
+
* order, which would quietly contradict the `ORDER BY` the caller asked for.
|
|
88
80
|
*/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!metric) {
|
|
95
|
-
throw new TypeError(`${this.dialectName} does not support vector distance metric: ${index.distance} (index "${index.name}")`);
|
|
81
|
+
vectorTuningStatements(meta, q) {
|
|
82
|
+
const indexType = this.tunedVectorIndex(meta, q)?.type;
|
|
83
|
+
const setting = indexType ? PgLikeSqlDialect.ANN_SETTINGS.get(indexType) : undefined;
|
|
84
|
+
if (!setting) {
|
|
85
|
+
return [];
|
|
96
86
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || index.distance === 'l1')) {
|
|
101
|
-
throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
|
|
87
|
+
const statements = [`SET LOCAL ${setting} = ${q.$candidates}`];
|
|
88
|
+
if (indexType === 'hnsw' && hasVectorNear(q.$where)) {
|
|
89
|
+
statements.push('SET LOCAL hnsw.iterative_scan = strict_order');
|
|
102
90
|
}
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
indexInclude(index) {
|
|
106
|
-
return index.include?.length ? ` INCLUDE (${index.include.map((column) => this.escapeId(column)).join(', ')})` : '';
|
|
91
|
+
return statements;
|
|
107
92
|
}
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
93
|
+
normalizeValue(value) {
|
|
94
|
+
if (value != null && typeof value === 'object' && Array.isArray(value)) {
|
|
95
|
+
return this.features.nativeArrays ? value : toPgArray(value);
|
|
111
96
|
}
|
|
112
|
-
|
|
113
|
-
if (index.m !== undefined)
|
|
114
|
-
params.push(`m = ${index.m}`);
|
|
115
|
-
if (index.efConstruction !== undefined)
|
|
116
|
-
params.push(`ef_construction = ${index.efConstruction}`);
|
|
117
|
-
if (index.lists !== undefined)
|
|
118
|
-
params.push(`lists = ${index.lists}`);
|
|
119
|
-
return params.length > 0 ? ` WITH (${params.join(', ')})` : '';
|
|
97
|
+
return super.normalizeValue(value);
|
|
120
98
|
}
|
|
121
99
|
placeholder(index) {
|
|
122
100
|
return `$${index}`;
|
|
@@ -223,16 +201,6 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
223
201
|
const ph = this.addValue(ctx.values, json);
|
|
224
202
|
return this.features.explicitJsonCast ? `(${ph}::text)::${type}` : `${ph}::${type}`;
|
|
225
203
|
}
|
|
226
|
-
/** Emit a pgvector-style distance expression: `"col" <op> $N::<vectorType>`. */
|
|
227
|
-
appendVectorSort(ctx, meta, key, search) {
|
|
228
|
-
const { colName, distance, field } = this.resolveVectorSortParams(meta, key, search);
|
|
229
|
-
const metric = this.vectorMetrics.get(distance);
|
|
230
|
-
if (!metric) {
|
|
231
|
-
throw new TypeError(`${this.dialectName} does not support vector distance metric: ${distance}`);
|
|
232
|
-
}
|
|
233
|
-
ctx.append(`${this.escapeId(colName)} ${metric.op} `);
|
|
234
|
-
this.appendVectorValue(ctx, search.$vector, field);
|
|
235
|
-
}
|
|
236
204
|
}
|
|
237
205
|
/**
|
|
238
206
|
* Converts a JS array to a Postgres array literal string: `{"val1","val2"}`.
|
|
@@ -2,17 +2,8 @@
|
|
|
2
2
|
* Kept out of `schema/canonicalType.ts`: importing one function from that migration/codegen module
|
|
3
3
|
* pulled all ~18 KB of its type-mapping tables into every consumer's bundle.
|
|
4
4
|
*/
|
|
5
|
-
import type { DialectName } from '../type/querier.js';
|
|
6
5
|
/** Vector cast types supported by pgvector. */
|
|
7
6
|
export type VectorCast = 'vector' | 'halfvec' | 'sparsevec';
|
|
8
|
-
/**
|
|
9
|
-
* The dialects that have more than one vector column type. pgvector is the only one: `halfvec` and
|
|
10
|
-
* `sparsevec` exist nowhere else (`HALFVEC(3)` is a syntax error on CockroachDB 26.2 and MariaDB 12.3,
|
|
11
|
-
* not merely unsupported at runtime), so a field declaring one is stored - and therefore cast - as
|
|
12
|
-
* plain `vector`. Both halves of that used to be stated separately, in this dialect layer and in the
|
|
13
|
-
* migration type maps.
|
|
14
|
-
*/
|
|
15
|
-
export declare const MULTI_VECTOR_TYPE_DIALECTS: ReadonlySet<DialectName>;
|
|
16
7
|
/**
|
|
17
8
|
* Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
|
|
18
9
|
* has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
|
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
* Kept out of `schema/canonicalType.ts`: importing one function from that migration/codegen module
|
|
3
3
|
* pulled all ~18 KB of its type-mapping tables into every consumer's bundle.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* The dialects that have more than one vector column type. pgvector is the only one: `halfvec` and
|
|
7
|
-
* `sparsevec` exist nowhere else (`HALFVEC(3)` is a syntax error on CockroachDB 26.2 and MariaDB 12.3,
|
|
8
|
-
* not merely unsupported at runtime), so a field declaring one is stored - and therefore cast - as
|
|
9
|
-
* plain `vector`. Both halves of that used to be stated separately, in this dialect layer and in the
|
|
10
|
-
* migration type maps.
|
|
11
|
-
*/
|
|
12
|
-
export const MULTI_VECTOR_TYPE_DIALECTS = new Set(['postgres']);
|
|
13
5
|
/**
|
|
14
6
|
* Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
|
|
15
7
|
* has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { EntityMeta, FieldOptions, QueryContext, QueryVectorSearch, VectorDistance } from '../type/index.js';
|
|
1
|
+
import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorSearch, VectorDistance, VectorMetric } from '../type/index.js';
|
|
2
2
|
import { AbstractDialect } from './abstractDialect.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { VectorCast } from './vectorCast.js';
|
|
4
4
|
/**
|
|
5
5
|
* Vector similarity search for SQL dialects: the `ORDER BY <distance>` expression, its projection as
|
|
6
6
|
* a named score, and the index metadata the schema generator reads.
|
|
@@ -9,18 +9,48 @@ import { type VectorCast } from './vectorCast.js';
|
|
|
9
9
|
* dialect above it - unlike the JSON operators, which are woven into the generic comparison
|
|
10
10
|
* machinery (`neExpr`, `numericCast`, `formatIn`, ...) and belong with it.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* {@link appendVectorSort}
|
|
12
|
+
* A dialect declares which metrics it has, and how it spells each, in {@link vectorMetrics}. Both
|
|
13
|
+
* shapes live in that one map - an operator (`"col" <=> $1`, Postgres/CockroachDB) or a function call
|
|
14
|
+
* (`VEC_DISTANCE_COSINE(col, ?)`, MariaDB/SQLite) - so {@link appendVectorSort} is written once and an
|
|
15
|
+
* engine with no vector search at all is simply the empty map.
|
|
15
16
|
*/
|
|
16
17
|
export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
17
18
|
readonly vectorExtension: string | undefined;
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Whether {@link vectorTuningStatements} only applies inside a transaction. `SET LOCAL` does
|
|
21
|
+
* nothing outside one, so the querier - the only layer that knows whether a transaction is open -
|
|
22
|
+
* refuses instead of running a tuning that would silently not apply.
|
|
22
23
|
*/
|
|
23
|
-
|
|
24
|
+
readonly vectorTuningNeedsTransaction: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* `SET`s that widen an ANN index's search for one query, run before it on the same connection.
|
|
27
|
+
*
|
|
28
|
+
* Keyed off `$sort` rather than a `$where` `$near`, because the ANN index is what ranks: pgvector
|
|
29
|
+
* reaches for HNSW on an `ORDER BY distance LIMIT`, while a bare distance predicate scans whatever
|
|
30
|
+
* the planner picks. Tuning a query that never touches the index would set a knob for nothing.
|
|
31
|
+
*
|
|
32
|
+
* Empty by default: SQLite, libSQL and Turso compute every distance, so there is no candidate list
|
|
33
|
+
* to widen, and a field with no ANN index has nothing to tune either.
|
|
34
|
+
*/
|
|
35
|
+
vectorTuningStatements<E>(_meta: EntityMeta<E>, _q: Query<E>): readonly string[];
|
|
36
|
+
/** The `$sort` key carrying a vector search, if the query ranks by one. */
|
|
37
|
+
protected vectorSortKey<E>(q: Query<E>): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The ANN index `$candidates` would tune for this query, and nothing when there is none to tune -
|
|
40
|
+
* no `$candidates`, no vector ranking, or a field with no ANN index on it. One place, so the two
|
|
41
|
+
* dialects that act on it cannot disagree about when tuning applies.
|
|
42
|
+
*
|
|
43
|
+
* Validates here rather than at each emitter because the number is spelled into the statement
|
|
44
|
+
* rather than bound: `SET LOCAL hnsw.ef_search = $1` is not a thing either engine accepts. `/http`
|
|
45
|
+
* casts client JSON straight to `Query`, so `'abc'` and `null` both reach this.
|
|
46
|
+
*/
|
|
47
|
+
protected tunedVectorIndex<E>(meta: EntityMeta<E>, q: Query<E>): EntityIndexMeta | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Every distance metric this dialect has, and how it spells each. Empty means no vector search at
|
|
50
|
+
* all, which is what MySQL and D1 are. The key set is the single answer to "is this metric
|
|
51
|
+
* supported here", so a metric cannot be searchable and unindexable or the reverse.
|
|
52
|
+
*/
|
|
53
|
+
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
24
54
|
/** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
|
|
25
55
|
abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
26
56
|
/**
|
|
@@ -38,23 +68,23 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
38
68
|
*/
|
|
39
69
|
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[], _field?: FieldOptions): void;
|
|
40
70
|
/**
|
|
41
|
-
*
|
|
42
|
-
* rather than
|
|
71
|
+
* Whether this engine has pgvector's narrower vector types (`halfvec`, `sparsevec`) or only the one.
|
|
72
|
+
* Declared by the dialect that has them rather than looked up in a table keyed by dialect name.
|
|
43
73
|
*/
|
|
44
|
-
protected
|
|
74
|
+
protected readonly hasNarrowVectorTypes: boolean;
|
|
45
75
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
76
|
+
* The vector type this dialect actually has for a declared one, so the cast follows the column
|
|
77
|
+
* rather than naming a type the engine does not define.
|
|
48
78
|
*/
|
|
49
|
-
|
|
79
|
+
supportedVectorType(cast: VectorCast): VectorCast;
|
|
50
80
|
/**
|
|
51
81
|
* Append a vector distance projection.
|
|
52
82
|
* Delegates to `appendVectorSort` so each dialect's distance syntax is written once.
|
|
53
83
|
*/
|
|
54
84
|
protected appendVectorProjection<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
|
|
55
85
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
86
|
+
* The distance expression, in whichever of the two shapes this dialect spells it. One method for
|
|
87
|
+
* both, so the metric lookup and its refusal exist once rather than per shape.
|
|
58
88
|
*/
|
|
59
89
|
protected appendVectorSort<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
|
|
60
90
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { unsupportedVectorMetric } from '../type/vector.js';
|
|
2
|
+
import { findVectorIndex, findVectorSort } from '../util/dialect.util.js';
|
|
3
|
+
import { entityName } from '../util/object.util.js';
|
|
1
4
|
import { AbstractDialect } from './abstractDialect.js';
|
|
2
|
-
import { MULTI_VECTOR_TYPE_DIALECTS } from './vectorCast.js';
|
|
3
5
|
/**
|
|
4
6
|
* Vector similarity search for SQL dialects: the `ORDER BY <distance>` expression, its projection as
|
|
5
7
|
* a named score, and the index metadata the schema generator reads.
|
|
@@ -8,18 +10,62 @@ import { MULTI_VECTOR_TYPE_DIALECTS } from './vectorCast.js';
|
|
|
8
10
|
* dialect above it - unlike the JSON operators, which are woven into the generic comparison
|
|
9
11
|
* machinery (`neExpr`, `numericCast`, `formatIn`, ...) and belong with it.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* {@link appendVectorSort}
|
|
13
|
+
* A dialect declares which metrics it has, and how it spells each, in {@link vectorMetrics}. Both
|
|
14
|
+
* shapes live in that one map - an operator (`"col" <=> $1`, Postgres/CockroachDB) or a function call
|
|
15
|
+
* (`VEC_DISTANCE_COSINE(col, ?)`, MariaDB/SQLite) - so {@link appendVectorSort} is written once and an
|
|
16
|
+
* engine with no vector search at all is simply the empty map.
|
|
14
17
|
*/
|
|
15
18
|
export class VectorSqlDialect extends AbstractDialect {
|
|
16
19
|
vectorExtension = undefined;
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* Whether {@link vectorTuningStatements} only applies inside a transaction. `SET LOCAL` does
|
|
22
|
+
* nothing outside one, so the querier - the only layer that knows whether a transaction is open -
|
|
23
|
+
* refuses instead of running a tuning that would silently not apply.
|
|
21
24
|
*/
|
|
22
|
-
|
|
25
|
+
vectorTuningNeedsTransaction = false;
|
|
26
|
+
/**
|
|
27
|
+
* `SET`s that widen an ANN index's search for one query, run before it on the same connection.
|
|
28
|
+
*
|
|
29
|
+
* Keyed off `$sort` rather than a `$where` `$near`, because the ANN index is what ranks: pgvector
|
|
30
|
+
* reaches for HNSW on an `ORDER BY distance LIMIT`, while a bare distance predicate scans whatever
|
|
31
|
+
* the planner picks. Tuning a query that never touches the index would set a knob for nothing.
|
|
32
|
+
*
|
|
33
|
+
* Empty by default: SQLite, libSQL and Turso compute every distance, so there is no candidate list
|
|
34
|
+
* to widen, and a field with no ANN index has nothing to tune either.
|
|
35
|
+
*/
|
|
36
|
+
vectorTuningStatements(_meta, _q) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
/** The `$sort` key carrying a vector search, if the query ranks by one. */
|
|
40
|
+
vectorSortKey(q) {
|
|
41
|
+
return findVectorSort(q.$sort)?.key;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The ANN index `$candidates` would tune for this query, and nothing when there is none to tune -
|
|
45
|
+
* no `$candidates`, no vector ranking, or a field with no ANN index on it. One place, so the two
|
|
46
|
+
* dialects that act on it cannot disagree about when tuning applies.
|
|
47
|
+
*
|
|
48
|
+
* Validates here rather than at each emitter because the number is spelled into the statement
|
|
49
|
+
* rather than bound: `SET LOCAL hnsw.ef_search = $1` is not a thing either engine accepts. `/http`
|
|
50
|
+
* casts client JSON straight to `Query`, so `'abc'` and `null` both reach this.
|
|
51
|
+
*/
|
|
52
|
+
tunedVectorIndex(meta, q) {
|
|
53
|
+
const candidates = q.$candidates;
|
|
54
|
+
if (candidates === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isInteger(candidates) || candidates < 1) {
|
|
58
|
+
throw new TypeError(`$candidates must be a positive integer, got ${JSON.stringify(candidates)}`);
|
|
59
|
+
}
|
|
60
|
+
const key = this.vectorSortKey(q);
|
|
61
|
+
return key ? findVectorIndex(meta, key) : undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every distance metric this dialect has, and how it spells each. Empty means no vector search at
|
|
65
|
+
* all, which is what MySQL and D1 are. The key set is the single answer to "is this metric
|
|
66
|
+
* supported here", so a metric cannot be searchable and unindexable or the reverse.
|
|
67
|
+
*/
|
|
68
|
+
vectorMetrics = new Map();
|
|
23
69
|
/**
|
|
24
70
|
* Resolve common parameters for a vector similarity ORDER BY expression.
|
|
25
71
|
* Shared by all dialect overrides of `appendVectorSort`.
|
|
@@ -38,25 +84,16 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
38
84
|
ctx.addValue(`[${value.join(',')}]`);
|
|
39
85
|
}
|
|
40
86
|
/**
|
|
41
|
-
*
|
|
42
|
-
* rather than
|
|
87
|
+
* Whether this engine has pgvector's narrower vector types (`halfvec`, `sparsevec`) or only the one.
|
|
88
|
+
* Declared by the dialect that has them rather than looked up in a table keyed by dialect name.
|
|
43
89
|
*/
|
|
44
|
-
|
|
45
|
-
return MULTI_VECTOR_TYPE_DIALECTS.has(this.dialectName) ? cast : 'vector';
|
|
46
|
-
}
|
|
90
|
+
hasNarrowVectorTypes = false;
|
|
47
91
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
92
|
+
* The vector type this dialect actually has for a declared one, so the cast follows the column
|
|
93
|
+
* rather than naming a type the engine does not define.
|
|
50
94
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const fn = this.vectorDistanceFns.get(distance);
|
|
54
|
-
if (!fn) {
|
|
55
|
-
throw Error(`${this.dialectName} does not support vector distance metric: ${distance}`);
|
|
56
|
-
}
|
|
57
|
-
ctx.append(`${fn}(${this.escapeId(colName)}, `);
|
|
58
|
-
this.appendVectorValue(ctx, search.$vector, field);
|
|
59
|
-
ctx.append(')');
|
|
95
|
+
supportedVectorType(cast) {
|
|
96
|
+
return this.hasNarrowVectorTypes ? cast : 'vector';
|
|
60
97
|
}
|
|
61
98
|
/**
|
|
62
99
|
* Append a vector distance projection.
|
|
@@ -68,20 +105,31 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
68
105
|
// under that name and the driver keeps whichever it read last. Checked here rather than in the
|
|
69
106
|
// type because TypeScript cannot say "any string except these".
|
|
70
107
|
if (meta.fields[alias]) {
|
|
71
|
-
throw new TypeError(`$project '${alias}' collides with a field of '${
|
|
108
|
+
throw new TypeError(`$project '${alias}' collides with a field of '${entityName(meta)}'`);
|
|
72
109
|
}
|
|
73
110
|
this.appendVectorSort(ctx, meta, key, search);
|
|
74
111
|
ctx.append(` AS ${this.escapeId(alias)}`);
|
|
75
112
|
}
|
|
76
113
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
114
|
+
* The distance expression, in whichever of the two shapes this dialect spells it. One method for
|
|
115
|
+
* both, so the metric lookup and its refusal exist once rather than per shape.
|
|
79
116
|
*/
|
|
80
117
|
appendVectorSort(ctx, meta, key, search) {
|
|
81
|
-
if (this.
|
|
82
|
-
this.
|
|
118
|
+
if (this.vectorMetrics.size === 0) {
|
|
119
|
+
throw new TypeError(`${this.dialectName} does not support vector similarity search. Use raw() for vector queries.`);
|
|
120
|
+
}
|
|
121
|
+
const { colName, distance, field } = this.resolveVectorSortParams(meta, key, search);
|
|
122
|
+
const metric = this.vectorMetrics.get(distance);
|
|
123
|
+
if (!metric) {
|
|
124
|
+
throw unsupportedVectorMetric(this.dialectName, distance);
|
|
125
|
+
}
|
|
126
|
+
if ('fn' in metric) {
|
|
127
|
+
ctx.append(`${metric.fn}(${this.escapeId(colName)}, `);
|
|
128
|
+
this.appendVectorValue(ctx, search.$vector, field);
|
|
129
|
+
ctx.append(')');
|
|
83
130
|
return;
|
|
84
131
|
}
|
|
85
|
-
|
|
132
|
+
ctx.append(`${this.escapeId(colName)} ${metric.op} `);
|
|
133
|
+
this.appendVectorValue(ctx, search.$vector, field);
|
|
86
134
|
}
|
|
87
135
|
}
|
|
@@ -25,4 +25,4 @@ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity
|
|
|
25
25
|
* @example `@Index(['email'], { unique: true })`
|
|
26
26
|
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
27
27
|
*/
|
|
28
|
-
export declare function Index<E>(columns: readonly IndexColumnInput<FieldKey<E
|
|
28
|
+
export declare function Index<E>(columns: readonly IndexColumnInput<FieldKey<E>, E>[], options?: IndexOptions<E>): (entity: Type<E>) => void;
|
|
@@ -7,7 +7,7 @@ export declare function defineHook<E>(entity: Type<E>, methodName: string, event
|
|
|
7
7
|
* Declares a composite index. `unique` and the authored column sugar are normalized here, which is what
|
|
8
8
|
* lets the dialects render one shape instead of re-parsing it.
|
|
9
9
|
*/
|
|
10
|
-
export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<FieldKey<E
|
|
10
|
+
export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<FieldKey<E>, E>): EntityMeta<E>;
|
|
11
11
|
export declare function defineFilter<E>(entity: Type<E>, name: string, opts: FilterOptions<E>): EntityMeta<E>;
|
|
12
12
|
/**
|
|
13
13
|
* What a decorator bag and {@link EntityOptions} have in common at registration time. The keyed mapped
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
1
2
|
import { getKeys, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst } from '../../util/index.js';
|
|
2
3
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
3
4
|
// Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
|
|
@@ -67,8 +68,8 @@ export function defineIndex(entity, index) {
|
|
|
67
68
|
}
|
|
68
69
|
export function defineFilter(entity, name, opts) {
|
|
69
70
|
const meta = ensureMeta(entity);
|
|
70
|
-
if (name ===
|
|
71
|
-
throw TypeError(`'${entity.name}' filter name '
|
|
71
|
+
if (name === SOFT_DELETE_FILTER) {
|
|
72
|
+
throw TypeError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
|
|
72
73
|
}
|
|
73
74
|
if (opts.security && opts.onMissing === 'skip') {
|
|
74
75
|
throw TypeError(`'${entity.name}' security filter '${name}' cannot use onMissing: 'skip' (it must fail closed)`);
|
|
@@ -155,7 +156,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
155
156
|
meta.softDelete = softDeleteKeys[0];
|
|
156
157
|
if (!meta.filters)
|
|
157
158
|
meta.filters = {};
|
|
158
|
-
meta.filters[
|
|
159
|
+
meta.filters[SOFT_DELETE_FILTER] = { condition: { [meta.softDelete]: null }, default: true };
|
|
159
160
|
}
|
|
160
161
|
const id = getIdKey(meta);
|
|
161
162
|
if (!id) {
|
package/dist/http/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// the clause lists themselves, not the barrel: this module is in the browser bundle's graph
|
|
2
|
-
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_OBJECT_CLAUSES, } from '../type/query.js';
|
|
2
|
+
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, QUERY_ROOT_OBJECT_CLAUSES, } from '../type/query.js';
|
|
3
3
|
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
4
4
|
import { getKeys } from '../util/object.util.js';
|
|
5
5
|
/**
|
|
@@ -12,6 +12,7 @@ const ALLOWED_QUERY_KEYS = new Set([
|
|
|
12
12
|
...QUERY_OBJECT_CLAUSES,
|
|
13
13
|
...QUERY_ROOT_OBJECT_CLAUSES,
|
|
14
14
|
...QUERY_NUMBER_CLAUSES,
|
|
15
|
+
...QUERY_ROOT_NUMBER_CLAUSES,
|
|
15
16
|
...QUERY_BOOLEAN_CLAUSES,
|
|
16
17
|
'hardDelete',
|
|
17
18
|
'count',
|
|
@@ -52,7 +53,7 @@ export function parseQueryParams(params = {}) {
|
|
|
52
53
|
// A query string carries every value as text, so what decodes a clause is the shape its group
|
|
53
54
|
// declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:
|
|
54
55
|
// it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.
|
|
55
|
-
for (const key of QUERY_NUMBER_CLAUSES) {
|
|
56
|
+
for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {
|
|
56
57
|
if (query[key] !== undefined) {
|
|
57
58
|
query[key] = Number(query[key]);
|
|
58
59
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SqliteDialect } from '../sqlite/sqliteDialect.js';
|
|
2
|
-
import type { VectorDistance } from '../type/index.js';
|
|
2
|
+
import type { VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* SQLite Dialect specialization for the `@libsql/client` driver.
|
|
5
5
|
*
|
|
@@ -15,5 +15,5 @@ export declare class LibsqlDialect extends SqliteDialect {
|
|
|
15
15
|
* engine (see `TursoDialect`) and no libSQL build has an L1 metric. Both raise the same
|
|
16
16
|
* "does not support vector distance metric" error as any other unsupported metric.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
19
19
|
}
|
|
@@ -14,8 +14,8 @@ export class LibsqlDialect extends SqliteDialect {
|
|
|
14
14
|
* engine (see `TursoDialect`) and no libSQL build has an L1 metric. Both raise the same
|
|
15
15
|
* "does not support vector distance metric" error as any other unsupported metric.
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
['cosine', 'vector_distance_cos'],
|
|
19
|
-
['l2', 'vector_distance_l2'],
|
|
17
|
+
vectorMetrics = new Map([
|
|
18
|
+
['cosine', { fn: 'vector_distance_cos' }],
|
|
19
|
+
['l2', { fn: 'vector_distance_l2' }],
|
|
20
20
|
]);
|
|
21
21
|
}
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
2
|
-
import type { DialectFeatures, FieldOptions,
|
|
2
|
+
import type { DialectFeatures, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
3
|
export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
4
4
|
readonly dialectName = "mariadb";
|
|
5
5
|
readonly insertIdSource = "returning";
|
|
6
|
-
/**
|
|
7
|
-
* MariaDB has no functional indexes: `CREATE INDEX ... ((lower(col)))` is a syntax error even on
|
|
8
|
-
* 12.3, where the documented workaround is a generated column. So it keeps the prefix lengths the
|
|
9
|
-
* family shares and drops expressions.
|
|
10
|
-
*/
|
|
11
|
-
protected readonly indexFeatures: Set<IndexFeature>;
|
|
12
6
|
/** MariaDB has no `FOR ... OF`, so a lock cannot be narrowed to one table of a join. */
|
|
13
7
|
readonly supportsLockOf = false;
|
|
14
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Unlike MySQL: `VECTOR(n)` takes its dimension, every column of a vector index has to be NOT NULL,
|
|
10
|
+
* and `CREATE INDEX` takes `IF NOT EXISTS` - which MySQL's grammar has no place for.
|
|
11
|
+
*/
|
|
15
12
|
protected readonly featureOverrides: Partial<DialectFeatures>;
|
|
16
|
-
/** MariaDB 10.5+ supports `INSERT ... RETURNING`, so the ids are exact per row. */
|
|
17
13
|
protected upsertReturning<E>(entity: Type<E>): string;
|
|
18
14
|
/**
|
|
19
15
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
@@ -30,10 +26,8 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
30
26
|
protected jsonPullElem(alias: string): string;
|
|
31
27
|
/** Text-backed JSON compares as text, so use JSON_EQUALS for key-order-independent equality. */
|
|
32
28
|
protected jsonPullKeep(alias: string, operand: string): string;
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
/** MariaDB 11.7+ vector distance functions. */
|
|
36
|
-
protected readonly vectorDistanceFns: ReadonlyMap<VectorDistance, string>;
|
|
29
|
+
/** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+: the metric's own name, uppercased. */
|
|
30
|
+
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
37
31
|
/**
|
|
38
32
|
* A `VECTOR` column holds a packed little-endian float32 blob, and MariaDB refuses text where one
|
|
39
33
|
* belongs: inserting `'[1,2,3]'` fails with `Incorrect vector value`, and passing it to
|
|
@@ -42,11 +36,12 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
42
36
|
*/
|
|
43
37
|
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
|
|
44
38
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
39
|
+
* `SET STATEMENT mhnsw_ef_search=N FOR SELECT ...` - MariaDB scopes a variable to one statement, so
|
|
40
|
+
* the tuning needs neither a transaction nor a restore afterwards, and cannot leak to the next
|
|
41
|
+
* query on this pooled connection. That is why it prefixes the SQL here instead of coming back
|
|
42
|
+
* from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
|
|
48
43
|
*/
|
|
49
|
-
|
|
44
|
+
find<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
|
|
50
45
|
/** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
|
|
51
46
|
protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
|
|
52
47
|
}
|