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.
Files changed (78) hide show
  1. package/README.md +2 -0
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +3 -3
  4. package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
  5. package/dist/cockroachdb/cockroachDialect.js +8 -0
  6. package/dist/d1/d1SqliteDialect.d.ts +3 -0
  7. package/dist/d1/d1SqliteDialect.js +3 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +14 -2
  9. package/dist/dialect/abstractSqlDialect.js +48 -16
  10. package/dist/dialect/aliases.d.ts +2 -0
  11. package/dist/dialect/aliases.js +2 -0
  12. package/dist/dialect/hydrateColumn.d.ts +3 -2
  13. package/dist/dialect/hydrateColumn.js +10 -1
  14. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
  15. package/dist/dialect/mysqlLikeSqlDialect.js +6 -1
  16. package/dist/dialect/pgLikeSqlDialect.d.ts +21 -5
  17. package/dist/dialect/pgLikeSqlDialect.js +48 -15
  18. package/dist/dialect/queryJoins.d.ts +14 -4
  19. package/dist/dialect/queryJoins.js +31 -11
  20. package/dist/dialect/vectorCast.d.ts +2 -0
  21. package/dist/dialect/vectorCast.js +7 -0
  22. package/dist/dialect/vectorSqlDialect.d.ts +7 -3
  23. package/dist/dialect/vectorSqlDialect.js +15 -10
  24. package/dist/libsql/libsqlDialect.d.ts +1 -1
  25. package/dist/libsql/libsqlDialect.js +3 -3
  26. package/dist/maria/mariaDialect.d.ts +3 -9
  27. package/dist/maria/mariaDialect.js +5 -15
  28. package/dist/maria/mariadbQuerier.js +9 -3
  29. package/dist/migrate/ddl/index.d.ts +1 -0
  30. package/dist/migrate/ddl/index.js +4 -2
  31. package/dist/migrate/ddl/indexDdl.d.ts +2 -0
  32. package/dist/migrate/ddl/indexDdl.js +10 -2
  33. package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
  34. package/dist/migrate/ddl/pgIndexDdl.js +11 -11
  35. package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
  36. package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
  37. package/dist/migrate/generator/mongoCommand.d.ts +28 -0
  38. package/dist/migrate/generator/mongoCommand.js +8 -0
  39. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  40. package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
  41. package/dist/migrate/introspection/mongoIntrospector.js +33 -7
  42. package/dist/migrate/introspection/postgresIntrospector.js +19 -0
  43. package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
  44. package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
  45. package/dist/mongo/mongoDialect.d.ts +5 -3
  46. package/dist/mongo/mongoDialect.js +40 -17
  47. package/dist/mongo/mongodbQuerier.js +4 -5
  48. package/dist/mssql/mssqlDialect.js +1 -0
  49. package/dist/schema/canonicalType.js +10 -4
  50. package/dist/schema/indexDifferences.d.ts +5 -2
  51. package/dist/schema/indexDifferences.js +5 -0
  52. package/dist/schema/schemaASTBuilder.js +3 -2
  53. package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
  54. package/dist/sqlite/localSqliteQuerierPool.js +8 -11
  55. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
  56. package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
  57. package/dist/sqlite/sqliteDialect.d.ts +11 -1
  58. package/dist/sqlite/sqliteDialect.js +46 -3
  59. package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
  60. package/dist/sqlite/sqliteQuerierPool.js +10 -7
  61. package/dist/turso/tursoLocalDialect.d.ts +1 -1
  62. package/dist/turso/tursoLocalDialect.js +1 -1
  63. package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
  64. package/dist/turso/tursoLocalQuerierPool.js +3 -10
  65. package/dist/type/dialect.d.ts +5 -0
  66. package/dist/type/entity.d.ts +27 -13
  67. package/dist/type/migration.d.ts +7 -9
  68. package/dist/type/query.d.ts +11 -4
  69. package/dist/type/queryAggregate.d.ts +5 -16
  70. package/dist/type/utility.d.ts +13 -0
  71. package/dist/type/vector.d.ts +17 -0
  72. package/dist/type/vector.js +6 -0
  73. package/dist/util/ddlExpression.util.d.ts +2 -0
  74. package/dist/util/ddlExpression.util.js +5 -0
  75. package/dist/util/dialect.util.d.ts +21 -2
  76. package/dist/util/dialect.util.js +42 -2
  77. package/package.json +4 -3
  78. package/skills/uql-orm/SKILL.md +146 -0
@@ -1,11 +1,13 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
- import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, } from '../dialect/aliases.js';
3
+ import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
4
  import { groupPathField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
5
5
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
6
6
  import { COUNT_RESULT_KEY } from '../type/query.js';
7
7
  import { QueryRaw } from '../type/queryRaw.js';
8
- import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
8
+ import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, rankedTextSearch, someKey, targetKeyColumns, } from '../util/index.js';
9
+ /** A scalar field's operator as the aggregation operator computing it. */
10
+ const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
9
11
  /** Default {@link DialectFeatures} for MongoDB. */
10
12
  export const mongoDialectFeatures = {
11
13
  ifNotExists: false,
@@ -18,6 +20,7 @@ export const mongoDialectFeatures = {
18
20
  commentSyntax: 'none',
19
21
  vectorIndexRequiresNotNull: false,
20
22
  vectorSupportsLength: false,
23
+ vectorBytes: false,
21
24
  supportsTimestamptz: false,
22
25
  stringSizing: 'bounded-text',
23
26
  supportsUnsigned: false,
@@ -453,9 +456,13 @@ export class MongoDialect extends AbstractDialect {
453
456
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
454
457
  * added for the sort alone would change what the caller gets back.
455
458
  */
456
- sort(entity, sort, populate) {
459
+ sort(entity, { $sort: sort, $populate: populate, $where: where }) {
457
460
  const meta = getMeta(entity);
458
461
  const normalized = {};
462
+ // Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
463
+ if (sort?.$text) {
464
+ rankedTextSearch(where);
465
+ }
459
466
  // The same join set the lookups are built from, so what an ordering may address and what the
460
467
  // pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
461
468
  // exactly as it does on the SQL dialects.
@@ -466,6 +473,13 @@ export class MongoDialect extends AbstractDialect {
466
473
  collectSort(meta, sort, joins, path, out) {
467
474
  for (const [key, value] of Object.entries(sort ?? {})) {
468
475
  const relation = meta.relations[key];
476
+ if (key === '$text') {
477
+ if (path) {
478
+ throw new TypeError(`$sort by $text is only supported on the queried entity, not on relation '${path.slice(0, -1)}'`);
479
+ }
480
+ out[TEXT_SCORE_ALIAS] = { $meta: 'textScore' };
481
+ continue;
482
+ }
469
483
  if (!relation) {
470
484
  // The queried entity's own vector search is lifted out before this walk, so one reaching it
471
485
  // sits under a relation, which a `$lookup` brings in one row at a time - there is nothing to
@@ -552,7 +566,7 @@ export class MongoDialect extends AbstractDialect {
552
566
  const relOpts = relationOf(meta, spec.relation);
553
567
  const query = spec.query ?? {};
554
568
  const tail = [
555
- ...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query.$sort) }] : []),
569
+ ...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query) }] : []),
556
570
  ...this.pagerStages(query),
557
571
  spec.field
558
572
  ? {
@@ -668,7 +682,7 @@ export class MongoDialect extends AbstractDialect {
668
682
  return [
669
683
  ...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
670
684
  ...this.readStages(entity, q, {
671
- sort: this.sort(entity, q.$sort, q.$populate),
685
+ sort: this.sort(entity, q),
672
686
  pager: this.pagerStages(q),
673
687
  }),
674
688
  ];
@@ -911,8 +925,14 @@ export class MongoDialect extends AbstractDialect {
911
925
  const set = {};
912
926
  const push = {};
913
927
  const pull = {};
928
+ const arithmetic = {};
914
929
  const unset = new Set();
915
930
  for (const [key, value] of Object.entries(persistable)) {
931
+ if (isFieldUpdateOp(value)) {
932
+ const [op, operand] = fieldUpdateOf(value);
933
+ arithmetic[key] = { [MONGO_ARITHMETIC[op]]: [{ $ifNull: [`$${key}`, 0] }, { $literal: operand }] };
934
+ continue;
935
+ }
916
936
  if (!isJsonUpdateOp(value)) {
917
937
  set[key] = value;
918
938
  continue;
@@ -930,7 +950,7 @@ export class MongoDialect extends AbstractDialect {
930
950
  pull[`${key}.${path}`] = v;
931
951
  }
932
952
  }
933
- return { set, push, pull, unset };
953
+ return { set, push, pull, arithmetic, unset };
934
954
  }
935
955
  /**
936
956
  * Turn a persistable payload into a MongoDB update, mapping UQL's JSON operators onto their native
@@ -939,12 +959,12 @@ export class MongoDialect extends AbstractDialect {
939
959
  */
940
960
  getUpdateFilter(persistable) {
941
961
  const groups = this.groupUpdateOperators(persistable);
942
- const { set, push, pull, unset } = groups;
962
+ const { set, push, pull, arithmetic, unset } = groups;
943
963
  const exprKeys = [...Object.keys(pull), ...Object.keys(set), ...Object.keys(push)];
944
- // Native `$pull` fails on a value that is no array, and MongoDB rejects two operators targeting one
945
- // path in a single update document: either forces the pipeline form.
964
+ // Native `$pull` fails on a value that is no array, native `$inc`/`$mul` on a `null`, and MongoDB
965
+ // rejects two operators targeting one path in a single update document: each forces the pipeline form.
946
966
  const allPaths = [...exprKeys, ...unset];
947
- if (hasKeys(pull) || new Set(allPaths).size < allPaths.length) {
967
+ if (hasKeys(pull) || hasKeys(arithmetic) || new Set(allPaths).size < allPaths.length) {
948
968
  return this.getUpdatePipeline(groups, new Set(exprKeys));
949
969
  }
950
970
  return {
@@ -954,11 +974,11 @@ export class MongoDialect extends AbstractDialect {
954
974
  };
955
975
  }
956
976
  /**
957
- * A JSON update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
977
+ * An update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
958
978
  * `$pull`, `$set`, `$push`, then `$unset`, as SQL does, with values as `$literal` so `$x` stays data.
959
979
  */
960
- getUpdatePipeline({ set, push, pull, unset }, exprPaths) {
961
- const assignments = {};
980
+ getUpdatePipeline({ set, push, pull, arithmetic, unset }, exprPaths) {
981
+ const assignments = { ...arithmetic };
962
982
  for (const path of exprPaths) {
963
983
  let expr = { $ifNull: [`$${path}`, []] };
964
984
  if (path in pull) {
@@ -1016,10 +1036,10 @@ export class MongoDialect extends AbstractDialect {
1016
1036
  */
1017
1037
  buildAggregateStages(entity, q, opts) {
1018
1038
  const meta = getMeta(entity);
1019
- const joins = resolveGroupJoins(meta, q.$group);
1039
+ const { joins, where } = resolveGroupJoins(meta, q);
1020
1040
  const { groupId, accumulators, columns, named } = this.buildGroupSpec(meta, parseGroupMap(q.$group, q.$select), joins);
1021
1041
  const pipeline = [
1022
- ...this.matchStages(entity, q.$where, opts, named),
1042
+ ...this.matchStages(entity, where, opts, named),
1023
1043
  ...this.lookupStages(meta, joins),
1024
1044
  { $group: { _id: hasKeys(groupId) ? groupId : null, ...accumulators } },
1025
1045
  // `$group` answers with `_id` even when grouping by nothing, and with what `columns` read to the end.
@@ -1220,6 +1240,10 @@ export class MongoDialect extends AbstractDialect {
1220
1240
  }
1221
1241
  return { vectorKey: found.key, vectorSearch: found.search, regularSort: regularSort };
1222
1242
  }
1243
+ /** The Atlas index a `$vectorSearch` over `column` reads, and migrations create: its declared name, else `<column>_index`. */
1244
+ vectorSearchIndexName(name, column) {
1245
+ return name ?? `${column}_index`;
1246
+ }
1223
1247
  /**
1224
1248
  * Build a `$vectorSearch` aggregation pipeline stage.
1225
1249
  * Merges `$where` into `$vectorSearch.filter` for optimal pre-filtering.
@@ -1231,8 +1255,7 @@ export class MongoDialect extends AbstractDialect {
1231
1255
  throw new TypeError(`Field '${key}' not found in entity '${meta.name}'`);
1232
1256
  }
1233
1257
  const colName = this.resolveColumnName(key, field);
1234
- // Resolve index name from @Index metadata, or fall back to convention
1235
- const indexName = findVectorIndex(meta, key)?.name ?? `${colName}_index`;
1258
+ const indexName = this.vectorSearchIndexName(findVectorIndex(meta, key)?.name, colName);
1236
1259
  if (!limit) {
1237
1260
  throw new TypeError(`$vectorSearch requires $limit (vector sort on '${key}' of '${meta.name}')`);
1238
1261
  }
@@ -2,7 +2,7 @@ import { AGGREGATE_VALUE_ALIAS } from '../dialect/aliases.js';
2
2
  import { hasRequiredJoin } from '../dialect/queryJoins.js';
3
3
  import { fieldOf, getMeta, namesKey, soleIdOf } from '../entity/index.js';
4
4
  import { AbstractQuerier, enrichError } from '../querier/index.js';
5
- import { clone, getKeys, getSoftDeleteValue, hasKeys, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
5
+ import { clone, getKeys, getSoftDeleteValue, hasKeys, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, } from '../util/index.js';
6
6
  /**
7
7
  * `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
8
8
  * as *unlimited*, so a read that passed it straight to the driver came back with the whole
@@ -93,7 +93,7 @@ export class MongodbQuerier extends AbstractQuerier {
93
93
  if (hasKeys(select)) {
94
94
  cursor.project(select);
95
95
  }
96
- const sort = this.dialect.sort(entity, q.$sort, q.$populate);
96
+ const sort = this.dialect.sort(entity, q);
97
97
  if (hasKeys(sort)) {
98
98
  cursor.sort(sort);
99
99
  }
@@ -113,13 +113,13 @@ export class MongodbQuerier extends AbstractQuerier {
113
113
  buildVectorPipeline(entity, q, vectorSort, opts) {
114
114
  const scoreAlias = vectorSort.vectorSearch.$project;
115
115
  return [
116
- this.dialect.buildVectorSearchStage(entity, vectorSort.vectorKey, vectorSort.vectorSearch, q.$where, q.$limit ?? 10, opts, q.$candidates),
116
+ this.dialect.buildVectorSearchStage(entity, vectorSort.vectorKey, vectorSort.vectorSearch, q.$where, q.$limit ?? 10, opts, vectorCandidates(q)),
117
117
  // The score becomes a real field before anything reads it, so the lookups and the projection
118
118
  // that follow treat it like any other - and a query with no projection keeps its own columns.
119
119
  ...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
120
120
  // `$vectorSearch` has already applied `$limit`, so the pager is its own.
121
121
  ...this.dialect.readStages(entity, q, {
122
- sort: this.dialect.sort(entity, vectorSort.regularSort, q.$populate),
122
+ sort: this.dialect.sort(entity, { ...q, $sort: vectorSort.regularSort }),
123
123
  project: scoreAlias ? { [scoreAlias]: 1 } : undefined,
124
124
  }),
125
125
  ];
@@ -189,7 +189,6 @@ export class MongodbQuerier extends AbstractQuerier {
189
189
  return this.timed('internalUpdateMany', undefined, async () => {
190
190
  const persistable = this.dialect.getPersistable(getMeta(entity), payload, 'onUpdate');
191
191
  const filter = this.dialect.where(entity, qm.$where, opts);
192
- // Maps JSON operators ($set/$unset/$push/$pull) onto their native MongoDB equivalents.
193
192
  const update = this.dialect.getUpdateFilter(persistable);
194
193
  const { matchedCount } = await this.execute((session) => this.collection(entity).updateMany(filter, update, { session }));
195
194
  return matchedCount;
@@ -25,6 +25,7 @@ const MSSQL_FEATURES = {
25
25
  commentSyntax: 'none',
26
26
  vectorIndexRequiresNotNull: false,
27
27
  vectorSupportsLength: true,
28
+ vectorBytes: false,
28
29
  supportsTimestamptz: false,
29
30
  stringSizing: 'varchar',
30
31
  supportsUnsigned: false,
@@ -80,6 +80,7 @@ const SQL_TO_CANONICAL = {
80
80
  image: { category: 'blob', size: 'big' },
81
81
  // === Vector (for AI/embeddings) ===
82
82
  vector: { category: 'vector' },
83
+ f32_blob: { category: 'vector' },
83
84
  halfvec: { category: 'halfvec' },
84
85
  sparsevec: { category: 'sparsevec' },
85
86
  };
@@ -204,8 +205,8 @@ const ENGINE_TYPES = {
204
205
  sizes: MYSQL_SIZES,
205
206
  decimal: { precision: 10, scale: 2 },
206
207
  },
207
- // SQLite uses affinity, so no size variants.
208
- sqlite: { scalars: withVectorType(SQLITE_SCALAR_MAP, 'TEXT') },
208
+ // SQLite uses affinity, so no size variants. `F32_BLOB` is libSQL's vector type; elsewhere just a name of BLOB affinity.
209
+ sqlite: { scalars: withVectorType(SQLITE_SCALAR_MAP, 'F32_BLOB') },
209
210
  // 2025 and up; below that the server refuses the type rather than storing it as text.
210
211
  mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'VECTOR'), sizes: MSSQL_SIZES },
211
212
  mongodb: { scalars: withVectorType(MONGO_SCALAR_MAP, 'array') },
@@ -238,7 +239,7 @@ export function sqlToCanonical(sqlType) {
238
239
  const unsigned = normalized.includes('unsigned');
239
240
  const withoutUnsigned = normalized.replace(/\s*unsigned\s*/i, ' ').trim();
240
241
  // Extract base type and parameters: "VARCHAR(255)" -> ["varchar", "255"]
241
- const match = withoutUnsigned.match(/^([a-z][a-z0-9 ]*?)(?:\(([^)]+)\))?$/);
242
+ const match = withoutUnsigned.match(/^([a-z][a-z0-9_ ]*?)(?:\(([^)]+)\))?$/);
242
243
  const base = match ? SQL_TO_CANONICAL[match[1]] : undefined;
243
244
  if (!match || !base) {
244
245
  return { category: 'string', raw: sqlType };
@@ -332,8 +333,13 @@ export function canonicalToTypeScript(type) {
332
333
  * the engine settles an unstated bound. Migrations and drift both compare through it.
333
334
  */
334
335
  export function engineType(dialect) {
335
- return (type) => sqlToCanonical(canonicalToSql(type, dialect));
336
+ return (type) => {
337
+ const stored = sqlToCanonical(canonicalToSql(type, dialect));
338
+ // SQLite keeps a vector in any column, so one created as `TEXT` before vectors were blobs stays as it is.
339
+ return dialect.dialectName === 'sqlite' && isVectorCategory(stored.category) ? SQLITE_TEXT : stored;
340
+ };
336
341
  }
342
+ const SQLITE_TEXT = { category: 'string', size: 'small' };
337
343
  /**
338
344
  * Convert UQL FieldOptions to a canonical type.
339
345
  */
@@ -1,6 +1,9 @@
1
1
  import type { IndexNode } from './types.js';
2
- /** What an introspector reports about an index, and so all a diff may compare; apart from `IndexFeature`, what an engine emits. */
3
- export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include';
2
+ /**
3
+ * What an introspector reports about an index, and so all a diff may compare; apart from `IndexFeature`, what an engine emits.
4
+ * `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it.
5
+ */
6
+ export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector';
4
7
  /**
5
8
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
6
9
  * over an expression, whose text the engine reprints, falls back to its name.
@@ -1,3 +1,4 @@
1
+ import { isVectorIndexType } from '../type/vector.js';
1
2
  /**
2
3
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
3
4
  * over an expression, whose text the engine reprints, falls back to its name.
@@ -45,6 +46,10 @@ export function describeIndexDifferences(source, target, facets) {
45
46
  if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
46
47
  differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
47
48
  }
49
+ if (facets.has('vector') && isVectorIndexType(source.type) !== isVectorIndexType(target.type)) {
50
+ const [expected, actual] = [source, target].map((index) => (isVectorIndexType(index.type) ? 'yes' : 'no'));
51
+ differences.push(`vector index: ${actual} -> ${expected}`);
52
+ }
48
53
  if (facets.has('include')) {
49
54
  // Order carries no meaning in an `INCLUDE` list, so it is compared as a set.
50
55
  const [sourceInclude, targetInclude] = [source.include ?? [], target.include ?? []].map((columns) => [...columns].sort().join(', '));
@@ -1,5 +1,5 @@
1
1
  import { fieldOf, foreignKeysOf, getMeta, soleIdOf } from '../entity/metadata/definition.js';
2
- import { declaredIndexes, indexNameParts, renderIndexColumn } from '../util/ddlExpression.util.js';
2
+ import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../util/ddlExpression.util.js';
3
3
  import { isInlinedExpression } from '../util/field.util.js';
4
4
  import { isSoleIdField } from '../util/field.util.js';
5
5
  import { isAutoIncrement } from '../util/field.util.js';
@@ -198,7 +198,7 @@ function addCompositeIndex(ctx, table, meta, idxMeta) {
198
198
  });
199
199
  if (!resolved.length)
200
200
  return;
201
- const name = idxMeta.name ?? derivedIndexName(table.name, indexNameParts(resolved));
201
+ const name = declaredIndexName(idxMeta.name, table.name, resolved);
202
202
  ctx.ast.addIndex({
203
203
  name,
204
204
  table,
@@ -211,5 +211,6 @@ function addCompositeIndex(ctx, table, meta, idxMeta) {
211
211
  m: idxMeta.m,
212
212
  efConstruction: idxMeta.efConstruction,
213
213
  lists: idxMeta.lists,
214
+ config: idxMeta.config,
214
215
  });
215
216
  }
@@ -1,7 +1,6 @@
1
1
  import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
2
- import type { ExtraOptions } from '../type/index.js';
3
- import { SqliteDialect } from './sqliteDialect.js';
4
- import { type SqlitePreparedStatement, SqliteQuerier } from './sqliteQuerier.js';
2
+ import type { SqliteDialect } from './sqliteDialect.js';
3
+ import { type SqliteDatabase, type SqlitePreparedStatement, SqliteQuerier } from './sqliteQuerier.js';
5
4
  /** What every local SQLite pool accepts on top of its driver's own options. */
6
5
  export type LocalSqlitePoolOptions = {
7
6
  /**
@@ -27,12 +26,12 @@ export type LocalSqliteDatabase = {
27
26
  export declare function adaptSqlite<S extends Omit<SqlitePreparedStatement, 'reader'>>(db: Omit<LocalSqliteDatabase, 'prepare'> & {
28
27
  prepare(sql: string): S;
29
28
  }, reads: (stmt: S) => boolean): LocalSqliteDatabase;
30
- /** A pool for a SQLite file opened in this process, configured the same way whichever driver's {@link createDb} opens it. */
31
- export declare abstract class AbstractLocalSqliteQuerierPool<O extends LocalSqlitePoolOptions> extends AbstractSharedHandleQuerierPool<LocalSqliteDatabase, SqliteQuerier, SqliteDialect> {
32
- readonly opts?: O | undefined;
33
- constructor(opts?: O | undefined, extra?: ExtraOptions);
34
- /** Opens the driver's database, and nothing more: the caller configures it. */
35
- protected abstract createDb(): Promise<LocalSqliteDatabase>;
36
- protected openDb(): Promise<LocalSqliteDatabase>;
37
- protected buildQuerier(db: LocalSqliteDatabase): SqliteQuerier;
29
+ /** `db` with each loadable extension installed, which `node:sqlite` refuses unless opened to allow them. */
30
+ export declare function loadExtensions(db: LocalSqliteDatabase, extensions?: readonly string[]): LocalSqliteDatabase;
31
+ /** A pool for a database file opened in this process, configured the same way whichever driver's {@link createDb} opens it. */
32
+ export declare abstract class AbstractLocalSqliteQuerierPool<DB extends SqliteDatabase, D extends SqliteDialect> extends AbstractSharedHandleQuerierPool<DB, SqliteQuerier, D> {
33
+ /** Opens the driver's database, reading integers as `bigint`, which the querier decodes exactly past 2^53. */
34
+ protected abstract createDb(): Promise<DB>;
35
+ protected openDb(): Promise<DB>;
36
+ protected buildQuerier(db: DB): SqliteQuerier;
38
37
  }
@@ -1,6 +1,4 @@
1
- import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
1
  import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
- import { SqliteDialect } from './sqliteDialect.js';
4
2
  import { applySqlitePragmas } from './sqlitePragmas.js';
5
3
  import { SqliteQuerier } from './sqliteQuerier.js';
6
4
  /**
@@ -22,19 +20,18 @@ export function adaptSqlite(db, reads) {
22
20
  close: () => db.close(),
23
21
  };
24
22
  }
25
- /** A pool for a SQLite file opened in this process, configured the same way whichever driver's {@link createDb} opens it. */
26
- export class AbstractLocalSqliteQuerierPool extends AbstractSharedHandleQuerierPool {
27
- opts;
28
- constructor(opts, extra) {
29
- super(new SqliteDialect(dialectOptionsFrom(extra)), extra);
30
- this.opts = opts;
23
+ /** `db` with each loadable extension installed, which `node:sqlite` refuses unless opened to allow them. */
24
+ export function loadExtensions(db, extensions = []) {
25
+ for (const extension of extensions) {
26
+ db.loadExtension(extension);
31
27
  }
28
+ return db;
29
+ }
30
+ /** A pool for a database file opened in this process, configured the same way whichever driver's {@link createDb} opens it. */
31
+ export class AbstractLocalSqliteQuerierPool extends AbstractSharedHandleQuerierPool {
32
32
  async openDb() {
33
33
  const db = await this.createDb();
34
34
  await applySqlitePragmas(db);
35
- for (const extension of this.opts?.extensions ?? []) {
36
- db.loadExtension(extension);
37
- }
38
35
  return db;
39
36
  }
40
37
  buildQuerier(db) {
@@ -1,5 +1,6 @@
1
1
  import type { ExtraOptions } from '../type/index.js';
2
2
  import { AbstractLocalSqliteQuerierPool, type LocalSqliteDatabase, type LocalSqlitePoolOptions } from './localSqliteQuerierPool.js';
3
+ import { SqliteDialect } from './sqliteDialect.js';
3
4
  /**
4
5
  * The `DatabaseSync` options worth surfacing, plus the loadable extensions to install. Declared here
5
6
  * rather than imported from `node:sqlite` so this module needs no ambient Node types; unknown keys
@@ -15,8 +16,9 @@ export type NodeSqlitePoolOptions = LocalSqlitePoolOptions & {
15
16
  * A pool over Node's built-in `node:sqlite`, needing no dependency at all. {@link Sqlite3QuerierPool} is the
16
17
  * faster choice for read-heavy work, and the one on Bun.
17
18
  */
18
- export declare class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool<NodeSqlitePoolOptions> {
19
+ export declare class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool<LocalSqliteDatabase, SqliteDialect> {
19
20
  readonly filename: string;
20
- constructor(filename?: string, opts?: NodeSqlitePoolOptions, extra?: ExtraOptions);
21
+ readonly opts?: NodeSqlitePoolOptions | undefined;
22
+ constructor(filename?: string, opts?: NodeSqlitePoolOptions | undefined, extra?: ExtraOptions);
21
23
  protected createDb(): Promise<LocalSqliteDatabase>;
22
24
  }
@@ -1,24 +1,27 @@
1
- import { AbstractLocalSqliteQuerierPool, adaptSqlite, } from './localSqliteQuerierPool.js';
1
+ import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
+ import { AbstractLocalSqliteQuerierPool, adaptSqlite, loadExtensions, } from './localSqliteQuerierPool.js';
3
+ import { SqliteDialect } from './sqliteDialect.js';
2
4
  /**
3
5
  * A pool over Node's built-in `node:sqlite`, needing no dependency at all. {@link Sqlite3QuerierPool} is the
4
6
  * faster choice for read-heavy work, and the one on Bun.
5
7
  */
6
8
  export class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool {
7
9
  filename;
10
+ opts;
8
11
  constructor(filename = ':memory:', opts, extra) {
9
- super(opts, extra);
12
+ super(new SqliteDialect(dialectOptionsFrom(extra)), extra);
10
13
  this.filename = filename;
14
+ this.opts = opts;
11
15
  }
12
16
  async createDb() {
13
17
  const { DatabaseSync } = await import('node:sqlite');
14
18
  const { extensions, ...driverOpts } = this.opts ?? {};
15
19
  const nodeDb = new DatabaseSync(this.filename, {
16
20
  ...driverOpts,
17
- // Integers as `bigint`, which the querier decodes exactly past 2^53.
18
21
  readBigInts: true,
19
22
  // `node:sqlite` refuses `loadExtension` unless the database was opened with this on.
20
23
  ...(extensions?.length ? { allowExtension: true } : undefined),
21
24
  });
22
- return adaptSqlite(nodeDb, (stmt) => stmt.columns().length > 0);
25
+ return loadExtensions(adaptSqlite(nodeDb, (stmt) => stmt.columns().length > 0), extensions);
23
26
  }
24
27
  }
@@ -1,6 +1,6 @@
1
1
  import { AbstractSqlDialect, type DerivedRelation, type HydrateKind, type RelationRows } from '../dialect/abstractSqlDialect.js';
2
2
  import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
3
- import type { EntityMeta, FieldOptions, QueryContext, QueryPager, QueryTextSearchOptions, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
+ import { type EntityMeta, type FieldOptions, type Query, type QueryContext, type QueryPager, type QueryTextSearchOptions, type QueryWhere, type SqlDialectFeatures, type Type, type VectorDistance, type VectorMetric } from '../type/index.js';
4
4
  /** What SQLite and the engines derived from it have. */
5
5
  export declare const SQLITE_FEATURES: SqlDialectFeatures;
6
6
  export declare class SqliteDialect extends AbstractSqlDialect {
@@ -24,6 +24,12 @@ export declare class SqliteDialect extends AbstractSqlDialect {
24
24
  * their own vector functions instead, so `LibsqlDialect` overrides this.
25
25
  */
26
26
  readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
27
+ /**
28
+ * A read ranked by the metric its field's vector index measures, and paged, narrowed to the rowids of
29
+ * that index's nearest rows, which libSQL's `vector_top_k` answers: `$candidates` of them, else as many
30
+ * as the page reaches. Their exact distance still orders them. Unchanged on an engine with no such index.
31
+ */
32
+ protected rankedWhere<E>(meta: EntityMeta<E>, q: Query<E>, prefix: string | undefined): QueryWhere<E> | undefined;
27
33
  /**
28
34
  * SQLite does not support the `DEFAULT` keyword inside `VALUES`. Inline the metadata default
29
35
  * when declared, else `NULL` (which is also how SQLite auto-generates INTEGER PRIMARY KEYs).
@@ -51,7 +57,9 @@ export declare class SqliteDialect extends AbstractSqlDialect {
51
57
  protected readonly carriedFields: {
52
58
  numeric: (expr: string, field: FieldOptions) => string;
53
59
  blob: (expr: string) => string;
60
+ vector: (expr: string) => string;
54
61
  };
62
+ private bytesAsText;
55
63
  /** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
56
64
  protected hydrateKind(field: FieldOptions | undefined): HydrateKind | undefined;
57
65
  /**
@@ -59,6 +67,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
59
67
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
60
68
  */
61
69
  protected appendTextSearch<E>(ctx: QueryContext, entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
70
+ /** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
71
+ protected appendTextRank<E>(ctx: QueryContext, meta: EntityMeta<E>): void;
62
72
  protected jsonLength(slot: JsonSlot): string;
63
73
  /** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
64
74
  protected jsonElemFrom(slot: JsonSlot, alias: string): string;
@@ -1,7 +1,10 @@
1
1
  import { AbstractSqlDialect, } from '../dialect/abstractSqlDialect.js';
2
2
  import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
3
3
  import { chainedCall, groupsPerCall, jsonSetCall, jsonPath, jsonArraySlotArgs, jsonSlotArgs, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
4
- import { textSearchFields } from '../util/dialect.util.js';
4
+ import { QueryRaw, } from '../type/index.js';
5
+ import { indexDistance, isVectorIndexType } from '../type/vector.js';
6
+ import { declaredIndexName } from '../util/ddlExpression.util.js';
7
+ import { findVectorIndex, findVectorSort, textSearchFields, vectorCandidates } from '../util/dialect.util.js';
5
8
  import { columnFamily, isIntegerColumn } from '../util/field.util.js';
6
9
  /** What SQLite and the engines derived from it have. */
7
10
  export const SQLITE_FEATURES = {
@@ -14,7 +17,8 @@ export const SQLITE_FEATURES = {
14
17
  generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
15
18
  commentSyntax: 'none',
16
19
  vectorIndexRequiresNotNull: false,
17
- vectorSupportsLength: false,
20
+ vectorSupportsLength: true,
21
+ vectorBytes: true,
18
22
  supportsTimestamptz: false,
19
23
  stringSizing: 'text',
20
24
  supportsUnsigned: false,
@@ -55,6 +59,37 @@ export class SqliteDialect extends AbstractSqlDialect {
55
59
  ['l2', { fn: 'vec_distance_L2' }],
56
60
  ['l1', { fn: 'vec_distance_L1' }],
57
61
  ]);
62
+ /**
63
+ * A read ranked by the metric its field's vector index measures, and paged, narrowed to the rowids of
64
+ * that index's nearest rows, which libSQL's `vector_top_k` answers: `$candidates` of them, else as many
65
+ * as the page reaches. Their exact distance still orders them. Unchanged on an engine with no such index.
66
+ */
67
+ rankedWhere(meta, q, prefix) {
68
+ const ranked = findVectorSort(q.$sort);
69
+ const k = vectorCandidates(q) ?? (q.$limit === undefined ? undefined : (q.$skip ?? 0) + q.$limit);
70
+ const index = ranked && findVectorIndex(meta, ranked.key);
71
+ if (!ranked || !index || !isVectorIndexType(index.type) || k === undefined) {
72
+ return q.$where;
73
+ }
74
+ const { colName, distance, field } = this.resolveVectorDistance(meta, ranked.key, ranked.search);
75
+ if (indexDistance(index) !== distance || !this.vectorMetrics.get(distance)?.index) {
76
+ return q.$where;
77
+ }
78
+ const name = declaredIndexName(index.name, this.resolveTableName(meta), [{ column: colName }]);
79
+ const table = this.escapeId(prefix ?? this.resolveTableAlias(meta), true, true);
80
+ const nearest = new QueryRaw(({ ctx }) => {
81
+ ctx.append(`${table}rowid IN (SELECT id FROM vector_top_k(`);
82
+ ctx.addValue(name);
83
+ ctx.append(', ');
84
+ this.appendVectorValue(ctx, ranked.search.$vector, field);
85
+ ctx.append(', ');
86
+ ctx.addValue(k);
87
+ ctx.append('))');
88
+ });
89
+ const where = {};
90
+ where.$and = q.$where ? [q.$where, nearest] : [nearest];
91
+ return where;
92
+ }
58
93
  /**
59
94
  * SQLite does not support the `DEFAULT` keyword inside `VALUES`. Inline the metadata default
60
95
  * when declared, else `NULL` (which is also how SQLite auto-generates INTEGER PRIMARY KEYs).
@@ -109,8 +144,12 @@ export class SqliteDialect extends AbstractSqlDialect {
109
144
  */
110
145
  carriedFields = {
111
146
  numeric: (expr, field) => (isIntegerColumn(field) ? `CAST(${expr} AS TEXT)` : expr),
112
- blob: (expr) => `${this.escape(BYTES_PREFIX)} || hex(${expr})`,
147
+ blob: (expr) => this.bytesAsText(expr),
148
+ vector: (expr) => `CASE WHEN typeof(${expr}) = 'blob' THEN ${this.bytesAsText(expr)} ELSE ${expr} END`,
113
149
  };
150
+ bytesAsText(expr) {
151
+ return `${this.escape(BYTES_PREFIX)} || hex(${expr})`;
152
+ }
114
153
  /** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
115
154
  hydrateKind(field) {
116
155
  return columnFamily(field?.type) === 'date' ? undefined : super.hydrateKind(field);
@@ -124,6 +163,10 @@ export class SqliteDialect extends AbstractSqlDialect {
124
163
  ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
125
164
  ctx.addValue(search.$value);
126
165
  }
166
+ /** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
167
+ appendTextRank(ctx, meta) {
168
+ ctx.append(`-BM25(${this.escapedTableName(meta)})`);
169
+ }
127
170
  jsonLength(slot) {
128
171
  return `JSON_ARRAY_LENGTH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))})`;
129
172
  }
@@ -1,19 +1,20 @@
1
1
  import type { Options } from 'better-sqlite3';
2
2
  import type { ExtraOptions } from '../type/index.js';
3
3
  import { AbstractLocalSqliteQuerierPool, type LocalSqliteDatabase, type LocalSqlitePoolOptions } from './localSqliteQuerierPool.js';
4
+ import { SqliteDialect } from './sqliteDialect.js';
4
5
  /** Driver options, plus the loadable extensions to install on the connection. */
5
6
  export type Sqlite3PoolOptions = Options & LocalSqlitePoolOptions;
6
7
  /**
7
8
  * Pool for `better-sqlite3`, or `bun:sqlite` when running under Bun - the same file, through whichever
8
9
  * driver the runtime provides.
9
10
  */
10
- export declare class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool<Sqlite3PoolOptions> {
11
+ export declare class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool<LocalSqliteDatabase, SqliteDialect> {
11
12
  readonly filename: string | Buffer;
12
- constructor(filename?: string | Buffer, opts?: Sqlite3PoolOptions, extra?: ExtraOptions);
13
+ readonly opts?: Sqlite3PoolOptions | undefined;
14
+ constructor(filename?: string | Buffer, opts?: Sqlite3PoolOptions | undefined, extra?: ExtraOptions);
13
15
  /**
14
- * Both drivers read integers as `bigint`, which the querier decodes exactly past 2^53. `bun:sqlite`
15
- * rejects option keys it does not know, so `extensions` is stripped out, and opens only a path, so a
16
- * serialized database - which better-sqlite3 takes as its filename - is deserialized there instead.
16
+ * `bun:sqlite` rejects option keys it does not know, so `extensions` is stripped out, and opens only a
17
+ * path, so a serialized database - which better-sqlite3 takes as its filename - is deserialized there instead.
17
18
  */
18
19
  protected createDb(): Promise<LocalSqliteDatabase>;
19
20
  }
@@ -1,18 +1,21 @@
1
- import { AbstractLocalSqliteQuerierPool, adaptSqlite, } from './localSqliteQuerierPool.js';
1
+ import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
+ import { AbstractLocalSqliteQuerierPool, adaptSqlite, loadExtensions, } from './localSqliteQuerierPool.js';
3
+ import { SqliteDialect } from './sqliteDialect.js';
2
4
  /**
3
5
  * Pool for `better-sqlite3`, or `bun:sqlite` when running under Bun - the same file, through whichever
4
6
  * driver the runtime provides.
5
7
  */
6
8
  export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
7
9
  filename;
10
+ opts;
8
11
  constructor(filename = ':memory:', opts, extra) {
9
- super(opts, extra);
12
+ super(new SqliteDialect(dialectOptionsFrom(extra)), extra);
10
13
  this.filename = filename;
14
+ this.opts = opts;
11
15
  }
12
16
  /**
13
- * Both drivers read integers as `bigint`, which the querier decodes exactly past 2^53. `bun:sqlite`
14
- * rejects option keys it does not know, so `extensions` is stripped out, and opens only a path, so a
15
- * serialized database - which better-sqlite3 takes as its filename - is deserialized there instead.
17
+ * `bun:sqlite` rejects option keys it does not know, so `extensions` is stripped out, and opens only a
18
+ * path, so a serialized database - which better-sqlite3 takes as its filename - is deserialized there instead.
16
19
  */
17
20
  async createDb() {
18
21
  const { extensions, ...driverOpts } = this.opts ?? {};
@@ -22,9 +25,9 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
22
25
  const bunDb = typeof this.filename === 'string'
23
26
  ? new Database(this.filename, bunOpts)
24
27
  : Database.deserialize(this.filename, bunOpts);
25
- return adaptSqlite(bunDb, (stmt) => stmt.columnNames.length > 0);
28
+ return loadExtensions(adaptSqlite(bunDb, (stmt) => stmt.columnNames.length > 0), extensions);
26
29
  }
27
30
  const { default: BetterSqlite3 } = await import('better-sqlite3');
28
- return new BetterSqlite3(this.filename, driverOpts).defaultSafeIntegers(true);
31
+ return loadExtensions(new BetterSqlite3(this.filename, driverOpts).defaultSafeIntegers(true), extensions);
29
32
  }
30
33
  }
@@ -2,7 +2,7 @@ import type { VectorDistance, VectorMetric } from '../type/index.js';
2
2
  import { TursoDialect } from './tursoDialect.js';
3
3
  /**
4
4
  * SQLite Dialect specialization for the embedded Turso engine, the Rust engine alone: it adds a
5
- * dot-product distance to libSQL's cosine and L2, and caps no function call.
5
+ * dot-product distance to libSQL's cosine and L2, caps no function call, and has no vector index.
6
6
  */
7
7
  export declare class TursoLocalDialect extends TursoDialect {
8
8
  readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;