Skip to content

Commit

Permalink
Merge pull request #10 from Alexander-Ignition/decoding-row
Browse files Browse the repository at this point in the history
Create Row and RowDecoder
  • Loading branch information
Alexander-Ignition authored Jan 11, 2025
2 parents 3b0838d + bbb9518 commit 7b284da
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 296 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(decoding: 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(decoding: Contact.self)
let select = try database.prepare("SELECT * FROM contacts;")
let contacts = try select.array(Contact.self)
```
2 changes: 1 addition & 1 deletion Sources/SQLyra/DatabaseError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public struct DatabaseError: Error, Equatable, Hashable {
public let code: Int32

/// A short error description.
public var message: String?
public let message: String?

/// A complete sentence (or more) describing why the operation failed.
public let details: String?
Expand Down
163 changes: 95 additions & 68 deletions Sources/SQLyra/PreparedStatement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ 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
column(at: index).name.map { name in (name, index) }
columnName(at: index).map { name in (name, index) }
}
)

Expand All @@ -34,20 +34,6 @@ public final class PreparedStatement: DatabaseHandle {
try check(sqlite3_step(stmt), SQLITE_DONE)
}

/// The new row of data is ready for processing.
///
/// - Throws: ``DatabaseError``
public func step() throws -> Bool {
switch sqlite3_step(stmt) {
case SQLITE_DONE:
return false
case SQLITE_ROW:
return true
case let code:
throw DatabaseError(code: code, db: db)
}
}

/// Reset the prepared statement.
///
/// The ``PreparedStatement/reset()`` function is called to reset a prepared statement object back to its initial state, ready to be re-executed.
Expand All @@ -59,32 +45,6 @@ public final class PreparedStatement: DatabaseHandle {
public func reset() throws -> PreparedStatement {
try check(sqlite3_reset(stmt))
}

/// Reset all bindings on a prepared statement.
///
/// Contrary to the intuition of many, ``PreparedStatement/reset()`` does not reset the bindings on a prepared statement.
/// Use this routine to reset all host parameters to NULL.
///
/// - Throws: ``DatabaseError``
@discardableResult
public func clearBindings() throws -> PreparedStatement {
try check(sqlite3_clear_bindings(stmt))
}

// MARK: - Decodable

public func array<T>(decoding type: T.Type) throws -> [T] where T: Decodable {
var array: [T] = []
while try step() {
let value = try decode(type)
array.append(value)
}
return array
}

public func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
try StatementDecoder().decode(type, from: self)
}
}

// MARK: - Retrieving Statement SQL
Expand Down Expand Up @@ -112,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 @@ -138,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 All @@ -162,52 +123,118 @@ extension PreparedStatement {
}
return try check(code)
}

/// Reset all bindings on a prepared statement.
///
/// Contrary to the intuition of many, ``PreparedStatement/reset()`` does not reset the bindings on a prepared statement.
/// Use this routine to reset all host parameters to NULL.
///
/// - Throws: ``DatabaseError``
@discardableResult
public func clearBindings() throws -> PreparedStatement {
try check(sqlite3_clear_bindings(stmt))
}
}

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

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 column(at index: Int32) -> Column {
Column(index: index, statement: self)
/// 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
}
}

public func column(for name: String) -> Column? {
columnIndexByName[name].map { Column(index: $0, statement: self) }
// MARK: - Result values from a Query

extension PreparedStatement {
/// The new row of data is ready for processing.
///
/// - Throws: ``DatabaseError``
public func row() throws -> Row? {
switch sqlite3_step(stmt) {
case SQLITE_DONE: nil
case SQLITE_ROW: Row(statement: self)
case let code: throw DatabaseError(code: code, db: db)
}
}

/// Information about a single column of the current result row of a query.
public struct Column {
let index: Int32
public func array<T>(_ type: T.Type) throws -> [T] where T: Decodable {
try array(type, using: RowDecoder.default)
}

public func array<T>(_ type: T.Type, using decoder: RowDecoder) throws -> [T] where T: Decodable {
var array: [T] = []
while let row = try row() {
let value = try row.decode(type, using: decoder)
array.append(value)
}
return array
}

@dynamicMemberLookup
public struct Row {
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 subscript(dynamicMember name: String) -> Value? {
self[name]
}

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

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 var isNull: Bool { sqlite3_column_type(stmt, index) == SQLITE_NULL }
public func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
try decode(type, using: RowDecoder.default)
}

public func decode<T>(_ type: T.Type, using decoder: RowDecoder) throws -> T where T: Decodable {
try decoder.decode(type, from: self)
}
}

/// Result value from a query.
public struct Value {
let index: Int32
let statement: PreparedStatement
private var stmt: OpaquePointer { statement.stmt }

/// 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
Loading

0 comments on commit 7b284da

Please sign in to comment.