@an5/adapters 0.2.10 → 0.2.13
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 +163 -2
- package/dist/an5Adapter.d.ts +37 -0
- package/dist/an5Adapter.js +190 -65
- package/dist/base/metadata.d.ts +2 -0
- package/dist/base/metadata.js +10 -0
- package/dist/base/sql.d.ts +2 -0
- package/dist/base/sql.js +24 -25
- package/dist/base/types.d.ts +14 -0
- package/dist/googlesheets/adapter.js +16 -8
- package/dist/googlesheets/config.d.ts +13 -0
- package/dist/googlesheets/config.js +5 -1
- package/dist/googlesheets/oauth.d.ts +3 -0
- package/dist/googlesheets/oauth.js +43 -0
- package/dist/googlesheets/parseConnectionString.js +12 -0
- package/dist/googlesheets/tableClient.js +5 -23
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -1
- package/dist/sqlite/browserEngine.d.ts +13 -0
- package/dist/sqlite/browserEngine.js +13 -0
- package/dist/sqlite/engine.d.ts +5 -0
- package/dist/sqlite/engine.js +10 -0
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/sqlite/vector.d.ts +155 -0
- package/dist/sqlite/vector.js +436 -0
- package/dotnet/An5Adapter.cs +85 -27
- package/dotnet/Base/Core.cs +14 -0
- package/dotnet/Mssql/MssqlEngine.cs +44 -1
- package/dotnet/Postgres/PostgresEngine.cs +2 -0
- package/dotnet/Sqlite/SqliteEngine.cs +82 -2
- package/dotnet/Sqlite/SqliteVectors.cs +401 -0
- package/golang/an5_adapter.go +84 -32
- package/golang/base/vectors.go +263 -0
- package/golang/sqlite/vector.go +248 -0
- package/golang/sqlite_vector_test.go +288 -0
- package/golang/table_client.go +67 -3
- package/java/pom.xml +177 -0
- package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
- package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
- package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
- package/java/src/main/java/an5/adapters/An5Query.java +100 -0
- package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
- package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
- package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
- package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
- package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
- package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
- package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
- package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
- package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
- package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
- package/java/src/test/java/An5AdaptersSmoke.java +733 -0
- package/java/src/test/java/Json.java +174 -0
- package/java/src/test/java/QuerySemantics.java +111 -0
- package/kotlin/build.gradle.kts +145 -0
- package/kotlin/settings.gradle.kts +10 -0
- package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
- package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
- package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
- package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
- package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
- package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
- package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
- package/native/sqlite/CMakeLists.txt +12 -0
- package/native/sqlite/README.md +117 -0
- package/native/sqlite/an5_vector.c +209 -0
- package/package.json +40 -9
- package/python/an5_adapter.py +37 -6
- package/python/base/__init__.py +15 -0
- package/python/base/vectors.py +434 -0
- package/python/sqlite/__init__.py +2 -2
- package/python/sqlite/provider.py +21 -1
- package/python/table_client.py +129 -59
- package/rust/src/adapter.rs +342 -24
- package/rust/src/base/mod.rs +7 -0
- package/rust/src/base/vectors.rs +293 -0
- package/rust/src/lib.rs +3 -3
- package/scripts/sqlite-native-build.js +32 -0
- package/swift/Package.swift +22 -0
- package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
- package/swift/Sources/An5Adapters/Metadata.swift +183 -0
- package/swift/Sources/An5Adapters/Query.swift +105 -0
- package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
- package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
- package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
- package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
- package/swift/Sources/An5Adapters/TableClient.swift +671 -0
- package/swift/Sources/An5Adapters/Types.swift +127 -0
- package/swift/Sources/An5Adapters/Vectors.swift +86 -0
- package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
- package/swift/Sources/CSQLite/module.modulemap +8 -0
- package/swift/Sources/CSQLite/shim.h +9 -0
- package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
- package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
- package/typescript/src/an5Adapter.ts +200 -58
- package/typescript/src/base/metadata.ts +8 -0
- package/typescript/src/base/sql.ts +23 -16
- package/typescript/src/base/types.ts +15 -0
- package/typescript/src/googlesheets/adapter.ts +17 -8
- package/typescript/src/googlesheets/config.ts +10 -1
- package/typescript/src/googlesheets/oauth.ts +31 -0
- package/typescript/src/googlesheets/parseConnectionString.ts +8 -0
- package/typescript/src/googlesheets/tableClient.ts +5 -19
- package/typescript/src/index.ts +13 -0
- package/typescript/src/sqlite/browserEngine.ts +24 -0
- package/typescript/src/sqlite/engine.ts +13 -0
- package/typescript/src/sqlite/index.ts +1 -1
- package/typescript/src/sqlite/vector.ts +541 -0
- package/golang/sqlite/sqlite.go +0 -14
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// ─── SQLite vector search ──────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// SQLite has no vector type, so `VECTOR(n)` columns are stored as a BLOB of
|
|
4
|
+
// little-endian float32 values (see `encodeVector`) and searched in one of four
|
|
5
|
+
// ways. The order is fixed so every runtime that speaks SQLite can implement it:
|
|
6
|
+
//
|
|
7
|
+
// 1. sqlite-vec native scalar distance functions, if the extension loads.
|
|
8
|
+
// 2. udf `an5_vec_cosine` / `an5_vec_l2` / `an5_vec_ip` registered by
|
|
9
|
+
// the driver. Reads the BLOB and the legacy JSON text.
|
|
10
|
+
// 3. sql `json_each` brute force in plain SQL. No UDF needed, so it
|
|
11
|
+
// works in browsers and on JDBC, but only for JSON text.
|
|
12
|
+
// 4. memory the adapter loads the column and scores it in the client.
|
|
13
|
+
//
|
|
14
|
+
// Strategies 1-3 rank in the database and only transfer the `take` rows that
|
|
15
|
+
// match, which is the reason they are preferred over the in-memory path.
|
|
16
|
+
|
|
17
|
+
export type VectorMetric = 'cosine' | 'euclidean' | 'dot';
|
|
18
|
+
export type SqliteVectorStrategy = 'sqlite-vec' | 'udf' | 'sql' | 'memory';
|
|
19
|
+
export type VectorStrategyPreference = 'auto' | SqliteVectorStrategy;
|
|
20
|
+
|
|
21
|
+
/** Scalar functions shipped by the sqlite-vec extension. */
|
|
22
|
+
export const SQLITE_VEC_FUNCTIONS: Record<VectorMetric, string> = {
|
|
23
|
+
cosine: 'vec_distance_cosine',
|
|
24
|
+
euclidean: 'vec_distance_l2',
|
|
25
|
+
dot: 'vec_distance_ip',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Scalar functions the adapter registers itself when the driver allows it. */
|
|
29
|
+
export const AN5_VECTOR_FUNCTIONS: Record<VectorMetric, string> = {
|
|
30
|
+
cosine: 'an5_vec_cosine',
|
|
31
|
+
euclidean: 'an5_vec_l2',
|
|
32
|
+
dot: 'an5_vec_ip',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const BYTES_PER_FLOAT = 4;
|
|
36
|
+
|
|
37
|
+
function normalizeMetric(metric: unknown): VectorMetric {
|
|
38
|
+
return metric === 'euclidean' || metric === 'dot' ? metric : 'cosine';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Codec ─────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function toBytes(value: unknown): Uint8Array | null {
|
|
44
|
+
if (value instanceof Uint8Array) return value;
|
|
45
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value;
|
|
46
|
+
if (ArrayBuffer.isView(value)) {
|
|
47
|
+
const view = value as ArrayBufferView;
|
|
48
|
+
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
49
|
+
}
|
|
50
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read a numeric array out of the shapes a driver can hand back. */
|
|
55
|
+
export function parseVector(value: unknown): number[] | null {
|
|
56
|
+
if (value === null || value === undefined) return null;
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
const out = new Array<number>(value.length);
|
|
59
|
+
for (let i = 0; i < value.length; i++) {
|
|
60
|
+
const n = Number(value[i]);
|
|
61
|
+
if (!Number.isFinite(n)) return null;
|
|
62
|
+
out[i] = n;
|
|
63
|
+
}
|
|
64
|
+
return out.length ? out : null;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (!trimmed.startsWith('[')) return null;
|
|
69
|
+
try {
|
|
70
|
+
return parseVector(JSON.parse(trimmed));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decode a stored vector: a float32 BLOB, a legacy JSON text column, or an
|
|
80
|
+
* array the caller already decoded.
|
|
81
|
+
*/
|
|
82
|
+
export function decodeVector(value: unknown): number[] | null {
|
|
83
|
+
const direct = parseVector(value);
|
|
84
|
+
if (direct) return direct;
|
|
85
|
+
|
|
86
|
+
const bytes = toBytes(value);
|
|
87
|
+
if (!bytes || bytes.length === 0 || bytes.length % BYTES_PER_FLOAT !== 0) return null;
|
|
88
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
89
|
+
const out = new Array<number>(bytes.length / BYTES_PER_FLOAT);
|
|
90
|
+
for (let i = 0; i < out.length; i++) {
|
|
91
|
+
out[i] = view.getFloat32(i * BYTES_PER_FLOAT, true);
|
|
92
|
+
if (!Number.isFinite(out[i])) return null;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Encode a vector as the little-endian float32 BLOB `VECTOR(n)` columns store. */
|
|
98
|
+
export function encodeVector(values: readonly number[]): Uint8Array {
|
|
99
|
+
const buf = typeof Buffer !== 'undefined'
|
|
100
|
+
? Buffer.alloc(values.length * BYTES_PER_FLOAT)
|
|
101
|
+
: new Uint8Array(values.length * BYTES_PER_FLOAT);
|
|
102
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
103
|
+
for (let i = 0; i < values.length; i++) {
|
|
104
|
+
if (!Number.isFinite(values[i])) throw new TypeError('Vector elements must be finite numbers');
|
|
105
|
+
view.setFloat32(i * BYTES_PER_FLOAT, values[i]!, true);
|
|
106
|
+
if (!Number.isFinite(view.getFloat32(i * BYTES_PER_FLOAT, true))) {
|
|
107
|
+
throw new RangeError('Vector element exceeds float32 range');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return buf;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** True when the value still needs encoding on its way into a `VECTOR(n)` column. */
|
|
114
|
+
export function needsVectorEncoding(value: unknown): boolean {
|
|
115
|
+
return Array.isArray(value) || value instanceof Float32Array || value instanceof Float64Array;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** True when a generated metadata entry describes a `VECTOR(n)` column. */
|
|
119
|
+
export function isVectorField(def: unknown): boolean {
|
|
120
|
+
if (!def) return false;
|
|
121
|
+
if (typeof def === 'string') return def.includes('[]') || /^VECTOR\b/i.test(def.trim());
|
|
122
|
+
const record = def as { ts?: string; sql?: string; type?: string; kind?: string };
|
|
123
|
+
if (record.kind === 'vector') return true;
|
|
124
|
+
const sql = String(record.sql ?? record.type ?? '');
|
|
125
|
+
if (/^VECTOR\s*(\(|$)/i.test(sql.trim())) return true;
|
|
126
|
+
return String(record.ts ?? '').includes('[]');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Distance ──────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Distance between two equal-length vectors, lower is closer. Mirrors the
|
|
133
|
+
* functions registered on the drivers so a search ranks the same way whichever
|
|
134
|
+
* strategy ran.
|
|
135
|
+
*/
|
|
136
|
+
export function vectorDistance(a: readonly number[], b: readonly number[], metric: unknown): number | null {
|
|
137
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return null;
|
|
138
|
+
let dot = 0;
|
|
139
|
+
let magA = 0;
|
|
140
|
+
let magB = 0;
|
|
141
|
+
for (let i = 0; i < a.length; i++) {
|
|
142
|
+
const av = Number(a[i]) || 0;
|
|
143
|
+
const bv = Number(b[i]) || 0;
|
|
144
|
+
dot += av * bv;
|
|
145
|
+
magA += av * av;
|
|
146
|
+
magB += bv * bv;
|
|
147
|
+
}
|
|
148
|
+
const resolved = normalizeMetric(metric);
|
|
149
|
+
if (resolved === 'euclidean') {
|
|
150
|
+
let sum = 0;
|
|
151
|
+
for (let i = 0; i < a.length; i++) {
|
|
152
|
+
const diff = (Number(a[i]) || 0) - (Number(b[i]) || 0);
|
|
153
|
+
sum += diff * diff;
|
|
154
|
+
}
|
|
155
|
+
return Math.sqrt(sum);
|
|
156
|
+
}
|
|
157
|
+
if (resolved === 'dot') return -dot;
|
|
158
|
+
return magA && magB ? 1 - dot / (Math.sqrt(magA) * Math.sqrt(magB)) : 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Query builders ────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/** `json_valid` fails on a float32 BLOB, so the JSON path needs this guard. */
|
|
164
|
+
function jsonText(column: string): string {
|
|
165
|
+
return `CASE WHEN json_valid(${column}) THEN ${column} ELSE '[]' END`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function jsonDistanceExpr(metric: VectorMetric, column: string): string {
|
|
169
|
+
const row = jsonText(column);
|
|
170
|
+
const sameLength = `(SELECT COUNT(*) FROM json_each(${row})) = (SELECT COUNT(*) FROM q)`;
|
|
171
|
+
const terms: Record<VectorMetric, string> = {
|
|
172
|
+
cosine: `1.0 - (SELECT SUM(je.value * q.v) FROM json_each(${row}) je JOIN q ON q.k = je.key)`
|
|
173
|
+
+ ` / NULLIF(sqrt((SELECT SUM(q.v * q.v) FROM q)) * sqrt((SELECT SUM(je.value * je.value) FROM json_each(${row}) je)), 0)`,
|
|
174
|
+
euclidean: `sqrt((SELECT SUM((je.value - q.v) * (je.value - q.v)) FROM json_each(${row}) je JOIN q ON q.k = je.key))`,
|
|
175
|
+
dot: `-(SELECT SUM(je.value * q.v) FROM json_each(${row}) je JOIN q ON q.k = je.key)`,
|
|
176
|
+
};
|
|
177
|
+
return `CASE WHEN ${sameLength} THEN ${terms[metric]} END`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface SqliteVectorQuery {
|
|
181
|
+
sql: string;
|
|
182
|
+
params: Record<string, any>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface BuildArgs {
|
|
186
|
+
strategy: SqliteVectorStrategy;
|
|
187
|
+
metric: VectorMetric;
|
|
188
|
+
/** Already quoted, e.g. `[documents]`. */
|
|
189
|
+
table: string;
|
|
190
|
+
/** Already quoted column reference. */
|
|
191
|
+
column: string;
|
|
192
|
+
/** The query embedding. */
|
|
193
|
+
vector: number[];
|
|
194
|
+
take: number;
|
|
195
|
+
/** A WHERE tail built by the caller, or empty. */
|
|
196
|
+
tail?: string;
|
|
197
|
+
/** Pre-bound WHERE parameters shared with the caller. */
|
|
198
|
+
params: Record<string, any>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Ranking SQL for one strategy. `sqlite-vec` and `udf` bind the query vector as
|
|
203
|
+
* a float32 BLOB; `sql` binds it as JSON text because `json_each` reads text.
|
|
204
|
+
*/
|
|
205
|
+
export function buildSqliteVectorQuery(args: BuildArgs): SqliteVectorQuery {
|
|
206
|
+
const { strategy, metric, table, column, take } = args;
|
|
207
|
+
const params = args.params;
|
|
208
|
+
const tail = args.tail ?? '';
|
|
209
|
+
const queryVector = args.vector as number[];
|
|
210
|
+
// A row whose stored vector cannot be scored (a NULL, a text column, another
|
|
211
|
+
// dimension) yields a NULL distance; those rows are dropped rather than
|
|
212
|
+
// reported, so a caller never has to sort them out of the result.
|
|
213
|
+
const rank = (inner: string) =>
|
|
214
|
+
`SELECT * FROM (${inner}) AS an5_ranked WHERE distance IS NOT NULL ORDER BY distance ASC LIMIT ${take}`;
|
|
215
|
+
|
|
216
|
+
if (strategy === 'sqlite-vec') {
|
|
217
|
+
params.an5_vector = encodeVector(queryVector);
|
|
218
|
+
params.an5_vector_bytes = queryVector.length * BYTES_PER_FLOAT;
|
|
219
|
+
const fn = SQLITE_VEC_FUNCTIONS[metric];
|
|
220
|
+
const stored = `vec_f32(${column})`;
|
|
221
|
+
const distance = `CASE WHEN vec_length(${stored}) = ${queryVector.length} THEN ${fn}(${stored}, @an5_vector) END`;
|
|
222
|
+
// vec_f32 accepts JSON text and float32 BLOBs, preserving legacy rows.
|
|
223
|
+
return {
|
|
224
|
+
sql: rank(`SELECT *, ${distance} AS distance FROM ${table}${tail ? `${tail} AND` : ' WHERE'} ${column} IS NOT NULL`),
|
|
225
|
+
params,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (strategy === 'udf') {
|
|
230
|
+
params.an5_vector = encodeVector(queryVector);
|
|
231
|
+
const distance = `${AN5_VECTOR_FUNCTIONS[metric]}(${column}, @an5_vector)`;
|
|
232
|
+
return { sql: rank(`SELECT *, ${distance} AS distance FROM ${table}${tail}`), params };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
params.an5_vector_json = JSON.stringify(queryVector);
|
|
236
|
+
const distance = jsonDistanceExpr(metric, column);
|
|
237
|
+
return {
|
|
238
|
+
sql: 'WITH q AS (SELECT je.key AS k, CAST(je.value AS REAL) AS v FROM json_each(@an5_vector_json) je) '
|
|
239
|
+
+ rank(`SELECT *, ${distance} AS distance FROM ${table}${tail}`),
|
|
240
|
+
params,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Capabilities ──────────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
export interface SqliteVectorCapabilities {
|
|
247
|
+
/** AN5's C extension is loaded; keep its functions instead of driver callbacks. */
|
|
248
|
+
native?: boolean;
|
|
249
|
+
/** `vec_version()` answered, so the sqlite-vec extension is loaded. */
|
|
250
|
+
vec: boolean;
|
|
251
|
+
/** The adapter's own distance functions are registered with the driver. */
|
|
252
|
+
udf: boolean;
|
|
253
|
+
/** `json_each()` is available, which the pure-SQL strategy needs. */
|
|
254
|
+
json1: boolean;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface SqliteVectorOptions {
|
|
258
|
+
/** Path to the sqlite-vec extension binary to load before probing. */
|
|
259
|
+
sqliteVec?: string | undefined;
|
|
260
|
+
/** Pin a strategy instead of probing for the fastest one available. */
|
|
261
|
+
vectorStrategy?: VectorStrategyPreference | undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A driver hook the engine can supply to register the distance functions and
|
|
266
|
+
* load the sqlite-vec extension. Every field is optional: a driver that offers
|
|
267
|
+
* nothing simply reports no capabilities and the search falls back.
|
|
268
|
+
*/
|
|
269
|
+
/**
|
|
270
|
+
* Registers a scalar function with the driver. Implementations that take a
|
|
271
|
+
* determinism flag (better-sqlite3) supply it themselves.
|
|
272
|
+
*/
|
|
273
|
+
export type SqliteVectorFunctionRegistrar = (
|
|
274
|
+
name: string,
|
|
275
|
+
fn: (...args: any[]) => any,
|
|
276
|
+
) => void;
|
|
277
|
+
|
|
278
|
+
export interface SqliteVectorHooks {
|
|
279
|
+
/** Raw driver handle, used for `loadExtension`. */
|
|
280
|
+
native?: () => any;
|
|
281
|
+
/** Register `name` with the driver's scalar-function API. */
|
|
282
|
+
registerFunction?: SqliteVectorFunctionRegistrar;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function probed<T>(fn: () => T): T | null {
|
|
286
|
+
try {
|
|
287
|
+
return fn();
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the ordered list of strategies to try for this connection.
|
|
295
|
+
*
|
|
296
|
+
* `declaredType` is the column's DDL type: the JSON strategy only reaches rows
|
|
297
|
+
* stored as text, so a `BLOB` column drops it from the order.
|
|
298
|
+
*/
|
|
299
|
+
export function planSqliteVectorStrategies(
|
|
300
|
+
caps: SqliteVectorCapabilities,
|
|
301
|
+
declaredType: string | null,
|
|
302
|
+
preference?: VectorStrategyPreference,
|
|
303
|
+
): SqliteVectorStrategy[] {
|
|
304
|
+
const wanted = preference && preference !== 'auto' ? [preference] : [];
|
|
305
|
+
const available: SqliteVectorStrategy[] = [];
|
|
306
|
+
if (caps.native && caps.udf) available.push('udf');
|
|
307
|
+
if (caps.vec) available.push('sqlite-vec');
|
|
308
|
+
if (caps.udf && !caps.native) available.push('udf');
|
|
309
|
+
if (caps.json1) available.push('sql');
|
|
310
|
+
if (wanted.length > 0) return wanted.filter((s) => s !== 'memory');
|
|
311
|
+
const plan = available;
|
|
312
|
+
// A BLOB column has no JSON to read, so `json_each` can only produce NULLs.
|
|
313
|
+
const type = String(declaredType ?? '').toUpperCase();
|
|
314
|
+
if (type && !/TEXT|CHAR|CLOB|STRING|JSON/.test(type)) {
|
|
315
|
+
return plan.filter((s) => s !== 'sql');
|
|
316
|
+
}
|
|
317
|
+
return plan;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Register `an5_vec_*` on a driver and load sqlite-vec when a path is configured.
|
|
322
|
+
* Safe to call repeatedly; each driver hook is used at most once.
|
|
323
|
+
*/
|
|
324
|
+
export class SqliteVectorSupport {
|
|
325
|
+
private caps: SqliteVectorCapabilities | null = null;
|
|
326
|
+
/**
|
|
327
|
+
* Declared column types and the "any binary row" probe, keyed by column.
|
|
328
|
+
*
|
|
329
|
+
* Both cost a query, and neither changes while a schema is in use. A stale
|
|
330
|
+
* answer is still safe: the probe only decides which strategy to try first, and
|
|
331
|
+
* a strategy that fails falls through to the next one rather than reporting a
|
|
332
|
+
* wrong result.
|
|
333
|
+
*/
|
|
334
|
+
private readonly columnTypes = new Map<string, string | null>();
|
|
335
|
+
private readonly binaryRows = new Map<string, boolean>();
|
|
336
|
+
|
|
337
|
+
constructor(
|
|
338
|
+
private readonly hooks: SqliteVectorHooks,
|
|
339
|
+
private readonly options: SqliteVectorOptions = {},
|
|
340
|
+
) {}
|
|
341
|
+
|
|
342
|
+
async capabilities(): Promise<SqliteVectorCapabilities> {
|
|
343
|
+
if (this.caps) return this.caps;
|
|
344
|
+
const caps: SqliteVectorCapabilities = { vec: false, udf: false, json1: false };
|
|
345
|
+
|
|
346
|
+
const native = probed(() => this.hooks.native?.());
|
|
347
|
+
if (native) {
|
|
348
|
+
if (this.options.sqliteVec) {
|
|
349
|
+
probed(() => native.loadExtension(this.options.sqliteVec));
|
|
350
|
+
}
|
|
351
|
+
if (probed(() => native.prepare('SELECT vec_version()').get()) !== null) caps.vec = true;
|
|
352
|
+
if (probed(() => native.prepare('SELECT an5_vector_version()').get()) !== null) {
|
|
353
|
+
caps.native = true;
|
|
354
|
+
caps.udf = true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const register = this.hooks.registerFunction;
|
|
359
|
+
if (register && !caps.native) {
|
|
360
|
+
// better-sqlite3 takes an options object where every other driver takes a
|
|
361
|
+
// plain function, so both shapes are offered and the driver picks.
|
|
362
|
+
const names = Object.values(AN5_VECTOR_FUNCTIONS);
|
|
363
|
+
const impls = (Object.keys(AN5_VECTOR_FUNCTIONS) as VectorMetric[]).map((metric) => {
|
|
364
|
+
const fn = (...args: any[]) => {
|
|
365
|
+
const a = decodeVector(args[0]);
|
|
366
|
+
const b = decodeVector(args[1]);
|
|
367
|
+
return a && b ? vectorDistance(a, b, metric) : null;
|
|
368
|
+
};
|
|
369
|
+
return { name: AN5_VECTOR_FUNCTIONS[metric], fn };
|
|
370
|
+
});
|
|
371
|
+
let all = true;
|
|
372
|
+
for (const { name, fn } of impls) {
|
|
373
|
+
if (probed(() => register(name, fn)) === null) { all = false; break; }
|
|
374
|
+
}
|
|
375
|
+
// A driver that accepts the registration is trusted here; a query that
|
|
376
|
+
// still fails falls through to the next strategy.
|
|
377
|
+
caps.udf = all && names.length > 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.caps = caps;
|
|
381
|
+
return caps;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Confirms JSON1 and a usable sqlite-vec/UDF before a query is built for them. */
|
|
385
|
+
async probe(exec: (sql: string, params?: Record<string, any>) => Promise<any[]>): Promise<SqliteVectorCapabilities> {
|
|
386
|
+
const caps = await this.capabilities();
|
|
387
|
+
if (!caps.json1) {
|
|
388
|
+
try {
|
|
389
|
+
await exec("SELECT json_valid('[1]') AS an5_json_ok");
|
|
390
|
+
caps.json1 = true;
|
|
391
|
+
} catch {
|
|
392
|
+
caps.json1 = false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return caps;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The column's declared DDL type, remembered per column. */
|
|
399
|
+
async columnType(
|
|
400
|
+
exec: (sql: string, params?: Record<string, any>) => Promise<any[]>,
|
|
401
|
+
table: string,
|
|
402
|
+
column: string,
|
|
403
|
+
): Promise<string | null> {
|
|
404
|
+
const key = `${table}.${column}`;
|
|
405
|
+
if (!this.columnTypes.has(key)) {
|
|
406
|
+
const found = await readSqliteColumnType(exec, table, column).catch(() => null);
|
|
407
|
+
this.columnTypes.set(key, found);
|
|
408
|
+
}
|
|
409
|
+
return this.columnTypes.get(key) ?? null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Whether any row stores raw bytes, which `json_each` cannot read.
|
|
414
|
+
*
|
|
415
|
+
* Checked only for the `sql` strategy, where a legacy JSON column that has
|
|
416
|
+
* since taken new float32 rows would otherwise rank just the old ones.
|
|
417
|
+
*/
|
|
418
|
+
async hasBinaryRows(
|
|
419
|
+
exec: (sql: string, params?: Record<string, any>) => Promise<any[]>,
|
|
420
|
+
table: string,
|
|
421
|
+
column: string,
|
|
422
|
+
tail: string,
|
|
423
|
+
params: Record<string, any>,
|
|
424
|
+
): Promise<boolean> {
|
|
425
|
+
const key = `${table}.${column}`;
|
|
426
|
+
if (!this.binaryRows.has(key)) {
|
|
427
|
+
const rows = await exec(
|
|
428
|
+
`SELECT 1 FROM ${table}${tail ? `${tail} AND` : ' WHERE'} typeof(${column}) = 'blob' LIMIT 1`,
|
|
429
|
+
params,
|
|
430
|
+
).catch(() => []);
|
|
431
|
+
this.binaryRows.set(key, rows.length > 0);
|
|
432
|
+
}
|
|
433
|
+
return this.binaryRows.get(key) === true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
reset(): void {
|
|
437
|
+
this.caps = null;
|
|
438
|
+
this.columnTypes.clear();
|
|
439
|
+
this.binaryRows.clear();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Reads a vector column's declared DDL type, or null when it cannot be found. */
|
|
444
|
+
export async function readSqliteColumnType(
|
|
445
|
+
exec: (sql: string, params?: Record<string, any>) => Promise<any[]>,
|
|
446
|
+
table: string,
|
|
447
|
+
column: string,
|
|
448
|
+
): Promise<string | null> {
|
|
449
|
+
const rows = await exec("SELECT type FROM pragma_table_info(@an5_table) WHERE name = @an5_column", {
|
|
450
|
+
an5_table: table,
|
|
451
|
+
an5_column: column,
|
|
452
|
+
});
|
|
453
|
+
const value = rows?.[0]?.type;
|
|
454
|
+
return value ? String(value) : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export interface SqliteVectorSearchArgs {
|
|
458
|
+
exec: (sql: string, params?: Record<string, any>) => Promise<any[]>;
|
|
459
|
+
/** Absent when the engine cannot register functions or load extensions. */
|
|
460
|
+
vectorSupport?: SqliteVectorSupport | null;
|
|
461
|
+
/** Quoted table reference, e.g. `[documents]`. */
|
|
462
|
+
table: string;
|
|
463
|
+
/** Unquoted table name, for `pragma_table_info`. */
|
|
464
|
+
rawTable: string;
|
|
465
|
+
/** Quoted column reference. */
|
|
466
|
+
column: string;
|
|
467
|
+
/** Unquoted column name. */
|
|
468
|
+
rawColumn: string;
|
|
469
|
+
vector: number[];
|
|
470
|
+
metric: VectorMetric;
|
|
471
|
+
take: number;
|
|
472
|
+
/** A WHERE tail (leading space included) or empty. */
|
|
473
|
+
tail?: string | undefined;
|
|
474
|
+
/** Parameter object shared with the caller's WHERE clause. */
|
|
475
|
+
params: Record<string, any>;
|
|
476
|
+
preference?: VectorStrategyPreference | undefined;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Rank a SQLite table by distance inside the database, trying each available
|
|
481
|
+
* strategy in order. Returns null when the connection offers none, so the
|
|
482
|
+
* caller can fall back to scoring in memory.
|
|
483
|
+
*/
|
|
484
|
+
export async function runSqliteVectorSearch(
|
|
485
|
+
args: SqliteVectorSearchArgs,
|
|
486
|
+
): Promise<{ rows: Record<string, any>[]; strategy: SqliteVectorStrategy } | null> {
|
|
487
|
+
if (!Array.isArray(args.vector) || args.vector.length === 0) return null;
|
|
488
|
+
|
|
489
|
+
const caps = args.vectorSupport
|
|
490
|
+
? await args.vectorSupport.probe(args.exec)
|
|
491
|
+
: { vec: false, udf: false, json1: false };
|
|
492
|
+
if (!args.vectorSupport) {
|
|
493
|
+
try {
|
|
494
|
+
await args.exec("SELECT json_valid('[1]') AS an5_json_ok");
|
|
495
|
+
caps.json1 = true;
|
|
496
|
+
} catch {
|
|
497
|
+
caps.json1 = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let declaredType: string | null = null;
|
|
502
|
+
try {
|
|
503
|
+
declaredType = args.vectorSupport
|
|
504
|
+
? await args.vectorSupport.columnType(args.exec, args.rawTable, args.rawColumn)
|
|
505
|
+
: await readSqliteColumnType(args.exec, args.rawTable, args.rawColumn);
|
|
506
|
+
} catch {
|
|
507
|
+
declaredType = null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const strategy of planSqliteVectorStrategies(caps, declaredType, args.preference)) {
|
|
511
|
+
if (strategy === 'sql') {
|
|
512
|
+
const binary = args.vectorSupport
|
|
513
|
+
? await args.vectorSupport.hasBinaryRows(args.exec, args.rawTable, args.rawColumn, args.tail ?? '', args.params)
|
|
514
|
+
: (await args.exec(
|
|
515
|
+
`SELECT 1 FROM ${args.table}${args.tail ? `${args.tail} AND` : ' WHERE'} typeof(${args.column}) = 'blob' LIMIT 1`,
|
|
516
|
+
args.params,
|
|
517
|
+
)).length > 0;
|
|
518
|
+
if (binary) continue;
|
|
519
|
+
}
|
|
520
|
+
const params: Record<string, any> = { ...args.params };
|
|
521
|
+
const query = buildSqliteVectorQuery({
|
|
522
|
+
strategy,
|
|
523
|
+
metric: normalizeMetric(args.metric),
|
|
524
|
+
table: args.table,
|
|
525
|
+
column: args.column,
|
|
526
|
+
vector: args.vector,
|
|
527
|
+
take: args.take,
|
|
528
|
+
tail: args.tail ?? '',
|
|
529
|
+
params,
|
|
530
|
+
});
|
|
531
|
+
try {
|
|
532
|
+
const rows = await args.exec(query.sql, params);
|
|
533
|
+
return { rows: rows ?? [], strategy };
|
|
534
|
+
} catch {
|
|
535
|
+
// The driver advertised the capability but the query failed, e.g. an
|
|
536
|
+
// extension that did not really load. The next strategy is cheaper than
|
|
537
|
+
// reporting the failure, and memory is always available as a last resort.
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
package/golang/sqlite/sqlite.go
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Package sqlite provides SQLite specific dialect execution and query generation for AN5 Go Adapters.
|
|
2
|
-
package sqlite
|
|
3
|
-
|
|
4
|
-
import (
|
|
5
|
-
"an5adapters/base"
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
// BuildVectorSearchQuery returns empty string as SQLite uses in-memory vector fallback by default.
|
|
9
|
-
func BuildVectorSearchQuery(table, field, metric string, dim, take int, where string) string {
|
|
10
|
-
_ = base.QuoteTable(table, base.DialectSqlite)
|
|
11
|
-
_ = base.QuoteIdentifier(field, base.DialectSqlite)
|
|
12
|
-
// Native vector extension not present in standard sqlite; in-memory fallback is used.
|
|
13
|
-
return ""
|
|
14
|
-
}
|