@lobomfz/db 0.4.0 → 0.5.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.
- package/README.md +55 -4
- package/package.json +8 -5
- package/src/database.ts +184 -271
- package/src/dialect/serialize.ts +1 -1
- package/src/env.ts +2 -1
- package/src/fts/resolve.ts +208 -0
- package/src/fts/sql.ts +148 -0
- package/src/fts/tokenizer.ts +27 -0
- package/src/index.ts +18 -1
- package/src/json.ts +13 -0
- package/src/migration/data-loss-error.ts +29 -0
- package/src/migration/diff.ts +269 -25
- package/src/migration/drift-error.ts +9 -0
- package/src/migration/execute.ts +78 -26
- package/src/migration/guided.ts +136 -0
- package/src/migration/introspect.ts +138 -1
- package/src/migration/registry.ts +43 -0
- package/src/migration/types.ts +110 -1
- package/src/plugin.ts +206 -3
- package/src/schema-compiler.ts +362 -0
- package/src/types.ts +110 -12
- package/src/vector-index/load.ts +67 -0
- package/src/vector-index/nearest.ts +68 -0
- package/src/vector-index/resolve.ts +78 -0
- package/src/vector-index/sql.ts +76 -0
- package/src/vector.ts +13 -0
- package/src/write-validation-plugin.ts +92 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { Type } from "arktype";
|
|
2
|
+
|
|
3
|
+
import type { DbFieldMeta } from "./env.js";
|
|
4
|
+
import type { FtsTableInfo } from "./fts/resolve.js";
|
|
5
|
+
import type { GeneratedPreset } from "./generated.js";
|
|
6
|
+
import type { DesiredColumn, DesiredTable } from "./migration/types.js";
|
|
7
|
+
import type { IndexDefinition, StructureProp } from "./types.js";
|
|
8
|
+
import type { VectorAnchorInfo } from "./vector-index/resolve.js";
|
|
9
|
+
|
|
10
|
+
type Prop = {
|
|
11
|
+
key: string;
|
|
12
|
+
kind: "required" | "optional";
|
|
13
|
+
domain?: string;
|
|
14
|
+
nullable?: boolean;
|
|
15
|
+
isBoolean?: boolean;
|
|
16
|
+
isInteger?: boolean;
|
|
17
|
+
isDate?: boolean;
|
|
18
|
+
isBlob?: boolean;
|
|
19
|
+
isJson?: boolean;
|
|
20
|
+
isVector?: boolean;
|
|
21
|
+
vectorDim?: number;
|
|
22
|
+
jsonSchema?: Type;
|
|
23
|
+
meta?: DbFieldMeta;
|
|
24
|
+
generated?: GeneratedPreset;
|
|
25
|
+
defaultValue?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const typeMap: Record<string, string> = {
|
|
29
|
+
string: "TEXT",
|
|
30
|
+
number: "REAL",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class SchemaCompiler {
|
|
34
|
+
compileTable(name: string, schema: Type, indexDefs: IndexDefinition[]) {
|
|
35
|
+
const props = this.parseSchemaProps(schema);
|
|
36
|
+
const columns = props.map((prop) => this.compileColumn(prop));
|
|
37
|
+
|
|
38
|
+
const indexes = indexDefs.map((indexDef) => ({
|
|
39
|
+
name: this.generateIndexName(name, indexDef.columns, indexDef.unique ?? false),
|
|
40
|
+
columns: indexDef.columns,
|
|
41
|
+
sql: this.generateCreateIndexSQL(name, indexDef),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const table: DesiredTable = {
|
|
45
|
+
name,
|
|
46
|
+
sql: this.generateCreateTableSQL(name, props),
|
|
47
|
+
columns,
|
|
48
|
+
indexes,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
table,
|
|
53
|
+
tableInfo: this.buildTableInfo(props),
|
|
54
|
+
vectorAnchor: this.buildVectorAnchorInfo(props),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createWriteSchema(schema: Type) {
|
|
59
|
+
const autoIncrementColumns = this.parseSchemaProps(schema)
|
|
60
|
+
.filter((prop) => prop.generated === "autoincrement")
|
|
61
|
+
.map((prop) => prop.key);
|
|
62
|
+
|
|
63
|
+
if (autoIncrementColumns.length === 0) {
|
|
64
|
+
return schema;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (schema as any).omit(...autoIncrementColumns) as Type;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private parseSchemaProps(schema: Type) {
|
|
71
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
72
|
+
|
|
73
|
+
if (!structureProps) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return structureProps.map((p) => this.normalizeProp(p, schema));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private normalizeProp(structureProp: StructureProp, parentSchema: Type) {
|
|
81
|
+
const { key, value: v, inner } = structureProp;
|
|
82
|
+
const kind: Prop["kind"] = structureProp.required ? "required" : "optional";
|
|
83
|
+
const defaultValue = inner.default;
|
|
84
|
+
|
|
85
|
+
const concrete = v.branches.filter((b) => b.unit !== null && b.domain !== "undefined");
|
|
86
|
+
const nullable = concrete.length < v.branches.length;
|
|
87
|
+
|
|
88
|
+
const branchMeta = v.branches.find((b) => b.meta && Object.keys(b.meta).length > 0)?.meta;
|
|
89
|
+
const meta = { ...branchMeta, ...v.meta };
|
|
90
|
+
const generated = meta._generated === "vector" ? undefined : meta._generated;
|
|
91
|
+
|
|
92
|
+
if (meta._generated === "vector") {
|
|
93
|
+
return {
|
|
94
|
+
key,
|
|
95
|
+
kind,
|
|
96
|
+
nullable,
|
|
97
|
+
isVector: true,
|
|
98
|
+
vectorDim: meta._vectorDim,
|
|
99
|
+
meta,
|
|
100
|
+
defaultValue,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (v.proto === Date || concrete.some((b) => b.proto === Date)) {
|
|
105
|
+
return { key, kind, nullable, isDate: true, generated, defaultValue };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (v.proto === Uint8Array || concrete.some((b) => b.proto === Uint8Array)) {
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
kind,
|
|
112
|
+
nullable,
|
|
113
|
+
isBlob: true,
|
|
114
|
+
meta,
|
|
115
|
+
generated,
|
|
116
|
+
defaultValue,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (concrete.length > 0 && concrete.every((b) => b.domain === "boolean")) {
|
|
121
|
+
return { key, kind, nullable, isBoolean: true, generated, defaultValue };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (concrete.some((b) => !!b.structure)) {
|
|
125
|
+
return {
|
|
126
|
+
key,
|
|
127
|
+
kind,
|
|
128
|
+
nullable,
|
|
129
|
+
isJson: true,
|
|
130
|
+
jsonSchema: (parentSchema as any).get(key) as Type,
|
|
131
|
+
meta,
|
|
132
|
+
generated,
|
|
133
|
+
defaultValue,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const branch = concrete[0];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
key,
|
|
141
|
+
kind,
|
|
142
|
+
nullable,
|
|
143
|
+
domain: branch?.domain,
|
|
144
|
+
isInteger: !!branch?.inner?.divisor,
|
|
145
|
+
meta,
|
|
146
|
+
generated,
|
|
147
|
+
defaultValue,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private compileColumn(prop: Prop): DesiredColumn {
|
|
152
|
+
const isNotNull = this.columnConstraint(prop) === "NOT NULL";
|
|
153
|
+
const defaultClause = this.defaultClause(prop);
|
|
154
|
+
const hasLiteralDefault = prop.generated !== "now" && defaultClause !== null;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: prop.key,
|
|
158
|
+
addable: !isNotNull || hasLiteralDefault,
|
|
159
|
+
columnDef: this.addColumnDef(prop),
|
|
160
|
+
nullableAddDef: this.nullableAddColumnDef(prop),
|
|
161
|
+
type: this.sqlType(prop),
|
|
162
|
+
notnull: isNotNull,
|
|
163
|
+
defaultValue: defaultClause
|
|
164
|
+
? defaultClause.replace("DEFAULT ", "").replace(/^\((.+)\)$/, "$1")
|
|
165
|
+
: null,
|
|
166
|
+
unique: !!prop.meta?.unique,
|
|
167
|
+
references: prop.meta?.references ?? null,
|
|
168
|
+
onDelete: prop.meta?.onDelete?.toUpperCase() ?? null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private sqlType(prop: Prop) {
|
|
173
|
+
if (prop.isJson) {
|
|
174
|
+
return "TEXT";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (prop.isBlob || prop.isVector) {
|
|
178
|
+
return "BLOB";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (prop.isDate || prop.isBoolean || prop.isInteger) {
|
|
182
|
+
return "INTEGER";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (prop.domain) {
|
|
186
|
+
return typeMap[prop.domain] ?? "TEXT";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return "TEXT";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private columnConstraint(prop: Prop) {
|
|
193
|
+
if (prop.generated === "autoincrement") {
|
|
194
|
+
return "PRIMARY KEY AUTOINCREMENT";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (prop.meta?.primaryKey) {
|
|
198
|
+
return "PRIMARY KEY";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (prop.kind === "required" && !prop.nullable) {
|
|
202
|
+
return "NOT NULL";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private defaultClause(prop: Prop) {
|
|
209
|
+
if (prop.generated === "now") {
|
|
210
|
+
return "DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER))";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (prop.defaultValue === undefined || prop.generated === "autoincrement") {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (prop.defaultValue === null) {
|
|
218
|
+
return "DEFAULT NULL";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof prop.defaultValue === "string") {
|
|
222
|
+
return `DEFAULT '${prop.defaultValue}'`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof prop.defaultValue === "number" || typeof prop.defaultValue === "boolean") {
|
|
226
|
+
return `DEFAULT ${prop.defaultValue}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Unsupported default value type: ${typeof prop.defaultValue} ${JSON.stringify(prop)}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private columnDef(prop: Prop) {
|
|
235
|
+
return [
|
|
236
|
+
`"${prop.key}"`,
|
|
237
|
+
this.sqlType(prop),
|
|
238
|
+
this.columnConstraint(prop),
|
|
239
|
+
prop.meta?.unique ? "UNIQUE" : null,
|
|
240
|
+
this.defaultClause(prop),
|
|
241
|
+
]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join(" ");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private referencesClause(prop: Prop) {
|
|
247
|
+
const ref = prop.meta?.references;
|
|
248
|
+
|
|
249
|
+
if (!ref) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const [table, column] = ref.split(".");
|
|
254
|
+
|
|
255
|
+
let clause = ` REFERENCES "${table}"("${column}")`;
|
|
256
|
+
|
|
257
|
+
if (prop.meta?.onDelete) {
|
|
258
|
+
clause += ` ON DELETE ${prop.meta.onDelete.toUpperCase()}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return clause;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private foreignKey(prop: Prop) {
|
|
265
|
+
const clause = this.referencesClause(prop);
|
|
266
|
+
|
|
267
|
+
if (!clause) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return `FOREIGN KEY ("${prop.key}")${clause}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private addColumnDef(prop: Prop) {
|
|
275
|
+
return `${this.columnDef(prop)}${this.referencesClause(prop) ?? ""}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private nullableAddColumnDef(prop: Prop) {
|
|
279
|
+
return `"${prop.key}" ${this.sqlType(prop)}${this.referencesClause(prop) ?? ""}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private generateCreateTableSQL(tableName: string, props: Prop[]) {
|
|
283
|
+
const columns: string[] = [];
|
|
284
|
+
const fks: string[] = [];
|
|
285
|
+
|
|
286
|
+
for (const prop of props) {
|
|
287
|
+
columns.push(this.columnDef(prop));
|
|
288
|
+
|
|
289
|
+
const fk = this.foreignKey(prop);
|
|
290
|
+
|
|
291
|
+
if (fk) {
|
|
292
|
+
fks.push(fk);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.concat(fks).join(", ")})`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private generateIndexName(tableName: string, columns: string[], unique: boolean) {
|
|
300
|
+
const prefix = unique ? "ux" : "ix";
|
|
301
|
+
|
|
302
|
+
return `${prefix}_${tableName}_${columns.join("_")}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private generateCreateIndexSQL(tableName: string, indexDef: IndexDefinition) {
|
|
306
|
+
const indexName = this.generateIndexName(tableName, indexDef.columns, indexDef.unique ?? false);
|
|
307
|
+
const unique = indexDef.unique ? "UNIQUE " : "";
|
|
308
|
+
const columns = indexDef.columns.map((c) => `"${c}"`).join(", ");
|
|
309
|
+
|
|
310
|
+
return `CREATE ${unique}INDEX "${indexName}" ON "${tableName}" (${columns})`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private buildTableInfo(props: Prop[]): FtsTableInfo {
|
|
314
|
+
const pkProp = props.find(
|
|
315
|
+
(p) => p.generated === "autoincrement" || p.meta?.primaryKey === true,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
columns: new Set(props.map((p) => p.key)),
|
|
320
|
+
pkColumn: pkProp?.key ?? null,
|
|
321
|
+
pkIsInteger: pkProp ? this.sqlType(pkProp) === "INTEGER" : false,
|
|
322
|
+
foreignKeys: props.flatMap((p) => {
|
|
323
|
+
const ref = p.meta?.references;
|
|
324
|
+
|
|
325
|
+
if (!ref) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const [referencedTable, referencedColumn] = ref.split(".");
|
|
330
|
+
|
|
331
|
+
if (!referencedTable || !referencedColumn) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Invalid references format "${ref}" for column "${p.key}": expected "table.column"`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return [{ column: p.key, referencedTable, referencedColumn }];
|
|
338
|
+
}),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private buildVectorAnchorInfo(props: Prop[]): VectorAnchorInfo {
|
|
343
|
+
const pkProp = props.find(
|
|
344
|
+
(p) => p.generated === "autoincrement" || p.meta?.primaryKey === true,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const vectorColumns = new Map<string, number>();
|
|
348
|
+
|
|
349
|
+
for (const p of props) {
|
|
350
|
+
if (p.isVector && typeof p.vectorDim === "number") {
|
|
351
|
+
vectorColumns.set(p.key, p.vectorDim);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
columns: new Set(props.map((p) => p.key)),
|
|
357
|
+
pkColumn: pkProp?.key ?? null,
|
|
358
|
+
pkIsInteger: pkProp ? this.sqlType(pkProp) === "INTEGER" : false,
|
|
359
|
+
vectorColumns,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { Generated } from "kysely";
|
|
1
|
+
import type { ColumnType, Generated, Kysely } from "kysely";
|
|
2
2
|
import type { Type } from "arktype";
|
|
3
3
|
|
|
4
|
+
import type { DbFieldMeta } from "./env.js";
|
|
5
|
+
|
|
4
6
|
export type ArkBranch = {
|
|
5
7
|
domain?: string;
|
|
6
8
|
proto?: unknown;
|
|
7
9
|
unit?: unknown;
|
|
8
10
|
structure?: unknown;
|
|
11
|
+
inner?: { divisor?: unknown };
|
|
12
|
+
meta?: DbFieldMeta;
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
export type StructureProp = {
|
|
@@ -15,6 +19,7 @@ export type StructureProp = {
|
|
|
15
19
|
value: Type & {
|
|
16
20
|
branches: ArkBranch[];
|
|
17
21
|
proto?: unknown;
|
|
22
|
+
meta: DbFieldMeta;
|
|
18
23
|
};
|
|
19
24
|
};
|
|
20
25
|
|
|
@@ -25,12 +30,21 @@ type IsOptional<T, K extends keyof T> = undefined extends T[K] ? true : false;
|
|
|
25
30
|
|
|
26
31
|
type TransformColumn<T> = T extends (infer U)[] ? U[] : T;
|
|
27
32
|
|
|
33
|
+
type IsVectorColumn<TOutput, K extends keyof TOutput> =
|
|
34
|
+
NonNullable<TOutput[K]> extends Float32Array ? true : false;
|
|
35
|
+
|
|
36
|
+
type VectorInsert = Float32Array | number[];
|
|
37
|
+
|
|
28
38
|
type TransformField<TOutput, TInput, K extends keyof TOutput & keyof TInput> =
|
|
29
|
-
|
|
39
|
+
IsVectorColumn<TOutput, K> extends true
|
|
30
40
|
? IsOptional<TOutput, K> extends true
|
|
31
|
-
?
|
|
32
|
-
:
|
|
33
|
-
:
|
|
41
|
+
? ColumnType<Float32Array | null, VectorInsert | null, VectorInsert | null>
|
|
42
|
+
: ColumnType<Float32Array, VectorInsert, VectorInsert>
|
|
43
|
+
: IsOptional<TInput, K> extends true
|
|
44
|
+
? IsOptional<TOutput, K> extends true
|
|
45
|
+
? TransformColumn<NonNullable<TOutput[K]>> | null
|
|
46
|
+
: Generated<TransformColumn<NonNullable<TOutput[K]>>>
|
|
47
|
+
: TransformColumn<TOutput[K]>;
|
|
34
48
|
|
|
35
49
|
type TransformTable<TOutput, TInput> = {
|
|
36
50
|
[K in keyof TOutput & keyof TInput]: TransformField<TOutput, TInput, K>;
|
|
@@ -48,12 +62,12 @@ export type SqliteMasterRow = {
|
|
|
48
62
|
sql: string | null;
|
|
49
63
|
};
|
|
50
64
|
|
|
51
|
-
export type TablesFromSchemas<T extends SchemaRecord> = {
|
|
52
|
-
[K in keyof T]: InferTableType<T[K]>;
|
|
53
|
-
} & { sqlite_master: SqliteMasterRow };
|
|
54
|
-
|
|
55
65
|
type TableColumns<T extends SchemaRecord, K extends keyof T> = keyof ExtractOutput<T[K]> & string;
|
|
56
66
|
|
|
67
|
+
type VectorColumnName<T extends SchemaRecord, K extends keyof T> = {
|
|
68
|
+
[C in TableColumns<T, K>]: NonNullable<ExtractOutput<T[K]>[C]> extends Float32Array ? C : never;
|
|
69
|
+
}[TableColumns<T, K>];
|
|
70
|
+
|
|
57
71
|
export type IndexDefinition<Columns extends string = string> = {
|
|
58
72
|
columns: Columns[];
|
|
59
73
|
unique?: boolean;
|
|
@@ -70,18 +84,102 @@ export type DatabasePragmas = {
|
|
|
70
84
|
busy_timeout_ms?: number;
|
|
71
85
|
};
|
|
72
86
|
|
|
73
|
-
export type
|
|
87
|
+
export type FtsTokenizer =
|
|
88
|
+
| { type: "unicode61"; removeDiacritics?: 0 | 1 | 2; categories?: string }
|
|
89
|
+
| { type: "trigram" };
|
|
90
|
+
|
|
91
|
+
export type FtsFieldRef = string | { source: string; via?: string };
|
|
92
|
+
|
|
93
|
+
export type FtsEntry<T extends SchemaRecord> = {
|
|
94
|
+
anchor: keyof T & string;
|
|
95
|
+
fields: Record<string, FtsFieldRef>;
|
|
96
|
+
tokenizer?: FtsTokenizer;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type FtsConfig<T extends SchemaRecord> = Record<string, FtsEntry<T>>;
|
|
100
|
+
|
|
101
|
+
export type FtsRowType<E extends FtsEntry<any>> = { rowid: number } & {
|
|
102
|
+
[K in keyof E["fields"]]: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type VectorMetric = "cosine" | "l2" | "l1";
|
|
106
|
+
|
|
107
|
+
export type VectorIndexEntry<T extends SchemaRecord> = {
|
|
108
|
+
[K in keyof T & string]: {
|
|
109
|
+
anchor: K;
|
|
110
|
+
column: VectorColumnName<T, K>;
|
|
111
|
+
metric?: VectorMetric;
|
|
112
|
+
};
|
|
113
|
+
}[keyof T & string];
|
|
114
|
+
|
|
115
|
+
export type VectorIndexConfig<T extends SchemaRecord> = Record<string, VectorIndexEntry<T>>;
|
|
116
|
+
|
|
117
|
+
export type DatabaseExtension = { kind: "sqlite-vec"; path?: string };
|
|
118
|
+
|
|
119
|
+
export type DatabaseSchema<
|
|
120
|
+
T extends SchemaRecord,
|
|
121
|
+
F extends FtsConfig<T> = FtsConfig<T>,
|
|
122
|
+
V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
|
|
123
|
+
> = {
|
|
74
124
|
tables: T;
|
|
75
125
|
indexes?: IndexesConfig<T>;
|
|
126
|
+
fts?: F;
|
|
127
|
+
vectorIndexes?: V;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type FtsTables<F extends FtsConfig<any>> = string extends keyof F
|
|
131
|
+
? unknown
|
|
132
|
+
: { [K in keyof F]: FtsRowType<F[K]> };
|
|
133
|
+
|
|
134
|
+
type VectorIndexRow = {
|
|
135
|
+
rowid: ColumnType<number, never, never>;
|
|
136
|
+
distance: ColumnType<number, never, never>;
|
|
76
137
|
};
|
|
77
138
|
|
|
139
|
+
type VectorIndexTables<V extends VectorIndexConfig<any>> = string extends keyof V
|
|
140
|
+
? unknown
|
|
141
|
+
: { [K in keyof V]: VectorIndexRow };
|
|
142
|
+
|
|
143
|
+
export type TablesFromSchemas<
|
|
144
|
+
T extends SchemaRecord,
|
|
145
|
+
F extends FtsConfig<T> = FtsConfig<T>,
|
|
146
|
+
V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
|
|
147
|
+
> = {
|
|
148
|
+
[K in keyof T]: InferTableType<T[K]>;
|
|
149
|
+
} & FtsTables<F> &
|
|
150
|
+
VectorIndexTables<V> & { sqlite_master: SqliteMasterRow };
|
|
151
|
+
|
|
152
|
+
export type FtsApi<F extends FtsConfig<any>> = string extends keyof F
|
|
153
|
+
? never
|
|
154
|
+
: { rebuild(name: keyof F & string): void };
|
|
155
|
+
|
|
156
|
+
export type VectorIndexApi<V extends VectorIndexConfig<any>> = string extends keyof V
|
|
157
|
+
? never
|
|
158
|
+
: { rebuild(name: keyof V & string): void };
|
|
159
|
+
|
|
78
160
|
export type JsonValidation = {
|
|
79
161
|
onRead?: boolean;
|
|
80
162
|
};
|
|
81
163
|
|
|
82
|
-
export type
|
|
164
|
+
export type DataMigration<
|
|
165
|
+
T extends SchemaRecord,
|
|
166
|
+
F extends FtsConfig<T> = FtsConfig<T>,
|
|
167
|
+
V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
|
|
168
|
+
> = {
|
|
169
|
+
id: string;
|
|
170
|
+
run: (ctx: { db: Kysely<TablesFromSchemas<T, F, V>> }) => Promise<void> | void;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type DatabaseOptions<
|
|
174
|
+
T extends SchemaRecord,
|
|
175
|
+
F extends FtsConfig<T> = FtsConfig<T>,
|
|
176
|
+
V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
|
|
177
|
+
> = {
|
|
83
178
|
path: string;
|
|
84
|
-
schema: DatabaseSchema<T>;
|
|
179
|
+
schema: DatabaseSchema<T, F, V>;
|
|
85
180
|
pragmas?: DatabasePragmas;
|
|
86
181
|
validation?: JsonValidation;
|
|
182
|
+
extensions?: DatabaseExtension[];
|
|
183
|
+
failOnDataLoss?: boolean;
|
|
184
|
+
migration?: DataMigration<T, F, V>;
|
|
87
185
|
};
|
|
@@ -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
|
+
}
|