@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.
Files changed (91) hide show
  1. package/README.md +137 -5
  2. package/dist/an5Adapter.d.ts +42 -1
  3. package/dist/an5Adapter.js +146 -38
  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 +432 -0
  15. package/dotnet/An5Adapter.cs +99 -28
  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 +1030 -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 +431 -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 +155 -31
  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 +537 -0
  91. 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
+ }
@@ -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 args.Data {
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>