@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,183 @@
1
+ import Foundation
2
+
3
+ /// One column of a model, as emitted by the generator into the metadata.
4
+ public struct FieldMeta: Equatable, Sendable {
5
+ public let name: String
6
+ public let type: String
7
+ public let sqlType: String?
8
+ public let isOptional: Bool
9
+ public let hasDefault: Bool
10
+ public let isId: Bool
11
+ public let description: String?
12
+
13
+ public init(
14
+ name: String,
15
+ type: String,
16
+ sqlType: String? = nil,
17
+ isOptional: Bool = false,
18
+ hasDefault: Bool = false,
19
+ isId: Bool = false,
20
+ description: String? = nil
21
+ ) {
22
+ self.name = name
23
+ self.type = type
24
+ self.sqlType = sqlType
25
+ self.isOptional = isOptional
26
+ self.hasDefault = hasDefault
27
+ self.isId = isId
28
+ self.description = description
29
+ }
30
+ }
31
+
32
+ extension FieldMeta {
33
+
34
+ /// Decodes one field entry out of the metadata the generated client passes in.
35
+ public init?(field: [String: Any?]) {
36
+ guard let name = field["name"] as? String else { return nil }
37
+ self.init(
38
+ name: name,
39
+ type: field["type"] as? String ?? "string",
40
+ sqlType: (field["sql"] as? String) ?? (field["sqlType"] as? String),
41
+ isOptional: (field["isOptional"] as? Bool) ?? (field["optional"] as? Bool) ?? false,
42
+ hasDefault: (field["hasDefault"] as? Bool) ?? (field["default"] as? Bool) ?? false,
43
+ isId: (field["isId"] as? Bool) ?? (field["id"] as? Bool) ?? false,
44
+ description: field["description"] as? String
45
+ )
46
+ }
47
+
48
+ }
49
+
50
+ /// One relation of a model.
51
+ ///
52
+ /// `relationType` is `"one"` or `"many"`. The two sides of the join are named the way the
53
+ /// foreign key is stored: `foreignKey` is the column on the related model that points back,
54
+ /// `localKey` the column on this model it points to.
55
+ public struct RelationDef: Equatable, Sendable {
56
+ public let modelName: String
57
+ public let relationType: String
58
+ public let foreignKey: String
59
+ public let localKey: String
60
+
61
+ public var isMany: Bool { relationType == "many" }
62
+
63
+ public init(modelName: String, relationType: String, foreignKey: String, localKey: String) {
64
+ self.modelName = modelName
65
+ self.relationType = relationType.isEmpty ? "many" : relationType
66
+ self.foreignKey = foreignKey
67
+ self.localKey = localKey
68
+ }
69
+ }
70
+
71
+ extension RelationDef {
72
+
73
+ public init?(relation: [String: Any?]) {
74
+ guard let modelName = (relation["modelName"] as? String)
75
+ ?? (relation["model_name"] as? String)
76
+ ?? (relation["model"] as? String) else { return nil }
77
+ self.init(
78
+ modelName: modelName,
79
+ relationType: (relation["relationType"] as? String)
80
+ ?? (relation["relation_type"] as? String)
81
+ ?? (relation["type"] as? String)
82
+ ?? "many",
83
+ foreignKey: (relation["foreignKey"] as? String) ?? (relation["foreign_key"] as? String) ?? "",
84
+ localKey: (relation["localKey"] as? String) ?? (relation["local_key"] as? String) ?? ""
85
+ )
86
+ }
87
+
88
+ }
89
+
90
+ /// The generated models' table names, columns and relations.
91
+ ///
92
+ /// A value type rather than a process-wide singleton: a Swift app may hold more than one
93
+ /// database — a local on-device SQLite and a synced server one — and a global registry would
94
+ /// make the second one's table names overwrite the first's.
95
+ public struct Metadata: Sendable {
96
+
97
+ public let modelToTable: [String: String]
98
+ public let modelFields: [String: [FieldMeta]]
99
+ public let relationMap: [String: [String: RelationDef]]
100
+
101
+ public static let empty = Metadata(modelToTable: [:], modelFields: [:], relationMap: [:])
102
+
103
+ public init(
104
+ modelToTable: [String: String],
105
+ modelFields: [String: [FieldMeta]],
106
+ relationMap: [String: [String: RelationDef]]
107
+ ) {
108
+ self.modelToTable = modelToTable
109
+ self.modelFields = modelFields
110
+ self.relationMap = relationMap
111
+ }
112
+
113
+ /// The metadata the generated client passes in, accepting either spelling of each key.
114
+ public init?(_ raw: [String: Any?]) {
115
+ let tables = (raw["model_to_table"] as? [String: Any?]) ?? (raw["modelToTable"] as? [String: Any?])
116
+ guard let tables else { return nil }
117
+
118
+ var parsedTables: [String: String] = [:]
119
+ for (model, table) in tables {
120
+ if let text = table as? String { parsedTables[model] = text }
121
+ }
122
+
123
+ var parsedFields: [String: [FieldMeta]] = [:]
124
+ if let fields = (raw["model_fields"] as? [String: Any?]) ?? (raw["modelFields"] as? [String: Any?]) {
125
+ for (model, entries) in fields {
126
+ guard let list = entries as? [Any] else { continue }
127
+ parsedFields[model] = list.compactMap { entry in
128
+ (entry as? [String: Any?]).flatMap { FieldMeta(field: $0) }
129
+ }
130
+ }
131
+ }
132
+
133
+ var parsedRelations: [String: [String: RelationDef]] = [:]
134
+ if let relations = (raw["relation_map"] as? [String: Any?])
135
+ ?? (raw["relationMap"] as? [String: Any?])
136
+ ?? (raw["RELATION_MAP"] as? [String: Any?]) {
137
+ for (model, entries) in relations {
138
+ guard let list = entries as? [String: Any?] else { continue }
139
+ var forModel: [String: RelationDef] = [:]
140
+ for (name, entry) in list {
141
+ if let definition = (entry as? [String: Any?]).flatMap({ RelationDef(relation: $0) }) {
142
+ forModel[name] = definition
143
+ }
144
+ }
145
+ parsedRelations[model] = forModel
146
+ }
147
+ }
148
+
149
+ self.init(modelToTable: parsedTables, modelFields: parsedFields, relationMap: parsedRelations)
150
+ }
151
+
152
+ /// The model's name as the metadata keys it, accepting a different capitalisation.
153
+ ///
154
+ /// The generated client registers tables under their PascalCase name while the metadata
155
+ /// keys them in camelCase. A plain lookup would find no columns at all and silently skip
156
+ /// `isId` — never generating the primary key.
157
+ public func modelKey(_ modelName: String) -> String {
158
+ if modelToTable[modelName] != nil || modelFields[modelName] != nil { return modelName }
159
+ let camel = modelName.prefix(1).lowercased() + modelName.dropFirst()
160
+ if modelToTable[camel] != nil || modelFields[camel] != nil { return camel }
161
+ let lower = modelName.lowercased()
162
+ if modelToTable[lower] != nil || modelFields[lower] != nil { return lower }
163
+ return modelName
164
+ }
165
+
166
+ /// The table a model maps to, or the model name when the schema declared none.
167
+ public func table(_ modelName: String) -> String {
168
+ modelToTable[modelKey(modelName)] ?? modelName
169
+ }
170
+
171
+ public func fields(_ modelName: String) -> [FieldMeta] {
172
+ modelFields[modelKey(modelName)] ?? modelFields[modelName] ?? []
173
+ }
174
+
175
+ /// The primary key column, or `nil` when the schema marked none.
176
+ public func idField(_ modelName: String) -> FieldMeta? {
177
+ fields(modelName).first { $0.isId }
178
+ }
179
+
180
+ public func relations(_ modelName: String) -> [String: RelationDef] {
181
+ relationMap[modelName] ?? relationMap[modelKey(modelName)] ?? [:]
182
+ }
183
+ }
@@ -0,0 +1,105 @@
1
+ import Foundation
2
+
3
+ /// A query's filters, ordering, paging, projection and eager loads.
4
+ ///
5
+ /// Every field has a default, so `Query()` means the same thing an omitted filter means in
6
+ /// the other adapters: no filter, no order, no limit.
7
+ public struct Query {
8
+
9
+ public var filter: Where?
10
+ public var orderBy: [[String: String]]
11
+ public var skip: Int
12
+ public var take: Int?
13
+ public var select: [String]?
14
+ public var include: Include?
15
+
16
+ public init(
17
+ filter: Where? = nil,
18
+ orderBy: [[String: String]] = [],
19
+ skip: Int = 0,
20
+ take: Int? = nil,
21
+ select: [String]? = nil,
22
+ include: Include? = nil
23
+ ) {
24
+ self.filter = filter
25
+ self.orderBy = orderBy
26
+ self.skip = max(0, skip)
27
+ self.take = take
28
+ self.select = select
29
+ self.include = include
30
+ }
31
+
32
+ /// The same query with a row cap, leaving the caller's query untouched.
33
+ ///
34
+ /// Mutating it instead would silently cap every later use of the value, so a
35
+ /// `findFirst` would quietly limit a caller's subsequent `findMany`.
36
+ public func limited(to take: Int?) -> Query {
37
+ var copy = self
38
+ copy.take = take
39
+ return copy
40
+ }
41
+
42
+ // ─── Filter building ────────────────────────────────────────────────────────
43
+ // Named after the operators they produce rather than Swift's own vocabulary, so a
44
+ // filter reads the same in Swift as it does in every other AN5 client.
45
+
46
+ /// `"age" >= 18`
47
+ public static func gte(_ column: String, _ value: Any?) -> Where { [column: ["gte": value]] }
48
+ /// `"age" <= 18`
49
+ public static func lte(_ column: String, _ value: Any?) -> Where { [column: ["lte": value]] }
50
+ /// `"age" > 18`
51
+ public static func gt(_ column: String, _ value: Any?) -> Where { [column: ["gt": value]] }
52
+ /// `"age" < 18`
53
+ public static func lt(_ column: String, _ value: Any?) -> Where { [column: ["lt": value]] }
54
+ /// `"name" == "Ada"`
55
+ public static func equals(_ column: String, _ value: Any?) -> Where { [column: ["equals": value]] }
56
+ /// `"name" != "Ada"`
57
+ public static func not(_ column: String, _ value: Any?) -> Where { [column: ["not": value]] }
58
+ /// `"name" contains "da"`
59
+ public static func contains(_ column: String, _ value: String) -> Where { [column: ["contains": value]] }
60
+ /// `"name" startsWith "Ad"`
61
+ public static func startsWith(_ column: String, _ value: String) -> Where { [column: ["startsWith": value]] }
62
+ /// `"name" endsWith "ce"`
63
+ public static func endsWith(_ column: String, _ value: String) -> Where { [column: ["endsWith": value]] }
64
+ /// `"id"` is `NULL`.
65
+ public static func isNull(_ column: String) -> Where { [column: NSNull()] }
66
+ /// `"id"` is not `NULL`.
67
+ public static func isNotNull(_ column: String) -> Where { [column: ["not": NSNull()]] }
68
+ /// `"id"` in `values`; an empty list matches nothing.
69
+ public static func `in`(_ column: String, _ values: [Any?]) -> Where { [column: ["in": values]] }
70
+ /// `"id"` not in `values`; an empty list matches everything.
71
+ public static func notIn(_ column: String, _ values: [Any?]) -> Where { [column: ["notIn": values]] }
72
+
73
+ /// Any of `filters`; an empty list matches nothing.
74
+ public static func or(_ filters: Where...) -> Where { ["OR": filters] }
75
+ /// All of `filters`; an empty list matches everything.
76
+ public static func and(_ filters: Where...) -> Where { ["AND": filters] }
77
+ /// None of `filters`; an empty list matches everything.
78
+ public static func not(_ filters: Where...) -> Where { ["NOT": filters] }
79
+ }
80
+
81
+ /// Which aggregates a query computes.
82
+ ///
83
+ /// One type covers both a flat aggregate and a grouped one, because the metrics are the
84
+ /// same; `by` is what makes it a group.
85
+ public struct Aggregate {
86
+ public var count = false
87
+ public var sum: [String] = []
88
+ public var average: [String] = []
89
+ public var minimum: [String] = []
90
+ public var maximum: [String] = []
91
+ public var filter: Where?
92
+ public var by: [String] = []
93
+ public var orderBy: [[String: String]] = []
94
+ public var skip = 0
95
+ public var take: Int?
96
+
97
+ public init() {}
98
+
99
+ public var isEmpty: Bool { !count && sum.isEmpty && average.isEmpty && minimum.isEmpty && maximum.isEmpty }
100
+ }
101
+
102
+ public enum SortDirection: String, Sendable {
103
+ case asc = "ASC"
104
+ case desc = "DESC"
105
+ }
@@ -0,0 +1,129 @@
1
+ import Foundation
2
+
3
+ extension Row {
4
+
5
+ // Typed reads off a row.
6
+ //
7
+ // A cast is not enough: a `BOOL` column arrives as an `Int` from one driver and a `Bool`
8
+ // from another, a `NUMERIC` that fits in an `Int` still arrives as a `Decimal`, and a
9
+ // date column arrives as a `String`, a `Double` of epoch seconds, or an `NSDate`. Each
10
+ // accessor accepts every form, so a generated model never has to know which driver is
11
+ // underneath it.
12
+ //
13
+ // A missing column and a `NULL` column are different things: a lookup yields
14
+ // `Any??`, and these flatten it so "absent" reads as `nil` rather than as a nested
15
+ // optional the caller has to unwrap by hand.
16
+
17
+ /// The column's value as text, or `nil` for a SQL `NULL`.
18
+ public func string(_ column: String) -> String? {
19
+ guard let value = self[column] ?? nil else { return nil }
20
+ if let text = value as? String { return text }
21
+ if let data = value as? Data { return String(data: data, encoding: .utf8) }
22
+ return String(describing: value)
23
+ }
24
+
25
+ public func int(_ column: String) -> Int? {
26
+ guard let value = self[column] ?? nil else { return nil }
27
+ if let number = value as? NSNumber { return number.intValue }
28
+ if let text = value as? String { return Int(text.trimmingCharacters(in: .whitespaces)) }
29
+ return nil
30
+ }
31
+
32
+ public func int64(_ column: String) -> Int64? {
33
+ guard let value = self[column] ?? nil else { return nil }
34
+ if let number = value as? NSNumber { return number.int64Value }
35
+ if let text = value as? String { return Int64(text.trimmingCharacters(in: .whitespaces)) }
36
+ return nil
37
+ }
38
+
39
+ public func double(_ column: String) -> Double? {
40
+ guard let value = self[column] ?? nil else { return nil }
41
+ if let number = value as? NSNumber { return number.doubleValue }
42
+ if let text = value as? String { return Double(text.trimmingCharacters(in: .whitespaces)) }
43
+ return nil
44
+ }
45
+
46
+ /// The column's value as an exact decimal.
47
+ ///
48
+ /// Taken from the string form where there is one: a `Double` has already lost the digits
49
+ /// that do not fit in a binary fraction, so `Decimal(double)` would preserve the loss.
50
+ public func decimal(_ column: String) -> Decimal? {
51
+ guard let value = self[column] ?? nil else { return nil }
52
+ if let decimal = value as? Decimal { return decimal }
53
+ if let text = value as? String { return Decimal(string: text, locale: Locale(identifier: "en_US_POSIX")) }
54
+ if let number = value as? NSNumber { return Decimal(string: number.stringValue) }
55
+ return nil
56
+ }
57
+
58
+ public func bool(_ column: String) -> Bool? {
59
+ guard let value = self[column] ?? nil else { return nil }
60
+ if let flag = value as? Bool { return flag }
61
+ if let number = value as? NSNumber { return number.intValue != 0 }
62
+ if let text = value as? String {
63
+ switch text.lowercased() {
64
+ case "1", "true": return true
65
+ case "0", "false": return false
66
+ default: return nil
67
+ }
68
+ }
69
+ return nil
70
+ }
71
+
72
+ /// The column's value as a date, accepting the text forms the engines produce.
73
+ ///
74
+ /// Timestamps come back with or without a `T` separator and with an optional offset, and
75
+ /// a driver with no date type hands back epoch seconds, so all three are read here.
76
+ public func date(_ column: String) -> Date? {
77
+ guard let value = self[column] ?? nil else { return nil }
78
+ if let date = value as? Date { return date }
79
+ if let number = value as? NSNumber { return Date(timeIntervalSince1970: number.doubleValue) }
80
+ guard var text = value as? String else { return nil }
81
+ text = text.trimmingCharacters(in: .whitespaces)
82
+ guard !text.isEmpty else { return nil }
83
+ text = text.replacingOccurrences(of: " ", with: "T")
84
+
85
+ let withZone = DateFormatter()
86
+ withZone.locale = Locale(identifier: "en_US_POSIX")
87
+ withZone.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
88
+ if let date = withZone.date(from: text) { return date }
89
+ if let date = withZone.date(from: text + "Z") { return date }
90
+
91
+ let plain = DateFormatter()
92
+ plain.locale = Locale(identifier: "en_US_POSIX")
93
+ plain.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
94
+ return plain.date(from: text)
95
+ }
96
+
97
+ /// The column's value as bytes, or `nil` for a text column.
98
+ public func data(_ column: String) -> Data? {
99
+ guard let value = self[column] ?? nil else { return nil }
100
+ if let data = value as? Data { return data }
101
+ if let text = value as? String { return text.data(using: .utf8) }
102
+ return nil
103
+ }
104
+
105
+ /// The column's value as a vector, or `nil` for `NULL`.
106
+ ///
107
+ /// A `VECTOR(n)` column holds float32 bytes, but a column written before that
108
+ /// encoding held JSON text, so both are accepted; so is an array a driver has
109
+ /// already decoded. Decoding is the same code the ranking query uses, which is
110
+ /// what keeps a column readable as the vector it is stored as.
111
+ public func vector(_ column: String) -> [Double]? {
112
+ SqliteVectors.decode(self[column] ?? nil)
113
+ }
114
+
115
+ /// The eager-loaded relation rows under `relation`, or an empty list.
116
+ public func related(_ relation: String) -> [Row] {
117
+ (self[relation] ?? nil) as? [Row] ?? []
118
+ }
119
+
120
+ /// The eager-loaded single relation under `relation`, or `nil`.
121
+ public func relatedOne(_ relation: String) -> Row? {
122
+ (self[relation] ?? nil) as? Row
123
+ }
124
+
125
+ /// The `_count` total for a relation, or `0`.
126
+ public func relationCount(_ relation: String) -> Int {
127
+ ((self["_count"] ?? nil) as? [String: Int])?[relation] ?? 0
128
+ }
129
+ }