Skip to content

Commit

Permalink
[PA-4228] Release 0.5.3 with better Sqlite error logging (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
bnickel authored Apr 10, 2024
1 parent 7701987 commit b7f209e
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 36 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

## [0.5.3]

### Changed

- Improved trace logging for failed Sqlite queries.

## [0.5.2]

### Added
Expand Down Expand Up @@ -165,6 +171,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for platforms targeting Swift: macOS, watchOS, iOS, iPadOS, tvOS.

[Unreleased]: https://github.com/heap/heap-swift-core-sdk/compare/0.5.2...main
[0.5.3]: https://github.com/heap/heap-swift-core-sdk/compare/0.5.2...0.5.3
[0.5.2]: https://github.com/heap/heap-swift-core-sdk/compare/0.5.1...0.5.2
[0.5.1]: https://github.com/heap/heap-swift-core-sdk/compare/0.5.0...0.5.1
[0.5.0]: https://github.com/heap/heap-swift-core-sdk/compare/0.4.0...0.5.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import SQLite3
import Foundation

struct SqliteError: Error, Equatable, Hashable {
struct SqliteError: Error, Equatable, Hashable, CustomStringConvertible {
let code: Int32
let message: String
let file: String
let line: UInt

var description: String {
"Sqlite command at \(file):\(line) failed with error \(code) and the following message: \(message)"
}
}

fileprivate extension Int32 {

/// Throws an error if a value is not a successful Sqlite return code.
func assertSuccess() throws {
func assertSuccess(message: @autoclosure () -> String, file: String, line: UInt) throws {
switch self {
case SQLITE_OK, SQLITE_ROW, SQLITE_DONE:
return
default:
throw SqliteError(code: self)
throw SqliteError(code: self, message: message(), file: file, line: line)
}
}

func assertSuccess(message: @autoclosure () -> String, in statement: SqliteConnection.Statement) throws {
try assertSuccess(message: "\(message()) in \(statement.query)", file: statement.file, line: statement.line)
}
}

/// A lightweight wrapper around Sqlite that bridges common types.
Expand All @@ -30,9 +41,9 @@ final class SqliteConnection {
databaseUrl = url
}

func connect() throws {
func connect(file: String = #fileID, line: UInt = #line) throws {
guard ppDb == nil else { return }
try sqlite3_open(databaseUrl.path, &ppDb).assertSuccess()
try sqlite3_open(databaseUrl.path, &ppDb).assertSuccess(message: "Failed to open database", file: file, line: line)
}

/// Creates
Expand All @@ -41,12 +52,12 @@ final class SqliteConnection {
/// - parameters: A list of indexed parameters to apply, of known convertable types.
/// - rowCallback: A callback to execute after each row is read (if any). This is used for extracting row data.
/// - Throws: A `SqliteError` if an error is encountered on any step of the process.
func perform(query: String, parameters: [Sqlite3Parameter] = [], rowCallback: (_ row: Row) throws -> Void = { _ in }) throws {
func perform(query: String, parameters: [Sqlite3Parameter] = [], file: String = #fileID, line: UInt = #line, rowCallback: (_ row: Row) throws -> Void = { _ in }) throws {
guard let ppDb = ppDb else {
throw SqliteError(code: SQLITE_ERROR)
throw SqliteError(code: SQLITE_ERROR, message: "Database pointer is nil", file: file, line: line)
}

let statement = try Statement(query: query, db: ppDb)
let statement = try Statement(query: query, db: ppDb, file: file, line: line)
try statement.bindIndexedParameters(parameters)
defer { statement.finalize() }
try statement.stepUntilDone(rowCallback)
Expand All @@ -64,34 +75,40 @@ final class SqliteConnection {

/// A wrapper around a prepared query.
final class Statement {
private var pointer: OpaquePointer?
private(set) var pointer: OpaquePointer?
let query: String
let file: String
let line: UInt

fileprivate init(pointer: OpaquePointer?) {
fileprivate init(pointer: OpaquePointer?, query: String, file: String = #fileID, line: UInt = #line) {
self.pointer = pointer
self.query = query
self.file = file
self.line = line
}

fileprivate convenience init(query: String, db: OpaquePointer) throws {
fileprivate convenience init(query: String, db: OpaquePointer, file: String = #fileID, line: UInt = #line) throws {
var pointer: OpaquePointer?
try sqlite3_prepare_v2(db, query, -1, &pointer, nil).assertSuccess()
self.init(pointer: pointer)
try sqlite3_prepare_v2(db, query, -1, &pointer, nil).assertSuccess(message: "Failed to prepare query: \(query)", file: file, line: line)
self.init(pointer: pointer, query: query)
}

/// Binds parameters to the query.
func bindIndexedParameters(_ parameters: [Sqlite3Parameter]) throws {
for (parameter, index) in zip(parameters, 1...) {
try parameter.bind(at: index, statementPointer: pointer)
try parameter.bind(at: index, statement: self)
}
}

/// Performs a step of the query.
/// - Returns: True if the execution returned a row.
private func step() throws -> Bool {
guard let pointer = pointer else {
throw SqliteError(code: SQLITE_ERROR)
throw SqliteError(code: SQLITE_ERROR, message: "Statement pointer is nil for query: \(query)", file: file, line: line)
}

let result = sqlite3_step(pointer)
try result.assertSuccess()
try result.assertSuccess(message: "Step failed for query: \(query)", file: file, line: line)
if result == SQLITE_DONE {
finalize()
}
Expand Down Expand Up @@ -168,48 +185,48 @@ private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.sel

/// A protocol for binding Swift data types to Sqlite parameters.
protocol Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws
func bind(at index: Int, statement: SqliteConnection.Statement) throws
}

extension Int: Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws {
try sqlite3_bind_int64(statementPointer, Int32(index), Int64(self)).assertSuccess()
func bind(at index: Int, statement: SqliteConnection.Statement) throws {
try sqlite3_bind_int64(statement.pointer, Int32(index), Int64(self)).assertSuccess(message: "Failed to bind integer \"\(self)\" at index \(index)", in: statement)
}
}

extension Bool: Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws {
try (self ? 1 : 0).bind(at: index, statementPointer: statementPointer)
func bind(at index: Int, statement: SqliteConnection.Statement) throws {
try (self ? 1 : 0).bind(at: index, statement: statement)
}
}

extension String: Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws {
try sqlite3_bind_text(statementPointer, Int32(index), self, -1, SQLITE_TRANSIENT).assertSuccess()
func bind(at index: Int, statement: SqliteConnection.Statement) throws {
try sqlite3_bind_text(statement.pointer, Int32(index), self, -1, SQLITE_TRANSIENT).assertSuccess(message: "Failed to bind text \"\(self)\" at index \(index)", in: statement)
}
}

extension Data: Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws {
func bind(at index: Int, statement: SqliteConnection.Statement) throws {
try self.withUnsafeBytes { (pointer: UnsafeRawBufferPointer) in
try sqlite3_bind_blob(statementPointer, Int32(index), pointer.baseAddress, Int32(self.count), SQLITE_TRANSIENT).assertSuccess()
try sqlite3_bind_blob(statement.pointer, Int32(index), pointer.baseAddress, Int32(self.count), SQLITE_TRANSIENT).assertSuccess(message: "Failed to bind data of length \(count) at index \(index)", in: statement)
}
}
}

/// This rounds to the nearest second, which is close enough for us.
extension Date: Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws {
try Int(timeIntervalSinceReferenceDate).bind(at: index, statementPointer: statementPointer)
func bind(at index: Int, statement: SqliteConnection.Statement) throws {
try Int(timeIntervalSinceReferenceDate).bind(at: index, statement: statement)
}
}

extension Optional: Sqlite3Parameter where Wrapped: Sqlite3Parameter {
func bind(at index: Int, statementPointer: OpaquePointer?) throws {
func bind(at index: Int, statement: SqliteConnection.Statement) throws {
if let value = self {
try value.bind(at: index, statementPointer: statementPointer)
try value.bind(at: index, statement: statement)
} else {
try sqlite3_bind_null(statementPointer, Int32(index)).assertSuccess()
try sqlite3_bind_null(statement.pointer, Int32(index)).assertSuccess(message: "Failed to bind null at index \(index)", in: statement)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ extension OperationQueue {
}

extension Operation {
static func forSqlite(connection: SqliteConnection, block: @escaping (_ connection: SqliteConnection) throws -> Void) -> Operation {
static func forSqlite(connection: SqliteConnection, file: String = #fileID, line: UInt = #line, block: @escaping (_ connection: SqliteConnection) throws -> Void) -> Operation {
BlockOperation {
do {
try block(connection)
} catch {
HeapLogger.shared.trace("Error occurred executing query: \(error)")
HeapLogger.shared.trace("Error occurred executing query: \(error)", file: file, line: line)
}
}
}
Expand All @@ -30,9 +30,9 @@ class SqliteDataStore: DataStoreProtocol {
private let connection: SqliteConnection
internal let dataStoreSettings: DataStoreSettings

func performOnSqliteQueue(waitUntilFinished: Bool = false, block: @escaping (_ connection: SqliteConnection) throws -> Void) {
func performOnSqliteQueue(waitUntilFinished: Bool = false, file: String = #fileID, line: UInt = #line, block: @escaping (_ connection: SqliteConnection) throws -> Void) {
OperationQueue.sqliteDataStoreQueue.addOperations([
.forSqlite(connection: connection, block: block),
.forSqlite(connection: connection, file: file, line: line, block: block),
], waitUntilFinished: waitUntilFinished)
}

Expand Down
2 changes: 1 addition & 1 deletion Development/Sources/HeapSwiftCore/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct Version {
static let minor = 5

/// Revision number.
static let revision = 2
static let revision = 3

/// Optional pre-release version
static let prerelease: String? = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ Create Table TestTable (

}

it("throws with meaningful data") {
let query = "Insert Into TestTable (FakeColumn) Values (1);"
do {
try connection.perform(query: query)
XCTFail("The above code should have failed")
} catch let error as SqliteError {
expect(error.code).to(equal(1))
expect(error.message).to(contain(query))
expect("\(error)").to(contain("HeapSwiftCoreTests/SqliteConnectionSpec.swift:71"))
expect("\(error)").to(contain("error 1"))
expect("\(error)").to(contain(query))
}
}

it("can edit and query tables") {

// Fun fact! Sqlite only executes the first statement in a query.
Expand Down
2 changes: 1 addition & 1 deletion HeapSwiftCore.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'HeapSwiftCore'
s.version = '0.5.2'
s.version = '0.5.3'
s.license = { :type => 'MIT' }
s.summary = 'The core Heap library used for apps on Apple platforms.'
s.homepage = 'https://heap.io'
Expand Down

0 comments on commit b7f209e

Please sign in to comment.