@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.
- package/package.json +8 -5
- package/src/database.ts +218 -13
- 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 +15 -1
- 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 +206 -3
- package/src/types.ts +94 -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
package/src/fts/sql.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ResolvedFtsPlan } from "./resolve.js";
|
|
2
|
+
import { tokenizeOption } from "./tokenizer.js";
|
|
3
|
+
|
|
4
|
+
export const FTS_TRIGGER_PREFIX = "__fts_sync_";
|
|
5
|
+
|
|
6
|
+
export type FtsTriggerEvent = "AI" | "AU" | "AD";
|
|
7
|
+
|
|
8
|
+
export function ftsTriggerName(fts: string, sourceTable: string, event: FtsTriggerEvent) {
|
|
9
|
+
return `${FTS_TRIGGER_PREFIX}${fts}_${sourceTable}_${event}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type GeneratedTrigger = { name: string; sql: string };
|
|
13
|
+
|
|
14
|
+
export function generateCreateFtsSql(plan: ResolvedFtsPlan) {
|
|
15
|
+
const cols = plan.fields.map((f) => `"${f.name}"`).join(", ");
|
|
16
|
+
const tokenize = tokenizeOption(plan.tokenizer);
|
|
17
|
+
|
|
18
|
+
return `CREATE VIRTUAL TABLE "${plan.name}" USING fts5(${cols}, ${tokenize})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function insertValueFor(field: ResolvedFtsPlan["fields"][number], anchor: string) {
|
|
22
|
+
if (field.sourceTable === anchor) {
|
|
23
|
+
return `COALESCE(NEW."${field.sourceColumn}", '')`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `''`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function anchorTriggers(plan: ResolvedFtsPlan): GeneratedTrigger[] {
|
|
30
|
+
const fts = plan.name;
|
|
31
|
+
const anchor = plan.anchor;
|
|
32
|
+
const pk = plan.anchorPkColumn;
|
|
33
|
+
|
|
34
|
+
const allCols = plan.fields.map((f) => `"${f.name}"`).join(", ");
|
|
35
|
+
const insertValues = plan.fields.map((f) => insertValueFor(f, anchor)).join(", ");
|
|
36
|
+
|
|
37
|
+
const anchorFields = plan.fields.filter((f) => f.sourceTable === anchor);
|
|
38
|
+
|
|
39
|
+
const aiName = ftsTriggerName(fts, anchor, "AI");
|
|
40
|
+
const adName = ftsTriggerName(fts, anchor, "AD");
|
|
41
|
+
|
|
42
|
+
const triggers: GeneratedTrigger[] = [
|
|
43
|
+
{
|
|
44
|
+
name: aiName,
|
|
45
|
+
sql:
|
|
46
|
+
`CREATE TRIGGER "${aiName}" AFTER INSERT ON "${anchor}" BEGIN ` +
|
|
47
|
+
`INSERT INTO "${fts}"(rowid, ${allCols}) VALUES (NEW."${pk}", ${insertValues}); ` +
|
|
48
|
+
`END`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: adName,
|
|
52
|
+
sql:
|
|
53
|
+
`CREATE TRIGGER "${adName}" AFTER DELETE ON "${anchor}" BEGIN ` +
|
|
54
|
+
`DELETE FROM "${fts}" WHERE rowid = OLD."${pk}"; ` +
|
|
55
|
+
`END`,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
if (anchorFields.length > 0) {
|
|
60
|
+
const trackedCols = anchorFields.map((f) => `"${f.sourceColumn}"`).join(", ");
|
|
61
|
+
const setClause = anchorFields
|
|
62
|
+
.map((f) => `"${f.name}" = COALESCE(NEW."${f.sourceColumn}", '')`)
|
|
63
|
+
.join(", ");
|
|
64
|
+
const auName = ftsTriggerName(fts, anchor, "AU");
|
|
65
|
+
|
|
66
|
+
triggers.push({
|
|
67
|
+
name: auName,
|
|
68
|
+
sql:
|
|
69
|
+
`CREATE TRIGGER "${auName}" AFTER UPDATE OF ${trackedCols} ON "${anchor}" BEGIN ` +
|
|
70
|
+
`UPDATE "${fts}" SET ${setClause} WHERE rowid = NEW."${pk}"; ` +
|
|
71
|
+
`END`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return triggers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sourceTriggers(plan: ResolvedFtsPlan): GeneratedTrigger[] {
|
|
79
|
+
const triggers: GeneratedTrigger[] = [];
|
|
80
|
+
const fts = plan.name;
|
|
81
|
+
|
|
82
|
+
for (const source of plan.sources) {
|
|
83
|
+
const fields = plan.fields.filter((f) => f.sourceTable === source.table);
|
|
84
|
+
|
|
85
|
+
if (fields.length === 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const trackedCols = fields.map((f) => `"${f.sourceColumn}"`).join(", ");
|
|
90
|
+
const setNew = fields
|
|
91
|
+
.map((f) => `"${f.name}" = COALESCE(NEW."${f.sourceColumn}", '')`)
|
|
92
|
+
.join(", ");
|
|
93
|
+
const clearSet = fields.map((f) => `"${f.name}" = ''`).join(", ");
|
|
94
|
+
|
|
95
|
+
const aiName = ftsTriggerName(fts, source.table, "AI");
|
|
96
|
+
const auName = ftsTriggerName(fts, source.table, "AU");
|
|
97
|
+
const adName = ftsTriggerName(fts, source.table, "AD");
|
|
98
|
+
|
|
99
|
+
triggers.push({
|
|
100
|
+
name: aiName,
|
|
101
|
+
sql:
|
|
102
|
+
`CREATE TRIGGER "${aiName}" AFTER INSERT ON "${source.table}" BEGIN ` +
|
|
103
|
+
`UPDATE "${fts}" SET ${setNew} WHERE rowid = NEW."${source.fkColumn}"; ` +
|
|
104
|
+
`END`,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
triggers.push({
|
|
108
|
+
name: auName,
|
|
109
|
+
sql:
|
|
110
|
+
`CREATE TRIGGER "${auName}" AFTER UPDATE OF ${trackedCols} ON "${source.table}" BEGIN ` +
|
|
111
|
+
`UPDATE "${fts}" SET ${setNew} WHERE rowid = NEW."${source.fkColumn}"; ` +
|
|
112
|
+
`END`,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
triggers.push({
|
|
116
|
+
name: adName,
|
|
117
|
+
sql:
|
|
118
|
+
`CREATE TRIGGER "${adName}" AFTER DELETE ON "${source.table}" BEGIN ` +
|
|
119
|
+
`UPDATE "${fts}" SET ${clearSet} WHERE rowid = OLD."${source.fkColumn}"; ` +
|
|
120
|
+
`END`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return triggers;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function generateFtsTriggers(plan: ResolvedFtsPlan) {
|
|
128
|
+
return [...anchorTriggers(plan), ...sourceTriggers(plan)];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function generateRebuildSelect(plan: ResolvedFtsPlan) {
|
|
132
|
+
const cols = plan.fields.map((f) => `"${f.name}"`).join(", ");
|
|
133
|
+
const selectExprs = plan.fields
|
|
134
|
+
.map((f) => `COALESCE("${f.sourceTable}"."${f.sourceColumn}", '')`)
|
|
135
|
+
.join(", ");
|
|
136
|
+
const joins = plan.sources
|
|
137
|
+
.map(
|
|
138
|
+
(s) =>
|
|
139
|
+
`LEFT JOIN "${s.table}" ON "${s.table}"."${s.fkColumn}" = "${plan.anchor}"."${plan.anchorPkColumn}"`,
|
|
140
|
+
)
|
|
141
|
+
.join(" ");
|
|
142
|
+
|
|
143
|
+
const from = joins
|
|
144
|
+
? `FROM "${plan.anchor}" ${joins}`
|
|
145
|
+
: `FROM "${plan.anchor}"`;
|
|
146
|
+
|
|
147
|
+
return `INSERT INTO "${plan.name}"(rowid, ${cols}) SELECT "${plan.anchor}"."${plan.anchorPkColumn}", ${selectExprs} ${from}`;
|
|
148
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FtsTokenizer } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_TOKENIZER: FtsTokenizer = { type: "unicode61", removeDiacritics: 2 };
|
|
4
|
+
|
|
5
|
+
export function tokenizeClause(tokenizer: FtsTokenizer) {
|
|
6
|
+
if (tokenizer.type === "trigram") {
|
|
7
|
+
return "trigram";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const parts: string[] = ["unicode61"];
|
|
11
|
+
|
|
12
|
+
if (tokenizer.removeDiacritics !== undefined) {
|
|
13
|
+
parts.push(`remove_diacritics ${tokenizer.removeDiacritics}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (tokenizer.categories) {
|
|
17
|
+
parts.push(`categories '${tokenizer.categories}'`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return parts.join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function tokenizeOption(tokenizer: FtsTokenizer | undefined) {
|
|
24
|
+
const clause = tokenizeClause(tokenizer ?? DEFAULT_TOKENIZER);
|
|
25
|
+
|
|
26
|
+
return `tokenize = '${clause.replaceAll("'", "''")}'`;
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { Database } from "./database.js";
|
|
2
2
|
export { generated, type GeneratedPreset } from "./generated.js";
|
|
3
|
+
export { vector } from "./vector.js";
|
|
4
|
+
export { nearest, type NearestOptions } from "./vector-index/nearest.js";
|
|
5
|
+
export { ExtensionLoadError } from "./vector-index/load.js";
|
|
3
6
|
export { JsonParseError } from "./errors.js";
|
|
4
|
-
export { jsonArrayFrom, jsonObjectFrom } from "
|
|
7
|
+
export { jsonArrayFrom, jsonObjectFrom } from "./json.js";
|
|
5
8
|
export {
|
|
6
9
|
sql,
|
|
7
10
|
type Selectable,
|
|
@@ -16,6 +19,7 @@ export { ValidationError } from "./validation-error.js";
|
|
|
16
19
|
export type { DbFieldMeta } from "./env.js";
|
|
17
20
|
export type {
|
|
18
21
|
DatabaseOptions,
|
|
22
|
+
DatabaseExtension,
|
|
19
23
|
SchemaRecord,
|
|
20
24
|
TablesFromSchemas,
|
|
21
25
|
InferTableType,
|
|
@@ -25,4 +29,14 @@ export type {
|
|
|
25
29
|
DatabaseSchema,
|
|
26
30
|
JsonValidation,
|
|
27
31
|
SqliteMasterRow,
|
|
32
|
+
FtsTokenizer,
|
|
33
|
+
FtsConfig,
|
|
34
|
+
FtsEntry,
|
|
35
|
+
FtsFieldRef,
|
|
36
|
+
FtsRowType,
|
|
37
|
+
FtsApi,
|
|
38
|
+
VectorIndexApi,
|
|
39
|
+
VectorIndexConfig,
|
|
40
|
+
VectorIndexEntry,
|
|
41
|
+
VectorMetric,
|
|
28
42
|
} from "./types.js";
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RawBuilder, Simplify } from "kysely";
|
|
2
|
+
import {
|
|
3
|
+
jsonArrayFrom as baseJsonArrayFrom,
|
|
4
|
+
jsonObjectFrom as baseJsonObjectFrom,
|
|
5
|
+
} from "kysely/helpers/sqlite";
|
|
6
|
+
|
|
7
|
+
export const jsonArrayFrom: <O>(
|
|
8
|
+
expr: Parameters<typeof baseJsonArrayFrom<O>>[0],
|
|
9
|
+
) => RawBuilder<Simplify<O>[]> = baseJsonArrayFrom as never;
|
|
10
|
+
|
|
11
|
+
export const jsonObjectFrom: <O>(
|
|
12
|
+
expr: Parameters<typeof baseJsonObjectFrom<O>>[0],
|
|
13
|
+
) => RawBuilder<Simplify<O> | null> = baseJsonObjectFrom as never;
|
package/src/migration/diff.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResolvedFtsPlan } from "../fts/resolve.js";
|
|
2
|
+
import {
|
|
3
|
+
generateCreateFtsSql,
|
|
4
|
+
generateFtsTriggers,
|
|
5
|
+
generateRebuildSelect,
|
|
6
|
+
} from "../fts/sql.js";
|
|
7
|
+
import { tokenizeClause } from "../fts/tokenizer.js";
|
|
8
|
+
import type { ResolvedVectorIndex } from "../vector-index/resolve.js";
|
|
9
|
+
import {
|
|
10
|
+
generateCreateVecSql,
|
|
11
|
+
generateRebuildVectorSql,
|
|
12
|
+
generateVectorTriggers,
|
|
13
|
+
} from "../vector-index/sql.js";
|
|
14
|
+
import type { FtsTokenizer } from "../types.js";
|
|
15
|
+
import type {
|
|
16
|
+
ColumnSchema,
|
|
17
|
+
IntrospectedFts,
|
|
18
|
+
IntrospectedTable,
|
|
19
|
+
IntrospectedVectorIndex,
|
|
20
|
+
ColumnCopy,
|
|
21
|
+
MigrationOp,
|
|
22
|
+
} from "./types.js";
|
|
2
23
|
|
|
3
24
|
export interface DesiredColumn extends ColumnSchema {
|
|
4
25
|
addable: boolean;
|
|
@@ -26,6 +47,10 @@ export class Differ {
|
|
|
26
47
|
constructor(
|
|
27
48
|
private desired: DesiredTable[],
|
|
28
49
|
private existing: Map<string, IntrospectedTable>,
|
|
50
|
+
private desiredFts: ResolvedFtsPlan[] = [],
|
|
51
|
+
private existingFts: Map<string, IntrospectedFts> = new Map(),
|
|
52
|
+
private desiredVectorIndexes: ResolvedVectorIndex[] = [],
|
|
53
|
+
private existingVectorIndexes: Map<string, IntrospectedVectorIndex> = new Map(),
|
|
29
54
|
) {
|
|
30
55
|
this.desiredNames = new Set(desired.map((t) => t.name));
|
|
31
56
|
}
|
|
@@ -34,6 +59,8 @@ export class Differ {
|
|
|
34
59
|
this.diffTables();
|
|
35
60
|
this.dropOrphans();
|
|
36
61
|
this.diffIndexes();
|
|
62
|
+
this.diffFts();
|
|
63
|
+
this.diffVectorIndexes();
|
|
37
64
|
return this.ops;
|
|
38
65
|
}
|
|
39
66
|
|
|
@@ -217,4 +244,170 @@ export class Differ {
|
|
|
217
244
|
}
|
|
218
245
|
}
|
|
219
246
|
}
|
|
247
|
+
|
|
248
|
+
private diffFts() {
|
|
249
|
+
const desiredNames = new Set(this.desiredFts.map((p) => p.name));
|
|
250
|
+
|
|
251
|
+
for (const plan of this.desiredFts) {
|
|
252
|
+
const existing = this.existingFts.get(plan.name);
|
|
253
|
+
const triggers = generateFtsTriggers(plan);
|
|
254
|
+
const createSql = generateCreateFtsSql(plan);
|
|
255
|
+
const rebuildSql = generateRebuildSelect(plan);
|
|
256
|
+
|
|
257
|
+
if (!existing) {
|
|
258
|
+
this.ops.push({
|
|
259
|
+
type: "CreateFts",
|
|
260
|
+
name: plan.name,
|
|
261
|
+
createSql,
|
|
262
|
+
triggers,
|
|
263
|
+
});
|
|
264
|
+
this.ops.push({
|
|
265
|
+
type: "RebuildFts",
|
|
266
|
+
name: plan.name,
|
|
267
|
+
rebuildSql,
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const desiredCols = plan.fields.map((f) => f.name);
|
|
273
|
+
const colsMatch =
|
|
274
|
+
desiredCols.length === existing.columns.length &&
|
|
275
|
+
desiredCols.every((c, i) => existing.columns[i] === c);
|
|
276
|
+
const triggersMatch =
|
|
277
|
+
existing.triggers.size === triggers.length &&
|
|
278
|
+
triggers.every(
|
|
279
|
+
(t) => normalizeSql(existing.triggers.get(t.name) ?? "") === normalizeSql(t.sql),
|
|
280
|
+
);
|
|
281
|
+
const tokenizerMatch = ftsTokenizerMatches(existing.sql, plan.tokenizer);
|
|
282
|
+
|
|
283
|
+
if (colsMatch && triggersMatch && tokenizerMatch) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.ops.push({
|
|
288
|
+
type: "DropFts",
|
|
289
|
+
name: plan.name,
|
|
290
|
+
triggerNames: [...existing.triggers.keys()],
|
|
291
|
+
});
|
|
292
|
+
this.ops.push({
|
|
293
|
+
type: "CreateFts",
|
|
294
|
+
name: plan.name,
|
|
295
|
+
createSql,
|
|
296
|
+
triggers,
|
|
297
|
+
});
|
|
298
|
+
this.ops.push({
|
|
299
|
+
type: "RebuildFts",
|
|
300
|
+
name: plan.name,
|
|
301
|
+
rebuildSql,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const [name, existing] of this.existingFts) {
|
|
306
|
+
if (desiredNames.has(name)) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.ops.push({
|
|
311
|
+
type: "DropFts",
|
|
312
|
+
name,
|
|
313
|
+
triggerNames: [...existing.triggers.keys()],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private diffVectorIndexes() {
|
|
319
|
+
const desiredNames = new Set(this.desiredVectorIndexes.map((p) => p.name));
|
|
320
|
+
|
|
321
|
+
for (const plan of this.desiredVectorIndexes) {
|
|
322
|
+
const existing = this.existingVectorIndexes.get(plan.name);
|
|
323
|
+
const triggers = generateVectorTriggers(plan);
|
|
324
|
+
const createSql = generateCreateVecSql(plan);
|
|
325
|
+
const rebuildSql = generateRebuildVectorSql(plan);
|
|
326
|
+
|
|
327
|
+
if (!existing) {
|
|
328
|
+
this.ops.push({
|
|
329
|
+
type: "CreateVectorIndex",
|
|
330
|
+
name: plan.name,
|
|
331
|
+
createSql,
|
|
332
|
+
triggers,
|
|
333
|
+
});
|
|
334
|
+
this.ops.push({
|
|
335
|
+
type: "RebuildVectorIndex",
|
|
336
|
+
name: plan.name,
|
|
337
|
+
rebuildSql,
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const triggersMatch =
|
|
343
|
+
existing.triggers.size === triggers.length &&
|
|
344
|
+
triggers.every(
|
|
345
|
+
(t) => normalizeSql(existing.triggers.get(t.name) ?? "") === normalizeSql(t.sql),
|
|
346
|
+
);
|
|
347
|
+
const shapeMatch = vectorShapeMatches(existing.sql, plan);
|
|
348
|
+
|
|
349
|
+
if (triggersMatch && shapeMatch) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.ops.push({
|
|
354
|
+
type: "DropVectorIndex",
|
|
355
|
+
name: plan.name,
|
|
356
|
+
triggerNames: [...existing.triggers.keys()],
|
|
357
|
+
});
|
|
358
|
+
this.ops.push({
|
|
359
|
+
type: "CreateVectorIndex",
|
|
360
|
+
name: plan.name,
|
|
361
|
+
createSql,
|
|
362
|
+
triggers,
|
|
363
|
+
});
|
|
364
|
+
this.ops.push({
|
|
365
|
+
type: "RebuildVectorIndex",
|
|
366
|
+
name: plan.name,
|
|
367
|
+
rebuildSql,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const [name, existing] of this.existingVectorIndexes) {
|
|
372
|
+
if (desiredNames.has(name)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.ops.push({
|
|
377
|
+
type: "DropVectorIndex",
|
|
378
|
+
name,
|
|
379
|
+
triggerNames: [...existing.triggers.keys()],
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ftsTokenizerMatches(existingSql: string, desired: FtsTokenizer) {
|
|
386
|
+
const match = existingSql.match(/tokenize\s*=\s*'((?:''|[^'])*)'/i);
|
|
387
|
+
|
|
388
|
+
if (!match) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return match[1]!.replaceAll("''", "'") === tokenizeClause(desired);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function vectorShapeMatches(existingSql: string, desired: ResolvedVectorIndex) {
|
|
396
|
+
const dimMatch = existingSql.match(/float\s*\[\s*(\d+)\s*\]/i);
|
|
397
|
+
const metricMatch = existingSql.match(/distance_metric\s*=\s*([a-zA-Z0-9_]+)/i);
|
|
398
|
+
const colMatch = existingSql.match(/"([^"]+)"\s+float\s*\[/i);
|
|
399
|
+
|
|
400
|
+
if (!dimMatch || !metricMatch || !colMatch) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
Number.parseInt(dimMatch[1]!, 10) === desired.dim &&
|
|
406
|
+
metricMatch[1]!.toLowerCase() === desired.metric &&
|
|
407
|
+
colMatch[1] === desired.column
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function normalizeSql(sql: string): string {
|
|
412
|
+
return sql.replaceAll(/\s+/g, " ").trim();
|
|
220
413
|
}
|
package/src/migration/execute.ts
CHANGED
|
@@ -73,6 +73,48 @@ export class Executor {
|
|
|
73
73
|
case "DropIndex": {
|
|
74
74
|
return this.db.run(`DROP INDEX "${op.index}"`);
|
|
75
75
|
}
|
|
76
|
+
case "CreateFts": {
|
|
77
|
+
this.db.run(op.createSql);
|
|
78
|
+
|
|
79
|
+
for (const trigger of op.triggers) {
|
|
80
|
+
this.db.run(trigger.sql);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
case "DropFts": {
|
|
86
|
+
for (const name of op.triggerNames) {
|
|
87
|
+
this.db.run(`DROP TRIGGER IF EXISTS "${name}"`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.db.run(`DROP TABLE IF EXISTS "${op.name}"`);
|
|
91
|
+
}
|
|
92
|
+
case "RebuildFts": {
|
|
93
|
+
this.db.run(`DELETE FROM "${op.name}"`);
|
|
94
|
+
|
|
95
|
+
return this.db.run(op.rebuildSql);
|
|
96
|
+
}
|
|
97
|
+
case "CreateVectorIndex": {
|
|
98
|
+
this.db.run(op.createSql);
|
|
99
|
+
|
|
100
|
+
for (const trigger of op.triggers) {
|
|
101
|
+
this.db.run(trigger.sql);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case "DropVectorIndex": {
|
|
107
|
+
for (const name of op.triggerNames) {
|
|
108
|
+
this.db.run(`DROP TRIGGER IF EXISTS "${name}"`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this.db.run(`DROP TABLE IF EXISTS "${op.name}"`);
|
|
112
|
+
}
|
|
113
|
+
case "RebuildVectorIndex": {
|
|
114
|
+
this.db.run(`DELETE FROM "${op.name}"`);
|
|
115
|
+
|
|
116
|
+
return this.db.run(op.rebuildSql);
|
|
117
|
+
}
|
|
76
118
|
}
|
|
77
119
|
}
|
|
78
120
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { FTS_TRIGGER_PREFIX } from "../fts/sql.js";
|
|
4
|
+
import { VEC_TRIGGER_PREFIX } from "../vector-index/sql.js";
|
|
5
|
+
import type {
|
|
6
|
+
IntrospectedColumn,
|
|
7
|
+
IntrospectedFts,
|
|
8
|
+
IntrospectedIndex,
|
|
9
|
+
IntrospectedTable,
|
|
10
|
+
IntrospectedVectorIndex,
|
|
11
|
+
} from "./types.js";
|
|
4
12
|
|
|
5
13
|
type TableListRow = {
|
|
6
14
|
name: string;
|
|
@@ -12,6 +20,7 @@ type TableXInfoRow = {
|
|
|
12
20
|
type: string;
|
|
13
21
|
notnull: number;
|
|
14
22
|
dflt_value: string | null;
|
|
23
|
+
hidden: number;
|
|
15
24
|
};
|
|
16
25
|
|
|
17
26
|
type IndexListRow = {
|
|
@@ -31,18 +40,30 @@ type ForeignKeyListRow = {
|
|
|
31
40
|
on_delete: string;
|
|
32
41
|
};
|
|
33
42
|
|
|
43
|
+
type MasterRow = {
|
|
44
|
+
name: string;
|
|
45
|
+
sql: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
34
48
|
export class Introspector {
|
|
35
49
|
constructor(private db: Database) {}
|
|
36
50
|
|
|
37
51
|
introspect() {
|
|
38
52
|
const tables = new Map<string, IntrospectedTable>();
|
|
39
53
|
const tableRows = this.db.prepare("PRAGMA table_list").all() as TableListRow[];
|
|
54
|
+
const virtualPrefixes = tableRows
|
|
55
|
+
.filter((r) => r.type === "virtual")
|
|
56
|
+
.map((r) => `${r.name}_`);
|
|
40
57
|
|
|
41
58
|
for (const row of tableRows) {
|
|
42
59
|
if (row.type !== "table" || row.name.startsWith("sqlite_")) {
|
|
43
60
|
continue;
|
|
44
61
|
}
|
|
45
62
|
|
|
63
|
+
if (virtualPrefixes.some((prefix) => row.name.startsWith(prefix))) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
const indexRows = this.db.prepare(`PRAGMA index_list("${row.name}")`).all() as IndexListRow[];
|
|
47
68
|
const uniqueCols = this.uniqueColumns(indexRows);
|
|
48
69
|
const fkMap = this.foreignKeys(row.name);
|
|
@@ -56,6 +77,117 @@ export class Introspector {
|
|
|
56
77
|
return tables;
|
|
57
78
|
}
|
|
58
79
|
|
|
80
|
+
hasVec0Tables() {
|
|
81
|
+
const row = this.db
|
|
82
|
+
.prepare(
|
|
83
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND sql LIKE '%USING vec0%' LIMIT 1",
|
|
84
|
+
)
|
|
85
|
+
.get();
|
|
86
|
+
|
|
87
|
+
return row !== null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
introspectFts() {
|
|
91
|
+
const fts = new Map<string, IntrospectedFts>();
|
|
92
|
+
const tableRows = this.db.prepare("PRAGMA table_list").all() as TableListRow[];
|
|
93
|
+
const libraryTriggers = this.triggersByPrefix(FTS_TRIGGER_PREFIX);
|
|
94
|
+
|
|
95
|
+
for (const row of tableRows) {
|
|
96
|
+
if (row.type !== "virtual" || row.name.startsWith("sqlite_")) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const master = this.db
|
|
101
|
+
.prepare(`SELECT name, sql FROM sqlite_master WHERE name = ? AND type = 'table'`)
|
|
102
|
+
.get(row.name) as MasterRow | null;
|
|
103
|
+
|
|
104
|
+
if (!master?.sql || !/USING\s+fts5/i.test(master.sql)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const colRows = this.db.prepare(`PRAGMA table_xinfo("${row.name}")`).all() as TableXInfoRow[];
|
|
109
|
+
const columns = colRows.filter((c) => c.hidden !== 1).map((c) => c.name);
|
|
110
|
+
|
|
111
|
+
fts.set(row.name, {
|
|
112
|
+
name: row.name,
|
|
113
|
+
columns,
|
|
114
|
+
sql: master.sql,
|
|
115
|
+
triggers: new Map(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.assignTriggersByPrefix(libraryTriggers, FTS_TRIGGER_PREFIX, fts);
|
|
120
|
+
|
|
121
|
+
return fts;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
introspectVectorIndexes() {
|
|
125
|
+
const indexes = new Map<string, IntrospectedVectorIndex>();
|
|
126
|
+
const tableRows = this.db.prepare("PRAGMA table_list").all() as TableListRow[];
|
|
127
|
+
const libraryTriggers = this.triggersByPrefix(VEC_TRIGGER_PREFIX);
|
|
128
|
+
|
|
129
|
+
for (const row of tableRows) {
|
|
130
|
+
if (row.type !== "virtual" || row.name.startsWith("sqlite_")) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const master = this.db
|
|
135
|
+
.prepare(`SELECT name, sql FROM sqlite_master WHERE name = ? AND type = 'table'`)
|
|
136
|
+
.get(row.name) as MasterRow | null;
|
|
137
|
+
|
|
138
|
+
if (!master?.sql || !/USING\s+vec0/i.test(master.sql)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
indexes.set(row.name, {
|
|
143
|
+
name: row.name,
|
|
144
|
+
sql: master.sql,
|
|
145
|
+
triggers: new Map(),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.assignTriggersByPrefix(libraryTriggers, VEC_TRIGGER_PREFIX, indexes);
|
|
150
|
+
|
|
151
|
+
return indexes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private assignTriggersByPrefix(
|
|
155
|
+
triggers: Map<string, string>,
|
|
156
|
+
prefix: string,
|
|
157
|
+
target: Map<string, { triggers: Map<string, string> }>,
|
|
158
|
+
) {
|
|
159
|
+
const sortedNames = [...target.keys()].sort((a, b) => b.length - a.length);
|
|
160
|
+
|
|
161
|
+
for (const [triggerName, triggerSql] of triggers) {
|
|
162
|
+
const withoutPrefix = triggerName.slice(prefix.length);
|
|
163
|
+
|
|
164
|
+
for (const name of sortedNames) {
|
|
165
|
+
if (withoutPrefix.startsWith(`${name}_`)) {
|
|
166
|
+
target.get(name)!.triggers.set(triggerName, triggerSql);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private triggersByPrefix(prefix: string) {
|
|
174
|
+
const rows = this.db
|
|
175
|
+
.prepare(
|
|
176
|
+
`SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND name LIKE '${prefix}%'`,
|
|
177
|
+
)
|
|
178
|
+
.all() as MasterRow[];
|
|
179
|
+
|
|
180
|
+
const map = new Map<string, string>();
|
|
181
|
+
|
|
182
|
+
for (const row of rows) {
|
|
183
|
+
if (row.sql !== null) {
|
|
184
|
+
map.set(row.name, row.sql);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return map;
|
|
189
|
+
}
|
|
190
|
+
|
|
59
191
|
private uniqueColumns(indexRows: IndexListRow[]) {
|
|
60
192
|
const unique = new Set<string>();
|
|
61
193
|
|