Skip to content

Commit 3b0838d

Browse files
Merge pull request #9 from Alexander-Ignition/result-columns
Result values from a Column
2 parents e3f2447 + b2b3db4 commit 3b0838d

File tree

3 files changed

+53
-44
lines changed

3 files changed

+53
-44
lines changed

Sources/SQLyra/PreparedStatement.swift

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ public final class PreparedStatement: DatabaseHandle {
1111
/// Find the database handle of a prepared statement.
1212
var db: OpaquePointer! { sqlite3_db_handle(stmt) }
1313

14+
private(set) lazy var columnIndexByName = [String: Int32](
15+
uniqueKeysWithValues: (0..<columnCount).compactMap { index in
16+
column(at: index).name.map { name in (name, index) }
17+
}
18+
)
19+
1420
init(stmt: OpaquePointer) {
1521
self.stmt = stmt
1622
}
@@ -158,49 +164,51 @@ extension PreparedStatement {
158164
}
159165
}
160166

161-
// MARK: - Columns
167+
// MARK: - Result values from a Query
162168

163169
extension PreparedStatement {
164-
/// Number of columns in a `PreparedStatement`.
170+
/// Return the number of columns in the result set.
165171
public var columnCount: Int32 { sqlite3_column_count(stmt) }
166172

167-
public func columnName(at index: Int32) -> String? {
168-
sqlite3_column_name(stmt, index).string
173+
public func column(at index: Int32) -> Column {
174+
Column(index: index, statement: self)
169175
}
170176

171-
var columnIndexByName: [String: Int32] {
172-
[String: Int32](
173-
uniqueKeysWithValues: (0..<columnCount).compactMap { index in
174-
columnName(at: index).map { name in (name, index) }
175-
}
176-
)
177+
public func column(for name: String) -> Column? {
178+
columnIndexByName[name].map { Column(index: $0, statement: self) }
177179
}
178-
}
179180

180-
// MARK: - Result values from a Query
181+
/// Information about a single column of the current result row of a query.
182+
public struct Column {
183+
let index: Int32
184+
let statement: PreparedStatement
185+
private var stmt: OpaquePointer { statement.stmt }
181186

182-
extension PreparedStatement {
187+
/// Returns the name assigned to a specific column in the result set of the SELECT statement.
188+
///
189+
/// The name of a result column is the value of the "AS" clause for that column, if there is an AS clause.
190+
/// If there is no AS clause then the name of the column is unspecified and may change from one release of SQLite to the next.
191+
public var name: String? { sqlite3_column_name(stmt, index).string }
183192

184-
public func columnString(at index: Int32) -> String? {
185-
sqlite3_column_text(stmt, index).flatMap { String(cString: $0) }
186-
}
193+
public var isNull: Bool { sqlite3_column_type(stmt, index) == SQLITE_NULL }
187194

188-
public func columnInt64(at index: Int32) -> Int64 {
189-
sqlite3_column_int64(stmt, index)
190-
}
195+
/// 64-bit INTEGER result.
196+
public var int64: Int64 { sqlite3_column_int64(stmt, index) }
191197

192-
public func columnDouble(at index: Int32) -> Double {
193-
sqlite3_column_double(stmt, index)
194-
}
198+
/// 64-bit IEEE floating point number.
199+
public var double: Double { sqlite3_column_double(stmt, index) }
195200

196-
public func columnBlob(at index: Int32) -> Data? {
197-
sqlite3_column_blob(stmt, index).map { bytes in
198-
Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
201+
/// UTF-8 TEXT result.
202+
public var string: String? {
203+
sqlite3_column_text(stmt, index).flatMap { String(cString: $0) }
199204
}
200-
}
201205

202-
public func columnNull(at index: Int32) -> Bool {
203-
sqlite3_column_type(stmt, index) == SQLITE_NULL
206+
/// BLOB result.
207+
public var blob: Data? {
208+
sqlite3_column_blob(stmt, index).map { bytes in
209+
Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
210+
}
211+
}
204212
}
205213
}
206214

Sources/SQLyra/StatementDecoder.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,18 @@ public struct StatementDecoder {
2020

2121
private final class _StatementDecoder {
2222
let statement: PreparedStatement
23-
let columns: [String: Int32]
2423
let userInfo: [CodingUserInfoKey: Any]
2524
private(set) var codingPath: [any CodingKey] = []
2625

2726
init(statement: PreparedStatement, userInfo: [CodingUserInfoKey: Any]) {
2827
self.statement = statement
2928
self.userInfo = userInfo
30-
self.columns = statement.columnIndexByName
3129
self.codingPath.reserveCapacity(3)
3230
}
3331

3432
@inline(__always)
3533
func null<K>(for key: K) -> Bool where K: CodingKey {
36-
columns[key.stringValue].map { statement.columnNull(at: $0) } ?? true
34+
statement.column(for: key.stringValue)?.isNull ?? true
3735
}
3836

3937
@inline(__always)
@@ -44,17 +42,20 @@ private final class _StatementDecoder {
4442
@inline(__always)
4543
func string<K>(forKey key: K, single: Bool = false) throws -> String where K: CodingKey {
4644
let index = try columnIndex(forKey: key, single: single)
47-
guard let value = statement.columnString(at: index) else {
45+
guard let value = statement.column(at: index).string else {
4846
throw DecodingError.valueNotFound(String.self, context(key, single, ""))
4947
}
5048
return value
5149
}
5250

5351
@inline(__always)
54-
func floating<T, K>(_ type: T.Type, forKey key: K, single: Bool = false) throws -> T
55-
where T: BinaryFloatingPoint, K: CodingKey {
52+
func floating<T, K>(
53+
_ type: T.Type,
54+
forKey key: K,
55+
single: Bool = false
56+
) throws -> T where T: BinaryFloatingPoint, K: CodingKey {
5657
let index = try columnIndex(forKey: key, single: single)
57-
let value = statement.columnDouble(at: index)
58+
let value = statement.column(at: index).double
5859
guard let number = type.init(exactly: value) else {
5960
throw DecodingError.dataCorrupted(context(key, single, numberNotFit(type, value: "\(value)")))
6061
}
@@ -64,7 +65,7 @@ private final class _StatementDecoder {
6465
@inline(__always)
6566
func integer<T, K>(_ type: T.Type, forKey key: K, single: Bool = false) throws -> T where T: Numeric, K: CodingKey {
6667
let index = try columnIndex(forKey: key, single: single)
67-
let value = statement.columnInt64(at: index)
68+
let value = statement.column(at: index).int64
6869
guard let number = type.init(exactly: value) else {
6970
throw DecodingError.dataCorrupted(context(key, single, numberNotFit(type, value: "\(value)")))
7071
}
@@ -79,7 +80,7 @@ private final class _StatementDecoder {
7980
) throws -> T where T: Decodable, K: CodingKey {
8081
if type == Data.self {
8182
let index = try columnIndex(forKey: key, single: single)
82-
guard let data = statement.columnBlob(at: index) else {
83+
guard let data = statement.column(at: index).blob else {
8384
throw DecodingError.valueNotFound(Data.self, context(key, single, ""))
8485
}
8586
// swift-format-ignore: NeverForceUnwrap
@@ -96,7 +97,7 @@ private final class _StatementDecoder {
9697
}
9798

9899
private func columnIndex<K>(forKey key: K, single: Bool) throws -> Int32 where K: CodingKey {
99-
guard let index = columns[key.stringValue] else {
100+
guard let index = statement.columnIndexByName[key.stringValue] else {
100101
throw DecodingError.keyNotFound(key, context(key, single, "Column index not found for key: \(key)"))
101102
}
102103
return index
@@ -169,9 +170,9 @@ extension _StatementDecoder {
169170
struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
170171
let decoder: _StatementDecoder
171172
var codingPath: [any CodingKey] { decoder.codingPath }
172-
var allKeys: [Key] { decoder.columns.keys.compactMap { Key(stringValue: $0) } }
173+
var allKeys: [Key] { decoder.statement.columnIndexByName.keys.compactMap { Key(stringValue: $0) } }
173174

174-
func contains(_ key: Key) -> Bool { decoder.columns.keys.contains(key.stringValue) }
175+
func contains(_ key: Key) -> Bool { decoder.statement.columnIndexByName.keys.contains(key.stringValue) }
175176
func decodeNil(forKey key: Key) throws -> Bool { decoder.null(for: key) }
176177
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try decoder.bool(forKey: key) }
177178
func decode(_ type: String.Type, forKey key: Key) throws -> String { try decoder.string(forKey: key) }

Tests/SQLyraTests/PreparedStatementTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ struct PreparedStatementTests {
8080
var contracts: [Contact] = []
8181
while try select.step() {
8282
let contact = Contact(
83-
id: Int(select.columnInt64(at: 0)),
84-
name: select.columnString(at: 1) ?? "",
85-
rating: select.columnDouble(at: 2),
86-
image: select.columnBlob(at: 3)
83+
id: Int(select.column(at: 0).int64),
84+
name: select.column(at: 1).string ?? "",
85+
rating: select.column(at: 2).double,
86+
image: select.column(at: 3).blob
8787
)
8888
contracts.append(contact)
8989
}

0 commit comments

Comments
 (0)