Skip to content

Commit

Permalink
RowDecoderTests
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander-Ignition committed Jan 11, 2025
1 parent 5748b01 commit 4b0a064
Show file tree
Hide file tree
Showing 8 changed files with 456 additions and 100 deletions.
15 changes: 8 additions & 7 deletions Playgrounds/README.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

[Documentation](https://alexander-ignition.github.io/SQLyra/documentation/sqlyra/)

- Note: this readme file is available as Xcode playground in Playgrounds/README.playground
> this readme file is available as Xcode playground in Playgrounds/README.playground

## Open

Expand All @@ -26,21 +26,21 @@ let database = try Database.open(

Create table for contacts with fields `id` and `name`.
*/
try database.execute(
"""
let sql = """
CREATE TABLE contacts(
id INT PRIMARY KEY NOT NULL,
name TEXT
);
"""
)
try database.execute(sql)
/*:
## Insert

Insert new contacts Paul and John.
*/
try database.execute("INSERT INTO contacts (id, name) VALUES (1, 'Paul');")
try database.execute("INSERT INTO contacts (id, name) VALUES (2, 'John');")
let insert = try database.prepare("INSERT INTO contacts (id, name) VALUES (?, ?);")
try insert.bind(parameters: 1, "Paul").execute().reset()
try insert.bind(parameters: 2, "John").execute()
/*:
## Select

Expand All @@ -51,4 +51,5 @@ struct Contact: Codable {
let name: String
}

let contacts = try database.prepare("SELECT * FROM contacts;").array(Contact.self)
let select = try database.prepare("SELECT * FROM contacts;")
let contacts = try select.array(Contact.self)
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Swift SQLite wrapper.

[Documentation](https://alexander-ignition.github.io/SQLyra/documentation/sqlyra/)

- Note: this readme file is available as Xcode playground in Playgrounds/README.playground
> this readme file is available as Xcode playground in Playgrounds/README.playground
## Open

Expand All @@ -25,21 +25,21 @@ let database = try Database.open(

Create table for contacts with fields `id` and `name`.
```swift
try database.execute(
"""
let sql = """
CREATE TABLE contacts(
id INT PRIMARY KEY NOT NULL,
name TEXT
);
"""
)
try database.execute(sql)
```
## Insert

Insert new contacts Paul and John.
```swift
try database.execute("INSERT INTO contacts (id, name) VALUES (1, 'Paul');")
try database.execute("INSERT INTO contacts (id, name) VALUES (2, 'John');")
let insert = try database.prepare("INSERT INTO contacts (id, name) VALUES (?, ?);")
try insert.bind(parameters: 1, "Paul").execute().reset()
try insert.bind(parameters: 2, "John").execute()
```
## Select

Expand All @@ -50,5 +50,6 @@ struct Contact: Codable {
let name: String
}

let contacts = try database.prepare("SELECT * FROM contacts;").array(Contact.self)
let select = try database.prepare("SELECT * FROM contacts;")
let contacts = try select.array(Contact.self)
```
74 changes: 39 additions & 35 deletions Sources/SQLyra/PreparedStatement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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](
private(set) lazy var columnIndexByName = [String: Int](
uniqueKeysWithValues: (0..<columnCount).compactMap { index in
columnName(at: index).map { name in (name, index) }
}
Expand Down Expand Up @@ -72,16 +72,16 @@ extension PreparedStatement {

extension PreparedStatement {
/// Number of SQL parameters.
public var parameterCount: Int32 { sqlite3_bind_parameter_count(stmt) }
public var parameterCount: Int { Int(sqlite3_bind_parameter_count(stmt)) }

/// Name of a SQL parameter.
public func parameterName(at index: Int32) -> String? {
sqlite3_bind_parameter_name(stmt, index).map { String(cString: $0) }
public func parameterName(at index: Int) -> String? {
sqlite3_bind_parameter_name(stmt, Int32(index)).map { String(cString: $0) }
}

/// Index of a parameter with a given name.
public func parameterIndex(for name: String) -> Int32 {
sqlite3_bind_parameter_index(stmt, name)
public func parameterIndex(for name: String) -> Int {
Int(sqlite3_bind_parameter_index(stmt, name))
}
}

Expand All @@ -98,13 +98,14 @@ extension PreparedStatement {
@discardableResult
public func bind(parameters: SQLParameter...) throws -> PreparedStatement {
for (index, parameter) in parameters.enumerated() {
try bind(index: Int32(index + 1), parameter: parameter)
try bind(index: index + 1, parameter: parameter)
}
return self
}

@discardableResult
public func bind(index: Int32, parameter: SQLParameter) throws -> PreparedStatement {
public func bind(index: Int, parameter: SQLParameter) throws -> PreparedStatement {
let index = Int32(index)
let code =
switch parameter {
case .null:
Expand Down Expand Up @@ -139,10 +140,14 @@ extension PreparedStatement {

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

public func columnName(at index: Int32) -> String? {
sqlite3_column_name(stmt, index).string
/// 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 func columnName(at index: Int) -> String? {
sqlite3_column_name(stmt, Int32(index)).string
}
}

Expand All @@ -154,12 +159,9 @@ extension PreparedStatement {
/// - Throws: ``DatabaseError``
public func row() throws -> Row? {
switch sqlite3_step(stmt) {
case SQLITE_DONE:
return nil
case SQLITE_ROW:
return Row(statement: self)
case let code:
throw DatabaseError(code: code, db: db)
case SQLITE_DONE: nil
case SQLITE_ROW: Row(statement: self)
case let code: throw DatabaseError(code: code, db: db)
}
}

Expand All @@ -180,16 +182,19 @@ extension PreparedStatement {
public struct Row {
let statement: PreparedStatement

public subscript(dynamicMember name: String) -> Column! {
public subscript(dynamicMember name: String) -> Value? {
self[name]
}

public subscript(name: String) -> Column? {
statement.columnIndexByName[name].map { self[$0] }
public subscript(name: String) -> Value? {
statement.columnIndexByName[name].flatMap { self[$0] }
}

public subscript(index: Int32) -> Column {
Column(index: index, statement: statement)
public subscript(index: Int) -> Value? {
if sqlite3_column_type(statement.stmt, Int32(index)) == SQLITE_NULL {
return nil
}
return Value(index: Int32(index), statement: statement)
}

public func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
Expand All @@ -201,36 +206,35 @@ extension PreparedStatement {
}
}

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

/// 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 var isNull: Bool { sqlite3_column_type(stmt, index) == SQLITE_NULL }

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

/// 32-bit INTEGER result.
public var int32: Int32 { sqlite3_column_int(stmt, index) }

/// A platform-specific integer.
public var int: Int { Int(int64) }

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

/// Size of a BLOB or a UTF-8 TEXT result in bytes.
public var count: Int { Int(sqlite3_column_bytes(stmt, index)) }

/// UTF-8 TEXT result.
public var string: String? {
sqlite3_column_text(stmt, index).flatMap { String(cString: $0) }
}

/// BLOB result.
public var blob: Data? {
sqlite3_column_blob(stmt, index).map { bytes in
Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
}
sqlite3_column_blob(stmt, index).map { Data(bytes: $0, count: count) }
}
}
}
Expand Down
81 changes: 35 additions & 46 deletions Sources/SQLyra/RowDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@ private struct _RowDecoder: Decoder {
}

func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "")
throw DecodingError.typeMismatch(PreparedStatement.self, context)
throw DecodingError.typeMismatch(PreparedStatement.self, .context(codingPath, ""))
}

func singleValueContainer() throws -> any SingleValueDecodingContainer {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "")
throw DecodingError.typeMismatch(PreparedStatement.self, context)
throw DecodingError.typeMismatch(PreparedStatement.self, .context(codingPath, ""))
}

// MARK: - KeyedDecodingContainer
Expand All @@ -63,7 +61,7 @@ private struct _RowDecoder: Decoder {
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { try decoder.integer(type, forKey: key) }
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { try decoder.integer(type, forKey: key) }
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable {
return try decoder.decode(type, forKey: key)
try decoder.decode(type, forKey: key)
}

func superDecoder() throws -> any Decoder { fatalError() }
Expand All @@ -81,7 +79,7 @@ private struct _RowDecoder: Decoder {

@inline(__always)
func null<K>(for key: K) -> Bool where K: CodingKey {
row[key.stringValue]?.isNull ?? true
row[key.stringValue] == nil
}

@inline(__always)
Expand All @@ -90,68 +88,55 @@ private struct _RowDecoder: Decoder {
}

@inline(__always)
func integer<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: Numeric, K: CodingKey {
let value = try column(forKey: key)
guard let number = type.init(exactly: value.int64) else {
let message = "Parsed SQL integer <\(value)> does not fit in \(type)."
let context = DecodingError.Context(codingPath: [key], debugDescription: message)
throw DecodingError.dataCorrupted(context)
}
return number
func string<K>(forKey key: K) throws -> String where K: CodingKey {
try columnValue(String.self, forKey: key).string ?? ""
}

@inline(__always)
func floating<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: BinaryFloatingPoint, K: CodingKey {
let value = try column(forKey: key)
guard let number = type.init(exactly: value.double) else {
let message = "Parsed SQL double <\(value)> does not fit in \(type)."
let context = DecodingError.Context(codingPath: [key], debugDescription: message)
throw DecodingError.dataCorrupted(context)
func integer<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: Numeric, K: CodingKey {
let value = try columnValue(type, forKey: key)
let int64 = value.int64
guard let number = type.init(exactly: int64) else {
throw DecodingError.dataCorrupted(.context([key], "Parsed SQL int64 <\(int64)> does not fit in \(type)."))
}
return number
}

@inline(__always)
func string<K>(forKey key: K) throws -> String where K: CodingKey {
let value = try column(forKey: key)
guard let value = value.string else {
throw DecodingError.valueNotFound(String.self, .codingPath([key]))
func floating<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: BinaryFloatingPoint, K: CodingKey {
let value = try columnValue(type, forKey: key)
let double = value.double
guard let number = type.init(exactly: double) else {
throw DecodingError.dataCorrupted(.context([key], "Parsed SQL double <\(double)> does not fit in \(type)."))
}
return value
return number
}

@inline(__always)
func decode<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: Decodable, K: CodingKey {
if type == Data.self {
let value = try column(forKey: key)
guard let data = value.blob else {
throw DecodingError.valueNotFound(Data.self, .codingPath([key]))
}
let value = try columnValue(type, forKey: key)
let data = value.blob ?? Data()
// swift-format-ignore: NeverForceUnwrap
return data as! T
}
let decoder = _ColumnDecoder(key: key, decoder: self)
let decoder = _ValueDecoder(key: key, decoder: self)
return try type.init(from: decoder)
}

@inline(__always)
private func column<K>(forKey key: K) throws -> PreparedStatement.Column where K: CodingKey {
private func columnValue<T, K>(_ type: T.Type, forKey key: K) throws -> PreparedStatement.Value where K: CodingKey {
guard let index = row.statement.columnIndexByName[key.stringValue] else {
let message = "Column index not found for key: \(key)"
let context = DecodingError.Context(codingPath: [key], debugDescription: message)
throw DecodingError.keyNotFound(key, context)
throw DecodingError.keyNotFound(key, .context([key], "Column index not found for key: \(key)"))
}
return row[index]
}
}

private extension DecodingError.Context {
static func codingPath(_ path: [any CodingKey]) -> DecodingError.Context {
DecodingError.Context(codingPath: path, debugDescription: "")
guard let column = row[index] else {
throw DecodingError.valueNotFound(type, .context([key], "Column value not found for key: \(key)"))
}
return column
}
}

private struct _ColumnDecoder: Decoder, SingleValueDecodingContainer {
private struct _ValueDecoder: Decoder, SingleValueDecodingContainer {
let key: any CodingKey
let decoder: _RowDecoder

Expand All @@ -161,13 +146,11 @@ private struct _ColumnDecoder: Decoder, SingleValueDecodingContainer {
var codingPath: [any CodingKey] { [key] }

func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "")
throw DecodingError.typeMismatch(PreparedStatement.Column.self, context)
throw DecodingError.typeMismatch(PreparedStatement.Value.self, .context(codingPath, ""))
}

func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "")
throw DecodingError.typeMismatch(PreparedStatement.Column.self, context)
throw DecodingError.typeMismatch(PreparedStatement.Value.self, .context(codingPath, ""))
}

func singleValueContainer() throws -> any SingleValueDecodingContainer {
Expand All @@ -193,3 +176,9 @@ private struct _ColumnDecoder: Decoder, SingleValueDecodingContainer {
func decode(_ type: UInt64.Type) throws -> UInt64 { try decoder.integer(type, forKey: key) }
func decode<T>(_ type: T.Type) throws -> T where T: Decodable { try decoder.decode(type, forKey: key) }
}

private extension DecodingError.Context {
static func context(_ codingPath: [any CodingKey], _ message: String) -> DecodingError.Context {
DecodingError.Context(codingPath: codingPath, debugDescription: message)
}
}
Loading

0 comments on commit 4b0a064

Please sign in to comment.