@an5/adapters 0.2.11 → 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.
Files changed (91) hide show
  1. package/README.md +133 -2
  2. package/dist/an5Adapter.d.ts +37 -0
  3. package/dist/an5Adapter.js +138 -35
  4. package/dist/base/types.d.ts +14 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +8 -1
  7. package/dist/sqlite/browserEngine.d.ts +13 -0
  8. package/dist/sqlite/browserEngine.js +13 -0
  9. package/dist/sqlite/engine.d.ts +5 -0
  10. package/dist/sqlite/engine.js +10 -0
  11. package/dist/sqlite/index.d.ts +1 -0
  12. package/dist/sqlite/index.js +1 -0
  13. package/dist/sqlite/vector.d.ts +155 -0
  14. package/dist/sqlite/vector.js +436 -0
  15. package/dotnet/An5Adapter.cs +85 -27
  16. package/dotnet/Base/Core.cs +14 -0
  17. package/dotnet/Mssql/MssqlEngine.cs +44 -1
  18. package/dotnet/Postgres/PostgresEngine.cs +2 -0
  19. package/dotnet/Sqlite/SqliteEngine.cs +82 -2
  20. package/dotnet/Sqlite/SqliteVectors.cs +401 -0
  21. package/golang/an5_adapter.go +84 -32
  22. package/golang/base/vectors.go +263 -0
  23. package/golang/sqlite/vector.go +248 -0
  24. package/golang/sqlite_vector_test.go +288 -0
  25. package/golang/table_client.go +67 -3
  26. package/java/pom.xml +177 -0
  27. package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
  28. package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
  29. package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
  30. package/java/src/main/java/an5/adapters/An5Query.java +100 -0
  31. package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
  32. package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
  33. package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
  34. package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
  35. package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
  36. package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
  37. package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
  38. package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
  39. package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
  40. package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
  41. package/java/src/test/java/An5AdaptersSmoke.java +733 -0
  42. package/java/src/test/java/Json.java +174 -0
  43. package/java/src/test/java/QuerySemantics.java +111 -0
  44. package/kotlin/build.gradle.kts +145 -0
  45. package/kotlin/settings.gradle.kts +10 -0
  46. package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
  47. package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
  48. package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
  49. package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
  50. package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
  51. package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
  52. package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
  53. package/native/sqlite/CMakeLists.txt +12 -0
  54. package/native/sqlite/README.md +117 -0
  55. package/native/sqlite/an5_vector.c +209 -0
  56. package/package.json +33 -7
  57. package/python/an5_adapter.py +37 -6
  58. package/python/base/__init__.py +15 -0
  59. package/python/base/vectors.py +434 -0
  60. package/python/sqlite/__init__.py +2 -2
  61. package/python/sqlite/provider.py +21 -1
  62. package/python/table_client.py +129 -59
  63. package/rust/src/adapter.rs +342 -24
  64. package/rust/src/base/mod.rs +7 -0
  65. package/rust/src/base/vectors.rs +293 -0
  66. package/rust/src/lib.rs +3 -3
  67. package/scripts/sqlite-native-build.js +32 -0
  68. package/swift/Package.swift +22 -0
  69. package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
  70. package/swift/Sources/An5Adapters/Metadata.swift +183 -0
  71. package/swift/Sources/An5Adapters/Query.swift +105 -0
  72. package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
  73. package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
  74. package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
  75. package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
  76. package/swift/Sources/An5Adapters/TableClient.swift +671 -0
  77. package/swift/Sources/An5Adapters/Types.swift +127 -0
  78. package/swift/Sources/An5Adapters/Vectors.swift +86 -0
  79. package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
  80. package/swift/Sources/CSQLite/module.modulemap +8 -0
  81. package/swift/Sources/CSQLite/shim.h +9 -0
  82. package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
  83. package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
  84. package/typescript/src/an5Adapter.ts +146 -29
  85. package/typescript/src/base/types.ts +15 -0
  86. package/typescript/src/index.ts +13 -0
  87. package/typescript/src/sqlite/browserEngine.ts +24 -0
  88. package/typescript/src/sqlite/engine.ts +13 -0
  89. package/typescript/src/sqlite/index.ts +1 -1
  90. package/typescript/src/sqlite/vector.ts +541 -0
  91. package/golang/sqlite/sqlite.go +0 -14
@@ -0,0 +1,401 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Linq;
4
+ using System.Text.Json;
5
+
6
+ namespace An5Orm
7
+ {
8
+ // ─── SQLite vector search ────────────────────────────────────────────────────
9
+ //
10
+ // SQLite has no vector type, so a `VECTOR(n)` column stores a BLOB of
11
+ // little-endian float32 values (see `EncodeVector`) and is ranked in one of
12
+ // four ways, in this order:
13
+ //
14
+ // 1. sqlite-vec native scalar distances, when the extension loads.
15
+ // 2. udf an5_vec_cosine / an5_vec_l2 / an5_vec_ip registered with
16
+ // the connection. Reads the BLOB and the legacy JSON text.
17
+ // 3. sql json_each brute force in plain SQL. Needs no user
18
+ // function, but only reaches rows stored as JSON text.
19
+ // 4. memory the column is loaded and scored in C#.
20
+ //
21
+ // Strategies 1-3 rank inside the database and transfer only the matching
22
+ // rows, which is why they are preferred over the in-memory path.
23
+
24
+ public static class SqliteVectors
25
+ {
26
+ /// <summary>Scalar functions the adapter registers itself.</summary>
27
+ public static readonly Dictionary<string, string> An5Functions = new()
28
+ {
29
+ ["cosine"] = "an5_vec_cosine",
30
+ ["euclidean"] = "an5_vec_l2",
31
+ ["dot"] = "an5_vec_ip",
32
+ };
33
+
34
+ /// <summary>Scalar functions shipped by the sqlite-vec extension.</summary>
35
+ public static readonly Dictionary<string, string> SqliteVecFunctions = new()
36
+ {
37
+ ["cosine"] = "vec_distance_cosine",
38
+ ["euclidean"] = "vec_distance_l2",
39
+ ["dot"] = "vec_distance_ip",
40
+ };
41
+
42
+ public const string StrategySqliteVec = "sqlite-vec";
43
+ public const string StrategyUdf = "udf";
44
+ public const string StrategySql = "sql";
45
+ public const string StrategyMemory = "memory";
46
+
47
+ private const int BytesPerFloat = 4;
48
+
49
+ public static string NormalizeMetric(string metric)
50
+ => metric == "euclidean" || metric == "dot" ? metric : "cosine";
51
+
52
+ // ── Codec ───────────────────────────────────────────────────────────────
53
+
54
+ /// <summary>
55
+ /// Reads a numeric array out of the shapes a driver can hand back: a
56
+ /// list, a JSON text column, or raw float32 bytes.
57
+ /// </summary>
58
+ public static List<double> ParseVector(object value)
59
+ {
60
+ switch (value)
61
+ {
62
+ case null:
63
+ return null;
64
+ case List<double> list:
65
+ return list.Count > 0 ? list : null;
66
+ case double[] array:
67
+ return array.Length > 0 ? array.ToList() : null;
68
+ case float[] floats:
69
+ return floats.Length > 0 ? floats.Select(f => (double)f).ToList() : null;
70
+ case byte[] bytes:
71
+ return DecodeVector(bytes);
72
+ case string text:
73
+ var trimmed = text.Trim();
74
+ if (!trimmed.StartsWith("[")) return null;
75
+ try { return ParseVector(JsonSerializer.Deserialize<double[]>(trimmed)); }
76
+ catch { return null; }
77
+ case System.Collections.IEnumerable seq when value is not string:
78
+ try { return ParseVector(seq.Cast<object>().Select(o => Convert.ToDouble(o)).ToList()); }
79
+ catch { return null; }
80
+ default:
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /// <summary>
86
+ /// Decodes a stored vector: a float32 BLOB, a legacy JSON text column,
87
+ /// or an array the caller already decoded.
88
+ /// </summary>
89
+ public static List<double> DecodeVector(object value)
90
+ {
91
+ var direct = value is byte[] ? null : ParseVector(value);
92
+ if (direct != null) return direct;
93
+ if (!(value is byte[] bytes) || bytes.Length == 0 || bytes.Length % BytesPerFloat != 0) return null;
94
+ var outVec = new List<double>(bytes.Length / BytesPerFloat);
95
+ for (int i = 0; i < bytes.Length; i += BytesPerFloat)
96
+ outVec.Add(BitConverter.ToSingle(bytes, i));
97
+ return outVec;
98
+ }
99
+
100
+ /// <summary>Encodes a vector as the little-endian float32 BLOB the column stores.</summary>
101
+ public static byte[] EncodeVector(IReadOnlyList<double> values)
102
+ {
103
+ var bytes = new byte[values.Count * BytesPerFloat];
104
+ for (int i = 0; i < values.Count; i++)
105
+ {
106
+ // Little-endian, written through the integer view of the float:
107
+ // SQLite reads the bytes in memory order.
108
+ var bits = BitConverter.SingleToInt32Bits((float)values[i]);
109
+ bytes[i * BytesPerFloat] = (byte)(bits & 0xFF);
110
+ bytes[i * BytesPerFloat + 1] = (byte)((bits >> 8) & 0xFF);
111
+ bytes[i * BytesPerFloat + 2] = (byte)((bits >> 16) & 0xFF);
112
+ bytes[i * BytesPerFloat + 3] = (byte)((bits >> 24) & 0xFF);
113
+ }
114
+ return bytes;
115
+ }
116
+
117
+ /// <summary>True when the value still has to become bytes on its way into a column.</summary>
118
+ public static bool NeedsVectorEncoding(object value)
119
+ {
120
+ if (value == null || value is string || value is byte[]) return false;
121
+ if (value is System.Collections.IEnumerable seq) return !(value is System.Collections.IDictionary);
122
+ return false;
123
+ }
124
+
125
+ /// <summary>True when generated metadata describes a `VECTOR(n)` column.</summary>
126
+ public static bool IsVectorField(object definition)
127
+ {
128
+ if (definition == null) return false;
129
+ if (definition is string text)
130
+ return text.Contains("[]") || text.TrimStart().StartsWith("VECTOR", StringComparison.OrdinalIgnoreCase);
131
+
132
+ // The metadata carries a small record per field.
133
+ var kind = ReadMember(definition, "kind") as string;
134
+ if (kind == "vector") return true;
135
+ var sql = ReadMember(definition, "sql") as string ?? ReadMember(definition, "type") as string;
136
+ if (!string.IsNullOrWhiteSpace(sql))
137
+ {
138
+ var trimmed = sql.Trim();
139
+ if (trimmed.StartsWith("VECTOR", StringComparison.OrdinalIgnoreCase)
140
+ && (trimmed.Length == 6 || trimmed[6] == '(' || trimmed[6] == ' '))
141
+ return true;
142
+ }
143
+ var ts = ReadMember(definition, "ts") as string;
144
+ return ts != null && ts.Contains("[]");
145
+ }
146
+
147
+ private static object ReadMember(object target, string name)
148
+ {
149
+ if (target is System.Collections.IDictionary dict)
150
+ return dict.Contains(name) ? dict[name] : null;
151
+ var prop = target.GetType().GetProperty(name);
152
+ return prop != null ? prop.GetValue(target) : null;
153
+ }
154
+
155
+ // ── Distance ────────────────────────────────────────────────────────────
156
+
157
+ /// <summary>
158
+ /// Distance between two equal-length vectors, lower is closer. Mirrors
159
+ /// the SQL functions so a search ranks the same way whichever strategy ran.
160
+ /// </summary>
161
+ public static double? VectorDistance(IReadOnlyList<double> a, IReadOnlyList<double> b, string metric)
162
+ {
163
+ if (a == null || b == null || a.Count == 0 || a.Count != b.Count) return null;
164
+ var resolved = NormalizeMetric(metric);
165
+ if (resolved == "euclidean")
166
+ {
167
+ double sum = 0;
168
+ for (int i = 0; i < a.Count; i++)
169
+ {
170
+ var diff = a[i] - b[i];
171
+ sum += diff * diff;
172
+ }
173
+ return Math.Sqrt(sum);
174
+ }
175
+ double dot = 0, m1 = 0, m2 = 0;
176
+ for (int i = 0; i < a.Count; i++)
177
+ {
178
+ dot += a[i] * b[i];
179
+ m1 += a[i] * a[i];
180
+ m2 += b[i] * b[i];
181
+ }
182
+ if (resolved == "dot") return -dot;
183
+ return m1 > 0 && m2 > 0 ? 1.0 - dot / (Math.Sqrt(m1) * Math.Sqrt(m2)) : 1.0;
184
+ }
185
+
186
+ /// <summary>The scalar function registered with the driver for one metric.</summary>
187
+ public static Func<object[], object> MakeDistanceFunction(string metric)
188
+ => args =>
189
+ {
190
+ var a = DecodeVector(args.Length > 0 ? args[0] : null);
191
+ var b = DecodeVector(args.Length > 1 ? args[1] : null);
192
+ if (a == null || b == null) return DBNull.Value;
193
+ var distance = VectorDistance(a, b, metric);
194
+ return distance.HasValue ? (object)distance.Value : DBNull.Value;
195
+ };
196
+
197
+ // ── Query building ──────────────────────────────────────────────────────
198
+
199
+ /// <summary>`json_valid` fails on a float32 BLOB, so the JSON path needs this guard.</summary>
200
+ private static string JsonText(string column)
201
+ => $"CASE WHEN json_valid({column}) THEN {column} ELSE '[]' END";
202
+
203
+ private static string JsonDistanceExpr(string metric, string column)
204
+ {
205
+ var row = JsonText(column);
206
+ var sameLength = $"(SELECT COUNT(*) FROM json_each({row})) = (SELECT COUNT(*) FROM q)";
207
+ string terms;
208
+ switch (metric)
209
+ {
210
+ case "euclidean":
211
+ terms = $"sqrt((SELECT SUM((je.value - q.v) * (je.value - q.v)) FROM json_each({row}) je JOIN q ON q.k = je.key))";
212
+ break;
213
+ case "dot":
214
+ terms = $"-(SELECT SUM(je.value * q.v) FROM json_each({row}) je JOIN q ON q.k = je.key)";
215
+ break;
216
+ default:
217
+ terms = $"1.0 - (SELECT SUM(je.value * q.v) FROM json_each({row}) je JOIN q ON q.k = je.key)"
218
+ + $" / NULLIF(sqrt((SELECT SUM(q.v * q.v) FROM q)) * sqrt((SELECT SUM(je.value * je.value) FROM json_each({row}) je)), 0)";
219
+ break;
220
+ }
221
+ return $"CASE WHEN {sameLength} THEN {terms} END";
222
+ }
223
+
224
+ /// <summary>
225
+ /// Ranking SQL for one strategy, with the parameters it needs appended to
226
+ /// `parameters`. `sqlite-vec` and `udf` bind the query vector as a float32
227
+ /// BLOB; `sql` binds it as JSON text because `json_each` reads text.
228
+ /// </summary>
229
+ public static string BuildQuery(
230
+ string strategy, string metric, string table, string column,
231
+ IReadOnlyList<double> vector, int take, string tail,
232
+ Dictionary<string, object> parameters)
233
+ {
234
+ metric = NormalizeMetric(metric);
235
+ // A row whose stored vector cannot be scored (a NULL, a text column,
236
+ // another dimension) yields a NULL distance; those rows are dropped
237
+ // rather than reported, so a caller never has to sort them out.
238
+ string Rank(string inner)
239
+ => $"SELECT * FROM ({inner}) AS an5_ranked WHERE distance IS NOT NULL ORDER BY distance ASC LIMIT {take}";
240
+
241
+ if (strategy == StrategySqliteVec)
242
+ {
243
+ parameters["an5_vector"] = EncodeVector(vector);
244
+ parameters["an5_vector_bytes"] = vector.Count * BytesPerFloat;
245
+ // sqlite-vec only understands float32 BLOB operands, so rows stored
246
+ // any other way are excluded instead of aborting the query.
247
+ return Rank($"SELECT *, CASE WHEN vec_length(vec_f32({column})) = {vector.Count}"
248
+ + $" THEN {SqliteVecFunctions[metric]}(vec_f32({column}), @an5_vector) END AS distance FROM {table}"
249
+ + (string.IsNullOrWhiteSpace(tail) ? " WHERE " : tail + " AND ")
250
+ + $"{column} IS NOT NULL");
251
+ }
252
+
253
+ if (strategy == StrategyUdf)
254
+ {
255
+ parameters["an5_vector"] = EncodeVector(vector);
256
+ return Rank($"SELECT *, {An5Functions[metric]}({column}, @an5_vector) AS distance FROM {table}{tail}");
257
+ }
258
+
259
+ parameters["an5_vector_json"] = JsonSerializer.Serialize(vector);
260
+ return "WITH q AS (SELECT je.key AS k, CAST(je.value AS REAL) AS v FROM json_each(@an5_vector_json) je) "
261
+ + Rank($"SELECT *, {JsonDistanceExpr(metric, column)} AS distance FROM {table}{tail}");
262
+ }
263
+
264
+ // ── Capabilities ────────────────────────────────────────────────────────
265
+
266
+ /// <summary>
267
+ /// The ordered strategies to try. `declaredType` is the column's DDL type:
268
+ /// the JSON strategy only reaches rows stored as text, so a `BLOB` column
269
+ /// drops it from the order.
270
+ /// </summary>
271
+ public static List<string> PlanStrategies(
272
+ bool hasVec, bool hasUdf, bool hasJson1, string declaredType, string preference)
273
+ {
274
+ if (!string.IsNullOrWhiteSpace(preference) && preference != "auto")
275
+ return preference == StrategyMemory ? new List<string>() : new List<string> { preference };
276
+
277
+ var available = new List<string>();
278
+ if (hasVec) available.Add(StrategySqliteVec);
279
+ if (hasUdf) available.Add(StrategyUdf);
280
+ if (hasJson1) available.Add(StrategySql);
281
+
282
+ var declared = (declaredType ?? "").ToUpperInvariant();
283
+ if (declared.Length > 0
284
+ && !new[] { "TEXT", "CHAR", "CLOB", "STRING", "JSON" }.Any(t => declared.Contains(t)))
285
+ {
286
+ // A BLOB column has no JSON to read, so json_each can only produce NULLs.
287
+ available.Remove(StrategySql);
288
+ }
289
+ return available;
290
+ }
291
+
292
+ /// <summary>Reads a vector column's declared DDL type, or null when not found.</summary>
293
+ public static string ReadColumnType(
294
+ Func<string, Dictionary<string, object>, List<Dictionary<string, object>>> exec,
295
+ string table, string column)
296
+ {
297
+ try
298
+ {
299
+ var rows = exec(
300
+ "SELECT type FROM pragma_table_info(@an5_table) WHERE name = @an5_column",
301
+ new Dictionary<string, object> { ["an5_table"] = table, ["an5_column"] = column });
302
+ var value = rows.Count > 0 ? rows[0].GetValueOrDefault("type") : null;
303
+ return value == null ? null : value.ToString();
304
+ }
305
+ catch { return null; }
306
+ }
307
+
308
+ /// <summary>
309
+ /// Ranks a SQLite table inside the database. Returns null when the
310
+ /// connection offers no strategy, so the caller can fall back to scoring
311
+ /// in memory.
312
+ /// </summary>
313
+ public static List<Dictionary<string, object>> RunSearch(
314
+ Func<string, Dictionary<string, object>, List<Dictionary<string, object>>> exec,
315
+ string table, string rawTable, string column, string rawColumn,
316
+ IReadOnlyList<double> vector, string metric, int take, string tail,
317
+ Dictionary<string, object> parameters, SqliteVectorSupport support)
318
+ {
319
+ if (vector == null || vector.Count == 0) return null;
320
+
321
+ var caps = support?.Capabilities() ?? new SqliteVectorCapabilities { Json1 = true };
322
+ if (support != null) support.ProbeJson1(exec);
323
+
324
+ var declaredType = ReadColumnType(exec, rawTable, rawColumn);
325
+ foreach (var strategy in PlanStrategies(caps.Vec, caps.Udf, caps.Json1, declaredType, support?.Preference))
326
+ {
327
+ if (strategy == StrategySql) {
328
+ var binary = exec($"SELECT 1 FROM {table}" + (string.IsNullOrWhiteSpace(tail) ? " WHERE " : tail + " AND ")
329
+ + $"typeof({column}) = 'blob' LIMIT 1", parameters);
330
+ if (binary.Count > 0) continue;
331
+ }
332
+ var bound = new Dictionary<string, object>();
333
+ foreach (var kv in parameters) bound[kv.Key] = kv.Value;
334
+ var sql = BuildQuery(strategy, metric, table, column, vector, take, tail, bound);
335
+ try
336
+ {
337
+ var rows = exec(sql, bound);
338
+ if (rows != null) return rows;
339
+ }
340
+ catch
341
+ {
342
+ // The connection advertised the capability but the query
343
+ // failed, e.g. an extension that did not really load. The
344
+ // next strategy is cheaper than reporting the failure.
345
+ continue;
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+ }
351
+
352
+ /// <summary>What a SQLite connection can do, resolved once and cached.</summary>
353
+ public class SqliteVectorCapabilities
354
+ {
355
+ /// <summary>`vec_version()` answered, so the sqlite-vec extension is loaded.</summary>
356
+ public bool Vec { get; set; }
357
+ /// <summary>The adapter's own distance functions are registered.</summary>
358
+ public bool Udf { get; set; }
359
+ /// <summary>`json_each()` is available, which the pure-SQL strategy needs.</summary>
360
+ public bool Json1 { get; set; }
361
+ }
362
+
363
+ /// <summary>
364
+ /// The settings one SQLite connection needs for in-database vector search.
365
+ /// Filled in by the engine, which knows how to open connections.
366
+ /// </summary>
367
+ public class SqliteVectorSupport
368
+ {
369
+ /// <summary>Path to the sqlite-vec extension binary to load.</summary>
370
+ public string SqliteVecPath { get; set; }
371
+ /// <summary>Pin a strategy instead of probing for the fastest one available.</summary>
372
+ public string Preference { get; set; }
373
+
374
+ private SqliteVectorCapabilities _capabilities;
375
+
376
+ public SqliteVectorCapabilities Capabilities()
377
+ => _capabilities ??= new SqliteVectorCapabilities { Udf = true, Json1 = true };
378
+
379
+ /// <summary>Confirms JSON1 before a query is built that needs it.</summary>
380
+ public void ProbeJson1(Func<string, Dictionary<string, object>, List<Dictionary<string, object>>> exec)
381
+ {
382
+ var caps = Capabilities();
383
+ if (caps.Json1) return;
384
+ try
385
+ {
386
+ exec("SELECT json_valid('[1]') AS an5_json_ok", new Dictionary<string, object>());
387
+ caps.Json1 = true;
388
+ }
389
+ catch { caps.Json1 = false; }
390
+ }
391
+
392
+ /// <summary>Records what the connection actually manages, after the engine tried.</summary>
393
+ internal void Report(bool vec, bool udf, bool json1)
394
+ {
395
+ var caps = Capabilities();
396
+ caps.Vec = vec;
397
+ caps.Udf = udf;
398
+ caps.Json1 = json1;
399
+ }
400
+ }
401
+ }
@@ -68,6 +68,9 @@ func GetRelationsForModel(modelName string) map[string]RelationDef {
68
68
  type An5Adapter struct {
69
69
  DB *sql.DB
70
70
  Dialect Dialect
71
+ // VectorSupport carries the SQLite vector settings. Set it before the first
72
+ // search to load sqlite-vec or to pin a strategy.
73
+ VectorSupport sqlite.VectorSupport
71
74
  }
72
75
 
73
76
  // NewAn5Adapter constructs an An5Adapter instance.
@@ -167,14 +170,47 @@ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetV
167
170
  vecBytes, _ := json.Marshal(targetVector)
168
171
  vecStr := string(vecBytes)
169
172
 
170
- // 1. Primary path: Native database SQL vector query execution via dialect provider (mssql / postgres / sqlite)
173
+ // 1. SQLite ranks through sqlite-vec, a driver function or json_each,
174
+ // whichever this connection supports, so the table never has to leave the
175
+ // database. `database/sql` cannot register a user function, so the UDF
176
+ // strategy needs the driver to install them; see sqlite.RegisterVectorFunctions.
177
+ if a.Dialect == base.DialectSqlite {
178
+ tail := ""
179
+ if strings.TrimSpace(whereClause) != "" {
180
+ tail = " WHERE " + whereClause
181
+ }
182
+ support := a.VectorSupport
183
+ ranked, ran, err := sqlite.RunVectorSearch(
184
+ ctx,
185
+ a.DB,
186
+ a.QueryRaw,
187
+ base.QuoteTable(tableName, a.Dialect),
188
+ sqlite.NormalizeTableName(tableName),
189
+ base.QuoteIdentifier(vectorField, a.Dialect),
190
+ vectorField,
191
+ targetVector,
192
+ distanceMetric,
193
+ take,
194
+ tail,
195
+ args,
196
+ support,
197
+ )
198
+ if err != nil {
199
+ return nil, err
200
+ }
201
+ if ran {
202
+ return DecodeVectorRows(ranked, vectorField), nil
203
+ }
204
+ // No in-database strategy: score the column in Go instead.
205
+ return a.vectorSearchInMemory(ctx, tableName, targetVector, take, whereClause, vectorField, distanceMetric, args)
206
+ }
207
+
208
+ // 2. Primary path: Native database SQL vector query execution via dialect provider (mssql / postgres)
171
209
  var sqlQuery string
172
210
  if a.Dialect == base.DialectPostgres {
173
211
  sqlQuery = postgres.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
174
212
  } else if a.Dialect == base.DialectMssql {
175
213
  sqlQuery = mssql.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
176
- } else if a.Dialect == base.DialectSqlite {
177
- sqlQuery = sqlite.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
178
214
  }
179
215
 
180
216
  if sqlQuery != "" {
@@ -185,7 +221,7 @@ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetV
185
221
  }
186
222
  }
187
223
 
188
- // 2. Secondary fallback: Fetch rows and compute vector distance in-memory if DB lacks native vector extensions
224
+ // 3. Secondary fallback: Fetch rows and compute vector distance in-memory if DB lacks native vector extensions
189
225
  fallbackQuery := "SELECT * FROM " + base.QuoteTable(tableName, a.Dialect)
190
226
  if strings.TrimSpace(whereClause) != "" {
191
227
  fallbackQuery += " WHERE " + whereClause
@@ -199,6 +235,45 @@ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetV
199
235
  return VectorSearchFallback(rows, targetVector, take, vectorField, distanceMetric), nil
200
236
  }
201
237
 
238
+ // vectorSearchInMemory loads the table and scores the vector column in Go.
239
+ //
240
+ // The last resort for every provider, and the only option on a SQLite connection
241
+ // that offers neither sqlite-vec, a driver function nor JSON1. The whole table is
242
+ // loaded, so it is only reasonable for a table small enough to fit in memory.
243
+ func (a *An5Adapter) vectorSearchInMemory(ctx context.Context, tableName string, targetVector []float64, take int, whereClause string, vectorField string, distanceMetric string, args []interface{}) ([]map[string]interface{}, error) {
244
+ query := "SELECT * FROM " + base.QuoteTable(tableName, a.Dialect)
245
+ if strings.TrimSpace(whereClause) != "" {
246
+ query += " WHERE " + whereClause
247
+ }
248
+ rows, err := a.QueryRaw(ctx, query, args...)
249
+ if err != nil {
250
+ return nil, err
251
+ }
252
+ return VectorSearchFallback(rows, targetVector, take, vectorField, distanceMetric), nil
253
+ }
254
+
255
+ // DecodeVectorRows replaces every stored `VECTOR(n)` column with a decoded
256
+ // float64 slice, so a caller reads numbers rather than float32 bytes or JSON text.
257
+ func DecodeVectorRows(rows []map[string]interface{}, vectorField string) []map[string]interface{} {
258
+ for _, row := range rows {
259
+ for key, value := range row {
260
+ if key != vectorField {
261
+ continue
262
+ }
263
+ decoded := base.DecodeVector(value)
264
+ if decoded == nil {
265
+ continue
266
+ }
267
+ // A `distance` column is a real number, not a stored vector.
268
+ if key == "distance" {
269
+ continue
270
+ }
271
+ row[key] = decoded
272
+ }
273
+ }
274
+ return rows
275
+ }
276
+
202
277
  // VectorSearchFallback ranks rows in-memory.
203
278
  func VectorSearchFallback(rows []map[string]interface{}, targetVector []float64, take int, vectorField string, distanceMetric string) []map[string]interface{} {
204
279
  type scored struct {
@@ -213,36 +288,14 @@ func VectorSearchFallback(rows []map[string]interface{}, targetVector []float64,
213
288
  continue
214
289
  }
215
290
 
216
- var rowVec []float64
217
- switch v := raw.(type) {
218
- case string:
219
- _ = json.Unmarshal([]byte(v), &rowVec)
220
- case []byte:
221
- _ = json.Unmarshal(v, &rowVec)
222
- case []float64:
223
- rowVec = v
224
- case []interface{}:
225
- for _, item := range v {
226
- if f, ok := item.(float64); ok {
227
- rowVec = append(rowVec, f)
228
- }
229
- }
230
- }
231
-
232
- if len(rowVec) != len(targetVector) {
291
+ // A float32 BLOB, a legacy JSON text column and an already-decoded slice
292
+ // all read the same way.
293
+ rowVec := base.DecodeVector(raw)
294
+ dist, ok := base.VectorDistance(targetVector, rowVec, distanceMetric)
295
+ if !ok {
233
296
  continue
234
297
  }
235
298
 
236
- var dist float64
237
- switch strings.ToLower(distanceMetric) {
238
- case "euclidean":
239
- dist = base.EuclideanDistance(targetVector, rowVec)
240
- case "dot":
241
- dist = -base.DotProduct(targetVector, rowVec)
242
- default: // cosine
243
- dist = 1.0 - base.CosineSimilarity(targetVector, rowVec)
244
- }
245
-
246
299
  rCopy := make(map[string]interface{})
247
300
  for k, val := range r {
248
301
  rCopy[k] = val
@@ -311,4 +364,3 @@ func (a *An5Adapter) ExecuteProc(ctx context.Context, procName string, args ...i
311
364
  }
312
365
  return a.ExecuteRaw(ctx, sql, args...)
313
366
  }
314
-