@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,671 @@
1
+ import Foundation
2
+
3
+ /// CRUD and queries for one table, driven entirely by the registered metadata.
4
+ ///
5
+ /// Rows come back as `[String: Any?]`, so this client stays independent of the schema and of
6
+ /// the generator — the same reason the other adapters' table clients work on untyped rows.
7
+ /// The generated client wraps them in its own model types.
8
+ public struct TableClient {
9
+
10
+ public let adapter: An5Adapter
11
+ public let model: String
12
+
13
+ private var dialect: Dialect { adapter.dialect }
14
+ private var metadata: Metadata { adapter.metadata }
15
+
16
+ public init(adapter: An5Adapter, model: String) {
17
+ self.adapter = adapter
18
+ self.model = model
19
+ }
20
+
21
+ private var tableSQL: String { SQLBuilder.quoteTable(metadata.table(model), dialect: dialect) }
22
+
23
+ /// `WITH (NOLOCK)` is an MSSQL hint; leaving it in makes every other dialect fail.
24
+ private var noLock: String { dialect.supportsNoLock ? " WITH (NOLOCK)" : "" }
25
+
26
+ // ─── Reads ──────────────────────────────────────────────────────────────────
27
+
28
+ /// Every row, unfiltered.
29
+ public func findMany() throws -> [Row] {
30
+ try findMany(Query())
31
+ }
32
+
33
+ /// The rows matching `query`.
34
+ public func findMany(_ query: Query) throws -> [Row] {
35
+ let clause = SQLBuilder.whereClause(query.filter, dialect: dialect)
36
+ let orderBy = SQLBuilder.orderBy(query.orderBy, dialect: dialect)
37
+ let relations = metadata.relations(model)
38
+ let selected = query.select ?? []
39
+
40
+ let columns: String
41
+ if !selected.isEmpty && !hasRelationSelect(selected, relations) {
42
+ columns = "SELECT " + selected.map { dialect.quote($0) }.joined(separator: ", ")
43
+ } else {
44
+ columns = "SELECT *"
45
+ }
46
+
47
+ var sql = "\(columns) FROM \(tableSQL)\(noLock)"
48
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
49
+ if !orderBy.isEmpty { sql += " \(orderBy)" }
50
+ sql += dialect.pagination(take: query.take, skip: query.skip, orderBy: orderBy)
51
+
52
+ var rows = try adapter.query(sql, parameters(clause.parameters))
53
+ if let include = query.include, !include.isEmpty {
54
+ try resolveIncludes(relations, rows: &rows, include: include)
55
+ }
56
+ if let select = query.select {
57
+ if hasRelationSelect(select, relations) {
58
+ try resolveIncludes(relations, rows: &rows, include: relationSelect(select, relations))
59
+ }
60
+ rows = rows.map { project($0, selected: select) }
61
+ }
62
+ return decodeVectorRows(rows)
63
+ }
64
+
65
+ /// The rows matching `filter`, with the optional refinements `query` adds.
66
+ public func findMany(filter: Where, _ query: Query = Query()) throws -> [Row] {
67
+ var copy = query
68
+ copy.filter = filter
69
+ return try findMany(copy)
70
+ }
71
+
72
+ /// The first matching row, or `nil`. Always limited to one row server-side.
73
+ public func findFirst(_ query: Query) throws -> Row? {
74
+ try findMany(query.limited(to: 1)).first
75
+ }
76
+
77
+ public func findFirst(filter: Where) throws -> Row? {
78
+ try findFirst(Query(filter: filter))
79
+ }
80
+
81
+ /// The row `filter` identifies, or `nil`.
82
+ public func findUnique(filter: Where) throws -> Row? {
83
+ try findFirst(Query(filter: filter))
84
+ }
85
+
86
+ /// How many rows match.
87
+ public func count(_ filter: Where? = nil) throws -> Int {
88
+ let clause = SQLBuilder.whereClause(filter, dialect: dialect)
89
+ var sql = "SELECT COUNT(*) AS cnt FROM \(tableSQL)\(noLock)"
90
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
91
+ let rows = try adapter.query(sql, parameters(clause.parameters))
92
+ guard let first = rows.first, let value = first.values.first else { return 0 }
93
+ return (value as? NSNumber)?.intValue ?? 0
94
+ }
95
+
96
+ // ─── Writes ─────────────────────────────────────────────────────────────────
97
+
98
+ /// Inserts a row and returns it, re-read so server-side defaults come back with it.
99
+ ///
100
+ /// A model with an `@id` column and no value for it gets a generated identifier, which is
101
+ /// what makes a client-created row addressable without a database-side default.
102
+ @discardableResult
103
+ public func create(_ data: Values, _ query: Query = Query()) throws -> Row {
104
+ var scalars = data
105
+ let relationWrites = splitRelationWrites(scalars)
106
+ for key in relationWrites.keys { scalars.removeValue(forKey: key) }
107
+
108
+ if let idField = metadata.idField(model) {
109
+ let missing = scalars[idField.name].map { $0 == nil || $0 is NSNull } ?? true
110
+ if missing { scalars[idField.name] = UUID().uuidString }
111
+ }
112
+
113
+ let encoded = encodeVectorFields(scalars)
114
+ var columns: [String] = []
115
+ var values: [SQLValue] = []
116
+ for (column, value) in encoded {
117
+ // A null column is left out rather than bound as NULL: an unset column takes the
118
+ // schema's DEFAULT, which is what the caller meant by leaving it out.
119
+ guard let value, !(value is NSNull) else { continue }
120
+ columns.append(column)
121
+ values.append(SQLValue.encode(value) ?? .null)
122
+ }
123
+
124
+ if !columns.isEmpty {
125
+ let placeholders = columns.map { _ in dialect.placeholder }.joined(separator: ", ")
126
+ let sql = "INSERT INTO \(tableSQL) (\(columns.map { dialect.quote($0) }.joined(separator: ", ")))"
127
+ + " VALUES (\(placeholders))"
128
+ try adapter.execute(sql, parameters(values))
129
+ }
130
+
131
+ var created = scalars
132
+ if let idField = metadata.idField(model) {
133
+ let found = try findFirst(filter: [idField.name: scalars[idField.name] as Any?])
134
+ if let found { created = found }
135
+ }
136
+
137
+ try applyRelationCreates(created, relationWrites: relationWrites)
138
+ // `created` is never nil here, so the projection cannot come back empty either.
139
+ return try projectAfterWrite(created, query) ?? created
140
+ }
141
+
142
+ /// Inserts many rows, one statement each.
143
+ ///
144
+ /// Row-at-a-time on purpose: a batch would share one parameter list, so a single bad row
145
+ /// would lose every good row with it, and `skipDuplicates` has no meaning for a batch.
146
+ @discardableResult
147
+ public func createMany(_ rows: [Values], skipDuplicates: Bool = false) throws -> Int {
148
+ var count = 0
149
+ for row in rows {
150
+ do {
151
+ try create(row)
152
+ count += 1
153
+ } catch {
154
+ if !skipDuplicates { throw error }
155
+ }
156
+ }
157
+ return count
158
+ }
159
+
160
+ /// Updates the matching rows and returns the first of them re-read.
161
+ @discardableResult
162
+ public func update(filter: Where, data: Values, query: Query = Query()) throws -> Row? {
163
+ let clause = SQLBuilder.whereClause(filter, dialect: dialect, prefix: "w_")
164
+ var scalars = data
165
+ let relationWrites = splitRelationWrites(scalars)
166
+ for key in relationWrites.keys { scalars.removeValue(forKey: key) }
167
+
168
+ var setParts: [String] = []
169
+ var setValues: [SQLValue] = []
170
+ for (column, value) in encodeVectorFields(scalars) {
171
+ guard let value, !(value is NSNull) else { continue }
172
+ SQLBuilder.appendUpdateSet(&setParts, &setValues, column: column, value: value, dialect: dialect)
173
+ }
174
+
175
+ if !setParts.isEmpty {
176
+ var sql = "UPDATE \(tableSQL) SET \(setParts.joined(separator: ", "))"
177
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
178
+ try adapter.execute(sql, parameters(setValues + clause.parameters))
179
+ }
180
+
181
+ let updated = try findFirst(Query(filter: filter))
182
+ try applyRelationUpdates(updated, filter: filter, relationWrites: relationWrites)
183
+ return try projectAfterWrite(updated, query)
184
+ }
185
+
186
+ /// Updates every matching row and gives back how many changed.
187
+ @discardableResult
188
+ public func updateMany(filter: Where?, data: Values) throws -> Int {
189
+ let clause = SQLBuilder.whereClause(filter, dialect: dialect, prefix: "w_")
190
+ var setParts: [String] = []
191
+ var setValues: [SQLValue] = []
192
+ for (column, value) in encodeVectorFields(data) {
193
+ guard let value, !(value is NSNull) else { continue }
194
+ SQLBuilder.appendUpdateSet(&setParts, &setValues, column: column, value: value, dialect: dialect)
195
+ }
196
+ guard !setParts.isEmpty else { return 0 }
197
+
198
+ var sql = "UPDATE \(tableSQL) SET \(setParts.joined(separator: ", "))"
199
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
200
+ return try adapter.execute(sql, parameters(setValues + clause.parameters))
201
+ }
202
+
203
+ /// Deletes the matching rows and returns the one that was there first.
204
+ @discardableResult
205
+ public func delete(filter: Where) throws -> Row? {
206
+ let existing = try findFirst(filter: filter)
207
+ try deleteMany(filter: filter)
208
+ return existing
209
+ }
210
+
211
+ /// Deletes every matching row, or the whole table when `filter` is `nil`.
212
+ @discardableResult
213
+ public func deleteMany(filter: Where? = nil) throws -> Int {
214
+ let clause = SQLBuilder.whereClause(filter, dialect: dialect)
215
+ var sql = "DELETE FROM \(tableSQL)"
216
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
217
+ return try adapter.execute(sql, parameters(clause.parameters))
218
+ }
219
+
220
+ /// Updates the matching row when it exists, creates it otherwise.
221
+ @discardableResult
222
+ public func upsert(filter: Where, create: Values, update: Values) throws -> Row {
223
+ if let existing = try findFirst(filter: filter) {
224
+ return try self.update(filter: filter, data: update) ?? existing
225
+ }
226
+ return try self.create(create)
227
+ }
228
+
229
+ // ─── Aggregation ────────────────────────────────────────────────────────────
230
+
231
+ /// One row of aggregate values, keyed `_count`, `_sum_<field>` and so on.
232
+ public func aggregate(_ aggregate: Aggregate) throws -> Row {
233
+ let expressions = selectExpressions(aggregate, includeCount: aggregate.count)
234
+ guard !expressions.isEmpty else {
235
+ throw An5Error.invalidQuery("aggregate requires at least one aggregator column")
236
+ }
237
+ let clause = SQLBuilder.whereClause(aggregate.filter, dialect: dialect)
238
+ var sql = "SELECT \(expressions.joined(separator: ", ")) FROM \(tableSQL)"
239
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
240
+ return try adapter.query(sql, parameters(clause.parameters)).first ?? [:]
241
+ }
242
+
243
+ /// One row per group, each carrying the group's `_count` and aggregates.
244
+ public func groupBy(_ aggregate: Aggregate) throws -> [Row] {
245
+ guard !aggregate.by.isEmpty else {
246
+ throw An5Error.invalidQuery("groupBy requires at least one 'by' column")
247
+ }
248
+ let clause = SQLBuilder.whereClause(aggregate.filter, dialect: dialect)
249
+ let byColumns = aggregate.by.map { dialect.quote($0) }.joined(separator: ", ")
250
+ // Counted unconditionally: a group is only useful next to its size, and a group whose
251
+ // rows were all filtered out still has to come back as a zero rather than disappear.
252
+ var expressions = ["COUNT(*) AS _count"]
253
+ expressions += selectExpressions(aggregate, includeCount: false)
254
+
255
+ var sql = "SELECT \(byColumns), \(expressions.joined(separator: ", ")) FROM \(tableSQL)"
256
+ if !clause.isEmpty { sql += " WHERE \(clause.sql)" }
257
+ sql += " GROUP BY \(byColumns)"
258
+
259
+ var orderBy = SQLBuilder.orderBy(aggregate.orderBy, dialect: dialect)
260
+ let paging = aggregate.take != nil || aggregate.skip > 0
261
+ if orderBy.isEmpty && paging {
262
+ // Paging an unordered result gives an arbitrary slice of the groups; ordering by
263
+ // the group columns at least makes the same query return the same slice.
264
+ orderBy = "ORDER BY \(byColumns)"
265
+ }
266
+ if !orderBy.isEmpty { sql += " \(orderBy)" }
267
+ if paging {
268
+ sql += dialect.pagination(take: aggregate.take ?? 1, skip: aggregate.skip, orderBy: orderBy)
269
+ }
270
+ return try adapter.query(sql, parameters(clause.parameters))
271
+ }
272
+
273
+ private func selectExpressions(_ aggregate: Aggregate, includeCount: Bool) -> [String] {
274
+ var expressions: [String] = []
275
+ if includeCount { expressions.append("COUNT(*) AS _count") }
276
+ for (function, fields) in [("SUM", aggregate.sum), ("AVG", aggregate.average), ("MIN", aggregate.minimum), ("MAX", aggregate.maximum)] {
277
+ for field in fields {
278
+ expressions.append("\(function)(\(dialect.quote(field))) AS _\(function.lowercased())_\(field)")
279
+ }
280
+ }
281
+ return expressions
282
+ }
283
+
284
+ // ─── Vector search ──────────────────────────────────────────────────────────
285
+
286
+ /// The rows nearest `vector`, by the database's own vector functions where it has them
287
+ /// and in memory where it does not.
288
+ ///
289
+ /// SQLite has no vector operators at all, so it goes straight to the fallback rather than
290
+ /// building SQL that will only throw.
291
+ public func vectorSearch(
292
+ _ vector: [Double],
293
+ take: Int = 10,
294
+ filter: Where? = nil,
295
+ vectorField: String = "embedding",
296
+ metric: DistanceMetric = .cosine
297
+ ) throws -> [Row] {
298
+ let field = vectorField.isEmpty ? "embedding" : vectorField
299
+ let clause = SQLBuilder.whereClause(filter, dialect: dialect)
300
+ let quotedField = dialect.quote(field)
301
+
302
+ if dialect == .sqlite {
303
+ // SQLite has no vector operator, so the ranking runs through sqlite-vec, a
304
+ // registered distance function or json_each, whichever this driver supports.
305
+ if let ranked = try sqliteVectorSearch(
306
+ vector, take: take, field: field, metric: metric, clause: clause
307
+ ) {
308
+ return ranked
309
+ }
310
+ // No in-database strategy: score the column in Swift instead.
311
+ return try vectorSearchInMemory(
312
+ vector, take: take, filter: filter, field: field, metric: metric
313
+ )
314
+ }
315
+
316
+ do {
317
+ let sql: String
318
+ let values: [SQLValue] = [.text(Vectors.format(vector))]
319
+ if dialect == .postgres {
320
+ let op = metric == .euclidean ? "<->" : (metric == .dot ? "<#>" : "<=>")
321
+ sql = "SELECT *, (\(quotedField) \(op) \(dialect.placeholder)::vector) AS distance"
322
+ + " FROM \(tableSQL) WHERE \(quotedField) IS NOT NULL"
323
+ + (clause.isEmpty ? "" : " AND (\(clause.sql))")
324
+ + " ORDER BY distance ASC LIMIT \(take)"
325
+ } else {
326
+ let dimension = vector.count
327
+ sql = "SELECT TOP (\(take)) *, VECTOR_DISTANCE('\(metric.rawValue)',"
328
+ + " CAST(\(quotedField) AS VECTOR(\(dimension), float32)),"
329
+ + " CAST(\(dialect.placeholder) AS VECTOR(\(dimension), float32))) AS distance"
330
+ + " FROM \(tableSQL)\(noLock)"
331
+ + " WHERE \(quotedField) IS NOT NULL"
332
+ + (clause.isEmpty ? "" : " AND (\(clause.sql))")
333
+ + " ORDER BY distance ASC"
334
+ }
335
+ return try adapter.query(sql, parameters(values + clause.parameters))
336
+ } catch {
337
+ // The engine may have no vector extension installed at all. Falling through to the
338
+ // in-memory path is the documented behaviour, not a silent success.
339
+ return try vectorSearchInMemory(vector, take: take, filter: filter, field: field, metric: metric)
340
+ }
341
+ }
342
+
343
+ /// SQLite ranks a `VECTOR(n)` column inside the database.
344
+ ///
345
+ /// Returns `nil` when the connection offers no strategy, so the caller falls back to
346
+ /// scoring in Swift.
347
+ private func sqliteVectorSearch(
348
+ _ vector: [Double],
349
+ take: Int,
350
+ field: String,
351
+ metric: DistanceMetric,
352
+ clause: WhereClause
353
+ ) throws -> [Row]? {
354
+ guard !vector.isEmpty else { return [] }
355
+ let capabilities = adapter.sqliteCapabilities
356
+ let declared = (try? adapter.queryColumnType(table: metadata.table(model), column: field)) ?? nil
357
+ let tail = clause.isEmpty ? "" : " WHERE \(clause.sql)"
358
+ let column = dialect.quote(field)
359
+
360
+ for strategy in SqliteVectors.plan(
361
+ capabilities: capabilities, declaredType: declared, preference: adapter.vectorStrategy
362
+ ) {
363
+ if strategy == SqliteVectors.strategySQL {
364
+ let binary = try adapter.query("SELECT 1 FROM \(tableSQL)"
365
+ + (tail.isEmpty ? " WHERE " : tail + " AND ")
366
+ + "typeof(\(column)) = 'blob' LIMIT 1", parameters(clause.parameters))
367
+ if !binary.isEmpty { continue }
368
+ }
369
+ var bind: [SQLValue] = []
370
+ guard let sql = SqliteVectors.rankingQuery(
371
+ strategy: strategy, metric: metric, table: tableSQL, column: column,
372
+ vector: vector, take: take, tail: tail,
373
+ placeholder: dialect.placeholder, bind: &bind
374
+ ) else { continue }
375
+ // The strategy's own placeholders sit inside the ranked subquery, so their values
376
+ // lead the list; the caller's WHERE values follow, because its placeholders do too.
377
+ guard let rows = try? adapter.query(sql, parameters(bind + clause.parameters)) else {
378
+ // The connection advertised the capability but the query failed, e.g. an
379
+ // extension that did not really load. The next strategy is cheaper than
380
+ // reporting the failure.
381
+ continue
382
+ }
383
+ return decodeVectorRows(rows)
384
+ }
385
+ return nil
386
+ }
387
+
388
+ /// The `VECTOR(n)` columns of this model, from the generated metadata.
389
+ private var vectorFields: [String] {
390
+ metadata.fields(model).filter(SqliteVectors.isVectorField).map(\.name)
391
+ }
392
+
393
+ /// Encodes every `VECTOR(n)` value on its way into SQLite.
394
+ ///
395
+ /// The column holds a float32 BLOB, which is a third of the size of the JSON text it
396
+ /// replaces. Values that already arrived as text or bytes are left alone, so rows written
397
+ /// by an older version stay readable.
398
+ private func encodeVectorFields(_ values: Values) -> Values {
399
+ guard dialect == .sqlite else { return values }
400
+ let names = vectorFields
401
+ guard !names.isEmpty else { return values }
402
+ var out = values
403
+ for name in names {
404
+ guard let value = out[name], SqliteVectors.needsEncoding(value),
405
+ let decoded = SqliteVectors.decode(value) else { continue }
406
+ out[name] = SqliteVectors.encode(decoded)
407
+ }
408
+ return out
409
+ }
410
+
411
+ /// Decodes `VECTOR(n)` columns back into arrays of numbers after a read.
412
+ private func decodeVectorRows(_ rows: [Row]) -> [Row] {
413
+ guard dialect == .sqlite, !rows.isEmpty else { return rows }
414
+ let names = vectorFields
415
+ guard !names.isEmpty else { return rows }
416
+ return rows.map { row in
417
+ var out = row
418
+ for name in names where out[name] != nil {
419
+ guard let decoded = SqliteVectors.decode(out[name] ?? nil) else { continue }
420
+ out[name] = decoded
421
+ }
422
+ return out
423
+ }
424
+ }
425
+
426
+ private func vectorSearchInMemory(
427
+ _ vector: [Double],
428
+ take: Int,
429
+ filter: Where?,
430
+ field: String,
431
+ metric: DistanceMetric
432
+ ) throws -> [Row] {
433
+ let rows = try findMany(Query(filter: filter))
434
+ var scored: [(row: Row, distance: Double)] = []
435
+ for row in rows {
436
+ guard let stored = SqliteVectors.decode(cell(row, field), expectedLength: vector.count)
437
+ else { continue }
438
+ scored.append((row, DistanceMetric.distance(metric, vector, stored)))
439
+ }
440
+ scored.sort { $0.distance < $1.distance }
441
+ return scored.prefix(take).map { entry -> Row in
442
+ var row = entry.row
443
+ row["distance"] = entry.distance
444
+ return row
445
+ }
446
+ }
447
+
448
+ // ─── Relations ──────────────────────────────────────────────────────────────
449
+
450
+ /// Eager-loads relations onto rows already fetched.
451
+ private func resolveIncludes(
452
+ _ relations: [String: RelationDef],
453
+ rows: inout [Row],
454
+ include: Include
455
+ ) throws {
456
+ guard !rows.isEmpty, !include.isEmpty else { return }
457
+
458
+ for (key, value) in include {
459
+ if value == nil || (value is Bool && value as? Bool == false) { continue }
460
+
461
+ if key == "_count" {
462
+ try applyRelationCounts(relations, rows: &rows)
463
+ continue
464
+ }
465
+ guard let relation = relations[key] else { continue }
466
+
467
+ let isMany = relation.isMany
468
+ let joinKey = isMany ? relation.localKey : relation.foreignKey
469
+ let matchKey = isMany ? relation.foreignKey : relation.localKey
470
+
471
+ let uniqueKeys = uniqueValues(rows, column: joinKey)
472
+ guard !uniqueKeys.isEmpty else {
473
+ for index in rows.indices { rows[index][key] = isMany ? [Row]() : nil }
474
+ continue
475
+ }
476
+
477
+ let options = value as? [String: Any?] ?? [:]
478
+ var nested = Query()
479
+ var nestedFilter: Where = [matchKey: ["in": uniqueKeys]]
480
+ if let extra = options["where"] as? Where {
481
+ for (column, condition) in extra { nestedFilter[column] = condition }
482
+ }
483
+ nested.filter = nestedFilter
484
+ nested.orderBy = SQLBuilder.orderEntries(cell(options, "orderBy"))
485
+ if let skip = options["skip"] as? Int { nested.skip = max(0, skip) }
486
+ if let take = options["take"] as? Int { nested.take = take }
487
+
488
+ var related = try adapter.table(relation.modelName).findMany(nested)
489
+ if let nestedInclude = options["include"] as? Include {
490
+ try resolveIncludes(metadata.relations(relation.modelName), rows: &related, include: nestedInclude)
491
+ }
492
+ var projected = related
493
+ if let nestedSelect = options["select"] as? [String] {
494
+ projected = related.map { project($0, selected: nestedSelect) }
495
+ }
496
+
497
+ let groupColumn = isMany ? relation.foreignKey : relation.localKey
498
+ var groups: [String: [Row]] = [:]
499
+ for (index, row) in related.enumerated() {
500
+ groups[groupKey(cell(row, groupColumn)), default: []].append(projected[index])
501
+ }
502
+ let rowColumn = isMany ? relation.localKey : relation.foreignKey
503
+ for index in rows.indices {
504
+ let matches = groups[groupKey(cell(rows[index], rowColumn))] ?? []
505
+ rows[index][key] = isMany ? matches : matches.first
506
+ }
507
+ }
508
+ }
509
+
510
+ private func applyRelationCounts(_ relations: [String: RelationDef], rows: inout [Row]) throws {
511
+ for index in rows.indices { rows[index]["_count"] = [String: Int]() }
512
+ for (name, relation) in relations where relation.isMany {
513
+ let uniqueKeys = uniqueValues(rows, column: relation.localKey)
514
+ guard !uniqueKeys.isEmpty else {
515
+ for index in rows.indices {
516
+ var totals = rows[index]["_count"] as? [String: Int] ?? [:]
517
+ totals[name] = 0
518
+ rows[index]["_count"] = totals
519
+ }
520
+ continue
521
+ }
522
+ let related = try adapter.table(relation.modelName)
523
+ .findMany(filter: [relation.foreignKey: ["in": uniqueKeys]])
524
+ var counts: [String: Int] = [:]
525
+ for row in related {
526
+ let key = groupKey(cell(row, relation.foreignKey))
527
+ counts[key, default: 0] += 1
528
+ }
529
+ for index in rows.indices {
530
+ let key = groupKey(cell(rows[index], relation.localKey))
531
+ var totals = rows[index]["_count"] as? [String: Int] ?? [:]
532
+ totals[name] = counts[key] ?? 0
533
+ rows[index]["_count"] = totals
534
+ }
535
+ }
536
+ }
537
+
538
+ /// The relation objects in a write payload, keyed by relation name.
539
+ private func splitRelationWrites(_ data: Values) -> [String: Any?] {
540
+ let relations = metadata.relations(model)
541
+ return data.filter { relations[$0.key] != nil && $0.value is [String: Any?] }
542
+ }
543
+
544
+ private func applyRelationCreates(_ parent: Row, relationWrites: [String: Any?]) throws {
545
+ let relations = metadata.relations(model)
546
+ for (key, value) in relationWrites {
547
+ guard let relation = relations[key], let write = value as? [String: Any?] else { continue }
548
+ guard let create = write["create"] else { continue }
549
+ for item in asRows(create) {
550
+ var merged = item
551
+ merged[relation.foreignKey] = parent[relation.localKey]
552
+ try adapter.table(relation.modelName).create(merged)
553
+ }
554
+ }
555
+ }
556
+
557
+ private func applyRelationUpdates(
558
+ _ parent: Row?,
559
+ filter: Where,
560
+ relationWrites: [String: Any?]
561
+ ) throws {
562
+ let relations = metadata.relations(model)
563
+ for (key, value) in relationWrites {
564
+ guard let relation = relations[key], let write = value as? [String: Any?] else { continue }
565
+ let child = adapter.table(relation.modelName)
566
+
567
+ if let create = write["create"] {
568
+ for item in asRows(create) {
569
+ var merged = item
570
+ merged[relation.foreignKey] = parent?[relation.localKey]
571
+ try child.create(merged)
572
+ }
573
+ }
574
+ if let update = write["update"] as? [String: Any?] {
575
+ let spec = update
576
+ // Falls back to the parent's filter, never to "no filter": a relation update
577
+ // with no filter of its own would otherwise rewrite every related row.
578
+ let nestedFilter = (spec["where"] as? Where) ?? filter
579
+ let nestedData = (spec["data"] as? Values) ?? [:]
580
+ try child.update(filter: nestedFilter, data: nestedData)
581
+ }
582
+ if let disconnect = write["disconnect"] as? Where {
583
+ try child.updateMany(filter: disconnect, data: [relation.foreignKey: NSNull()])
584
+ }
585
+ }
586
+ }
587
+
588
+ private func projectAfterWrite(_ row: Row?, _ query: Query) throws -> Row? {
589
+ guard let row else { return nil }
590
+ var rows = [row]
591
+ let relations = metadata.relations(model)
592
+ if let include = query.include, !include.isEmpty {
593
+ try resolveIncludes(relations, rows: &rows, include: include)
594
+ }
595
+ if let select = query.select {
596
+ try resolveIncludes(relations, rows: &rows, include: relationSelect(select, relations))
597
+ return project(rows[0], selected: select)
598
+ }
599
+ return rows[0]
600
+ }
601
+
602
+ // ─── Helpers ────────────────────────────────────────────────────────────────
603
+
604
+ /// Already-encoded values on their way to the adapter's untyped entry point.
605
+ private func parameters(_ values: [SQLValue]) -> [Any?] {
606
+ values.map { $0.unwrapped }
607
+ }
608
+
609
+ private func hasRelationSelect(_ selected: [String], _ relations: [String: RelationDef]) -> Bool {
610
+ selected.contains { $0 == "_count" || relations[$0] != nil }
611
+ }
612
+
613
+ private func relationSelect(_ selected: [String], _ relations: [String: RelationDef]) -> Include {
614
+ var include: Include = [:]
615
+ for field in selected where field == "_count" || relations[field] != nil {
616
+ include[field] = true
617
+ }
618
+ return include
619
+ }
620
+
621
+ private func project(_ row: Row, selected: [String]) -> Row {
622
+ guard !selected.isEmpty else { return row }
623
+ var projected: Row = [:]
624
+ for field in selected where row.keys.contains(field) { projected[field] = row[field] }
625
+ if let count = row["_count"] { projected["_count"] = count }
626
+ return projected
627
+ }
628
+
629
+ private func uniqueValues(_ rows: [Row], column: String) -> [Any?] {
630
+ var seen = Set<String>()
631
+ var values: [Any?] = []
632
+ for row in rows {
633
+ guard let value = cell(row, column), !(value is NSNull) else { continue }
634
+ // Numbers are normalised so a key read as an Int in one row and a Double in the
635
+ // next does not become two `IN` values for the same row.
636
+ let key: String
637
+ if let number = value as? NSNumber { key = "\(number.doubleValue)" } else { key = text(value) }
638
+ if seen.insert(key).inserted { values.append(value) }
639
+ }
640
+ return values
641
+ }
642
+
643
+ /// A cell's value, flattened.
644
+ ///
645
+ /// A `[String: Any?]` lookup yields `Any??` — an absent column and a `NULL` column are
646
+ /// different things — and threading that double optional through every relation lookup
647
+ /// is where a `NULL` foreign key quietly stops matching its rows.
648
+ private func cell(_ row: Row, _ column: String) -> Any? {
649
+ row[column] ?? nil
650
+ }
651
+
652
+ private func groupKey(_ value: Any?) -> String {
653
+ guard let value, !(value is NSNull) else { return "\u{0}null" }
654
+ if let number = value as? NSNumber { return "\(number.doubleValue)" }
655
+ return text(value)
656
+ }
657
+
658
+ /// A value rendered as text for a join key, with no optional's debug decoration.
659
+ private func text(_ value: Any) -> String {
660
+ if let text = value as? String { return text }
661
+ if let uuid = value as? UUID { return uuid.uuidString }
662
+ if let data = value as? Foundation.Data { return data.base64EncodedString() }
663
+ return String(describing: value)
664
+ }
665
+
666
+ private func asRows(_ value: Any?) -> [Values] {
667
+ if let rows = value as? [Values] { return rows }
668
+ if let row = value as? Values { return [row] }
669
+ return []
670
+ }
671
+ }