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.
Files changed (85) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +5 -5
  3. package/dist/cockroachdb/cockroachDialect.d.ts +5 -24
  4. package/dist/cockroachdb/cockroachDialect.js +3 -29
  5. package/dist/dialect/abstractSqlDialect.d.ts +42 -12
  6. package/dist/dialect/abstractSqlDialect.js +117 -48
  7. package/dist/dialect/aliases.d.ts +6 -0
  8. package/dist/dialect/aliases.js +6 -0
  9. package/dist/dialect/index.d.ts +0 -5
  10. package/dist/dialect/index.js +2 -5
  11. package/dist/dialect/jsonSql.d.ts +17 -2
  12. package/dist/dialect/jsonSql.js +15 -0
  13. package/dist/dialect/mysqlLikeSqlDialect.d.ts +13 -14
  14. package/dist/dialect/mysqlLikeSqlDialect.js +16 -20
  15. package/dist/dialect/pgLikeSqlDialect.d.ts +17 -20
  16. package/dist/dialect/pgLikeSqlDialect.js +30 -62
  17. package/dist/dialect/vectorCast.d.ts +0 -9
  18. package/dist/dialect/vectorCast.js +0 -8
  19. package/dist/dialect/vectorSqlDialect.d.ts +47 -17
  20. package/dist/dialect/vectorSqlDialect.js +78 -30
  21. package/dist/entity/decorator/entity.d.ts +1 -1
  22. package/dist/entity/metadata/definition.d.ts +1 -1
  23. package/dist/entity/metadata/definition.js +4 -3
  24. package/dist/http/query.js +3 -2
  25. package/dist/libsql/libsqlDialect.d.ts +2 -2
  26. package/dist/libsql/libsqlDialect.js +3 -3
  27. package/dist/maria/mariaDialect.d.ts +12 -17
  28. package/dist/maria/mariaDialect.js +21 -36
  29. package/dist/maria/mariaVectorMetrics.d.ts +8 -0
  30. package/dist/maria/mariaVectorMetrics.js +10 -0
  31. package/dist/maria/mariadbQuerier.d.ts +5 -0
  32. package/dist/maria/mariadbQuerier.js +5 -0
  33. package/dist/migrate/builder/migrationBuilder.d.ts +8 -17
  34. package/dist/migrate/builder/migrationBuilder.js +48 -136
  35. package/dist/migrate/builder/types.d.ts +0 -2
  36. package/dist/migrate/ddl/index.d.ts +11 -0
  37. package/dist/migrate/ddl/index.js +34 -0
  38. package/dist/{dialect/indexSqlDialect.d.ts → migrate/ddl/indexDdl.d.ts} +23 -19
  39. package/dist/migrate/ddl/indexDdl.js +126 -0
  40. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +52 -0
  41. package/dist/migrate/ddl/mysqlIndexDdl.js +125 -0
  42. package/dist/migrate/ddl/pgIndexDdl.d.ts +36 -0
  43. package/dist/migrate/ddl/pgIndexDdl.js +87 -0
  44. package/dist/migrate/drift/driftDetector.js +6 -1
  45. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  46. package/dist/migrate/index.d.ts +1 -0
  47. package/dist/migrate/index.js +2 -0
  48. package/dist/migrate/introspection/mysqlIntrospector.d.ts +9 -3
  49. package/dist/migrate/introspection/mysqlIntrospector.js +27 -5
  50. package/dist/migrate/migrator.js +3 -2
  51. package/dist/migrate/schemaGenerator.d.ts +6 -6
  52. package/dist/migrate/schemaGenerator.js +13 -22
  53. package/dist/mongo/mongoDialect.d.ts +1 -2
  54. package/dist/mongo/mongoDialect.js +29 -22
  55. package/dist/mongo/mongodbQuerier.js +1 -1
  56. package/dist/mysql/mysqlDialect.d.ts +3 -6
  57. package/dist/mysql/mysqlDialect.js +4 -11
  58. package/dist/postgres/postgresDialect.d.ts +2 -0
  59. package/dist/postgres/postgresDialect.js +2 -0
  60. package/dist/querier/abstractSqlQuerier.d.ts +11 -0
  61. package/dist/querier/abstractSqlQuerier.js +35 -0
  62. package/dist/schema/canonicalType.js +35 -36
  63. package/dist/schema/indexDifferences.d.ts +2 -1
  64. package/dist/schema/indexDifferences.js +3 -2
  65. package/dist/sqlite/sqliteDialect.d.ts +2 -2
  66. package/dist/sqlite/sqliteDialect.js +5 -5
  67. package/dist/turso/tursoDialect.d.ts +2 -2
  68. package/dist/turso/tursoDialect.js +4 -4
  69. package/dist/type/dialect.d.ts +21 -7
  70. package/dist/type/dialect.js +17 -0
  71. package/dist/type/entity.d.ts +117 -8
  72. package/dist/type/entity.js +7 -0
  73. package/dist/type/query.d.ts +18 -0
  74. package/dist/type/query.js +6 -0
  75. package/dist/type/queryWhere.d.ts +46 -2
  76. package/dist/type/vector.d.ts +45 -7
  77. package/dist/type/vector.js +16 -1
  78. package/dist/util/dialect.util.d.ts +20 -1
  79. package/dist/util/dialect.util.js +49 -4
  80. package/dist/util/object.util.d.ts +7 -1
  81. package/dist/util/object.util.js +8 -0
  82. package/dist/util/relationQuery.util.d.ts +3 -1
  83. package/dist/util/relationQuery.util.js +8 -3
  84. package/package.json +1 -1
  85. 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
- inlineVectorIndex: false,
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
- normalizeValue(value) {
63
- if (value != null && typeof value === 'object' && Array.isArray(value)) {
64
- return this.features.nativeArrays ? value : toPgArray(value);
65
- }
66
- return super.normalizeValue(value);
67
- }
68
- /** pgvector's own index types; CockroachDB's native one widens this. */
69
- isVectorIndex(index) {
70
- return index.type === 'hnsw' || index.type === 'ivfflat';
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
- * A vector index's operator class is named `{type}_{metric}_ops`: an index on a `halfvec` column
84
- * needs `halfvec_cosine_ops`, and `vector_cosine_ops` there is rejected outright. An unsupported
85
- * distance throws rather than being omitted, since a bare `USING hnsw ("embedding")` would build
86
- * with the dialect's default metric instead of the one requested, with nothing signalling it.
87
- * Everything else takes the operator class the entry declares, e.g. `jsonb_path_ops` for GIN.
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
- indexColumnOpsClass(entry, index) {
90
- if (!this.isVectorIndex(index) || !index.distance) {
91
- return entry.opsClass ? ` ${entry.opsClass}` : '';
92
- }
93
- const metric = this.vectorMetrics.get(index.distance);
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 vectorType = this.supportedVectorType(index.vectorType ?? 'vector');
98
- const opsClass = `${vectorType}_${metric.opsSuffix}_ops`;
99
- // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
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 ` ${opsClass}`;
104
- }
105
- indexInclude(index) {
106
- return index.include?.length ? ` INCLUDE (${index.include.map((column) => this.escapeId(column)).join(', ')})` : '';
91
+ return statements;
107
92
  }
108
- indexTuning(index) {
109
- if (!this.isVectorIndex(index)) {
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
- const params = [];
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 { type VectorCast } from './vectorCast.js';
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
- * Two shapes exist across dialects: an operator (`"col" <=> $1`, Postgres/CockroachDB) or a function
13
- * call (`VEC_DISTANCE_COSINE(col, ?)`, MariaDB/SQLite). Dialects pick one by either overriding
14
- * {@link appendVectorSort} or filling {@link vectorDistanceFns}.
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
- * Mapping of UQL vector distance metrics to native SQL functions.
20
- * Override in dialects that use function-call syntax (e.g. SQLite, MariaDB).
21
- * Dialects with operator-based syntax (e.g. Postgres) leave this empty and override `appendVectorSort` directly.
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
- protected readonly vectorDistanceFns: ReadonlyMap<VectorDistance, string>;
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
- * The vector type this dialect actually has for a declared one, so the cast follows the column
42
- * rather than naming a type the engine does not define. See {@link MULTI_VECTOR_TYPE_DIALECTS}.
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 supportedVectorType(cast: VectorCast): VectorCast;
74
+ protected readonly hasNarrowVectorTypes: boolean;
45
75
  /**
46
- * Append a vector similarity function call: `fn(col, ?)`.
47
- * Used by dialects that express vector distance via SQL functions (SQLite, MariaDB).
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
- protected appendFunctionVectorSort<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
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
- * Append a vector similarity expression for `ORDER BY`.
57
- * Default: auto-delegates to `appendFunctionVectorSort` when `vectorDistanceFns` has entries.
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
- * Two shapes exist across dialects: an operator (`"col" <=> $1`, Postgres/CockroachDB) or a function
12
- * call (`VEC_DISTANCE_COSINE(col, ?)`, MariaDB/SQLite). Dialects pick one by either overriding
13
- * {@link appendVectorSort} or filling {@link vectorDistanceFns}.
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
- * Mapping of UQL vector distance metrics to native SQL functions.
19
- * Override in dialects that use function-call syntax (e.g. SQLite, MariaDB).
20
- * Dialects with operator-based syntax (e.g. Postgres) leave this empty and override `appendVectorSort` directly.
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
- vectorDistanceFns = new Map();
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
- * The vector type this dialect actually has for a declared one, so the cast follows the column
42
- * rather than naming a type the engine does not define. See {@link MULTI_VECTOR_TYPE_DIALECTS}.
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
- supportedVectorType(cast) {
45
- return MULTI_VECTOR_TYPE_DIALECTS.has(this.dialectName) ? cast : 'vector';
46
- }
90
+ hasNarrowVectorTypes = false;
47
91
  /**
48
- * Append a vector similarity function call: `fn(col, ?)`.
49
- * Used by dialects that express vector distance via SQL functions (SQLite, MariaDB).
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
- appendFunctionVectorSort(ctx, meta, key, search) {
52
- const { colName, distance, field } = this.resolveVectorSortParams(meta, key, search);
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 '${meta.name ?? String(meta.entity.name)}'`);
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
- * Append a vector similarity expression for `ORDER BY`.
78
- * Default: auto-delegates to `appendFunctionVectorSort` when `vectorDistanceFns` has entries.
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.vectorDistanceFns.size > 0) {
82
- this.appendFunctionVectorSort(ctx, meta, key, search);
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
- throw new TypeError(`${this.dialectName} does not support vector similarity sort. Use raw() for vector queries.`);
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>>[], options?: IndexOptions): (entity: Type<E>) => void;
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>>): EntityMeta<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 === 'softDelete') {
71
- throw TypeError(`'${entity.name}' filter name 'softDelete' is reserved; it is auto-registered from @Field({ softDelete })`);
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['softDelete'] = { condition: { [meta.softDelete]: null }, default: true };
159
+ meta.filters[SOFT_DELETE_FILTER] = { condition: { [meta.softDelete]: null }, default: true };
159
160
  }
160
161
  const id = getIdKey(meta);
161
162
  if (!id) {
@@ -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
- protected readonly vectorDistanceFns: ReadonlyMap<VectorDistance, string>;
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
- vectorDistanceFns = new Map([
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, IndexFeature, IndexSchema, QueryContext, Type, VectorDistance } from '../type/index.js';
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
- /** Unlike MySQL: `VECTOR(n)` takes its dimension, and its vector index is declared inline. */
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
- /** MariaDB's own names for the metrics its vector index accepts. */
34
- private static readonly INLINE_VECTOR_METRICS;
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
- * MariaDB declares a vector index inside `CREATE TABLE`: `VECTOR INDEX (col) M=n DISTANCE=metric`.
46
- * Its metric names are its own (`euclidean`, not `l2`), and an unsupported one throws rather than
47
- * being dropped, which would silently build the index on cosine instead.
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
- getInlineVectorIndexDeclaration(index: IndexSchema): string;
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
  }