@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ResolvedVectorIndex } from "./resolve.js";
|
|
2
|
+
|
|
3
|
+
export const VEC_TRIGGER_PREFIX = "__vec_sync_";
|
|
4
|
+
|
|
5
|
+
export type VecTriggerEvent = "AI" | "AU" | "AD";
|
|
6
|
+
|
|
7
|
+
export function vecTriggerName(index: string, anchor: string, event: VecTriggerEvent) {
|
|
8
|
+
return `${VEC_TRIGGER_PREFIX}${index}_${anchor}_${event}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type GeneratedTrigger = { name: string; sql: string };
|
|
12
|
+
|
|
13
|
+
const VALID_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
14
|
+
|
|
15
|
+
export function generateCreateVecSql(index: ResolvedVectorIndex) {
|
|
16
|
+
if (!VALID_IDENTIFIER.test(index.column)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`VectorIndex "${index.name}": column "${index.column}" must be a simple identifier (vec0 does not accept quoted column names)`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
`CREATE VIRTUAL TABLE "${index.name}" USING vec0(` +
|
|
24
|
+
`rowid INTEGER PRIMARY KEY, ` +
|
|
25
|
+
`${index.column} float[${index.dim}] distance_metric=${index.metric}` +
|
|
26
|
+
`)`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function generateVectorTriggers(index: ResolvedVectorIndex): GeneratedTrigger[] {
|
|
31
|
+
const anchor = index.anchor;
|
|
32
|
+
const pk = index.anchorPkColumn;
|
|
33
|
+
const col = index.column;
|
|
34
|
+
const vec = index.name;
|
|
35
|
+
|
|
36
|
+
const aiName = vecTriggerName(index.name, anchor, "AI");
|
|
37
|
+
const auName = vecTriggerName(index.name, anchor, "AU");
|
|
38
|
+
const adName = vecTriggerName(index.name, anchor, "AD");
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
name: aiName,
|
|
43
|
+
sql:
|
|
44
|
+
`CREATE TRIGGER "${aiName}" AFTER INSERT ON "${anchor}" ` +
|
|
45
|
+
`WHEN NEW."${col}" IS NOT NULL BEGIN ` +
|
|
46
|
+
`INSERT INTO "${vec}"(rowid, "${col}") VALUES (NEW."${pk}", NEW."${col}"); ` +
|
|
47
|
+
`END`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: auName,
|
|
51
|
+
sql:
|
|
52
|
+
`CREATE TRIGGER "${auName}" AFTER UPDATE OF "${col}" ON "${anchor}" BEGIN ` +
|
|
53
|
+
`DELETE FROM "${vec}" WHERE rowid = OLD."${pk}" AND NEW."${col}" IS NULL; ` +
|
|
54
|
+
`INSERT INTO "${vec}"(rowid, "${col}") ` +
|
|
55
|
+
`SELECT NEW."${pk}", NEW."${col}" WHERE OLD."${col}" IS NULL AND NEW."${col}" IS NOT NULL; ` +
|
|
56
|
+
`UPDATE "${vec}" SET "${col}" = NEW."${col}" ` +
|
|
57
|
+
`WHERE rowid = NEW."${pk}" AND OLD."${col}" IS NOT NULL AND NEW."${col}" IS NOT NULL; ` +
|
|
58
|
+
`END`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: adName,
|
|
62
|
+
sql:
|
|
63
|
+
`CREATE TRIGGER "${adName}" AFTER DELETE ON "${anchor}" BEGIN ` +
|
|
64
|
+
`DELETE FROM "${vec}" WHERE rowid = OLD."${pk}"; ` +
|
|
65
|
+
`END`,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function generateRebuildVectorSql(index: ResolvedVectorIndex) {
|
|
71
|
+
return (
|
|
72
|
+
`INSERT INTO "${index.name}"(rowid, "${index.column}") ` +
|
|
73
|
+
`SELECT "${index.anchorPkColumn}", "${index.column}" FROM "${index.anchor}" ` +
|
|
74
|
+
`WHERE "${index.column}" IS NOT NULL`
|
|
75
|
+
);
|
|
76
|
+
}
|
package/src/vector.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
export function vector(dim: number) {
|
|
4
|
+
if (!Number.isInteger(dim) || dim <= 0) {
|
|
5
|
+
throw new Error(`vector(dim) requires a positive integer dim, got ${dim}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return type
|
|
9
|
+
.instanceOf(Float32Array)
|
|
10
|
+
.or(type("number[]"))
|
|
11
|
+
.pipe((v) => (v instanceof Float32Array ? v : Float32Array.from(v)))
|
|
12
|
+
.configure({ _generated: "vector", _vectorDim: dim });
|
|
13
|
+
}
|