@an5/adapters 0.2.10 → 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 (109) hide show
  1. package/README.md +163 -2
  2. package/dist/an5Adapter.d.ts +37 -0
  3. package/dist/an5Adapter.js +190 -65
  4. package/dist/base/metadata.d.ts +2 -0
  5. package/dist/base/metadata.js +10 -0
  6. package/dist/base/sql.d.ts +2 -0
  7. package/dist/base/sql.js +24 -25
  8. package/dist/base/types.d.ts +14 -0
  9. package/dist/googlesheets/adapter.js +16 -8
  10. package/dist/googlesheets/config.d.ts +13 -0
  11. package/dist/googlesheets/config.js +5 -1
  12. package/dist/googlesheets/oauth.d.ts +3 -0
  13. package/dist/googlesheets/oauth.js +43 -0
  14. package/dist/googlesheets/parseConnectionString.js +12 -0
  15. package/dist/googlesheets/tableClient.js +5 -23
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +8 -1
  18. package/dist/sqlite/browserEngine.d.ts +13 -0
  19. package/dist/sqlite/browserEngine.js +13 -0
  20. package/dist/sqlite/engine.d.ts +5 -0
  21. package/dist/sqlite/engine.js +10 -0
  22. package/dist/sqlite/index.d.ts +1 -0
  23. package/dist/sqlite/index.js +1 -0
  24. package/dist/sqlite/vector.d.ts +155 -0
  25. package/dist/sqlite/vector.js +436 -0
  26. package/dotnet/An5Adapter.cs +85 -27
  27. package/dotnet/Base/Core.cs +14 -0
  28. package/dotnet/Mssql/MssqlEngine.cs +44 -1
  29. package/dotnet/Postgres/PostgresEngine.cs +2 -0
  30. package/dotnet/Sqlite/SqliteEngine.cs +82 -2
  31. package/dotnet/Sqlite/SqliteVectors.cs +401 -0
  32. package/golang/an5_adapter.go +84 -32
  33. package/golang/base/vectors.go +263 -0
  34. package/golang/sqlite/vector.go +248 -0
  35. package/golang/sqlite_vector_test.go +288 -0
  36. package/golang/table_client.go +67 -3
  37. package/java/pom.xml +177 -0
  38. package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
  39. package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
  40. package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
  41. package/java/src/main/java/an5/adapters/An5Query.java +100 -0
  42. package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
  43. package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
  44. package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
  45. package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
  46. package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
  47. package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
  48. package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
  49. package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
  50. package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
  51. package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
  52. package/java/src/test/java/An5AdaptersSmoke.java +733 -0
  53. package/java/src/test/java/Json.java +174 -0
  54. package/java/src/test/java/QuerySemantics.java +111 -0
  55. package/kotlin/build.gradle.kts +145 -0
  56. package/kotlin/settings.gradle.kts +10 -0
  57. package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
  58. package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
  59. package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
  60. package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
  61. package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
  62. package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
  63. package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
  64. package/native/sqlite/CMakeLists.txt +12 -0
  65. package/native/sqlite/README.md +117 -0
  66. package/native/sqlite/an5_vector.c +209 -0
  67. package/package.json +40 -9
  68. package/python/an5_adapter.py +37 -6
  69. package/python/base/__init__.py +15 -0
  70. package/python/base/vectors.py +434 -0
  71. package/python/sqlite/__init__.py +2 -2
  72. package/python/sqlite/provider.py +21 -1
  73. package/python/table_client.py +129 -59
  74. package/rust/src/adapter.rs +342 -24
  75. package/rust/src/base/mod.rs +7 -0
  76. package/rust/src/base/vectors.rs +293 -0
  77. package/rust/src/lib.rs +3 -3
  78. package/scripts/sqlite-native-build.js +32 -0
  79. package/swift/Package.swift +22 -0
  80. package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
  81. package/swift/Sources/An5Adapters/Metadata.swift +183 -0
  82. package/swift/Sources/An5Adapters/Query.swift +105 -0
  83. package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
  84. package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
  85. package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
  86. package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
  87. package/swift/Sources/An5Adapters/TableClient.swift +671 -0
  88. package/swift/Sources/An5Adapters/Types.swift +127 -0
  89. package/swift/Sources/An5Adapters/Vectors.swift +86 -0
  90. package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
  91. package/swift/Sources/CSQLite/module.modulemap +8 -0
  92. package/swift/Sources/CSQLite/shim.h +9 -0
  93. package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
  94. package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
  95. package/typescript/src/an5Adapter.ts +200 -58
  96. package/typescript/src/base/metadata.ts +8 -0
  97. package/typescript/src/base/sql.ts +23 -16
  98. package/typescript/src/base/types.ts +15 -0
  99. package/typescript/src/googlesheets/adapter.ts +17 -8
  100. package/typescript/src/googlesheets/config.ts +10 -1
  101. package/typescript/src/googlesheets/oauth.ts +31 -0
  102. package/typescript/src/googlesheets/parseConnectionString.ts +8 -0
  103. package/typescript/src/googlesheets/tableClient.ts +5 -19
  104. package/typescript/src/index.ts +13 -0
  105. package/typescript/src/sqlite/browserEngine.ts +24 -0
  106. package/typescript/src/sqlite/engine.ts +13 -0
  107. package/typescript/src/sqlite/index.ts +1 -1
  108. package/typescript/src/sqlite/vector.ts +541 -0
  109. package/golang/sqlite/sqlite.go +0 -14
@@ -0,0 +1,359 @@
1
+ import Foundation
2
+
3
+ /// The database dialects AN5 speaks, and everything that differs between them.
4
+ ///
5
+ /// The differences are not cosmetic: quoting, pagination and the `WITH (NOLOCK)` hint each
6
+ /// have to be produced per dialect, and getting one wrong yields SQL the target engine
7
+ /// cannot parse.
8
+ public enum Dialect: String, CaseIterable, Sendable {
9
+ case mssql
10
+ case postgres
11
+ case sqlite
12
+
13
+ /// The dialect a connection string points at.
14
+ ///
15
+ /// Lower-cased first: a URI scheme is case-insensitive, so `MySQL://` and `mysql://` are
16
+ /// the same connection.
17
+ public static func detect(_ connectionString: String) -> Dialect {
18
+ let value = connectionString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
19
+ if value.hasPrefix("postgres://") || value.hasPrefix("postgresql://") {
20
+ return .postgres
21
+ }
22
+ if value.hasPrefix("sqlite://")
23
+ || value.hasPrefix("sqlite:")
24
+ || value.hasPrefix("file:")
25
+ || value.hasSuffix(".db")
26
+ || value.hasSuffix(".sqlite")
27
+ || value.hasSuffix(".sqlite3")
28
+ || value == ":memory:" {
29
+ return .sqlite
30
+ }
31
+ return .mssql
32
+ }
33
+
34
+ /// The bind placeholder.
35
+ ///
36
+ /// Every driver AN5 speaks binds positionally, so this is `?` throughout; it exists so
37
+ /// the SQL builder never hardcodes a placeholder and a new driver has one place to differ.
38
+ public var placeholder: String { "?" }
39
+
40
+ /// Quotes a bare column or identifier name for this dialect.
41
+ public func quote(_ name: String) -> String {
42
+ let unwrapped = Dialect.strip(name, left: "[", right: "]")
43
+ switch self {
44
+ case .mssql:
45
+ return "[" + unwrapped.replacingOccurrences(of: "]", with: "]]") + "]"
46
+ case .postgres, .sqlite:
47
+ let bare = Dialect.strip(unwrapped, left: "\"", right: "\"")
48
+ return "\"" + bare.replacingOccurrences(of: "\"", with: "\"\"") + "\""
49
+ }
50
+ }
51
+
52
+ /// `WITH (NOLOCK)` is an MSSQL table hint; no other dialect has an equivalent.
53
+ public var supportsNoLock: Bool { self == .mssql }
54
+
55
+ /// The trailing limit clause.
56
+ ///
57
+ /// SQLite has `LIMIT ... OFFSET` like PostgreSQL. Emitting the MSSQL
58
+ /// `OFFSET ... FETCH NEXT` form against SQLite produces SQL it cannot parse.
59
+ public func pagination(take: Int?, skip: Int, orderBy: String) -> String {
60
+ guard let take else { return "" }
61
+ if self == .mssql {
62
+ let prefix = orderBy.isEmpty ? " ORDER BY (SELECT NULL)" : ""
63
+ return "\(prefix) OFFSET \(max(0, skip)) ROWS FETCH NEXT \(take) ROWS ONLY"
64
+ }
65
+ return " LIMIT \(take) OFFSET \(max(0, skip))"
66
+ }
67
+
68
+ static func strip(_ value: String, left: String, right: String) -> String {
69
+ guard value.count >= left.count + right.count,
70
+ value.hasPrefix(left),
71
+ value.hasSuffix(right) else { return value }
72
+ return String(value.dropFirst(left.count).dropLast(right.count))
73
+ }
74
+ }
75
+
76
+ /// The `WHERE` clause for a filter tree, with the values it binds in order.
77
+ public struct WhereClause {
78
+ public let sql: String
79
+ public let parameters: [SQLValue]
80
+
81
+ public var isEmpty: Bool { sql.isEmpty }
82
+
83
+ public init(sql: String = "", parameters: [SQLValue] = []) {
84
+ self.sql = sql
85
+ self.parameters = parameters
86
+ }
87
+ }
88
+
89
+ /// Turns a filter tree into SQL and the bind values that go with it.
90
+ public enum SQLBuilder {
91
+
92
+ /// The operator keys that make a map a filter rather than a nested condition.
93
+ static let operatorKeys: Set<String> = [
94
+ "equals", "in", "notIn", "contains", "startsWith", "endsWith", "not", "gte", "lte", "gt", "lt",
95
+ ]
96
+
97
+ /// Builds the `WHERE` clause and the parameters it binds, in order.
98
+ ///
99
+ /// Every value reaches the database as a bind parameter; none is ever formatted into the
100
+ /// SQL. `AND`/`OR`/`NOT` groups are supported, and an empty `in: []` becomes `1=0` rather
101
+ /// than the invalid `IN ()`.
102
+ public static func whereClause(_ filter: Where?, dialect: Dialect, prefix: String = "") -> WhereClause {
103
+ guard let filter, !filter.isEmpty else { return WhereClause() }
104
+
105
+ // A key with an underscore whose value is a map of plain columns is a flattened
106
+ // relation filter; hoisting it puts those conditions alongside the rest instead of
107
+ // quoting the whole thing as one column name.
108
+ var flattened: [String: Any?] = [:]
109
+ for (key, value) in filter {
110
+ if key.contains("_"), let nested = value as? Where, !isOperatorValue(nested) {
111
+ for (nestedKey, nestedValue) in nested { flattened[nestedKey] = nestedValue }
112
+ } else {
113
+ flattened[key] = value
114
+ }
115
+ }
116
+
117
+ var conditions: [String] = []
118
+ var parameters: [SQLValue] = []
119
+ for (key, value) in flattened {
120
+ switch key {
121
+ case "OR":
122
+ guard let branches = value as? [Where] else { continue }
123
+ var sub: [String] = []
124
+ for (index, branch) in branches.enumerated() {
125
+ let clause = self.whereClause(branch, dialect: dialect, prefix: "\(prefix)or\(index)_")
126
+ sub.append(clause.isEmpty ? "1=1" : clause.sql)
127
+ parameters.append(contentsOf: clause.parameters)
128
+ }
129
+ conditions.append(sub.isEmpty ? "1=0" : "(\(sub.joined(separator: " OR ")))")
130
+ case "AND":
131
+ let items = value as? [Where] ?? [value as? Where ?? [:]]
132
+ var sub: [String] = []
133
+ for (index, item) in items.enumerated() {
134
+ let clause = self.whereClause(item, dialect: dialect, prefix: "\(prefix)and\(index)_")
135
+ parameters.append(contentsOf: clause.parameters)
136
+ if !clause.isEmpty { sub.append(clause.sql) }
137
+ }
138
+ if !sub.isEmpty { conditions.append("(\(sub.joined(separator: " AND ")))") }
139
+ case "NOT":
140
+ let items = value as? [Where] ?? [value as? Where ?? [:]]
141
+ var sub: [String] = []
142
+ for (index, item) in items.enumerated() {
143
+ let clause = self.whereClause(item, dialect: dialect, prefix: "\(prefix)not\(index)_")
144
+ sub.append(clause.isEmpty ? "1=1" : clause.sql)
145
+ parameters.append(contentsOf: clause.parameters)
146
+ }
147
+ if !sub.isEmpty { conditions.append("NOT (\(sub.joined(separator: " OR ")))") }
148
+ default:
149
+ conditions.append(
150
+ condition(key: key, value: value, dialect: dialect, prefix: prefix, parameters: &parameters)
151
+ )
152
+ }
153
+ }
154
+ return WhereClause(sql: conditions.filter { !$0.isEmpty }.joined(separator: " AND "), parameters: parameters)
155
+ }
156
+
157
+ private static func condition(
158
+ key: String,
159
+ value: Any?,
160
+ dialect: Dialect,
161
+ prefix: String,
162
+ parameters: inout [SQLValue]
163
+ ) -> String {
164
+ let column = dialect.quote(key)
165
+ let placeholder = dialect.placeholder
166
+
167
+ if value == nil || value is NSNull {
168
+ return "\(column) IS NULL"
169
+ }
170
+ guard let operators = value as? Where else {
171
+ parameters.append(SQLValue.encode(value) ?? .null)
172
+ return "\(column) = \(placeholder)"
173
+ }
174
+
175
+ var conditions: [String] = []
176
+ if let inner = operators["not"] {
177
+ if inner == nil || inner is NSNull {
178
+ conditions.append("\(column) IS NOT NULL")
179
+ } else if let nested = inner as? Where {
180
+ let clause = self.whereClause([key: nested], dialect: dialect, prefix: prefix + key + "_not_")
181
+ parameters.append(contentsOf: clause.parameters)
182
+ if !clause.isEmpty { conditions.append("NOT (\(clause.sql))") }
183
+ } else {
184
+ parameters.append(SQLValue.encode(inner) ?? .null)
185
+ conditions.append("\(column) <> \(placeholder)")
186
+ }
187
+ }
188
+ if operators.keys.contains("equals") {
189
+ // Flattened: a lookup on a `[String: Any?]` yields `Any??`, and letting the
190
+ // compiler coerce it is how a `NULL` operand stops reading as "is null".
191
+ let equals = operators["equals"] ?? nil
192
+ if equals == nil || equals is NSNull {
193
+ conditions.append("\(column) IS NULL")
194
+ } else {
195
+ parameters.append(SQLValue.encode(equals) ?? .null)
196
+ conditions.append("\(column) = \(placeholder)")
197
+ }
198
+ }
199
+ for (op, template) in [("contains", "%@%"), ("startsWith", "@%"), ("endsWith", "%@")] {
200
+ guard let operand = operators[op] else { continue }
201
+ // Rendered through `encode` rather than interpolated directly: a filter built
202
+ // from a `[String: Any?]` dictionary hands the operand over wrapped in an
203
+ // `Optional`, and interpolating that would search for the text "Optional(da)".
204
+ parameters.append(.text(template.replacingOccurrences(of: "@", with: literal(operand))))
205
+ conditions.append("\(column) LIKE \(placeholder)")
206
+ }
207
+ for (op, sql) in [("gte", ">="), ("lte", "<="), ("gt", ">"), ("lt", "<")] {
208
+ guard let operand = operators[op] else { continue }
209
+ parameters.append(SQLValue.encode(operand) ?? .null)
210
+ conditions.append("\(column) \(sql) \(placeholder)")
211
+ }
212
+ for (op, sql, emptyResult) in [("in", " IN (", "1=0"), ("notIn", " NOT IN (", "1=1")] {
213
+ guard let raw = operators[op] else { continue }
214
+ // Accepts an untyped list whatever element type it was built with: a caller that
215
+ // wrote `["Ada", "Grace"]` produced a `[String]`, and a cast straight to
216
+ // `[Any?]` does not always see through that, which would silently turn an
217
+ // `in` filter into "match nothing".
218
+ let values: [Any?]
219
+ if let typed = raw as? [Any?] {
220
+ values = typed
221
+ } else if let list = raw as? [Any] {
222
+ values = list.map { $0 }
223
+ } else {
224
+ values = []
225
+ }
226
+ if values.isEmpty {
227
+ // `IN ()` is a syntax error in every dialect. An empty set can match nothing,
228
+ // so it becomes 1=0; the negated form is true of every row, so 1=1.
229
+ conditions.append(emptyResult)
230
+ continue
231
+ }
232
+ conditions.append(column + sql + values.map { _ in placeholder }.joined(separator: ", ") + ")")
233
+ parameters.append(contentsOf: values.map { SQLValue.encode($0) ?? .null })
234
+ }
235
+ return conditions.joined(separator: " AND ")
236
+ }
237
+
238
+ /// A bind value as the text a `LIKE` operand needs.
239
+ private static func literal(_ value: Any?) -> String {
240
+ switch SQLValue.encode(value) {
241
+ case .some(.text(let text)): return text
242
+ case .some(.integer(let number)): return String(number)
243
+ case .some(.real(let number)): return String(number)
244
+ case .some(.uuid(let uuid)): return uuid.uuidString
245
+ default: return ""
246
+ }
247
+ }
248
+
249
+ /// Builds the `ORDER BY` clause.
250
+ ///
251
+ /// Accepts every shape the other clients use for ordering — a single column, a
252
+ /// column/direction pair, a dictionary, or a list of dictionaries — because a generated
253
+ /// client may hand over whichever form its own language finds natural.
254
+ public static func orderBy(_ orderBy: Any?, dialect: Dialect) -> String {
255
+ var parts: [String] = []
256
+ for entry in orderEntries(orderBy) {
257
+ for (column, direction) in entry {
258
+ parts.append("\(dialect.quote(column)) \(direction.lowercased() == "desc" ? "DESC" : "ASC")")
259
+ }
260
+ }
261
+ return parts.isEmpty ? "" : "ORDER BY \(parts.joined(separator: ", "))"
262
+ }
263
+
264
+ /// Normalises any accepted ordering shape into column/direction entries.
265
+ static func orderEntries(_ orderBy: Any?) -> [[String: String]] {
266
+ if let list = orderBy as? [[String: String]] { return list }
267
+ if let list = orderBy as? [[String: Any?]] {
268
+ return list.map { $0.mapValues { ($0 as? String) ?? "asc" } }
269
+ }
270
+ if let single = orderBy as? [String: String] { return [single] }
271
+ if let single = orderBy as? [String: Any?] {
272
+ return [single.mapValues { ($0 as? String) ?? "asc" }]
273
+ }
274
+ if let pair = orderBy as? (String, String) { return [[pair.0: pair.1]] }
275
+ return []
276
+ }
277
+
278
+ /// Appends one `SET` assignment, expanding the arithmetic update operators.
279
+ public static func appendUpdateSet(
280
+ _ setParts: inout [String],
281
+ _ values: inout [SQLValue],
282
+ column: String,
283
+ value: Any?,
284
+ dialect: Dialect
285
+ ) {
286
+ let quoted = dialect.quote(column)
287
+ let placeholder = dialect.placeholder
288
+ if let operations = value as? [String: Any?] {
289
+ for (operation, symbol) in [("increment", "+"), ("decrement", "-"), ("multiply", "*"), ("divide", "/")] {
290
+ if let operand = operations[operation] {
291
+ setParts.append("\(quoted) = \(quoted) \(symbol) \(placeholder)")
292
+ values.append(SQLValue.encode(operand) ?? .null)
293
+ return
294
+ }
295
+ }
296
+ if let replacement = operations["set"] {
297
+ setParts.append("\(quoted) = \(placeholder)")
298
+ values.append(SQLValue.encode(replacement) ?? .null)
299
+ return
300
+ }
301
+ }
302
+ setParts.append("\(quoted) = \(placeholder)")
303
+ values.append(SQLValue.encode(value) ?? .null)
304
+ }
305
+
306
+ /// Quotes a possibly schema-qualified table name.
307
+ ///
308
+ /// An already-bracketed name is never returned as-is: that quoting is only correct on
309
+ /// MSSQL, so the brackets are stripped and re-applied for the target dialect instead of
310
+ /// letting one dialect's quoting follow the name into another.
311
+ public static func quoteTable(_ table: String, dialect: Dialect) -> String {
312
+ var parts = splitQualified(table)
313
+ if dialect == .sqlite, parts.count == 2, unquote(parts[0]).lowercased() == "dbo" {
314
+ parts = Array(parts.dropFirst())
315
+ }
316
+ if parts.isEmpty { parts = [table] }
317
+ return parts.map { dialect.quote($0) }.joined(separator: ".")
318
+ }
319
+
320
+ /// Splits `schema.table` without cutting a dot inside brackets or quotes.
321
+ ///
322
+ /// A plain split on `.` turns `[dbo].[users]` into two bracketed parts, which is right for
323
+ /// MSSQL but leaves the other dialects quoting a name that already carries brackets.
324
+ private static func splitQualified(_ table: String) -> [String] {
325
+ var parts: [String] = []
326
+ var buffer = ""
327
+ var closing: Character?
328
+ for character in table {
329
+ if let close = closing {
330
+ buffer.append(character)
331
+ if character == close { closing = nil }
332
+ continue
333
+ }
334
+ switch character {
335
+ case "[": closing = "]"; buffer.append(character)
336
+ case "\"": closing = "\""; buffer.append(character)
337
+ case ".": parts.append(buffer); buffer = ""
338
+ default: buffer.append(character)
339
+ }
340
+ }
341
+ parts.append(buffer)
342
+ return parts.filter { !$0.isEmpty }
343
+ }
344
+
345
+ private static func unquote(_ name: String) -> String {
346
+ Dialect.strip(Dialect.strip(name, left: "[", right: "]"), left: "\"", right: "\"")
347
+ }
348
+
349
+ /// A bind parameter name that is a legal identifier in every dialect.
350
+ public static func sanitizeParameterName(_ name: String) -> String {
351
+ var cleaned = String(name.map { $0.isLetter || $0.isNumber || $0 == "_" ? $0 : "_" })
352
+ if cleaned.isEmpty || !cleaned.first!.isLetter { cleaned = "p_" + cleaned }
353
+ return cleaned
354
+ }
355
+
356
+ private static func isOperatorValue(_ value: Where) -> Bool {
357
+ value.keys.contains { operatorKeys.contains($0) }
358
+ }
359
+ }
@@ -0,0 +1,301 @@
1
+ import Foundation
2
+ import CSQLite
3
+
4
+ /// Tells SQLite to copy a bound value rather than keep the pointer.
5
+ ///
6
+ /// `SQLITE_TRANSIENT` is a C macro, and Swift cannot import those, so the same value is
7
+ /// spelled out here: `((sqlite3_destructor_type)-1)`. Without it SQLite would read freed
8
+ /// memory the moment the Swift value went out of scope.
9
+ private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
10
+
11
+ /// What a registered distance function needs to remember about itself.
12
+ private final class MetricContext {
13
+ let metric: DistanceMetric
14
+
15
+ init(metric: DistanceMetric) {
16
+ self.metric = metric
17
+ }
18
+ }
19
+
20
+ /// The SQL surface the AN5 runtime needs, so the runtime is not welded to one driver.
21
+ ///
22
+ /// A protocol rather than a direct call into SQLite so an app can put its own database
23
+ /// layer underneath — GRDB, SQLite.swift, or a connection it already owns — without the
24
+ /// generated models or the query builder changing.
25
+ public protocol SQLDriver: AnyObject {
26
+ /// Runs a query and returns its rows keyed by column name.
27
+ func query(_ sql: String, _ parameters: [SQLValue]) throws -> [Row]
28
+
29
+ /// Runs a statement that returns no rows and gives back the affected row count.
30
+ @discardableResult
31
+ func execute(_ sql: String, _ parameters: [SQLValue]) throws -> Int
32
+
33
+ func beginTransaction() throws
34
+ func commit() throws
35
+ func rollback() throws
36
+ }
37
+
38
+ /// SQLite, through the system library.
39
+ ///
40
+ /// The database every mobile platform already has: no bundled engine, no extra binary, and
41
+ /// the same file on device and in a simulator. One connection is held for the driver's
42
+ /// lifetime rather than opened per statement, because an in-memory database disappears when
43
+ /// its last connection closes and because per-statement connections are the wrong shape for
44
+ /// a device.
45
+ ///
46
+ /// Not thread-safe: SQLite connections are not, and serialising every call would hide that
47
+ /// rather than fix it. Use one driver per thread, or an actor around one.
48
+ public final class SQLiteDriver: SQLDriver {
49
+
50
+ private var handle: OpaquePointer?
51
+ private let path: String
52
+
53
+ /// Opens a database, creating the file if it does not exist.
54
+ ///
55
+ /// - Parameter path: a file path, `:memory:` for an in-memory database, or a `file:`
56
+ /// URI.
57
+ public init(path: String) throws {
58
+ self.path = path
59
+ var handle: OpaquePointer?
60
+ let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX
61
+ let status = sqlite3_open_v2(path, &handle, flags, nil)
62
+ guard status == SQLITE_OK, let handle else {
63
+ let message = handle.map { String(cString: sqlite3_errmsg($0)) } ?? "status \(status)"
64
+ sqlite3_close_v2(handle)
65
+ throw An5Error.driver("cannot open \(path): \(message)")
66
+ }
67
+ self.handle = handle
68
+ // Foreign keys are off by default in SQLite, unlike every other dialect, so a schema
69
+ // that relies on them would silently accept rows nothing points at.
70
+ _ = try? execute("PRAGMA foreign_keys = ON", [])
71
+ // `sqlite3_enable_load_extension` is unavailable on Apple mobile platforms, where an
72
+ // application links SQLite statically and supplies its own extension instead.
73
+ #if os(Linux) || os(macOS) || os(Windows)
74
+ if let path = Self.sqliteVecPath {
75
+ // An extension is optional: without it the search still ranks, through the
76
+ // distance functions registered below or `json_each`.
77
+ _ = sqlite3_enable_load_extension(handle, 1)
78
+ _ = sqlite3_load_extension(handle, path, nil, nil)
79
+ _ = sqlite3_enable_load_extension(handle, 0)
80
+ }
81
+ #endif
82
+ registerVectorFunctions()
83
+ }
84
+
85
+ /// Where to load sqlite-vec from, set before the first driver is opened.
86
+ ///
87
+ /// A process-wide setting on purpose: the extension belongs to the SQLite build rather
88
+ /// than to one connection, and every AN5 connection wants the same one.
89
+ public static var sqliteVecPath: String?
90
+
91
+ /// Registers `an5_vec_cosine` / `an5_vec_l2` / `an5_vec_ip` so SQLite can rank a
92
+ /// `VECTOR(n)` column without the client loading it.
93
+ ///
94
+ /// This runs per connection because a user function only exists for the connection that
95
+ /// declared it.
96
+ private func registerVectorFunctions() {
97
+ // AN5's native extension or sqlite-vec already provide these names. Registering a
98
+ // Swift callback over them would silently replace native code with a slower path.
99
+ if (try? query("SELECT an5_vector_version()", [])) != nil { return }
100
+ for (metric, name) in SqliteVectors.an5Functions {
101
+ guard let resolved = DistanceMetric(rawValue: metric) else { continue }
102
+ registerVectorFunction(name, metric: resolved)
103
+ }
104
+ }
105
+
106
+ /// Registers one distance function.
107
+ ///
108
+ /// The metric travels in the function's user-data pointer rather than the callback
109
+ /// context: a C function pointer cannot capture anything, so the callback reads what it
110
+ /// needs back out of SQLite.
111
+ private func registerVectorFunction(_ name: String, metric: DistanceMetric) {
112
+ let userData = Unmanaged.passRetained(MetricContext(metric: metric)).toOpaque()
113
+ sqlite3_create_function_v2(
114
+ handle,
115
+ name,
116
+ 2,
117
+ SQLITE_UTF8 | SQLITE_DETERMINISTIC,
118
+ userData,
119
+ SQLiteDriver.vectorCallback,
120
+ nil, nil,
121
+ SQLiteDriver.vectorDestructor
122
+ )
123
+ }
124
+
125
+ /// The distance callback, at file scope for the C ABI.
126
+ ///
127
+ /// A `@convention(c)` function pointer cannot capture anything, not even `Self`, so
128
+ /// this is a free function rather than a method. Everything it needs arrives through
129
+ /// SQLite: the metric in the user-data pointer, the arguments in `values`.
130
+ private static let vectorCallback: @convention(c) (
131
+ OpaquePointer?, Int32, UnsafeMutablePointer<OpaquePointer?>?
132
+ ) -> Void = { context, count, values in
133
+ // A vector that cannot be scored leaves the result NULL, which the ranking query
134
+ // filters out rather than handing to the caller.
135
+ guard let context, count == 2, let values else { return }
136
+ guard let raw = sqlite3_user_data(context) else { return }
137
+ let metric = Unmanaged<MetricContext>.fromOpaque(raw).takeUnretainedValue().metric
138
+ guard let left = SQLiteDriver.sqliteVectorArgument(values[0]),
139
+ let right = SQLiteDriver.sqliteVectorArgument(values[1]),
140
+ left.count == right.count, !left.isEmpty else { return }
141
+ sqlite3_result_double(context, DistanceMetric.distance(metric, left, right))
142
+ }
143
+
144
+ /// Releases the metric box allocated for one registered function.
145
+ private static let vectorDestructor: @convention(c) (UnsafeMutableRawPointer?) -> Void = { pointer in
146
+ if let pointer { Unmanaged<MetricContext>.fromOpaque(pointer).release() }
147
+ }
148
+
149
+ /// Reads one bound argument: a BLOB of float32 or a legacy JSON text column.
150
+ ///
151
+ /// Static because the C callback above cannot capture `self`.
152
+ ///
153
+ /// `sqlite3_value` is an incomplete C type, so Swift imports it as
154
+ /// `OpaquePointer`.
155
+ private static func sqliteVectorArgument(_ value: OpaquePointer?) -> [Double]? {
156
+ guard let value else { return nil }
157
+ switch sqlite3_value_type(value) {
158
+ case SQLITE_BLOB:
159
+ let count = Int(sqlite3_value_bytes(value))
160
+ guard count > 0, let bytes = sqlite3_value_blob(value) else { return nil }
161
+ return SqliteVectors.decodeBlob(Data(bytes: bytes, count: count))
162
+ case SQLITE_TEXT:
163
+ guard let text = sqlite3_value_text(value) else { return nil }
164
+ return SqliteVectors.decode(String(cString: text))
165
+ default:
166
+ return nil
167
+ }
168
+ }
169
+
170
+ deinit {
171
+ sqlite3_close_v2(handle)
172
+ }
173
+
174
+ /// The file this driver opened, for logging and diagnostics.
175
+ public var databasePath: String { path }
176
+
177
+ public func query(_ sql: String, _ parameters: [SQLValue]) throws -> [Row] {
178
+ let statement = try prepare(sql, parameters)
179
+ defer { sqlite3_finalize(statement) }
180
+
181
+ var rows: [Row] = []
182
+ while true {
183
+ let status = sqlite3_step(statement)
184
+ if status == SQLITE_ROW {
185
+ rows.append(readRow(statement))
186
+ } else if status == SQLITE_DONE {
187
+ return rows
188
+ } else {
189
+ throw error(status, sql)
190
+ }
191
+ }
192
+ }
193
+
194
+ @discardableResult
195
+ public func execute(_ sql: String, _ parameters: [SQLValue]) throws -> Int {
196
+ let statement = try prepare(sql, parameters)
197
+ defer { sqlite3_finalize(statement) }
198
+
199
+ while true {
200
+ let status = sqlite3_step(statement)
201
+ if status == SQLITE_ROW || status == SQLITE_DONE {
202
+ return Int(sqlite3_changes(handle))
203
+ }
204
+ throw error(status, sql)
205
+ }
206
+ }
207
+
208
+ public func beginTransaction() throws {
209
+ try execute("BEGIN", [])
210
+ }
211
+
212
+ public func commit() throws {
213
+ try execute("COMMIT", [])
214
+ }
215
+
216
+ public func rollback() throws {
217
+ try execute("ROLLBACK", [])
218
+ }
219
+
220
+ // ─── Binding ───────────────────────────────────────────────────────────────
221
+
222
+ private func prepare(_ sql: String, _ parameters: [SQLValue]) throws -> OpaquePointer? {
223
+ var statement: OpaquePointer?
224
+ guard sqlite3_prepare_v2(handle, sql, -1, &statement, nil) == SQLITE_OK, let statement else {
225
+ throw error(sqlite3_errcode(handle), sql)
226
+ }
227
+ for (index, value) in parameters.enumerated() {
228
+ try bind(value, to: statement, at: Int32(index + 1))
229
+ }
230
+ return statement
231
+ }
232
+
233
+ private func bind(_ value: SQLValue, to statement: OpaquePointer?, at index: Int32) throws {
234
+ let status: Int32
235
+ switch value {
236
+ case .null:
237
+ status = sqlite3_bind_null(statement, index)
238
+ case .bool(let flag):
239
+ status = sqlite3_bind_int64(statement, index, flag ? 1 : 0)
240
+ case .integer(let number):
241
+ status = sqlite3_bind_int64(statement, index, number)
242
+ case .real(let number):
243
+ status = sqlite3_bind_double(statement, index, number)
244
+ case .text(let text):
245
+ status = sqlite3_bind_text(statement, index, text, -1, SQLITE_TRANSIENT)
246
+ case .blob(let data):
247
+ // A zero-length blob still has to be bound with its length: passing 0 would bind
248
+ // a NULL instead, because SQLite reads -1 as "use strlen" and 0 as "no bytes
249
+ // and no length".
250
+ status = data.isEmpty
251
+ ? sqlite3_bind_zeroblob(statement, index, 0)
252
+ : data.withUnsafeBytes { buffer in
253
+ sqlite3_bind_blob(statement, index, buffer.baseAddress, Int32(buffer.count), SQLITE_TRANSIENT)
254
+ }
255
+ case .date(let date):
256
+ status = sqlite3_bind_double(statement, index, date.timeIntervalSince1970)
257
+ case .uuid(let uuid):
258
+ status = sqlite3_bind_text(statement, index, uuid.uuidString, -1, SQLITE_TRANSIENT)
259
+ }
260
+ guard status == SQLITE_OK else {
261
+ throw error(status, "binding parameter \(index)")
262
+ }
263
+ }
264
+
265
+ private func readRow(_ statement: OpaquePointer?) -> Row {
266
+ var row: Row = [:]
267
+ let columns = sqlite3_column_count(statement)
268
+ for index in 0..<columns {
269
+ guard let rawName = sqlite3_column_name(statement, index) else { continue }
270
+ let name = String(cString: rawName)
271
+ switch sqlite3_column_type(statement, index) {
272
+ case SQLITE_INTEGER:
273
+ row[name] = sqlite3_column_int64(statement, index)
274
+ case SQLITE_FLOAT:
275
+ row[name] = sqlite3_column_double(statement, index)
276
+ case SQLITE_TEXT:
277
+ if let text = sqlite3_column_text(statement, index) {
278
+ row[name] = String(cString: text)
279
+ } else {
280
+ row[name] = nil
281
+ }
282
+ case SQLITE_BLOB:
283
+ if let bytes = sqlite3_column_blob(statement, index) {
284
+ row[name] = Foundation.Data(bytes: bytes, count: Int(sqlite3_column_bytes(statement, index)))
285
+ } else {
286
+ row[name] = nil
287
+ }
288
+ default:
289
+ // SQLITE_NULL, and anything a newer SQLite adds: absent rather than a
290
+ // fabricated zero, so "no value" never reads as "the value 0".
291
+ row[name] = nil
292
+ }
293
+ }
294
+ return row
295
+ }
296
+
297
+ private func error(_ status: Int32, _ context: String) -> An5Error {
298
+ let message = handle.map { String(cString: sqlite3_errmsg($0)) } ?? "unknown"
299
+ return .driver("\(message) — while running: \(context)")
300
+ }
301
+ }