Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Result values from a Column #9

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 36 additions & 28 deletions Sources/SQLyra/PreparedStatement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public final class PreparedStatement: DatabaseHandle {
/// Find the database handle of a prepared statement.
var db: OpaquePointer! { sqlite3_db_handle(stmt) }

private(set) lazy var columnIndexByName = [String: Int32](
uniqueKeysWithValues: (0..<columnCount).compactMap { index in
column(at: index).name.map { name in (name, index) }
}
)

init(stmt: OpaquePointer) {
self.stmt = stmt
}
Expand Down Expand Up @@ -158,49 +164,51 @@ extension PreparedStatement {
}
}

// MARK: - Columns
// MARK: - Result values from a Query

extension PreparedStatement {
/// Number of columns in a `PreparedStatement`.
/// Return the number of columns in the result set.
public var columnCount: Int32 { sqlite3_column_count(stmt) }

public func columnName(at index: Int32) -> String? {
sqlite3_column_name(stmt, index).string
public func column(at index: Int32) -> Column {
Column(index: index, statement: self)
}

var columnIndexByName: [String: Int32] {
[String: Int32](
uniqueKeysWithValues: (0..<columnCount).compactMap { index in
columnName(at: index).map { name in (name, index) }
}
)
public func column(for name: String) -> Column? {
columnIndexByName[name].map { Column(index: $0, statement: self) }
}
}

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

extension PreparedStatement {
/// Returns the name assigned to a specific column in the result set of the SELECT statement.
///
/// The name of a result column is the value of the "AS" clause for that column, if there is an AS clause.
/// 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.
public var name: String? { sqlite3_column_name(stmt, index).string }

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

public func columnInt64(at index: Int32) -> Int64 {
sqlite3_column_int64(stmt, index)
}
/// 64-bit INTEGER result.
public var int64: Int64 { sqlite3_column_int64(stmt, index) }

public func columnDouble(at index: Int32) -> Double {
sqlite3_column_double(stmt, index)
}
/// 64-bit IEEE floating point number.
public var double: Double { sqlite3_column_double(stmt, index) }

public func columnBlob(at index: Int32) -> Data? {
sqlite3_column_blob(stmt, index).map { bytes in
Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
/// UTF-8 TEXT result.
public var string: String? {
sqlite3_column_text(stmt, index).flatMap { String(cString: $0) }
}
}

public func columnNull(at index: Int32) -> Bool {
sqlite3_column_type(stmt, index) == SQLITE_NULL
/// BLOB result.
public var blob: Data? {
sqlite3_column_blob(stmt, index).map { bytes in
Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
}
}
}
}

Expand Down
25 changes: 13 additions & 12 deletions Sources/SQLyra/StatementDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,18 @@ public struct StatementDecoder {

private final class _StatementDecoder {
let statement: PreparedStatement
let columns: [String: Int32]
let userInfo: [CodingUserInfoKey: Any]
private(set) var codingPath: [any CodingKey] = []

init(statement: PreparedStatement, userInfo: [CodingUserInfoKey: Any]) {
self.statement = statement
self.userInfo = userInfo
self.columns = statement.columnIndexByName
self.codingPath.reserveCapacity(3)
}

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

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

@inline(__always)
func floating<T, K>(_ type: T.Type, forKey key: K, single: Bool = false) throws -> T
where T: BinaryFloatingPoint, K: CodingKey {
func floating<T, K>(
_ type: T.Type,
forKey key: K,
single: Bool = false
) throws -> T where T: BinaryFloatingPoint, K: CodingKey {
let index = try columnIndex(forKey: key, single: single)
let value = statement.columnDouble(at: index)
let value = statement.column(at: index).double
guard let number = type.init(exactly: value) else {
throw DecodingError.dataCorrupted(context(key, single, numberNotFit(type, value: "\(value)")))
}
Expand All @@ -64,7 +65,7 @@ private final class _StatementDecoder {
@inline(__always)
func integer<T, K>(_ type: T.Type, forKey key: K, single: Bool = false) throws -> T where T: Numeric, K: CodingKey {
let index = try columnIndex(forKey: key, single: single)
let value = statement.columnInt64(at: index)
let value = statement.column(at: index).int64
guard let number = type.init(exactly: value) else {
throw DecodingError.dataCorrupted(context(key, single, numberNotFit(type, value: "\(value)")))
}
Expand All @@ -79,7 +80,7 @@ private final class _StatementDecoder {
) throws -> T where T: Decodable, K: CodingKey {
if type == Data.self {
let index = try columnIndex(forKey: key, single: single)
guard let data = statement.columnBlob(at: index) else {
guard let data = statement.column(at: index).blob else {
throw DecodingError.valueNotFound(Data.self, context(key, single, ""))
}
// swift-format-ignore: NeverForceUnwrap
Expand All @@ -96,7 +97,7 @@ private final class _StatementDecoder {
}

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

func contains(_ key: Key) -> Bool { decoder.columns.keys.contains(key.stringValue) }
func contains(_ key: Key) -> Bool { decoder.statement.columnIndexByName.keys.contains(key.stringValue) }
func decodeNil(forKey key: Key) throws -> Bool { decoder.null(for: key) }
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try decoder.bool(forKey: key) }
func decode(_ type: String.Type, forKey key: Key) throws -> String { try decoder.string(forKey: key) }
Expand Down
8 changes: 4 additions & 4 deletions Tests/SQLyraTests/PreparedStatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ struct PreparedStatementTests {
var contracts: [Contact] = []
while try select.step() {
let contact = Contact(
id: Int(select.columnInt64(at: 0)),
name: select.columnString(at: 1) ?? "",
rating: select.columnDouble(at: 2),
image: select.columnBlob(at: 3)
id: Int(select.column(at: 0).int64),
name: select.column(at: 1).string ?? "",
rating: select.column(at: 2).double,
image: select.column(at: 3).blob
)
contracts.append(contact)
}
Expand Down