@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,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: ¶meters)
|
|
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
|
+
}
|