@lobomfz/db 0.4.0 → 0.4.1

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.
@@ -39,13 +39,67 @@ export type DropIndexOp = {
39
39
  index: string;
40
40
  };
41
41
 
42
+ export type FtsTriggerSpec = {
43
+ name: string;
44
+ sql: string;
45
+ };
46
+
47
+ export type CreateFtsOp = {
48
+ type: "CreateFts";
49
+ name: string;
50
+ createSql: string;
51
+ triggers: FtsTriggerSpec[];
52
+ };
53
+
54
+ export type DropFtsOp = {
55
+ type: "DropFts";
56
+ name: string;
57
+ triggerNames: string[];
58
+ };
59
+
60
+ export type RebuildFtsOp = {
61
+ type: "RebuildFts";
62
+ name: string;
63
+ rebuildSql: string;
64
+ };
65
+
66
+ export type VectorTriggerSpec = {
67
+ name: string;
68
+ sql: string;
69
+ };
70
+
71
+ export type CreateVectorIndexOp = {
72
+ type: "CreateVectorIndex";
73
+ name: string;
74
+ createSql: string;
75
+ triggers: VectorTriggerSpec[];
76
+ };
77
+
78
+ export type DropVectorIndexOp = {
79
+ type: "DropVectorIndex";
80
+ name: string;
81
+ triggerNames: string[];
82
+ };
83
+
84
+ export type RebuildVectorIndexOp = {
85
+ type: "RebuildVectorIndex";
86
+ name: string;
87
+ rebuildSql: string;
88
+ };
89
+
42
90
  export type MigrationOp =
43
91
  | CreateTableOp
44
92
  | DropTableOp
45
93
  | AddColumnOp
46
94
  | RebuildTableOp
47
95
  | CreateIndexOp
48
- | DropIndexOp;
96
+ | DropIndexOp
97
+ | CreateFtsOp
98
+ | DropFtsOp
99
+ | RebuildFtsOp
100
+ | CreateVectorIndexOp
101
+ | DropVectorIndexOp
102
+ | RebuildVectorIndexOp;
49
103
 
50
104
  export type ColumnSchema = {
51
105
  name: string;
@@ -72,3 +126,16 @@ export type IntrospectedTable = {
72
126
  indexes: IntrospectedIndex[];
73
127
  hasData: boolean;
74
128
  };
129
+
130
+ export type IntrospectedFts = {
131
+ name: string;
132
+ columns: string[];
133
+ sql: string;
134
+ triggers: Map<string, string>;
135
+ };
136
+
137
+ export type IntrospectedVectorIndex = {
138
+ name: string;
139
+ sql: string;
140
+ triggers: Map<string, string>;
141
+ };
package/src/plugin.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type OperationNode,
6
6
  type QueryId,
7
7
  type RootOperationNode,
8
+ type SelectionNode as KyselySelectionNode,
8
9
  type UnknownRow,
9
10
  AggregateFunctionNode,
10
11
  AliasNode,
@@ -12,7 +13,9 @@ import {
12
13
  ColumnNode,
13
14
  IdentifierNode,
14
15
  ParensNode,
16
+ RawNode,
15
17
  ReferenceNode,
18
+ SelectionNode,
16
19
  SelectQueryNode,
17
20
  TableNode,
18
21
  } from "kysely";
@@ -21,7 +24,11 @@ import { JsonParseError } from "./errors.js";
21
24
  import type { StructureProp } from "./types.js";
22
25
  import { ValidationError } from "./validation-error.js";
23
26
 
24
- type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
27
+ type ColumnCoercion =
28
+ | "boolean"
29
+ | "date"
30
+ | { type: "json"; schema: Type }
31
+ | { type: "vector"; dim: number };
25
32
 
26
33
  type ResolvedCoercion = { table: string; coercion: ColumnCoercion };
27
34
 
@@ -121,7 +128,27 @@ export class ResultHydrationPlugin implements KyselyPlugin {
121
128
  }
122
129
  }
123
130
 
131
+ private extractFieldMeta(prop: StructureProp) {
132
+ const rootMeta = (prop.value as any).meta as
133
+ | { _generated?: string; _vectorDim?: number }
134
+ | undefined;
135
+ const branchWithMeta = prop.value.branches.find(
136
+ (b) => (b as any).meta && Object.keys((b as any).meta).length > 0,
137
+ );
138
+ const fromBranch = (branchWithMeta as any)?.meta as
139
+ | { _generated?: string; _vectorDim?: number }
140
+ | undefined;
141
+
142
+ return { ...fromBranch, ...rootMeta };
143
+ }
144
+
124
145
  private getColumnCoercion(prop: StructureProp, parentSchema: Type) {
146
+ const meta = this.extractFieldMeta(prop);
147
+
148
+ if (meta._generated === "vector" && typeof meta._vectorDim === "number") {
149
+ return { type: "vector", dim: meta._vectorDim } satisfies ColumnCoercion;
150
+ }
151
+
125
152
  const concrete = prop.value.branches.filter(
126
153
  (branch) => branch.unit !== null && branch.domain !== "undefined",
127
154
  );
@@ -150,9 +177,151 @@ export class ResultHydrationPlugin implements KyselyPlugin {
150
177
  selectionPlans: this.getSelectionPlans(args.node),
151
178
  });
152
179
 
153
- return args.node;
180
+ return this.rewriteVectorsInJsonHelpers(args.node);
154
181
  };
155
182
 
183
+ private rewriteVectorsInJsonHelpers(node: RootOperationNode): RootOperationNode {
184
+ if (!SelectQueryNode.is(node)) {
185
+ return node;
186
+ }
187
+
188
+ return this.rewriteSelectQueryNode(node) as RootOperationNode;
189
+ }
190
+
191
+ private rewriteSelectQueryNode(node: SelectQueryNode): SelectQueryNode {
192
+ if (!node.selections) {
193
+ return node;
194
+ }
195
+
196
+ let changed = false;
197
+
198
+ const newSelections = node.selections.map((sel) => {
199
+ const rewritten = this.rewriteTopLevelSelection(sel);
200
+
201
+ if (rewritten !== sel) {
202
+ changed = true;
203
+ }
204
+
205
+ return rewritten;
206
+ });
207
+
208
+ if (!changed) {
209
+ return node;
210
+ }
211
+
212
+ return Object.freeze({ ...node, selections: Object.freeze(newSelections) });
213
+ }
214
+
215
+ private rewriteTopLevelSelection(sel: KyselySelectionNode): KyselySelectionNode {
216
+ const rewritten = this.rewriteNodeForJsonHelpers(sel.selection);
217
+
218
+ if (rewritten === sel.selection) {
219
+ return sel;
220
+ }
221
+
222
+ return SelectionNode.create(rewritten as any);
223
+ }
224
+
225
+ private rewriteNodeForJsonHelpers(node: OperationNode): OperationNode {
226
+ if (AliasNode.is(node)) {
227
+ const rewrittenInner = this.rewriteNodeForJsonHelpers(node.node);
228
+
229
+ if (rewrittenInner === node.node) {
230
+ return node;
231
+ }
232
+
233
+ return AliasNode.create(rewrittenInner, node.alias as any);
234
+ }
235
+
236
+ if (!this.isRawNode(node)) {
237
+ return node;
238
+ }
239
+
240
+ const helper = this.getJsonHelper(node);
241
+
242
+ if (!helper) {
243
+ return node;
244
+ }
245
+
246
+ return this.rewriteJsonHelper(node, helper.query);
247
+ }
248
+
249
+ private rewriteJsonHelper(rawNode: RawOperationNode, subquery: SelectQueryNode) {
250
+ const scope = this.getTableScope(subquery);
251
+
252
+ if (!subquery.selections) {
253
+ return rawNode;
254
+ }
255
+
256
+ let changed = false;
257
+
258
+ const newSubquerySelections = subquery.selections.map((sel) => {
259
+ const rewrittenNested = this.rewriteNodeForJsonHelpers(sel.selection);
260
+ const withNested =
261
+ rewrittenNested === sel.selection
262
+ ? sel
263
+ : SelectionNode.create(rewrittenNested as any);
264
+
265
+ if (withNested !== sel) {
266
+ changed = true;
267
+ }
268
+
269
+ const wrapped = this.wrapVectorSelectionWithHex(withNested, scope);
270
+
271
+ if (wrapped !== withNested) {
272
+ changed = true;
273
+ }
274
+
275
+ return wrapped;
276
+ });
277
+
278
+ if (!changed) {
279
+ return rawNode;
280
+ }
281
+
282
+ const newSubquery = Object.freeze({
283
+ ...subquery,
284
+ selections: Object.freeze(newSubquerySelections),
285
+ });
286
+ const newParameters = [...rawNode.parameters];
287
+ newParameters[1] = newSubquery;
288
+
289
+ return Object.freeze({
290
+ ...rawNode,
291
+ parameters: Object.freeze(newParameters),
292
+ });
293
+ }
294
+
295
+ private wrapVectorSelectionWithHex(sel: KyselySelectionNode, scope: Map<string, string>) {
296
+ const selectionNode = sel.selection;
297
+ const output = this.getSelectionOutputName(selectionNode);
298
+
299
+ if (!output) {
300
+ return sel;
301
+ }
302
+
303
+ const inner = AliasNode.is(selectionNode) ? selectionNode.node : selectionNode;
304
+
305
+ if (!ReferenceNode.is(inner) && !ColumnNode.is(inner)) {
306
+ return sel;
307
+ }
308
+
309
+ const resolved = this.resolveReferenceCoercion(inner, scope);
310
+
311
+ if (!resolved) {
312
+ return sel;
313
+ }
314
+
315
+ if (typeof resolved.coercion !== "object" || resolved.coercion.type !== "vector") {
316
+ return sel;
317
+ }
318
+
319
+ const hexNode = RawNode.create(["hex(", ")"], [inner]);
320
+ const aliased = AliasNode.create(hexNode, IdentifierNode.create(output));
321
+
322
+ return SelectionNode.create(aliased);
323
+ }
324
+
156
325
  private getTableFromNode(node: RootOperationNode) {
157
326
  switch (node.kind) {
158
327
  case "InsertQueryNode":
@@ -225,12 +394,16 @@ export class ResultHydrationPlugin implements KyselyPlugin {
225
394
 
226
395
  if (plan.coercion === "date") {
227
396
  if (typeof value === "number") {
228
- return new Date(value * 1000);
397
+ return new Date(value);
229
398
  }
230
399
 
231
400
  return value;
232
401
  }
233
402
 
403
+ if (plan.coercion.type === "vector") {
404
+ return this.hydrateVector(value);
405
+ }
406
+
234
407
  const parsed =
235
408
  typeof value === "string" ? this.parseJson(plan.table, plan.column, value) : value;
236
409
 
@@ -241,6 +414,36 @@ export class ResultHydrationPlugin implements KyselyPlugin {
241
414
  return parsed;
242
415
  }
243
416
 
417
+ private hydrateVector(value: unknown) {
418
+ if (value instanceof Float32Array) {
419
+ return value;
420
+ }
421
+
422
+ if (value instanceof Uint8Array) {
423
+ return new Float32Array(
424
+ value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength),
425
+ );
426
+ }
427
+
428
+ if (typeof value === "string") {
429
+ return this.hexToFloat32Array(value);
430
+ }
431
+
432
+ return value;
433
+ }
434
+
435
+ private hexToFloat32Array(hex: string) {
436
+ const byteLength = hex.length / 2;
437
+ const buffer = new ArrayBuffer(byteLength);
438
+ const bytes = new Uint8Array(buffer);
439
+
440
+ for (let i = 0; i < byteLength; i++) {
441
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
442
+ }
443
+
444
+ return new Float32Array(buffer);
445
+ }
446
+
244
447
  private parseStructuredValue(table: string, column: string, value: unknown) {
245
448
  if (value === null || value === undefined) {
246
449
  return value;
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Generated } from "kysely";
1
+ import type { ColumnType, Generated } from "kysely";
2
2
  import type { Type } from "arktype";
3
3
 
4
4
  export type ArkBranch = {
@@ -25,12 +25,21 @@ type IsOptional<T, K extends keyof T> = undefined extends T[K] ? true : false;
25
25
 
26
26
  type TransformColumn<T> = T extends (infer U)[] ? U[] : T;
27
27
 
28
+ type IsVectorColumn<TOutput, K extends keyof TOutput> =
29
+ NonNullable<TOutput[K]> extends Float32Array ? true : false;
30
+
31
+ type VectorInsert = Float32Array | number[];
32
+
28
33
  type TransformField<TOutput, TInput, K extends keyof TOutput & keyof TInput> =
29
- IsOptional<TInput, K> extends true
34
+ IsVectorColumn<TOutput, K> extends true
30
35
  ? IsOptional<TOutput, K> extends true
31
- ? TransformColumn<NonNullable<TOutput[K]>> | null
32
- : Generated<TransformColumn<NonNullable<TOutput[K]>>>
33
- : TransformColumn<TOutput[K]>;
36
+ ? ColumnType<Float32Array | null, VectorInsert | null, VectorInsert | null>
37
+ : ColumnType<Float32Array, VectorInsert, VectorInsert>
38
+ : IsOptional<TInput, K> extends true
39
+ ? IsOptional<TOutput, K> extends true
40
+ ? TransformColumn<NonNullable<TOutput[K]>> | null
41
+ : Generated<TransformColumn<NonNullable<TOutput[K]>>>
42
+ : TransformColumn<TOutput[K]>;
34
43
 
35
44
  type TransformTable<TOutput, TInput> = {
36
45
  [K in keyof TOutput & keyof TInput]: TransformField<TOutput, TInput, K>;
@@ -48,12 +57,12 @@ export type SqliteMasterRow = {
48
57
  sql: string | null;
49
58
  };
50
59
 
51
- export type TablesFromSchemas<T extends SchemaRecord> = {
52
- [K in keyof T]: InferTableType<T[K]>;
53
- } & { sqlite_master: SqliteMasterRow };
54
-
55
60
  type TableColumns<T extends SchemaRecord, K extends keyof T> = keyof ExtractOutput<T[K]> & string;
56
61
 
62
+ type VectorColumnName<T extends SchemaRecord, K extends keyof T> = {
63
+ [C in TableColumns<T, K>]: NonNullable<ExtractOutput<T[K]>[C]> extends Float32Array ? C : never;
64
+ }[TableColumns<T, K>];
65
+
57
66
  export type IndexDefinition<Columns extends string = string> = {
58
67
  columns: Columns[];
59
68
  unique?: boolean;
@@ -70,18 +79,91 @@ export type DatabasePragmas = {
70
79
  busy_timeout_ms?: number;
71
80
  };
72
81
 
73
- export type DatabaseSchema<T extends SchemaRecord> = {
82
+ export type FtsTokenizer =
83
+ | { type: "unicode61"; removeDiacritics?: 0 | 1 | 2; categories?: string }
84
+ | { type: "trigram" };
85
+
86
+ export type FtsFieldRef = string | { source: string; via?: string };
87
+
88
+ export type FtsEntry<T extends SchemaRecord> = {
89
+ anchor: keyof T & string;
90
+ fields: Record<string, FtsFieldRef>;
91
+ tokenizer?: FtsTokenizer;
92
+ };
93
+
94
+ export type FtsConfig<T extends SchemaRecord> = Record<string, FtsEntry<T>>;
95
+
96
+ export type FtsRowType<E extends FtsEntry<any>> = { rowid: number } & {
97
+ [K in keyof E["fields"]]: string;
98
+ };
99
+
100
+ export type VectorMetric = "cosine" | "l2" | "l1";
101
+
102
+ export type VectorIndexEntry<T extends SchemaRecord> = {
103
+ [K in keyof T & string]: {
104
+ anchor: K;
105
+ column: VectorColumnName<T, K>;
106
+ metric?: VectorMetric;
107
+ };
108
+ }[keyof T & string];
109
+
110
+ export type VectorIndexConfig<T extends SchemaRecord> = Record<string, VectorIndexEntry<T>>;
111
+
112
+ export type DatabaseExtension = { kind: "sqlite-vec"; path?: string };
113
+
114
+ export type DatabaseSchema<
115
+ T extends SchemaRecord,
116
+ F extends FtsConfig<T> = FtsConfig<T>,
117
+ V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
118
+ > = {
74
119
  tables: T;
75
120
  indexes?: IndexesConfig<T>;
121
+ fts?: F;
122
+ vectorIndexes?: V;
123
+ };
124
+
125
+ type FtsTables<F extends FtsConfig<any>> = string extends keyof F
126
+ ? unknown
127
+ : { [K in keyof F]: FtsRowType<F[K]> };
128
+
129
+ type VectorIndexRow = {
130
+ rowid: ColumnType<number, never, never>;
131
+ distance: ColumnType<number, never, never>;
76
132
  };
77
133
 
134
+ type VectorIndexTables<V extends VectorIndexConfig<any>> = string extends keyof V
135
+ ? unknown
136
+ : { [K in keyof V]: VectorIndexRow };
137
+
138
+ export type TablesFromSchemas<
139
+ T extends SchemaRecord,
140
+ F extends FtsConfig<T> = FtsConfig<T>,
141
+ V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
142
+ > = {
143
+ [K in keyof T]: InferTableType<T[K]>;
144
+ } & FtsTables<F> &
145
+ VectorIndexTables<V> & { sqlite_master: SqliteMasterRow };
146
+
147
+ export type FtsApi<F extends FtsConfig<any>> = string extends keyof F
148
+ ? never
149
+ : { rebuild(name: keyof F & string): void };
150
+
151
+ export type VectorIndexApi<V extends VectorIndexConfig<any>> = string extends keyof V
152
+ ? never
153
+ : { rebuild(name: keyof V & string): void };
154
+
78
155
  export type JsonValidation = {
79
156
  onRead?: boolean;
80
157
  };
81
158
 
82
- export type DatabaseOptions<T extends SchemaRecord> = {
159
+ export type DatabaseOptions<
160
+ T extends SchemaRecord,
161
+ F extends FtsConfig<T> = FtsConfig<T>,
162
+ V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
163
+ > = {
83
164
  path: string;
84
- schema: DatabaseSchema<T>;
165
+ schema: DatabaseSchema<T, F, V>;
85
166
  pragmas?: DatabasePragmas;
86
167
  validation?: JsonValidation;
168
+ extensions?: DatabaseExtension[];
87
169
  };
@@ -0,0 +1,67 @@
1
+ import type { Database as BunDatabase } from "bun:sqlite";
2
+ import { createRequire } from "node:module";
3
+
4
+ import type { DatabaseExtension } from "../types.js";
5
+
6
+ export class ExtensionLoadError extends Error {
7
+ readonly path: string;
8
+ override readonly cause: unknown;
9
+
10
+ constructor(path: string, cause: unknown, hint: string) {
11
+ const message =
12
+ cause instanceof Error ? cause.message : typeof cause === "string" ? cause : String(cause);
13
+
14
+ super(`Failed to load sqlite-vec extension at "${path}": ${message}. ${hint}`);
15
+ this.name = "ExtensionLoadError";
16
+ this.path = path;
17
+ this.cause = cause;
18
+ }
19
+ }
20
+
21
+ const installHint = "Install it with `bun add sqlite-vec` (or npm install sqlite-vec).";
22
+
23
+ function resolveSqliteVecPath(explicit: string | undefined) {
24
+ if (explicit) {
25
+ return explicit;
26
+ }
27
+
28
+ const require = createRequire(import.meta.url);
29
+
30
+ let mod: { getLoadablePath?: () => string };
31
+
32
+ try {
33
+ mod = require("sqlite-vec");
34
+ } catch (cause: any) {
35
+ throw new ExtensionLoadError("sqlite-vec", cause, installHint);
36
+ }
37
+
38
+ if (typeof mod.getLoadablePath !== "function") {
39
+ throw new ExtensionLoadError(
40
+ "sqlite-vec",
41
+ new Error("sqlite-vec does not export getLoadablePath"),
42
+ installHint,
43
+ );
44
+ }
45
+
46
+ try {
47
+ return mod.getLoadablePath();
48
+ } catch (cause: any) {
49
+ throw new ExtensionLoadError("sqlite-vec", cause, installHint);
50
+ }
51
+ }
52
+
53
+ export function loadExtensions(sqlite: BunDatabase, extensions: DatabaseExtension[]) {
54
+ for (const ext of extensions) {
55
+ if (ext.kind !== "sqlite-vec") {
56
+ continue;
57
+ }
58
+
59
+ const path = resolveSqliteVecPath(ext.path);
60
+
61
+ try {
62
+ sqlite.loadExtension(path);
63
+ } catch (cause: any) {
64
+ throw new ExtensionLoadError(path, cause, installHint);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,68 @@
1
+ import { sql, type SelectQueryBuilder } from "kysely";
2
+
3
+ import type { ResolvedVectorIndex } from "./resolve.js";
4
+
5
+ type IndexMeta = Pick<ResolvedVectorIndex, "anchor" | "anchorPkColumn" | "column" | "dim">;
6
+
7
+ const registry = new Map<string, IndexMeta>();
8
+
9
+ export function registerVectorIndex(index: ResolvedVectorIndex) {
10
+ registry.set(index.name, {
11
+ anchor: index.anchor,
12
+ anchorPkColumn: index.anchorPkColumn,
13
+ column: index.column,
14
+ dim: index.dim,
15
+ });
16
+ }
17
+
18
+ export function unregisterVectorIndex(name: string) {
19
+ registry.delete(name);
20
+ }
21
+
22
+ export type NearestOptions<IndexName extends string = string> = {
23
+ indexName: IndexName;
24
+ vector: Float32Array | number[];
25
+ k: number;
26
+ };
27
+
28
+ type WithDistance<O> = O & { distance: number };
29
+
30
+ function vectorToBytes(v: Float32Array | number[]) {
31
+ const f = v instanceof Float32Array ? v : Float32Array.from(v);
32
+
33
+ return new Uint8Array(f.buffer, f.byteOffset, f.byteLength);
34
+ }
35
+
36
+ export function nearest<IndexName extends string>(options: NearestOptions<IndexName>) {
37
+ return <DB extends { [K in IndexName]: unknown }, TB extends keyof DB & string, O>(
38
+ qb: SelectQueryBuilder<DB, TB, O>,
39
+ ): SelectQueryBuilder<DB, TB | (IndexName & keyof DB & string), WithDistance<O>> => {
40
+ const meta = registry.get(options.indexName);
41
+
42
+ if (!meta) {
43
+ throw new Error(
44
+ `Vector index "${options.indexName}" is not registered. Declare it in the schema's vectorIndexes and construct the Database before calling nearest.`,
45
+ );
46
+ }
47
+
48
+ const bytes = vectorToBytes(options.vector);
49
+ const indexName = options.indexName;
50
+ const column = meta.column;
51
+ const anchor = meta.anchor;
52
+ const anchorPk = meta.anchorPkColumn;
53
+
54
+ const anyQb = qb as unknown as {
55
+ innerJoin: (a: any, b: any, c: any) => any;
56
+ where: (expr: any) => any;
57
+ select: (expr: any) => any;
58
+ orderBy: (expr: any) => any;
59
+ };
60
+
61
+ return anyQb
62
+ .innerJoin(indexName, `${indexName}.rowid`, `${anchor}.${anchorPk}`)
63
+ .where(sql<boolean>`${sql.ref(`${indexName}.${column}`)} MATCH ${bytes}`)
64
+ .where(sql<boolean>`${sql.ref(`${indexName}.k`)} = ${options.k}`)
65
+ .select(sql<number>`${sql.ref(`${indexName}.distance`)}`.as("distance"))
66
+ .orderBy(`${indexName}.distance`);
67
+ };
68
+ }
@@ -0,0 +1,78 @@
1
+ import type { VectorIndexEntry, VectorMetric } from "../types.js";
2
+
3
+ export type VectorAnchorInfo = {
4
+ columns: Set<string>;
5
+ pkColumn: string | null;
6
+ pkIsInteger: boolean;
7
+ vectorColumns: Map<string, number>;
8
+ };
9
+
10
+ export type ResolvedVectorIndex = {
11
+ name: string;
12
+ anchor: string;
13
+ anchorPkColumn: string;
14
+ column: string;
15
+ dim: number;
16
+ metric: VectorMetric;
17
+ };
18
+
19
+ const DEFAULT_METRIC: VectorMetric = "cosine";
20
+
21
+ const VALID_METRICS = new Set<VectorMetric>(["cosine", "l2", "l1"]);
22
+
23
+ export function resolveVectorIndex(
24
+ name: string,
25
+ entry: VectorIndexEntry<any>,
26
+ anchors: Map<string, VectorAnchorInfo>,
27
+ ) {
28
+ const anchor = anchors.get(entry.anchor);
29
+
30
+ if (!anchor) {
31
+ throw new Error(
32
+ `VectorIndex "${name}": anchor table "${entry.anchor}" is not declared in schema`,
33
+ );
34
+ }
35
+
36
+ if (!anchor.pkColumn) {
37
+ throw new Error(
38
+ `VectorIndex "${name}": anchor table "${entry.anchor}" has no primary key; anchor PK must be an integer`,
39
+ );
40
+ }
41
+
42
+ if (!anchor.pkIsInteger) {
43
+ throw new Error(
44
+ `VectorIndex "${name}": anchor table "${entry.anchor}" primary key "${anchor.pkColumn}" must be an integer`,
45
+ );
46
+ }
47
+
48
+ if (!anchor.columns.has(entry.column)) {
49
+ throw new Error(
50
+ `VectorIndex "${name}": column "${entry.column}" does not exist on anchor "${entry.anchor}"`,
51
+ );
52
+ }
53
+
54
+ const dim = anchor.vectorColumns.get(entry.column);
55
+
56
+ if (dim === undefined) {
57
+ throw new Error(
58
+ `VectorIndex "${name}": column "${entry.anchor}.${entry.column}" is not a vector column`,
59
+ );
60
+ }
61
+
62
+ const metric = entry.metric ?? DEFAULT_METRIC;
63
+
64
+ if (!VALID_METRICS.has(metric)) {
65
+ throw new Error(
66
+ `VectorIndex "${name}": unknown metric "${metric}", expected one of cosine, l2, l1`,
67
+ );
68
+ }
69
+
70
+ return {
71
+ name,
72
+ anchor: entry.anchor,
73
+ anchorPkColumn: anchor.pkColumn,
74
+ column: entry.column,
75
+ dim,
76
+ metric,
77
+ };
78
+ }