uql-orm 0.70.0 → 0.71.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 (71) hide show
  1. package/README.md +2 -0
  2. package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
  3. package/dist/cockroachdb/cockroachDialect.js +8 -0
  4. package/dist/d1/d1SqliteDialect.d.ts +3 -0
  5. package/dist/d1/d1SqliteDialect.js +3 -1
  6. package/dist/dialect/abstractSqlDialect.d.ts +5 -0
  7. package/dist/dialect/abstractSqlDialect.js +12 -5
  8. package/dist/dialect/hydrateColumn.d.ts +3 -2
  9. package/dist/dialect/hydrateColumn.js +10 -1
  10. package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
  11. package/dist/dialect/pgLikeSqlDialect.d.ts +17 -5
  12. package/dist/dialect/pgLikeSqlDialect.js +33 -15
  13. package/dist/dialect/queryJoins.d.ts +14 -3
  14. package/dist/dialect/queryJoins.js +31 -11
  15. package/dist/dialect/vectorCast.d.ts +2 -0
  16. package/dist/dialect/vectorCast.js +7 -0
  17. package/dist/dialect/vectorSqlDialect.d.ts +7 -3
  18. package/dist/dialect/vectorSqlDialect.js +15 -10
  19. package/dist/libsql/libsqlDialect.d.ts +1 -1
  20. package/dist/libsql/libsqlDialect.js +3 -3
  21. package/dist/maria/mariaDialect.d.ts +3 -9
  22. package/dist/maria/mariaDialect.js +4 -13
  23. package/dist/maria/mariadbQuerier.js +9 -3
  24. package/dist/migrate/ddl/index.d.ts +1 -0
  25. package/dist/migrate/ddl/index.js +4 -2
  26. package/dist/migrate/ddl/indexDdl.d.ts +2 -0
  27. package/dist/migrate/ddl/indexDdl.js +10 -2
  28. package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
  29. package/dist/migrate/ddl/pgIndexDdl.js +11 -11
  30. package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
  31. package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
  32. package/dist/migrate/generator/mongoCommand.d.ts +28 -0
  33. package/dist/migrate/generator/mongoCommand.js +8 -0
  34. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  35. package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
  36. package/dist/migrate/introspection/mongoIntrospector.js +33 -7
  37. package/dist/migrate/introspection/postgresIntrospector.js +19 -0
  38. package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
  39. package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
  40. package/dist/mongo/mongoDialect.d.ts +2 -0
  41. package/dist/mongo/mongoDialect.js +8 -4
  42. package/dist/mongo/mongodbQuerier.js +2 -2
  43. package/dist/mssql/mssqlDialect.js +1 -0
  44. package/dist/schema/canonicalType.js +10 -4
  45. package/dist/schema/indexDifferences.d.ts +5 -2
  46. package/dist/schema/indexDifferences.js +5 -0
  47. package/dist/schema/schemaASTBuilder.js +3 -2
  48. package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
  49. package/dist/sqlite/localSqliteQuerierPool.js +8 -11
  50. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
  51. package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
  52. package/dist/sqlite/sqliteDialect.d.ts +9 -1
  53. package/dist/sqlite/sqliteDialect.js +42 -3
  54. package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
  55. package/dist/sqlite/sqliteQuerierPool.js +10 -7
  56. package/dist/turso/tursoLocalDialect.d.ts +1 -1
  57. package/dist/turso/tursoLocalDialect.js +1 -1
  58. package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
  59. package/dist/turso/tursoLocalQuerierPool.js +3 -10
  60. package/dist/type/dialect.d.ts +5 -0
  61. package/dist/type/entity.d.ts +14 -3
  62. package/dist/type/migration.d.ts +7 -9
  63. package/dist/type/queryAggregate.d.ts +4 -8
  64. package/dist/type/vector.d.ts +17 -0
  65. package/dist/type/vector.js +6 -0
  66. package/dist/util/ddlExpression.util.d.ts +2 -0
  67. package/dist/util/ddlExpression.util.js +5 -0
  68. package/dist/util/dialect.util.d.ts +11 -1
  69. package/dist/util/dialect.util.js +21 -1
  70. package/package.json +5 -3
  71. package/skills/uql-orm/SKILL.md +142 -0
@@ -4,12 +4,17 @@ import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.j
4
4
  * SQLite schema introspector
5
5
  */
6
6
  export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
7
+ /** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
8
+ indexFacets = new Set(this.dialect.hasVectorIndex() ? ['vector'] : []);
9
+ /** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
7
10
  getTableNamesQuery() {
8
11
  return /*sql*/ `
9
12
  SELECT name
10
13
  FROM sqlite_master
11
14
  WHERE type = 'table'
12
15
  AND name NOT LIKE 'sqlite_%'
16
+ AND name <> 'libsql_vector_meta_shadow'
17
+ AND name NOT IN (SELECT name || '_shadow' FROM sqlite_master WHERE type = 'index')
13
18
  ORDER BY name
14
19
  `;
15
20
  }
@@ -85,15 +90,24 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
85
90
  const isCompositeUnique = index.origin === 'u' && columns.length > 1;
86
91
  // `PRAGMA index_info` names an expression entry `null` (its `cid` is -2), and the expression text
87
92
  // lives only in `sqlite_master.sql`. Reporting `{ column: null }` put a column literally named
88
- // `null` into the diff, so an index UQL cannot describe is left out entirely instead.
93
+ // `null` into the diff, so an index UQL cannot describe is left out, libSQL's vector index aside.
89
94
  const named = columns.filter((column) => column.name !== null);
90
- if (named.length === columns.length && (isUserCreated || isCompositeUnique)) {
95
+ if (!isUserCreated && !isCompositeUnique) {
96
+ continue;
97
+ }
98
+ if (named.length === columns.length) {
91
99
  indexSchemas.push({
92
100
  name: index.name,
93
101
  entries: named.map((column) => ({ column: column.name })),
94
102
  unique: Boolean(index.unique),
95
103
  });
96
104
  }
105
+ else {
106
+ const vectorIndex = await this.getVectorIndex(read, index.name);
107
+ if (vectorIndex) {
108
+ indexSchemas.push(vectorIndex);
109
+ }
110
+ }
97
111
  }
98
112
  return indexSchemas;
99
113
  }
@@ -143,12 +157,24 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
143
157
  }
144
158
  return uniqueColumns;
145
159
  }
160
+ /** libSQL's `libsql_vector_idx(col, 'metric=...')`, read back from the statement that created it. */
161
+ async getVectorIndex(read, indexName) {
162
+ const [row] = await read(
163
+ /*sql*/ `SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?`, [indexName]);
164
+ const column = row?.sql?.match(/libsql_vector_idx\s*\(\s*[`"[]?([^`"\],\s)]+)/i)?.[1];
165
+ if (!column) {
166
+ return undefined;
167
+ }
168
+ const metric = row.sql?.match(/'metric=(\w+)'/i)?.[1]?.toLowerCase();
169
+ const distances = new Map([...this.dialect.vectorMetrics].map(([distance, { index }]) => [index, distance]));
170
+ return { name: indexName, entries: [{ column }], unique: false, type: 'vector', distance: distances.get(metric) };
171
+ }
146
172
  getIndexColumns(read, indexName) {
147
173
  return read(/*sql*/ `PRAGMA index_info(${this.escapeId(indexName)})`);
148
174
  }
149
175
  normalizeType(type) {
150
176
  // Extract base type without length/precision
151
- const match = type.match(/^([A-Za-z]+)/);
177
+ const match = type.match(/^([A-Za-z][A-Za-z0-9_]*)/);
152
178
  return match ? match[1].toUpperCase() : type.toUpperCase();
153
179
  }
154
180
  extractLength(type) {
@@ -275,6 +275,8 @@ export declare class MongoDialect extends AbstractDialect {
275
275
  * Returns `undefined` if no vector sort is present.
276
276
  */
277
277
  extractVectorSort<E extends Document>(sort: QuerySortMap<E> | undefined): ExtractedVectorSort<E> | undefined;
278
+ /** The Atlas index a `$vectorSearch` over `column` reads, and migrations create: its declared name, else `<column>_index`. */
279
+ protected vectorSearchIndexName(name: string | undefined, column: string): string;
278
280
  /**
279
281
  * Build a `$vectorSearch` aggregation pipeline stage.
280
282
  * Merges `$where` into `$vectorSearch.filter` for optimal pre-filtering.
@@ -18,6 +18,7 @@ export const mongoDialectFeatures = {
18
18
  commentSyntax: 'none',
19
19
  vectorIndexRequiresNotNull: false,
20
20
  vectorSupportsLength: false,
21
+ vectorBytes: false,
21
22
  supportsTimestamptz: false,
22
23
  stringSizing: 'bounded-text',
23
24
  supportsUnsigned: false,
@@ -1016,10 +1017,10 @@ export class MongoDialect extends AbstractDialect {
1016
1017
  */
1017
1018
  buildAggregateStages(entity, q, opts) {
1018
1019
  const meta = getMeta(entity);
1019
- const joins = resolveGroupJoins(meta, q.$group);
1020
+ const { joins, where } = resolveGroupJoins(meta, q);
1020
1021
  const { groupId, accumulators, columns, named } = this.buildGroupSpec(meta, parseGroupMap(q.$group, q.$select), joins);
1021
1022
  const pipeline = [
1022
- ...this.matchStages(entity, q.$where, opts, named),
1023
+ ...this.matchStages(entity, where, opts, named),
1023
1024
  ...this.lookupStages(meta, joins),
1024
1025
  { $group: { _id: hasKeys(groupId) ? groupId : null, ...accumulators } },
1025
1026
  // `$group` answers with `_id` even when grouping by nothing, and with what `columns` read to the end.
@@ -1220,6 +1221,10 @@ export class MongoDialect extends AbstractDialect {
1220
1221
  }
1221
1222
  return { vectorKey: found.key, vectorSearch: found.search, regularSort: regularSort };
1222
1223
  }
1224
+ /** The Atlas index a `$vectorSearch` over `column` reads, and migrations create: its declared name, else `<column>_index`. */
1225
+ vectorSearchIndexName(name, column) {
1226
+ return name ?? `${column}_index`;
1227
+ }
1223
1228
  /**
1224
1229
  * Build a `$vectorSearch` aggregation pipeline stage.
1225
1230
  * Merges `$where` into `$vectorSearch.filter` for optimal pre-filtering.
@@ -1231,8 +1236,7 @@ export class MongoDialect extends AbstractDialect {
1231
1236
  throw new TypeError(`Field '${key}' not found in entity '${meta.name}'`);
1232
1237
  }
1233
1238
  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`;
1239
+ const indexName = this.vectorSearchIndexName(findVectorIndex(meta, key)?.name, colName);
1236
1240
  if (!limit) {
1237
1241
  throw new TypeError(`$vectorSearch requires $limit (vector sort on '${key}' of '${meta.name}')`);
1238
1242
  }
@@ -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
@@ -113,7 +113,7 @@ 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' } } }] : []),
@@ -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
  /**
@@ -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);
@@ -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>;
@@ -1,7 +1,7 @@
1
1
  import { TursoDialect } from './tursoDialect.js';
2
2
  /**
3
3
  * SQLite Dialect specialization for the embedded Turso engine, the Rust engine alone: it adds a
4
- * dot-product distance to libSQL's cosine and L2, and caps no function call.
4
+ * dot-product distance to libSQL's cosine and L2, caps no function call, and has no vector index.
5
5
  */
6
6
  export class TursoLocalDialect extends TursoDialect {
7
7
  vectorMetrics = new Map([
@@ -1,15 +1,14 @@
1
1
  import type { connect } from '@tursodatabase/database';
2
- import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
- import { type SqliteDatabase, SqliteQuerier } from '../sqlite/sqliteQuerier.js';
2
+ import { AbstractLocalSqliteQuerierPool } from '../sqlite/localSqliteQuerierPool.js';
3
+ import type { SqliteDatabase } from '../sqlite/sqliteQuerier.js';
4
4
  import type { ExtraOptions } from '../type/index.js';
5
5
  import { TursoLocalDialect } from './tursoLocalDialect.js';
6
6
  /** The engine's own options: `readonly`, `timeout`, `encryption`, `experimental` and the rest. */
7
7
  export type TursoLocalOptions = NonNullable<Parameters<typeof connect>[1]>;
8
8
  /** A pool for the embedded Turso engine, on `uql-orm/turso/local` so its native binaries stay out of edge bundles. */
9
- export declare class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool<SqliteDatabase, SqliteQuerier, TursoLocalDialect> {
9
+ export declare class TursoLocalQuerierPool extends AbstractLocalSqliteQuerierPool<SqliteDatabase, TursoLocalDialect> {
10
10
  readonly filename: string;
11
11
  readonly opts?: TursoLocalOptions | undefined;
12
12
  constructor(filename?: string, opts?: TursoLocalOptions | undefined, extra?: ExtraOptions);
13
- protected openDb(): Promise<SqliteDatabase>;
14
- protected buildQuerier(db: SqliteDatabase): SqliteQuerier;
13
+ protected createDb(): Promise<SqliteDatabase>;
15
14
  }
@@ -1,10 +1,8 @@
1
1
  import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
- import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
- import { applySqlitePragmas } from '../sqlite/sqlitePragmas.js';
4
- import { SqliteQuerier } from '../sqlite/sqliteQuerier.js';
2
+ import { AbstractLocalSqliteQuerierPool } from '../sqlite/localSqliteQuerierPool.js';
5
3
  import { TursoLocalDialect } from './tursoLocalDialect.js';
6
4
  /** A pool for the embedded Turso engine, on `uql-orm/turso/local` so its native binaries stay out of edge bundles. */
7
- export class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool {
5
+ export class TursoLocalQuerierPool extends AbstractLocalSqliteQuerierPool {
8
6
  filename;
9
7
  opts;
10
8
  constructor(filename = ':memory:', opts, extra) {
@@ -12,15 +10,10 @@ export class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool {
12
10
  this.filename = filename;
13
11
  this.opts = opts;
14
12
  }
15
- async openDb() {
13
+ async createDb() {
16
14
  const { connect } = await import('@tursodatabase/database');
17
15
  const db = await connect(this.filename, this.opts);
18
- // Integers as `bigint`, which the querier decodes exactly past 2^53.
19
16
  db.defaultSafeIntegers(true);
20
- await applySqlitePragmas(db);
21
17
  return db;
22
18
  }
23
- buildQuerier(db) {
24
- return new SqliteQuerier(db, this.dialect, this.extra);
25
- }
26
19
  }
@@ -107,6 +107,11 @@ export interface DialectFeatures {
107
107
  readonly vectorIndexRequiresNotNull: boolean;
108
108
  /** Whether the dialect requires/allows (n) length constraints on vector types. */
109
109
  readonly vectorSupportsLength: boolean;
110
+ /**
111
+ * Whether a vector binds as its packed little-endian float32 bytes, which a blob column holds and the
112
+ * SQLite family and MariaDB read, rather than as `[1,2,3]` text they would reparse per row.
113
+ */
114
+ readonly vectorBytes: boolean;
110
115
  /** Whether the dialect natively supports the TIMESTAMPTZ alias/type. */
111
116
  readonly supportsTimestamptz: boolean;
112
117
  /**