uql-orm 0.69.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 (96) hide show
  1. package/README.md +2 -0
  2. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  3. package/dist/browser/type/clientQuerier.d.ts +5 -5
  4. package/dist/browser/uql-browser.min.js +2 -2
  5. package/dist/browser/uql-browser.min.js.map +4 -4
  6. package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
  7. package/dist/cockroachdb/cockroachDialect.js +10 -2
  8. package/dist/d1/d1SqliteDialect.d.ts +3 -0
  9. package/dist/d1/d1SqliteDialect.js +3 -1
  10. package/dist/dialect/abstractDialect.d.ts +8 -2
  11. package/dist/dialect/abstractDialect.js +17 -1
  12. package/dist/dialect/abstractSqlDialect.d.ts +21 -5
  13. package/dist/dialect/abstractSqlDialect.js +91 -46
  14. package/dist/dialect/aliases.d.ts +10 -7
  15. package/dist/dialect/aliases.js +10 -7
  16. package/dist/dialect/hydrateColumn.d.ts +3 -2
  17. package/dist/dialect/hydrateColumn.js +10 -1
  18. package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
  19. package/dist/dialect/pgLikeSqlDialect.d.ts +17 -5
  20. package/dist/dialect/pgLikeSqlDialect.js +34 -15
  21. package/dist/dialect/queryJoins.d.ts +19 -1
  22. package/dist/dialect/queryJoins.js +54 -11
  23. package/dist/dialect/vectorCast.d.ts +2 -0
  24. package/dist/dialect/vectorCast.js +7 -0
  25. package/dist/dialect/vectorSqlDialect.d.ts +7 -3
  26. package/dist/dialect/vectorSqlDialect.js +15 -10
  27. package/dist/entity/decorator/members.d.ts +25 -11
  28. package/dist/entity/metadata/definition.d.ts +8 -3
  29. package/dist/libsql/libsqlDialect.d.ts +1 -1
  30. package/dist/libsql/libsqlDialect.js +3 -3
  31. package/dist/maria/mariaDialect.d.ts +3 -9
  32. package/dist/maria/mariaDialect.js +4 -13
  33. package/dist/maria/mariadbQuerier.js +9 -3
  34. package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
  35. package/dist/migrate/ddl/index.d.ts +1 -0
  36. package/dist/migrate/ddl/index.js +4 -2
  37. package/dist/migrate/ddl/indexDdl.d.ts +2 -0
  38. package/dist/migrate/ddl/indexDdl.js +10 -2
  39. package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
  40. package/dist/migrate/ddl/pgIndexDdl.js +11 -11
  41. package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
  42. package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
  43. package/dist/migrate/generator/mongoCommand.d.ts +28 -0
  44. package/dist/migrate/generator/mongoCommand.js +8 -0
  45. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  46. package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
  47. package/dist/migrate/introspection/mongoIntrospector.js +33 -7
  48. package/dist/migrate/introspection/postgresIntrospector.js +19 -0
  49. package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
  50. package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
  51. package/dist/migrate/migrator.d.ts +2 -1
  52. package/dist/migrate/migrator.js +5 -3
  53. package/dist/mongo/mongoDialect.d.ts +33 -27
  54. package/dist/mongo/mongoDialect.js +178 -114
  55. package/dist/mongo/mongodbQuerier.d.ts +0 -2
  56. package/dist/mongo/mongodbQuerier.js +14 -13
  57. package/dist/mssql/mssqlDialect.js +4 -2
  58. package/dist/postgres/postgresDialect.js +2 -2
  59. package/dist/querier/abstractQuerier.d.ts +16 -11
  60. package/dist/querier/abstractQuerier.js +28 -8
  61. package/dist/querier/abstractQuerierPool.d.ts +9 -9
  62. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  63. package/dist/querier/abstractSqlQuerier.js +3 -3
  64. package/dist/schema/canonicalType.js +10 -4
  65. package/dist/schema/indexDifferences.d.ts +5 -2
  66. package/dist/schema/indexDifferences.js +5 -0
  67. package/dist/schema/schemaASTBuilder.js +3 -2
  68. package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
  69. package/dist/sqlite/localSqliteQuerierPool.js +8 -11
  70. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
  71. package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
  72. package/dist/sqlite/sqliteDialect.d.ts +9 -1
  73. package/dist/sqlite/sqliteDialect.js +43 -3
  74. package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
  75. package/dist/sqlite/sqliteQuerierPool.js +10 -7
  76. package/dist/turso/tursoDialect.d.ts +1 -1
  77. package/dist/turso/tursoDialect.js +6 -2
  78. package/dist/turso/tursoLocalDialect.d.ts +1 -1
  79. package/dist/turso/tursoLocalDialect.js +1 -1
  80. package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
  81. package/dist/turso/tursoLocalQuerierPool.js +3 -10
  82. package/dist/type/dialect.d.ts +11 -0
  83. package/dist/type/entity.d.ts +45 -5
  84. package/dist/type/migration.d.ts +14 -9
  85. package/dist/type/query.d.ts +6 -0
  86. package/dist/type/queryAggregate.d.ts +73 -42
  87. package/dist/type/queryAggregate.js +4 -21
  88. package/dist/type/universalQuerier.d.ts +9 -9
  89. package/dist/type/vector.d.ts +17 -0
  90. package/dist/type/vector.js +6 -0
  91. package/dist/util/ddlExpression.util.d.ts +2 -0
  92. package/dist/util/ddlExpression.util.js +5 -0
  93. package/dist/util/dialect.util.d.ts +17 -3
  94. package/dist/util/dialect.util.js +40 -6
  95. package/package.json +5 -3
  96. package/skills/uql-orm/SKILL.md +142 -0
@@ -0,0 +1,38 @@
1
+ import { indexDistance, isVectorIndexType, unsupportedVectorMetric } from '../../type/vector.js';
2
+ import { IndexDdl } from './indexDdl.js';
3
+ /**
4
+ * SQLite's `CREATE INDEX`, which names no index type. A vector index is libSQL's DiskANN where the dialect
5
+ * can index its metric, and a plain index on an engine with none, so an entity written for Postgres migrates.
6
+ */
7
+ export class SqliteIndexDdl extends IndexDdl {
8
+ indexColumn(entry, index) {
9
+ const metric = this.vectorIndexMetric(index);
10
+ if (metric === undefined) {
11
+ return super.indexColumn(entry, index);
12
+ }
13
+ const options = [
14
+ `metric=${metric}`,
15
+ ...(index.m === undefined ? [] : [`max_neighbors=${index.m}`]),
16
+ ...(index.efConstruction === undefined ? [] : [`insert_l=${index.efConstruction}`]),
17
+ ];
18
+ const args = [this.dialect.escapeId(entry.column), ...options.map((option) => this.dialect.escape(option))];
19
+ return `libsql_vector_idx(${args.join(', ')})`;
20
+ }
21
+ /** The metric libSQL's index names, or `undefined` where the index is no vector index or the engine has none. */
22
+ vectorIndexMetric(index) {
23
+ const { vectorMetrics, dialectName } = this.dialect;
24
+ if (!isVectorIndexType(index.type) || !this.dialect.hasVectorIndex()) {
25
+ return undefined;
26
+ }
27
+ const distance = indexDistance(index);
28
+ const metric = vectorMetrics.get(distance)?.index;
29
+ if (!metric) {
30
+ throw unsupportedVectorMetric(dialectName, distance, index.name);
31
+ }
32
+ // Its tables are named after the index, unquoted: any other name fails with "unable to initialize diskann".
33
+ if (!/^\w+$/.test(index.name) || index.entries.length !== 1) {
34
+ throw new TypeError(`libSQL names a vector index only by letters, digits and underscores, over one column (index "${index.name}")`);
35
+ }
36
+ return metric;
37
+ }
38
+ }
@@ -6,6 +6,24 @@ export type MongoIndexOptions = {
6
6
  readonly name: string;
7
7
  readonly partialFilterExpression?: Readonly<Record<string, unknown>>;
8
8
  };
9
+ /** A field of an Atlas vector search index: the vector itself, or one its `filter` pre-filters on. */
10
+ export type MongoVectorSearchField = {
11
+ readonly type: 'vector';
12
+ readonly path: string;
13
+ readonly numDimensions: number;
14
+ readonly similarity: string;
15
+ } | {
16
+ readonly type: 'filter';
17
+ readonly path: string;
18
+ };
19
+ /** An Atlas search index as `createSearchIndex` takes one. */
20
+ export type MongoSearchIndex = {
21
+ readonly name: string;
22
+ readonly type: 'vectorSearch';
23
+ readonly definition: {
24
+ readonly fields: readonly MongoVectorSearchField[];
25
+ };
26
+ };
9
27
  /** The commands {@link MongoSchemaGenerator} emits as JSON, one per statement. */
10
28
  export type MongoCommand = {
11
29
  readonly action: 'createCollection';
@@ -27,6 +45,14 @@ export type MongoCommand = {
27
45
  readonly action: 'dropIndex';
28
46
  readonly collection: string;
29
47
  readonly name: string;
48
+ } | {
49
+ readonly action: 'createSearchIndex';
50
+ readonly collection: string;
51
+ readonly index: MongoSearchIndex;
52
+ } | {
53
+ readonly action: 'dropSearchIndex';
54
+ readonly collection: string;
55
+ readonly name: string;
30
56
  };
31
57
  export declare function serializeMongoCommand(command: MongoCommand): string;
32
58
  /**
@@ -41,6 +67,8 @@ export type MongoCommandTarget = {
41
67
  drop(): Promise<unknown>;
42
68
  createIndex(key: MongoIndexKey, options: MongoIndexOptions): Promise<unknown>;
43
69
  dropIndex(name: string): Promise<unknown>;
70
+ createSearchIndex(index: MongoSearchIndex): Promise<unknown>;
71
+ dropSearchIndex(name: string): Promise<unknown>;
44
72
  };
45
73
  };
46
74
  /** Execute one emitted command. */
@@ -25,6 +25,10 @@ export function runMongoCommand(db, statement) {
25
25
  return db.collection(command.collection).createIndex(command.key, command.options);
26
26
  case 'dropIndex':
27
27
  return db.collection(command.collection).dropIndex(command.name);
28
+ case 'createSearchIndex':
29
+ return db.collection(command.collection).createSearchIndex(command.index);
30
+ case 'dropSearchIndex':
31
+ return db.collection(command.collection).dropSearchIndex(command.name);
28
32
  default:
29
33
  // Unreachable for a command this module produced; a hand-written statement lands here rather
30
34
  // than being silently skipped.
@@ -46,6 +50,10 @@ export function mongoCommandSource(statement, db) {
46
50
  return `${db}.collection(${literal(command.collection)}).createIndex(${literal(command.key)}, ${literal(command.options)})`;
47
51
  case 'dropIndex':
48
52
  return `${db}.collection(${literal(command.collection)}).dropIndex(${literal(command.name)})`;
53
+ case 'createSearchIndex':
54
+ return `${db}.collection(${literal(command.collection)}).createSearchIndex(${literal(command.index)})`;
55
+ case 'dropSearchIndex':
56
+ return `${db}.collection(${literal(command.collection)}).dropSearchIndex(${literal(command.name)})`;
49
57
  default:
50
58
  throw unsupportedMongoCommand(statement);
51
59
  }
@@ -35,6 +35,8 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
35
35
  generateAlterTableDown(diff: SchemaDiff): string[];
36
36
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
37
37
  generateCreateIndex(tableName: string, index: IndexSchema): string;
38
+ /** An Atlas vector search index: its vector field first, then each field a `$vectorSearch` pre-filters on. */
39
+ private generateCreateSearchIndex;
38
40
  generateDropIndex(tableName: string, indexName: string): string;
39
41
  /** A collection and its indexes, which is all a document store has: a column, a constraint or SQL throws. */
40
42
  generateOperation(operation: AnyMigrationOperation): string[];
@@ -1,16 +1,22 @@
1
1
  import { getMeta } from '../../entity/index.js';
2
2
  import { MongoDialect } from '../../mongo/mongoDialect.js';
3
3
  import { QueryRaw, } from '../../type/index.js';
4
- import { declaredIndexes, indexNameParts, renderIndexColumn } from '../../util/ddlExpression.util.js';
5
- import { derivedIndexName } from '../../util/sql.util.js';
4
+ import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
5
+ import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../../util/ddlExpression.util.js';
6
6
  import { assertIndexFeatures, assertIndexType } from '../ddl/indexDdl.js';
7
7
  import { assertIndexPredicate, refusedIndexPredicate } from '../indexPredicate.js';
8
8
  import { renderIndexDefinition } from './definitionToNode.js';
9
9
  import { serializeMongoCommand } from './mongoCommand.js';
10
- /** The index types a key spec can say: a plain key, or `'text'`. */
11
- const MONGO_INDEX_TYPES = new Set(['btree', 'fulltext']);
10
+ /** The index types a key spec can say, a plain key or `'text'`, and Atlas's vector search index. */
11
+ const MONGO_INDEX_TYPES = new Set(['btree', 'fulltext', 'vectorSearch']);
12
12
  /** A key spec's one feature beyond its keys: a partial filter. */
13
13
  const MONGO_INDEX_FEATURES = new Set(['partial']);
14
+ /** Atlas's name for each metric a vector search index scores by. */
15
+ const ATLAS_SIMILARITY = {
16
+ cosine: 'cosine',
17
+ l2: 'euclidean',
18
+ inner: 'dotProduct',
19
+ };
14
20
  export class MongoSchemaGenerator extends MongoDialect {
15
21
  defaultForeignKeyAction;
16
22
  constructor(namingStrategy, defaultForeignKeyAction) {
@@ -51,7 +57,11 @@ export class MongoSchemaGenerator extends MongoDialect {
51
57
  const entries = index.columns
52
58
  .map((entry) => renderIndexColumn(entry, () => this.compileDdl()))
53
59
  .map((entry) => ({ ...entry, column: this.columnOf(meta, entry.column) }));
54
- const name = index.name ?? derivedIndexName(collectionName, indexNameParts(entries));
60
+ const [first] = index.columns;
61
+ const vector = index.type === 'vectorSearch' && typeof first?.column === 'string' ? meta.fields[first.column] : undefined;
62
+ const name = vector
63
+ ? this.vectorSearchIndexName(index.name, entries[0].column)
64
+ : declaredIndexName(index.name, collectionName, entries);
55
65
  return {
56
66
  name,
57
67
  entries,
@@ -59,6 +69,8 @@ export class MongoSchemaGenerator extends MongoDialect {
59
69
  type: index.type,
60
70
  include: index.include,
61
71
  where: index.where && this.compileIndexPredicate(index.where, meta.entity, name),
72
+ distance: index.distance ?? vector?.distance,
73
+ dimensions: vector?.dimensions,
62
74
  };
63
75
  }
64
76
  /**
@@ -94,11 +106,16 @@ export class MongoSchemaGenerator extends MongoDialect {
94
106
  return (diff.indexesToAdd ?? []).map((index) => this.generateCreateIndex(diff.tableName, index));
95
107
  }
96
108
  generateAlterTableDown(diff) {
97
- return (diff.indexesToAdd ?? []).map((index) => this.generateDropIndex(diff.tableName, index.name));
109
+ return (diff.indexesToAdd ?? []).map((index) => index.type === 'vectorSearch'
110
+ ? serializeMongoCommand({ action: 'dropSearchIndex', collection: diff.tableName, name: index.name })
111
+ : this.generateDropIndex(diff.tableName, index.name));
98
112
  }
99
113
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
100
114
  generateCreateIndex(tableName, index) {
101
115
  assertIndexType(index, MONGO_INDEX_TYPES, this.dialectName);
116
+ if (index.type === 'vectorSearch') {
117
+ return this.generateCreateSearchIndex(tableName, index);
118
+ }
102
119
  assertIndexFeatures(index, MONGO_INDEX_FEATURES, this.dialectName);
103
120
  const key = {};
104
121
  for (const entry of index.entries) {
@@ -116,6 +133,33 @@ export class MongoSchemaGenerator extends MongoDialect {
116
133
  },
117
134
  });
118
135
  }
136
+ /** An Atlas vector search index: its vector field first, then each field a `$vectorSearch` pre-filters on. */
137
+ generateCreateSearchIndex(tableName, index) {
138
+ assertIndexFeatures(index, new Set(), this.dialectName);
139
+ const [vector, ...filters] = index.entries;
140
+ if (!vector || index.dimensions === undefined) {
141
+ throw new TypeError(`an Atlas vector search index states its field's dimensions (index "${index.name}")`);
142
+ }
143
+ const distance = indexDistance(index);
144
+ const similarity = ATLAS_SIMILARITY[distance];
145
+ if (!similarity) {
146
+ throw unsupportedVectorMetric(this.dialectName, distance, index.name);
147
+ }
148
+ return serializeMongoCommand({
149
+ action: 'createSearchIndex',
150
+ collection: tableName,
151
+ index: {
152
+ name: index.name,
153
+ type: 'vectorSearch',
154
+ definition: {
155
+ fields: [
156
+ { type: 'vector', path: vector.column, numDimensions: index.dimensions, similarity },
157
+ ...filters.map((entry) => ({ type: 'filter', path: entry.column })),
158
+ ],
159
+ },
160
+ },
161
+ });
162
+ }
119
163
  generateDropIndex(tableName, indexName) {
120
164
  return serializeMongoCommand({ action: 'dropIndex', collection: tableName, name: indexName });
121
165
  }
@@ -1,5 +1,7 @@
1
1
  import { createTableNode, SchemaAST } from '../../schema/schemaAST.js';
2
2
  import { isMongoQuerier, } from '../../type/index.js';
3
+ /** What a server without Atlas Search answers a search index command with. */
4
+ const SEARCH_NOT_ENABLED = 31082;
3
5
  /**
4
6
  * MongoDB schema introspector.
5
7
  * MongoDB doesn't have a fixed schema, so this primarily focuses on collections and indexes.
@@ -29,15 +31,27 @@ export class MongoSchemaIntrospector {
29
31
  }
30
32
  // Annotated rather than inferred: the driver's `indexes()` is overloaded and resolves to `any` on
31
33
  // some versions, which silently made every field below unchecked.
32
- const indexes = await db.collection(tableName).indexes();
34
+ const collection = db.collection(tableName);
35
+ const indexes = await collection.indexes();
36
+ const searchIndexes = await listSearchIndexes(collection);
33
37
  return {
34
38
  name: tableName,
35
39
  columns: [],
36
- indexes: indexes.map((idx) => ({
37
- name: idx.name ?? Object.keys(idx.key).join('_'),
38
- entries: Object.keys(idx.key).map((column) => ({ column })),
39
- unique: !!idx.unique,
40
- })),
40
+ indexes: [
41
+ ...indexes.map((idx) => ({
42
+ name: idx.name ?? Object.keys(idx.key).join('_'),
43
+ entries: Object.keys(idx.key).map((column) => ({ column })),
44
+ unique: !!idx.unique,
45
+ })),
46
+ ...searchIndexes
47
+ .filter((idx) => idx.type === 'vectorSearch')
48
+ .map((idx) => ({
49
+ name: idx.name,
50
+ entries: idx.latestDefinition.fields.map(({ path }) => ({ column: path })),
51
+ unique: false,
52
+ type: 'vectorSearch',
53
+ })),
54
+ ],
41
55
  };
42
56
  });
43
57
  }
@@ -61,6 +75,18 @@ export class MongoSchemaIntrospector {
61
75
  });
62
76
  }
63
77
  }
78
+ /** A collection's Atlas search indexes, none where the server has no Atlas Search. */
79
+ async function listSearchIndexes(collection) {
80
+ try {
81
+ return await collection.aggregate([{ $listSearchIndexes: {} }]).toArray();
82
+ }
83
+ catch (error) {
84
+ if (error instanceof Error && 'code' in error && error.code === SEARCH_NOT_ENABLED) {
85
+ return [];
86
+ }
87
+ throw error;
88
+ }
89
+ }
64
90
  async function hasCollection(db, name) {
65
91
  const collections = await db.listCollections({ name, type: 'collection' }, { nameOnly: true }).toArray();
66
92
  return collections.length > 0;
@@ -83,7 +109,7 @@ function buildTable({ name, indexes = [] }) {
83
109
  });
84
110
  }
85
111
  }
86
- table.indexes.push({ name: index.name, table, entries: index.entries, unique: index.unique });
112
+ table.indexes.push({ name: index.name, table, entries: index.entries, unique: index.unique, type: index.type });
87
113
  }
88
114
  return table;
89
115
  }
@@ -199,6 +199,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
199
199
  type: INDEX_TYPES.find((type) => type === rows[0].method),
200
200
  where: rows[0].predicate ?? undefined,
201
201
  include: include.length > 0 ? include : undefined,
202
+ ...fulltextIndex(rows),
202
203
  };
203
204
  });
204
205
  }
@@ -267,6 +268,24 @@ const FOREIGN_KEY_ACTION_CODES = {
267
268
  };
268
269
  const NUMBER_DEFAULT = /^\(?(-?\d+(?:\.\d+)?)\)?$/;
269
270
  const QUOTED_NUMBER_DEFAULT = /^'(-?\d+(?:\.\d+)?)'::(?:smallint|integer|bigint|numeric|real|double precision)$/;
271
+ /**
272
+ * A `fulltext` index read back as declared, from the document its `GIN` index (`inverted` on CockroachDB)
273
+ * is over as UQL builds it, `to_tsvector('english'::regconfig, COALESCE(title, ''::text) || ...)`: its
274
+ * columns and config, so it compares with the entity. Any other expression stays the expression it is.
275
+ */
276
+ function fulltextIndex(rows) {
277
+ const [row] = rows;
278
+ const document = rows.length === 1 && TEXT_INDEX_METHODS.has(row.method) && row.is_expression;
279
+ const config = document ? /^\(?to_tsvector\('((?:[^']|'')*)'::/i.exec(row.entry) : null;
280
+ const columns = config
281
+ ? [...row.entry.matchAll(/COALESCE\(("(?:[^"]|"")+"|[^,()\s]+),/gi)].map(([, column]) => column.startsWith('"') ? column.slice(1, -1).replaceAll('""', '"') : column)
282
+ : [];
283
+ if (!config || !columns.length) {
284
+ return undefined;
285
+ }
286
+ return { type: 'fulltext', entries: columns.map((column) => ({ column })), config: config[1].replaceAll("''", "'") };
287
+ }
288
+ const TEXT_INDEX_METHODS = new Set(['gin', 'inverted']);
270
289
  /**
271
290
  * Postgres states every entry in full: a plain column still reports `order: 'asc'`, and only a
272
291
  * non-default operator class is named. The diff defaults the entity side to match, so an option
@@ -1,9 +1,13 @@
1
+ import type { IndexFacet } from '../../schema/indexDifferences.js';
1
2
  import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
2
3
  import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
3
4
  /**
4
5
  * SQLite schema introspector
5
6
  */
6
7
  export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
8
+ /** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
9
+ readonly indexFacets: ReadonlySet<IndexFacet>;
10
+ /** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
7
11
  protected getTableNamesQuery(): string;
8
12
  protected tableExistsQuery(): string;
9
13
  protected parseTableExistsResult([row]: SqliteCountRow[]): boolean;
@@ -29,6 +33,8 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
29
33
  protected mapForeignKeysResult(_read: TableRowReader, tableName: string, results: SqliteForeignKeyRow[]): Promise<ForeignKeySchema[]>;
30
34
  protected mapPrimaryKeyResult(results: SqliteColumnRow[]): string[] | undefined;
31
35
  private getUniqueColumns;
36
+ /** libSQL's `libsql_vector_idx(col, 'metric=...')`, read back from the statement that created it. */
37
+ private getVectorIndex;
32
38
  private getIndexColumns;
33
39
  protected normalizeType(type: string): string;
34
40
  protected extractLength(type: string): number | undefined;
@@ -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) {
@@ -49,7 +49,8 @@ export declare class Migrator {
49
49
  /** Runs the list narrowed by `to`/`step`, stopping at the first failure: `up` over the pending, `down` over the executed reversed. */
50
50
  private runInOrder;
51
51
  /**
52
- * Run a single migration, within a transaction where the dialect has one for it
52
+ * Run a single migration, in a transaction where the dialect has one for it and the migration has not
53
+ * declared `transaction: false` - the opt-out a statement an engine refuses inside one needs.
53
54
  */
54
55
  runMigration(migration: Migration<Querier>, direction: 'up' | 'down'): Promise<MigrationResult>;
55
56
  /**
@@ -112,14 +112,15 @@ export class Migrator {
112
112
  return results;
113
113
  }
114
114
  /**
115
- * Run a single migration, within a transaction where the dialect has one for it
115
+ * Run a single migration, in a transaction where the dialect has one for it and the migration has not
116
+ * declared `transaction: false` - the opt-out a statement an engine refuses inside one needs.
116
117
  */
117
118
  async runMigration(migration, direction) {
118
119
  const startTime = Date.now();
119
120
  return this.target.withSession(async ({ querier, transaction }) => {
120
121
  try {
121
122
  this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
122
- await transaction(async () => {
123
+ const work = async () => {
123
124
  if (direction === 'up') {
124
125
  await migration.up(querier);
125
126
  await this.storage.logWithQuerier(querier, migration.name);
@@ -128,7 +129,8 @@ export class Migrator {
128
129
  await migration.down(querier);
129
130
  await this.storage.unlogWithQuerier(querier, migration.name);
130
131
  }
131
- });
132
+ };
133
+ await (migration.transaction === false ? work() : transaction(work));
132
134
  const duration = Date.now() - startTime;
133
135
  this.logger.logMigration(`Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`);
134
136
  return {
@@ -33,20 +33,15 @@ export declare class MongoDialect extends AbstractDialect {
33
33
  columnOf<E>(meta: EntityMeta<E>, key: string): string;
34
34
  where<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions): Filter<E>;
35
35
  /**
36
- * A `$where` that may constrain relations, as the `$lookup` stages it needs and the `$match` reading
37
- * them: each condition a lookup into a temporary field (`unset` names them), so `$or` keeps its meaning.
36
+ * The stages every pipeline starts with: the `$lookup` each relation condition needs, into a temporary
37
+ * field so `$or` keeps its meaning, the `$match` reading them, and the temporaries taken back out. Each
38
+ * relation aggregate the `$where` or `named` reads is left on the document, once, under its column.
38
39
  */
39
- whereWithRelations<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions): {
40
- readonly stages: MongoAggregationPipelineEntry<Document>[];
41
- readonly filter: Filter<E>;
42
- readonly unset: string[];
43
- };
44
- /** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
45
- constrainsRelations<E extends Document>(entity: Type<E>, where: QueryWhere<E> | undefined): boolean;
40
+ matchStages<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions, named?: readonly string[]): MongoAggregationPipelineEntry<Document>[];
46
41
  /**
47
42
  * Renders a `$where` tree without applying entity filters (used for same-scope group-operator
48
- * recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
49
- * given - a plain `find`/`updateMany` filter has nowhere to put them.
43
+ * recursion). A relation, or a relation aggregate, needs `$lookup` stages, so it is only accepted
44
+ * when `lookups` is given - a plain `find`/`updateMany` filter has nowhere to put them.
50
45
  */
51
46
  protected renderFilter<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, lookups?: RelationLookups): Filter<E>;
52
47
  /**
@@ -141,20 +136,14 @@ export declare class MongoDialect extends AbstractDialect {
141
136
  };
142
137
  /** Whether a read answers with a relation aggregate, which only the pipeline can build. */
143
138
  readsAggregates<E extends Document>(entity: Type<E>, q: Query<E>): boolean;
144
- /**
145
- * The relation aggregates one query reads: the ones its projection carries, plus any its `$where` or
146
- * `$sort` names, which a read materializes whether or not it answers with them.
147
- */
139
+ /** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
148
140
  private aggregateKeys;
149
141
  /**
150
- * The stages a relation aggregate a query names needs: the correlated lookup that reads the related
151
- * rows - narrowed, ordered and capped as the field declared - ending in the tally or total it wants,
152
- * and the `$addFields` that puts the value on the document under the field's own name.
153
- *
154
- * The same spec the SQL dialects render as a correlated subquery: a relation aggregate is data, so a
155
- * document engine builds it out of stages rather than being refused a language it does not speak.
142
+ * The relation aggregate `key` computes, put on the document under its column by the stages
143
+ * {@link aggregateStages} builds from the same spec SQL renders as a subquery. Once however many
144
+ * clauses read it; a key computing none adds nothing.
156
145
  */
157
- private aggregateFieldStages;
146
+ private appendAggregateField;
158
147
  /**
159
148
  * One relation aggregate on the document under `field`: the correlated lookup that reads the related
160
149
  * rows - narrowed, ordered and capped as the spec says - ending in the tally or total it wants, and
@@ -258,24 +247,41 @@ export declare class MongoDialect extends AbstractDialect {
258
247
  /**
259
248
  * Build MongoDB aggregation pipeline stages from a QueryAggregate.
260
249
  */
261
- buildAggregateStages<E extends Document, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Record<string, unknown>[];
250
+ buildAggregateStages<E extends Document, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): MongoAggregationPipelineEntry<Document>[];
262
251
  /**
263
- * Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage.
264
- * `distinctReducers` maps each DISTINCT alias (collected via `$addToSet`) to the `$project`
265
- * operator that reduces its set: `$size` for `$count`, `$sum`/`$avg` for the numeric ops.
252
+ * Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage, and the
253
+ * `columns` its `$project` reads each result from: a group key out of `_id`, a DISTINCT set by its
254
+ * size, and a `$sum` as null where it read no value, as SQL answers it. `named` is every field it reads,
255
+ * filters included, so each relation aggregate among them is put on the document first.
266
256
  */
267
257
  private buildGroupSpec;
258
+ /** `1` where `ref` holds a value, `0` where it is null or missing, which an expression tells apart. */
259
+ private static countOf;
260
+ /** Whether `ref` is null or missing: an expression compares a missing field as neither. */
261
+ private static isNullExpr;
262
+ /** The field a grouped path reads, on the document or on the joined one its lookup unwound. */
263
+ private groupedPath;
264
+ /**
265
+ * An aggregate's own `$where` as the expression a `$cond` tests, which a query filter is not: the
266
+ * comparisons and the logical operators translate, anything else is refused by name. `named` gathers
267
+ * the fields it reads, so a relation aggregate among them is on the document first.
268
+ */
269
+ private whereExpression;
270
+ /** One field's condition as an expression: a value it equals, a list it is in, or a map of comparisons. */
271
+ private fieldExpression;
268
272
  private buildHavingFilter;
269
273
  /**
270
274
  * Separate vector sort entries from regular sort entries.
271
275
  * Returns `undefined` if no vector sort is present.
272
276
  */
273
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;
274
280
  /**
275
281
  * Build a `$vectorSearch` aggregation pipeline stage.
276
282
  * Merges `$where` into `$vectorSearch.filter` for optimal pre-filtering.
277
283
  */
278
- buildVectorSearchStage<E extends Document>(entity: Type<E>, key: string, search: QueryVectorSearch, where: QueryWhere<E> | undefined, limit: number, opts?: QueryOptions, candidates?: number): Record<string, unknown>;
284
+ buildVectorSearchStage<E extends Document>(entity: Type<E>, key: string, search: QueryVectorSearch, where: QueryWhere<E> | undefined, limit: number, opts?: QueryOptions, candidates?: number): MongoAggregationPipelineEntry<Document>;
279
285
  }
280
286
  export type MongoAggregationPipelineEntry<E extends Document> = {
281
287
  $lookup?: MongoAggregationLookup;