@an5/adapters 0.2.11 → 0.2.14
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 +137 -5
- package/dist/an5Adapter.d.ts +42 -1
- package/dist/an5Adapter.js +146 -38
- package/dist/base/types.d.ts +14 -0
- 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 +432 -0
- package/dotnet/An5Adapter.cs +99 -28
- 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 +1030 -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 +33 -7
- package/python/an5_adapter.py +37 -6
- package/python/base/__init__.py +15 -0
- package/python/base/vectors.py +431 -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 +155 -31
- package/typescript/src/base/types.ts +15 -0
- 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 +537 -0
- package/golang/sqlite/sqlite.go +0 -14
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
package an5adapters_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"database/sql"
|
|
6
|
+
"math"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
an5adapters "an5adapters"
|
|
11
|
+
"an5adapters/base"
|
|
12
|
+
"an5adapters/sqlite"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// SQLite vector codec, strategy plan and ranking SQL.
|
|
16
|
+
//
|
|
17
|
+
// The adapter ships no SQLite driver, so these are unit tests: they pin the codec
|
|
18
|
+
// and the statements the search builds. That the statements are valid SQLite, and
|
|
19
|
+
// that they rank correctly, is covered end to end by the runtimes that do carry a
|
|
20
|
+
// driver — test/sqlite-vector.test.js (TypeScript), test/python/sqlite_vector.py
|
|
21
|
+
// (Python) and the .NET smoke. The package docstring in sqlite/vector.go is the
|
|
22
|
+
// shared specification every runtime implements.
|
|
23
|
+
|
|
24
|
+
var documentFields = map[string]interface{}{
|
|
25
|
+
"id": map[string]interface{}{"ts": "string", "sql": "TEXT", "isId": true},
|
|
26
|
+
"title": map[string]interface{}{"ts": "string", "sql": "TEXT"},
|
|
27
|
+
"embedding": map[string]interface{}{"ts": "number[] | string", "sql": "VECTOR(3)"},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func TestVectorCodec(t *testing.T) {
|
|
31
|
+
encoded := base.EncodeVector([]float64{1, -2, 0.5})
|
|
32
|
+
if len(encoded) != 12 {
|
|
33
|
+
t.Fatalf("EncodeVector length = %d, want 12 (float32 per value)", len(encoded))
|
|
34
|
+
}
|
|
35
|
+
// Little-endian float32: 1.0 is 0x3F800000.
|
|
36
|
+
if encoded[0] != 0x00 || encoded[1] != 0x00 || encoded[2] != 0x80 || encoded[3] != 0x3F {
|
|
37
|
+
t.Errorf("EncodeVector is not little-endian float32: %v", encoded[:4])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
decoded := base.DecodeVector(encoded)
|
|
41
|
+
if len(decoded) != 3 || decoded[0] != 1 || decoded[1] != -2 || decoded[2] != 0.5 {
|
|
42
|
+
t.Errorf("DecodeVector(blob) = %v, want [1 -2 0.5]", decoded)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if got := base.DecodeVector("[1, 0, 0]"); len(got) != 3 || got[0] != 1 {
|
|
46
|
+
t.Errorf("DecodeVector(legacy JSON text) = %v, want [1 0 0]", got)
|
|
47
|
+
}
|
|
48
|
+
if base.DecodeVector([]byte("[1, 0, 0]")) == nil {
|
|
49
|
+
t.Error("JSON text delivered as bytes must still decode")
|
|
50
|
+
}
|
|
51
|
+
if base.DecodeVector([]byte{1, 2, 3}) != nil {
|
|
52
|
+
t.Error("a partial blob must not decode")
|
|
53
|
+
}
|
|
54
|
+
if base.DecodeVector("not json") != nil {
|
|
55
|
+
t.Error("text that is not JSON must not decode")
|
|
56
|
+
}
|
|
57
|
+
if base.DecodeVector(nil) != nil {
|
|
58
|
+
t.Error("a null vector must stay null")
|
|
59
|
+
}
|
|
60
|
+
if got := base.DecodeVector([]float64{0.5, 1.5}); len(got) != 2 || got[1] != 1.5 {
|
|
61
|
+
t.Errorf("DecodeVector(already decoded) = %v, want [0.5 1.5]", got)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func TestNeedsVectorEncoding(t *testing.T) {
|
|
66
|
+
cases := []struct {
|
|
67
|
+
name string
|
|
68
|
+
value interface{}
|
|
69
|
+
want bool
|
|
70
|
+
}{
|
|
71
|
+
{"a float64 slice", []float64{1, 2}, true},
|
|
72
|
+
{"a float32 slice", []float32{1, 2}, true},
|
|
73
|
+
{"a generic slice", []interface{}{1.0, 2.0}, true},
|
|
74
|
+
{"stored bytes", []byte{0, 0, 128, 63}, false},
|
|
75
|
+
{"stored text", "[1,0,0]", false},
|
|
76
|
+
{"nothing", nil, false},
|
|
77
|
+
{"a scalar", 1.5, false},
|
|
78
|
+
}
|
|
79
|
+
for _, tc := range cases {
|
|
80
|
+
if got := base.NeedsVectorEncoding(tc.value); got != tc.want {
|
|
81
|
+
t.Errorf("NeedsVectorEncoding(%s) = %v, want %v", tc.name, got, tc.want)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestVectorDistance(t *testing.T) {
|
|
87
|
+
if got, ok := base.VectorDistance([]float64{1, 0}, []float64{1, 0}, "cosine"); !ok || math.Abs(got) > 1e-9 {
|
|
88
|
+
t.Errorf("cosine of a vector with itself = %v (%v), want 0", got, ok)
|
|
89
|
+
}
|
|
90
|
+
if got, _ := base.VectorDistance([]float64{1, 0}, []float64{0, 1}, "cosine"); math.Abs(got-1) > 1e-9 {
|
|
91
|
+
t.Errorf("cosine of orthogonal vectors = %v, want 1", got)
|
|
92
|
+
}
|
|
93
|
+
if got, _ := base.VectorDistance([]float64{0, 3}, []float64{4, 0}, "euclidean"); math.Abs(got-5) > 1e-9 {
|
|
94
|
+
t.Errorf("euclidean distance = %v, want 5", got)
|
|
95
|
+
}
|
|
96
|
+
if got, _ := base.VectorDistance([]float64{1, 2}, []float64{2, 4}, "dot"); got != -10 {
|
|
97
|
+
t.Errorf("dot distance is negated: got %v, want -10", got)
|
|
98
|
+
}
|
|
99
|
+
if _, ok := base.VectorDistance([]float64{1, 2}, []float64{1, 2, 3}, "cosine"); ok {
|
|
100
|
+
t.Error("mismatched dimensions must not be scored")
|
|
101
|
+
}
|
|
102
|
+
if _, ok := base.VectorDistance(nil, []float64{1}, "cosine"); ok {
|
|
103
|
+
t.Error("a missing vector must not be scored")
|
|
104
|
+
}
|
|
105
|
+
if got, _ := base.VectorDistance([]float64{0, 0}, []float64{1, 1}, "cosine"); got != 1 {
|
|
106
|
+
t.Errorf("a zero vector is maximally distant: got %v, want 1", got)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func TestIsVectorField(t *testing.T) {
|
|
111
|
+
cases := []struct {
|
|
112
|
+
name string
|
|
113
|
+
definition interface{}
|
|
114
|
+
want bool
|
|
115
|
+
}{
|
|
116
|
+
{"a VECTOR column", documentFields["embedding"], true},
|
|
117
|
+
{"only a ts type", map[string]interface{}{"ts": "number[] | string"}, true},
|
|
118
|
+
{"a kind marker", map[string]interface{}{"kind": "vector"}, true},
|
|
119
|
+
{"a TEXT column", documentFields["title"], false},
|
|
120
|
+
{"a bare string", "number[] | string", true},
|
|
121
|
+
{"nil", nil, false},
|
|
122
|
+
}
|
|
123
|
+
for _, tc := range cases {
|
|
124
|
+
if got := base.IsVectorField(tc.definition); got != tc.want {
|
|
125
|
+
t.Errorf("IsVectorField(%s) = %v, want %v", tc.name, got, tc.want)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func TestPlanVectorStrategies(t *testing.T) {
|
|
131
|
+
all := map[string]bool{"vec": true, "udf": true, "json1": true}
|
|
132
|
+
if got := base.PlanVectorStrategies(all, "BLOB", ""); len(got) != 2 || got[0] != "sqlite-vec" || got[1] != "udf" {
|
|
133
|
+
t.Errorf("a BLOB column must rank sqlite-vec then udf, got %v", got)
|
|
134
|
+
}
|
|
135
|
+
// A BLOB column has no JSON to read, so json_each could only produce NULLs.
|
|
136
|
+
if got := base.PlanVectorStrategies(map[string]bool{"json1": true}, "BLOB", ""); len(got) != 0 {
|
|
137
|
+
t.Errorf("a BLOB column drops the JSON strategy, got %v", got)
|
|
138
|
+
}
|
|
139
|
+
if got := base.PlanVectorStrategies(map[string]bool{"json1": true}, "TEXT", ""); len(got) != 1 || got[0] != "sql" {
|
|
140
|
+
t.Errorf("a TEXT column keeps the JSON strategy, got %v", got)
|
|
141
|
+
}
|
|
142
|
+
if got := base.PlanVectorStrategies(map[string]bool{}, "TEXT", ""); len(got) != 0 {
|
|
143
|
+
t.Errorf("no capability means no plan, got %v", got)
|
|
144
|
+
}
|
|
145
|
+
if got := base.PlanVectorStrategies(all, "BLOB", "udf"); len(got) != 1 || got[0] != "udf" {
|
|
146
|
+
t.Errorf("a pinned strategy wins, got %v", got)
|
|
147
|
+
}
|
|
148
|
+
if got := base.PlanVectorStrategies(all, "BLOB", "memory"); len(got) != 0 {
|
|
149
|
+
t.Errorf("memory is the caller's own path, not a query, got %v", got)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func TestRankingQuery(t *testing.T) {
|
|
154
|
+
tail := " WHERE [id] IN (?)"
|
|
155
|
+
|
|
156
|
+
for _, tc := range []struct {
|
|
157
|
+
strategy string
|
|
158
|
+
want []string
|
|
159
|
+
}{
|
|
160
|
+
{"sqlite-vec", []string{
|
|
161
|
+
"vec_distance_cosine(",
|
|
162
|
+
"vec_length(vec_f32([embedding])) = 3",
|
|
163
|
+
// The guard carries the caller's WHERE, joined with AND rather than
|
|
164
|
+
// appended as a second WHERE.
|
|
165
|
+
"[embedding] IS NOT NULL AND " + tail[len(" WHERE "):],
|
|
166
|
+
"an5_ranked",
|
|
167
|
+
"WHERE distance IS NOT NULL",
|
|
168
|
+
}},
|
|
169
|
+
{"udf", []string{"an5_vec_cosine(", "an5_ranked", "WHERE distance IS NOT NULL", tail}},
|
|
170
|
+
{"sql", []string{
|
|
171
|
+
"WITH q AS (",
|
|
172
|
+
"json_each(?) je",
|
|
173
|
+
"json_valid([embedding])",
|
|
174
|
+
"an5_ranked",
|
|
175
|
+
"WHERE distance IS NOT NULL",
|
|
176
|
+
tail,
|
|
177
|
+
}},
|
|
178
|
+
} {
|
|
179
|
+
query, args := sqlite.BuildRankingQuery(tc.strategy, "cosine", "[documents]", "[embedding]", []float64{1, 0, 0}, 5, tail, nil)
|
|
180
|
+
if query == "" {
|
|
181
|
+
t.Fatalf("%s produced no query", tc.strategy)
|
|
182
|
+
}
|
|
183
|
+
for _, fragment := range tc.want {
|
|
184
|
+
if !strings.Contains(query, fragment) {
|
|
185
|
+
t.Errorf("%s query is missing %q:\n%s", tc.strategy, fragment, query)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if !strings.Contains(query, "ORDER BY distance ASC LIMIT 5") {
|
|
189
|
+
t.Errorf("%s query does not order and limit:\n%s", tc.strategy, query)
|
|
190
|
+
}
|
|
191
|
+
if len(args) == 0 {
|
|
192
|
+
t.Errorf("%s query binds no query vector", tc.strategy)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The query vector is bound as float32 bytes for the two binary strategies and
|
|
197
|
+
// as JSON text for the json_each one.
|
|
198
|
+
_, blobArgs := sqlite.BuildRankingQuery("udf", "cosine", "[documents]", "[embedding]", []float64{1, 0, 0}, 5, "", nil)
|
|
199
|
+
if encoded, ok := blobArgs[0].([]byte); !ok || len(encoded) != 12 {
|
|
200
|
+
t.Errorf("udf query vector = %v, want 12 float32 bytes", blobArgs[0])
|
|
201
|
+
}
|
|
202
|
+
_, jsonArgs := sqlite.BuildRankingQuery("sql", "cosine", "[documents]", "[embedding]", []float64{1, 0, 0}, 5, "", nil)
|
|
203
|
+
if text, ok := jsonArgs[0].(string); !ok || text != "[1,0,0]" {
|
|
204
|
+
t.Errorf("sql query vector = %v, want the JSON text [1,0,0]", jsonArgs[0])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Each metric calls its own function.
|
|
208
|
+
for metric, fn := range map[string]string{
|
|
209
|
+
"cosine": "an5_vec_cosine(",
|
|
210
|
+
"euclidean": "an5_vec_l2(",
|
|
211
|
+
"dot": "an5_vec_ip(",
|
|
212
|
+
} {
|
|
213
|
+
query, _ := sqlite.BuildRankingQuery("udf", metric, "[documents]", "[embedding]", []float64{1, 0}, 5, "", nil)
|
|
214
|
+
if !strings.Contains(query, fn) {
|
|
215
|
+
t.Errorf("%s query does not use %s:\n%s", metric, fn, query)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Exactly one WHERE per ranking query: the sqlite-vec guard and the caller's
|
|
220
|
+
// clause are joined with AND, never appended as a second WHERE.
|
|
221
|
+
guarded, _ := sqlite.BuildRankingQuery("sqlite-vec", "cosine", "[documents]", "[embedding]", []float64{1, 0}, 5, tail, nil)
|
|
222
|
+
inner := guarded[strings.Index(guarded, "(")+1 : strings.Index(guarded, ") AS an5_ranked")]
|
|
223
|
+
if strings.Count(inner, " WHERE ") != 1 {
|
|
224
|
+
t.Errorf("the ranked subquery must carry exactly one WHERE:\n%s", inner)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A WHERE clause is placed inside the ranking query, before the rank filter.
|
|
228
|
+
query, _ := sqlite.BuildRankingQuery("udf", "cosine", "[documents]", "[embedding]", []float64{1, 0}, 5, " WHERE [id] = ?", nil)
|
|
229
|
+
if strings.Index(query, "WHERE [id] = ?") > strings.Index(query, "an5_ranked") {
|
|
230
|
+
t.Errorf("the caller's WHERE must stay inside the ranked subquery:\n%s", query)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func TestNormalizeTableName(t *testing.T) {
|
|
235
|
+
for input, want := range map[string]string{
|
|
236
|
+
"documents": "documents",
|
|
237
|
+
"[documents]": "documents",
|
|
238
|
+
"`documents`": "documents",
|
|
239
|
+
`"documents"`: "documents",
|
|
240
|
+
"[main].[docs]": "docs",
|
|
241
|
+
" [documents] ": "documents",
|
|
242
|
+
} {
|
|
243
|
+
if got := sqlite.NormalizeTableName(input); got != want {
|
|
244
|
+
t.Errorf("NormalizeTableName(%q) = %q, want %q", input, got, want)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func TestVectorSearchFallbackRanksRows(t *testing.T) {
|
|
250
|
+
// The in-memory fallback has to read every stored form of a vector: float32
|
|
251
|
+
// bytes, the legacy JSON text column, and a slice the driver already decoded.
|
|
252
|
+
rows := []map[string]interface{}{
|
|
253
|
+
{"id": "d1", "embedding": base.EncodeVector([]float64{1, 0, 0})},
|
|
254
|
+
{"id": "d2", "embedding": `[0.8,0.2,0]`},
|
|
255
|
+
{"id": "d3", "embedding": []float64{0, 1, 0}},
|
|
256
|
+
{"id": "d4", "embedding": base.EncodeVector([]float64{1, 0, 0, 1})},
|
|
257
|
+
{"id": "d5", "embedding": nil},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
ranked := an5adapters.VectorSearchFallback(rows, []float64{1, 0, 0}, 10, "embedding", "cosine")
|
|
261
|
+
got := ""
|
|
262
|
+
for i, row := range ranked {
|
|
263
|
+
if i > 0 {
|
|
264
|
+
got += ","
|
|
265
|
+
}
|
|
266
|
+
got += row["id"].(string)
|
|
267
|
+
}
|
|
268
|
+
if got != "d1,d2,d3" {
|
|
269
|
+
t.Errorf("row order = %s, want d1,d2,d3", got)
|
|
270
|
+
}
|
|
271
|
+
if dist, ok := ranked[0]["distance"].(float64); !ok || math.Abs(dist) > 1e-9 {
|
|
272
|
+
t.Errorf("closest distance = %v, want 0", ranked[0]["distance"])
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func TestCapabilitiesNeedsADatabase(t *testing.T) {
|
|
277
|
+
// A nil handle has to report no capability rather than panic, so the caller
|
|
278
|
+
// falls through to the in-memory path.
|
|
279
|
+
defer func() {
|
|
280
|
+
if recovered := recover(); recovered != nil {
|
|
281
|
+
t.Fatalf("Capabilities panicked on a closed handle: %v", recovered)
|
|
282
|
+
}
|
|
283
|
+
}()
|
|
284
|
+
caps := sqlite.Capabilities(context.Background(), (*sql.DB)(nil))
|
|
285
|
+
if caps["vec"] || caps["udf"] || caps["json1"] {
|
|
286
|
+
t.Errorf("Capabilities on no database = %v, want all false", caps)
|
|
287
|
+
}
|
|
288
|
+
}
|
package/golang/table_client.go
CHANGED
|
@@ -286,6 +286,67 @@ func projectFields(row map[string]interface{}, selectVal interface{}) map[string
|
|
|
286
286
|
return projected
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// vectorFields returns the `VECTOR(n)` columns of this model, from the generated
|
|
290
|
+
// metadata.
|
|
291
|
+
func (t *TableClient) vectorFields() map[string]bool {
|
|
292
|
+
fields, _ := base.GetFieldsForModel(t.TableName).(map[string]interface{})
|
|
293
|
+
names := map[string]bool{}
|
|
294
|
+
for name, def := range fields {
|
|
295
|
+
if base.IsVectorField(def) {
|
|
296
|
+
names[name] = true
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return names
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// encodeVectorFields encodes every `VECTOR(n)` value on its way into SQLite.
|
|
303
|
+
//
|
|
304
|
+
// The column holds a float32 BLOB, which is a third of the size of the JSON text
|
|
305
|
+
// it replaces and is what the in-database distance functions read. Values that
|
|
306
|
+
// already arrived as text or bytes are left alone, so rows written by an older
|
|
307
|
+
// version stay readable.
|
|
308
|
+
func (t *TableClient) encodeVectorFields(data map[string]interface{}) map[string]interface{} {
|
|
309
|
+
if t.Adapter == nil || t.Adapter.Dialect != base.DialectSqlite {
|
|
310
|
+
return data
|
|
311
|
+
}
|
|
312
|
+
names := t.vectorFields()
|
|
313
|
+
if len(names) == 0 {
|
|
314
|
+
return data
|
|
315
|
+
}
|
|
316
|
+
out := make(map[string]interface{}, len(data))
|
|
317
|
+
for key, value := range data {
|
|
318
|
+
out[key] = value
|
|
319
|
+
if names[key] && base.NeedsVectorEncoding(value) {
|
|
320
|
+
if decoded := base.DecodeVector(value); decoded != nil {
|
|
321
|
+
out[key] = base.EncodeVector(decoded)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return out
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// decodeVectorRows decodes `VECTOR(n)` columns back into float64 slices after a read.
|
|
329
|
+
func (t *TableClient) decodeVectorRows(rows []map[string]interface{}) []map[string]interface{} {
|
|
330
|
+
if t.Adapter == nil || t.Adapter.Dialect != base.DialectSqlite || len(rows) == 0 {
|
|
331
|
+
return rows
|
|
332
|
+
}
|
|
333
|
+
names := t.vectorFields()
|
|
334
|
+
if len(names) == 0 {
|
|
335
|
+
return rows
|
|
336
|
+
}
|
|
337
|
+
for _, row := range rows {
|
|
338
|
+
for key, value := range row {
|
|
339
|
+
if !names[key] {
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
if decoded := base.DecodeVector(value); decoded != nil {
|
|
343
|
+
row[key] = decoded
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return rows
|
|
348
|
+
}
|
|
349
|
+
|
|
289
350
|
// FindMany fetches rows matching structured ORM args (where/orderBy/skip/take/select/include).
|
|
290
351
|
func (t *TableClient) FindMany(ctx context.Context, args *FindManyArgs) ([]map[string]interface{}, error) {
|
|
291
352
|
whereCond, queryArgs := t.buildWhere(args.Where)
|
|
@@ -384,7 +445,7 @@ func (t *TableClient) FindMany(ctx context.Context, args *FindManyArgs) ([]map[s
|
|
|
384
445
|
rows[i] = projectFields(rows[i], args.Select)
|
|
385
446
|
}
|
|
386
447
|
}
|
|
387
|
-
return rows, nil
|
|
448
|
+
return t.decodeVectorRows(rows), nil
|
|
388
449
|
}
|
|
389
450
|
|
|
390
451
|
// FindFirst fetches the first row matching structured ORM args.
|
|
@@ -498,6 +559,7 @@ func (t *TableClient) Create(ctx context.Context, args *CreateArgs) (map[string]
|
|
|
498
559
|
}
|
|
499
560
|
|
|
500
561
|
data, relationWrites := splitRelationWrites(t.TableName, args.Data)
|
|
562
|
+
data = t.encodeVectorFields(data)
|
|
501
563
|
|
|
502
564
|
var cols []string
|
|
503
565
|
var vals []string
|
|
@@ -641,6 +703,7 @@ func (t *TableClient) Update(ctx context.Context, args *UpdateArgs) (map[string]
|
|
|
641
703
|
return nil, fmt.Errorf("update requires args")
|
|
642
704
|
}
|
|
643
705
|
data, relationWrites := splitRelationWrites(t.TableName, args.Data)
|
|
706
|
+
data = t.encodeVectorFields(data)
|
|
644
707
|
|
|
645
708
|
whereCond, whereArgs := t.buildWhere(args.Where)
|
|
646
709
|
setParts := []string{}
|
|
@@ -727,10 +790,11 @@ func (t *TableClient) UpdateMany(ctx context.Context, args *UpdateManyArgs) (map
|
|
|
727
790
|
if args == nil || len(args.Data) == 0 {
|
|
728
791
|
return map[string]interface{}{"count": 0}, nil
|
|
729
792
|
}
|
|
793
|
+
data := t.encodeVectorFields(args.Data)
|
|
730
794
|
whereCond, whereArgs := t.buildWhere(args.Where)
|
|
731
795
|
setParts := []string{}
|
|
732
796
|
setValues := []interface{}{}
|
|
733
|
-
for col, val := range
|
|
797
|
+
for col, val := range data {
|
|
734
798
|
appendUpdateSet(&setParts, &setValues, col, val, t.Adapter.Dialect)
|
|
735
799
|
}
|
|
736
800
|
if len(setParts) == 0 {
|
|
@@ -1326,4 +1390,4 @@ func placeholder(dialect base.Dialect, index int) string {
|
|
|
1326
1390
|
return fmt.Sprintf("$%d", index)
|
|
1327
1391
|
}
|
|
1328
1392
|
return "?"
|
|
1329
|
-
}
|
|
1393
|
+
}
|
package/java/pom.xml
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Maven manifest for the AN5 Java runtime.
|
|
4
|
+
|
|
5
|
+
There is no dependency block on purpose: the adapter speaks JDBC through `java.sql`, so a
|
|
6
|
+
consumer picks the driver it already has. That is what lets the same runtime serve SQL
|
|
7
|
+
Server, PostgreSQL and SQLite, and what keeps it usable on Android, where an adapter that
|
|
8
|
+
dragged in three drivers would not fit.
|
|
9
|
+
|
|
10
|
+
The Kotlin runtime is a separate module with its own manifest rather than a second source
|
|
11
|
+
root here: it is a different language, and a JVM build resolves it as its own artifact.
|
|
12
|
+
-->
|
|
13
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
14
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
15
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
16
|
+
<modelVersion>4.0.0</modelVersion>
|
|
17
|
+
|
|
18
|
+
<groupId>io.github.an5orm</groupId>
|
|
19
|
+
<artifactId>an5-adapters-java</artifactId>
|
|
20
|
+
<version>0.2.14</version>
|
|
21
|
+
<packaging>jar</packaging>
|
|
22
|
+
|
|
23
|
+
<name>AN5 ORM Java adapter</name>
|
|
24
|
+
<description>Standalone JDBC runtime for AN5 ORM: dialect-aware CRUD, relation eager loading and vector search.</description>
|
|
25
|
+
<url>https://github.com/an5ORM/an5Adapters</url>
|
|
26
|
+
<licenses>
|
|
27
|
+
<license>
|
|
28
|
+
<name>MIT License</name>
|
|
29
|
+
<url>https://opensource.org/licenses/MIT</url>
|
|
30
|
+
</license>
|
|
31
|
+
</licenses>
|
|
32
|
+
<developers>
|
|
33
|
+
<developer>
|
|
34
|
+
<name>AN5 ORM</name>
|
|
35
|
+
<organization>AN5</organization>
|
|
36
|
+
<url>https://github.com/an5ORM</url>
|
|
37
|
+
</developer>
|
|
38
|
+
</developers>
|
|
39
|
+
<scm>
|
|
40
|
+
<connection>scm:git:https://github.com/an5ORM/an5Adapters.git</connection>
|
|
41
|
+
<developerConnection>scm:git:ssh://git@github.com/an5ORM/an5Adapters.git</developerConnection>
|
|
42
|
+
<url>https://github.com/an5ORM/an5Adapters</url>
|
|
43
|
+
</scm>
|
|
44
|
+
|
|
45
|
+
<properties>
|
|
46
|
+
<!--
|
|
47
|
+
Java 8 bytecode, not the JDK in use: Android desugars Java 8 but not Java 17 language
|
|
48
|
+
features, so a runtime compiled to a newer target simply cannot be used on a device.
|
|
49
|
+
-->
|
|
50
|
+
<maven.compiler.release>8</maven.compiler.release>
|
|
51
|
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
52
|
+
</properties>
|
|
53
|
+
|
|
54
|
+
<build>
|
|
55
|
+
<sourceDirectory>src/main/java</sourceDirectory>
|
|
56
|
+
<testSourceDirectory>src/test/java</testSourceDirectory>
|
|
57
|
+
<plugins>
|
|
58
|
+
<plugin>
|
|
59
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
60
|
+
<artifactId>maven-surefire-plugin</artifactId>
|
|
61
|
+
<version>3.2.5</version>
|
|
62
|
+
<!--
|
|
63
|
+
The runtime tests need a JDBC driver on the classpath, and SQLite is the one that
|
|
64
|
+
has no licence or platform constraint. A plugin dependency is resolved for the
|
|
65
|
+
plugin's own classloader, where `test` is not an allowed scope — `runtime` is the
|
|
66
|
+
one that puts it on the test classpath, and a missing driver fails the build the
|
|
67
|
+
moment surefire tries to open a connection: the generated SQL is only ever
|
|
68
|
+
validated by running it.
|
|
69
|
+
-->
|
|
70
|
+
<configuration>
|
|
71
|
+
<environmentVariables>
|
|
72
|
+
<AN5_JAVA_TEST_DRIVER>sqlite</AN5_JAVA_TEST_DRIVER>
|
|
73
|
+
</environmentVariables>
|
|
74
|
+
</configuration>
|
|
75
|
+
<dependencies>
|
|
76
|
+
<dependency>
|
|
77
|
+
<groupId>org.xerial</groupId>
|
|
78
|
+
<artifactId>sqlite-jdbc</artifactId>
|
|
79
|
+
<version>3.46.1.3</version>
|
|
80
|
+
<scope>runtime</scope>
|
|
81
|
+
</dependency>
|
|
82
|
+
</dependencies>
|
|
83
|
+
</plugin>
|
|
84
|
+
</plugins>
|
|
85
|
+
</build>
|
|
86
|
+
|
|
87
|
+
<profiles>
|
|
88
|
+
<!--
|
|
89
|
+
`-Prelease` is the only thing that touches anything outside the checkout: sources,
|
|
90
|
+
javadoc, a detached GPG signature and an upload to Maven Central. Everything else —
|
|
91
|
+
`mvn install`, `mvn test`, a consumer resolving the jar — runs the plain build above
|
|
92
|
+
with no account, no key and no network beyond the plugins it already downloads.
|
|
93
|
+
|
|
94
|
+
Credentials come from ~/.m2/settings.xml: a server with id `central` (a Central Portal
|
|
95
|
+
token, supplied to CI as MAVEN_CENTRAL_USERNAME/MAVEN_CENTRAL_TOKEN) and
|
|
96
|
+
MAVEN_GPG_PASSPHRASE in the environment for the signing step.
|
|
97
|
+
-->
|
|
98
|
+
<profile>
|
|
99
|
+
<id>release</id>
|
|
100
|
+
<build>
|
|
101
|
+
<plugins>
|
|
102
|
+
<plugin>
|
|
103
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
104
|
+
<artifactId>maven-source-plugin</artifactId>
|
|
105
|
+
<version>3.3.1</version>
|
|
106
|
+
<executions>
|
|
107
|
+
<execution>
|
|
108
|
+
<id>attach-sources</id>
|
|
109
|
+
<goals>
|
|
110
|
+
<goal>jar-no-fork</goal>
|
|
111
|
+
</goals>
|
|
112
|
+
</execution>
|
|
113
|
+
</executions>
|
|
114
|
+
</plugin>
|
|
115
|
+
<plugin>
|
|
116
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
117
|
+
<artifactId>maven-javadoc-plugin</artifactId>
|
|
118
|
+
<version>3.11.2</version>
|
|
119
|
+
<configuration>
|
|
120
|
+
<doclint>none</doclint>
|
|
121
|
+
<quiet>true</quiet>
|
|
122
|
+
</configuration>
|
|
123
|
+
<executions>
|
|
124
|
+
<execution>
|
|
125
|
+
<id>attach-javadocs</id>
|
|
126
|
+
<goals>
|
|
127
|
+
<goal>jar</goal>
|
|
128
|
+
</goals>
|
|
129
|
+
</execution>
|
|
130
|
+
</executions>
|
|
131
|
+
</plugin>
|
|
132
|
+
<plugin>
|
|
133
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
134
|
+
<artifactId>maven-gpg-plugin</artifactId>
|
|
135
|
+
<version>3.2.7</version>
|
|
136
|
+
<configuration>
|
|
137
|
+
<passphrase>${env.MAVEN_GPG_PASSPHRASE}</passphrase>
|
|
138
|
+
<gpgArguments>
|
|
139
|
+
<arg>--pinentry-mode</arg>
|
|
140
|
+
<arg>loopback</arg>
|
|
141
|
+
</gpgArguments>
|
|
142
|
+
</configuration>
|
|
143
|
+
<executions>
|
|
144
|
+
<execution>
|
|
145
|
+
<id>sign-artifacts</id>
|
|
146
|
+
<phase>verify</phase>
|
|
147
|
+
<goals>
|
|
148
|
+
<goal>sign</goal>
|
|
149
|
+
</goals>
|
|
150
|
+
</execution>
|
|
151
|
+
</executions>
|
|
152
|
+
</plugin>
|
|
153
|
+
<plugin>
|
|
154
|
+
<groupId>org.sonatype.central</groupId>
|
|
155
|
+
<artifactId>central-publishing-maven-plugin</artifactId>
|
|
156
|
+
<version>0.8.0</version>
|
|
157
|
+
<extensions>true</extensions>
|
|
158
|
+
<configuration>
|
|
159
|
+
<publishingServerId>central</publishingServerId>
|
|
160
|
+
<autoPublish>true</autoPublish>
|
|
161
|
+
<waitUntil>published</waitUntil>
|
|
162
|
+
</configuration>
|
|
163
|
+
<executions>
|
|
164
|
+
<execution>
|
|
165
|
+
<id>publish</id>
|
|
166
|
+
<phase>verify</phase>
|
|
167
|
+
<goals>
|
|
168
|
+
<goal>publish</goal>
|
|
169
|
+
</goals>
|
|
170
|
+
</execution>
|
|
171
|
+
</executions>
|
|
172
|
+
</plugin>
|
|
173
|
+
</plugins>
|
|
174
|
+
</build>
|
|
175
|
+
</profile>
|
|
176
|
+
</profiles>
|
|
177
|
+
</project>
|