@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.
- package/README.md +133 -2
- package/dist/an5Adapter.d.ts +37 -0
- package/dist/an5Adapter.js +138 -35
- 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 +436 -0
- package/dotnet/An5Adapter.cs +85 -27
- package/dotnet/Base/Core.cs +14 -0
- package/dotnet/Mssql/MssqlEngine.cs +44 -1
- package/dotnet/Postgres/PostgresEngine.cs +2 -0
- package/dotnet/Sqlite/SqliteEngine.cs +82 -2
- package/dotnet/Sqlite/SqliteVectors.cs +401 -0
- package/golang/an5_adapter.go +84 -32
- package/golang/base/vectors.go +263 -0
- package/golang/sqlite/vector.go +248 -0
- package/golang/sqlite_vector_test.go +288 -0
- package/golang/table_client.go +67 -3
- package/java/pom.xml +177 -0
- package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
- package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
- package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
- package/java/src/main/java/an5/adapters/An5Query.java +100 -0
- package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
- package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
- package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
- package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
- package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
- package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
- package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
- package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
- package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
- package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
- package/java/src/test/java/An5AdaptersSmoke.java +733 -0
- package/java/src/test/java/Json.java +174 -0
- package/java/src/test/java/QuerySemantics.java +111 -0
- package/kotlin/build.gradle.kts +145 -0
- package/kotlin/settings.gradle.kts +10 -0
- package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
- package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
- package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
- package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
- package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
- package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
- package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
- package/native/sqlite/CMakeLists.txt +12 -0
- package/native/sqlite/README.md +117 -0
- package/native/sqlite/an5_vector.c +209 -0
- package/package.json +33 -7
- package/python/an5_adapter.py +37 -6
- package/python/base/__init__.py +15 -0
- package/python/base/vectors.py +434 -0
- package/python/sqlite/__init__.py +2 -2
- package/python/sqlite/provider.py +21 -1
- package/python/table_client.py +129 -59
- package/rust/src/adapter.rs +342 -24
- package/rust/src/base/mod.rs +7 -0
- package/rust/src/base/vectors.rs +293 -0
- package/rust/src/lib.rs +3 -3
- package/scripts/sqlite-native-build.js +32 -0
- package/swift/Package.swift +22 -0
- package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
- package/swift/Sources/An5Adapters/Metadata.swift +183 -0
- package/swift/Sources/An5Adapters/Query.swift +105 -0
- package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
- package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
- package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
- package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
- package/swift/Sources/An5Adapters/TableClient.swift +671 -0
- package/swift/Sources/An5Adapters/Types.swift +127 -0
- package/swift/Sources/An5Adapters/Vectors.swift +86 -0
- package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
- package/swift/Sources/CSQLite/module.modulemap +8 -0
- package/swift/Sources/CSQLite/shim.h +9 -0
- package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
- package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
- package/typescript/src/an5Adapter.ts +146 -29
- 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 +541 -0
- 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
|
+
}
|