@lobomfz/db 0.3.9 → 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.
- package/README.md +3 -3
- package/package.json +8 -5
- package/src/database.ts +235 -42
- 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/generated.ts +1 -1
- package/src/index.ts +16 -2
- package/src/json.ts +13 -0
- package/src/migration/diff.ts +194 -1
- package/src/migration/execute.ts +42 -0
- package/src/migration/introspect.ts +133 -1
- package/src/migration/types.ts +68 -1
- package/src/plugin.ts +269 -9
- package/src/types.ts +111 -13
- package/src/validation-error.ts +8 -4
- 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 +419 -52
package/src/migration/types.ts
CHANGED
|
@@ -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,16 +13,22 @@ 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";
|
|
19
22
|
|
|
20
23
|
import { JsonParseError } from "./errors.js";
|
|
21
|
-
import {
|
|
24
|
+
import type { StructureProp } from "./types.js";
|
|
25
|
+
import { ValidationError } from "./validation-error.js";
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
|
@@ -79,13 +86,90 @@ const jsonObjectFromFragments = [
|
|
|
79
86
|
const typePreservingAggregateFunctions = new Set(["max", "min"]);
|
|
80
87
|
|
|
81
88
|
export class ResultHydrationPlugin implements KyselyPlugin {
|
|
89
|
+
private columns = new Map<string, Map<string, ColumnCoercion>>();
|
|
90
|
+
private tableColumns = new Map<string, Set<string>>();
|
|
82
91
|
private queryPlans = new WeakMap<QueryId, QueryPlan>();
|
|
83
92
|
|
|
84
93
|
constructor(
|
|
85
|
-
|
|
86
|
-
private tableColumns: Map<string, Set<string>>,
|
|
94
|
+
schemas: Map<string, Type>,
|
|
87
95
|
private validation: { onRead: boolean },
|
|
88
|
-
) {
|
|
96
|
+
) {
|
|
97
|
+
this.registerSchemas(schemas);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private registerSchemas(schemas: Map<string, Type>) {
|
|
101
|
+
for (const [table, schema] of schemas) {
|
|
102
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
103
|
+
|
|
104
|
+
if (!structureProps) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.tableColumns.set(
|
|
109
|
+
table,
|
|
110
|
+
new Set(structureProps.map((prop) => prop.key)),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const columns = new Map<string, ColumnCoercion>();
|
|
114
|
+
|
|
115
|
+
for (const prop of structureProps) {
|
|
116
|
+
const coercion = this.getColumnCoercion(prop, schema);
|
|
117
|
+
|
|
118
|
+
if (!coercion) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
columns.set(prop.key, coercion);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (columns.size > 0) {
|
|
126
|
+
this.columns.set(table, columns);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
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
|
+
|
|
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
|
+
|
|
152
|
+
const concrete = prop.value.branches.filter(
|
|
153
|
+
(branch) => branch.unit !== null && branch.domain !== "undefined",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (prop.value.proto === Date || concrete.some((branch) => branch.proto === Date)) {
|
|
157
|
+
return "date" satisfies ColumnCoercion;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (concrete.length > 0 && concrete.every((branch) => branch.domain === "boolean")) {
|
|
161
|
+
return "boolean" satisfies ColumnCoercion;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (concrete.some((branch) => !!branch.structure)) {
|
|
165
|
+
return {
|
|
166
|
+
type: "json",
|
|
167
|
+
schema: (parentSchema as any).get(prop.key) as Type,
|
|
168
|
+
} satisfies ColumnCoercion;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
89
173
|
|
|
90
174
|
transformQuery: KyselyPlugin["transformQuery"] = (args) => {
|
|
91
175
|
this.queryPlans.set(args.queryId, {
|
|
@@ -93,9 +177,151 @@ export class ResultHydrationPlugin implements KyselyPlugin {
|
|
|
93
177
|
selectionPlans: this.getSelectionPlans(args.node),
|
|
94
178
|
});
|
|
95
179
|
|
|
96
|
-
return args.node;
|
|
180
|
+
return this.rewriteVectorsInJsonHelpers(args.node);
|
|
97
181
|
};
|
|
98
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
|
+
|
|
99
325
|
private getTableFromNode(node: RootOperationNode) {
|
|
100
326
|
switch (node.kind) {
|
|
101
327
|
case "InsertQueryNode":
|
|
@@ -141,7 +367,7 @@ export class ResultHydrationPlugin implements KyselyPlugin {
|
|
|
141
367
|
const result = schema(value);
|
|
142
368
|
|
|
143
369
|
if (result instanceof type.errors) {
|
|
144
|
-
throw new
|
|
370
|
+
throw new ValidationError(table, result.summary, col);
|
|
145
371
|
}
|
|
146
372
|
}
|
|
147
373
|
|
|
@@ -168,12 +394,16 @@ export class ResultHydrationPlugin implements KyselyPlugin {
|
|
|
168
394
|
|
|
169
395
|
if (plan.coercion === "date") {
|
|
170
396
|
if (typeof value === "number") {
|
|
171
|
-
return new Date(value
|
|
397
|
+
return new Date(value);
|
|
172
398
|
}
|
|
173
399
|
|
|
174
400
|
return value;
|
|
175
401
|
}
|
|
176
402
|
|
|
403
|
+
if (plan.coercion.type === "vector") {
|
|
404
|
+
return this.hydrateVector(value);
|
|
405
|
+
}
|
|
406
|
+
|
|
177
407
|
const parsed =
|
|
178
408
|
typeof value === "string" ? this.parseJson(plan.table, plan.column, value) : value;
|
|
179
409
|
|
|
@@ -184,6 +414,36 @@ export class ResultHydrationPlugin implements KyselyPlugin {
|
|
|
184
414
|
return parsed;
|
|
185
415
|
}
|
|
186
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
|
+
|
|
187
447
|
private parseStructuredValue(table: string, column: string, value: unknown) {
|
|
188
448
|
if (value === null || value === undefined) {
|
|
189
449
|
return value;
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import type { Generated } from "kysely";
|
|
1
|
+
import type { ColumnType, Generated } from "kysely";
|
|
2
2
|
import type { Type } from "arktype";
|
|
3
3
|
|
|
4
|
+
export type ArkBranch = {
|
|
5
|
+
domain?: string;
|
|
6
|
+
proto?: unknown;
|
|
7
|
+
unit?: unknown;
|
|
8
|
+
structure?: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type StructureProp = {
|
|
12
|
+
key: string;
|
|
13
|
+
required: boolean;
|
|
14
|
+
inner: { default?: unknown };
|
|
15
|
+
value: Type & {
|
|
16
|
+
branches: ArkBranch[];
|
|
17
|
+
proto?: unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
type ExtractInput<T> = T extends { inferIn: infer I } ? I : never;
|
|
5
22
|
type ExtractOutput<T> = T extends { infer: infer O } ? O : never;
|
|
6
23
|
|
|
@@ -8,12 +25,21 @@ type IsOptional<T, K extends keyof T> = undefined extends T[K] ? true : false;
|
|
|
8
25
|
|
|
9
26
|
type TransformColumn<T> = T extends (infer U)[] ? U[] : T;
|
|
10
27
|
|
|
28
|
+
type IsVectorColumn<TOutput, K extends keyof TOutput> =
|
|
29
|
+
NonNullable<TOutput[K]> extends Float32Array ? true : false;
|
|
30
|
+
|
|
31
|
+
type VectorInsert = Float32Array | number[];
|
|
32
|
+
|
|
11
33
|
type TransformField<TOutput, TInput, K extends keyof TOutput & keyof TInput> =
|
|
12
|
-
|
|
34
|
+
IsVectorColumn<TOutput, K> extends true
|
|
13
35
|
? IsOptional<TOutput, K> extends true
|
|
14
|
-
?
|
|
15
|
-
:
|
|
16
|
-
:
|
|
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]>;
|
|
17
43
|
|
|
18
44
|
type TransformTable<TOutput, TInput> = {
|
|
19
45
|
[K in keyof TOutput & keyof TInput]: TransformField<TOutput, TInput, K>;
|
|
@@ -31,12 +57,12 @@ export type SqliteMasterRow = {
|
|
|
31
57
|
sql: string | null;
|
|
32
58
|
};
|
|
33
59
|
|
|
34
|
-
export type TablesFromSchemas<T extends SchemaRecord> = {
|
|
35
|
-
[K in keyof T]: InferTableType<T[K]>;
|
|
36
|
-
} & { sqlite_master: SqliteMasterRow };
|
|
37
|
-
|
|
38
60
|
type TableColumns<T extends SchemaRecord, K extends keyof T> = keyof ExtractOutput<T[K]> & string;
|
|
39
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
|
+
|
|
40
66
|
export type IndexDefinition<Columns extends string = string> = {
|
|
41
67
|
columns: Columns[];
|
|
42
68
|
unique?: boolean;
|
|
@@ -53,19 +79,91 @@ export type DatabasePragmas = {
|
|
|
53
79
|
busy_timeout_ms?: number;
|
|
54
80
|
};
|
|
55
81
|
|
|
56
|
-
export type
|
|
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
|
+
> = {
|
|
57
119
|
tables: T;
|
|
58
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>;
|
|
59
132
|
};
|
|
60
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
|
+
|
|
61
155
|
export type JsonValidation = {
|
|
62
156
|
onRead?: boolean;
|
|
63
|
-
onWrite?: boolean;
|
|
64
157
|
};
|
|
65
158
|
|
|
66
|
-
export type DatabaseOptions<
|
|
159
|
+
export type DatabaseOptions<
|
|
160
|
+
T extends SchemaRecord,
|
|
161
|
+
F extends FtsConfig<T> = FtsConfig<T>,
|
|
162
|
+
V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
|
|
163
|
+
> = {
|
|
67
164
|
path: string;
|
|
68
|
-
schema: DatabaseSchema<T>;
|
|
165
|
+
schema: DatabaseSchema<T, F, V>;
|
|
69
166
|
pragmas?: DatabasePragmas;
|
|
70
167
|
validation?: JsonValidation;
|
|
168
|
+
extensions?: DatabaseExtension[];
|
|
71
169
|
};
|
package/src/validation-error.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
export class
|
|
1
|
+
export class ValidationError extends Error {
|
|
2
2
|
constructor(
|
|
3
3
|
readonly table: string,
|
|
4
|
-
readonly column: string,
|
|
5
4
|
readonly summary: string,
|
|
5
|
+
readonly column: string | null = null,
|
|
6
6
|
) {
|
|
7
|
-
super(
|
|
7
|
+
super(
|
|
8
|
+
column
|
|
9
|
+
? `Validation failed for ${table}.${column}: ${summary}`
|
|
10
|
+
: `Validation failed for ${table}: ${summary}`,
|
|
11
|
+
);
|
|
8
12
|
|
|
9
|
-
this.name = "
|
|
13
|
+
this.name = "ValidationError";
|
|
10
14
|
}
|
|
11
15
|
}
|
|
@@ -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
|
+
}
|