SqliteStatement.swift 109 lignes · 3420 octets
import Foundation
import SQLite3

private let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)

/// A single value bound to a `?` placeholder.
enum SqliteValue {
    case null
    case int(Int64)
    case real(Double)
    case text(String)
    case blob(Data)

    static func optText(_ value: String?) -> SqliteValue {
        value.map { .text($0) } ?? .null
    }

    static func optInt(_ value: Int64?) -> SqliteValue {
        value.map { .int($0) } ?? .null
    }

    static func bool(_ value: Bool) -> SqliteValue {
        .int(value ? 1 : 0)
    }

    /// Room semantics for `@PrimaryKey(autoGenerate = true)`: 0 means "not set",
    /// bound as NULL so SQLite assigns the next rowid.
    static func rowId(_ id: Int64) -> SqliteValue {
        id == 0 ? .null : .int(id)
    }
}

/// Column accessors for the current row of a stepped statement.
struct SqliteRow {
    let stmt: OpaquePointer

    func int64(_ index: Int32) -> Int64 { sqlite3_column_int64(stmt, index) }

    func int64OrNil(_ index: Int32) -> Int64? {
        isNull(index) ? nil : sqlite3_column_int64(stmt, index)
    }

    func int(_ index: Int32) -> Int { Int(sqlite3_column_int64(stmt, index)) }

    func bool(_ index: Int32) -> Bool { sqlite3_column_int64(stmt, index) != 0 }

    func text(_ index: Int32) -> String { textOrNil(index) ?? "" }

    func textOrNil(_ index: Int32) -> String? {
        guard let cString = sqlite3_column_text(stmt, index) else { return nil }
        return String(cString: cString)
    }

    private func isNull(_ index: Int32) -> Bool {
        sqlite3_column_type(stmt, index) == SQLITE_NULL
    }
}

/// Prepared-statement wrapper; finalizes on deinit.
final class SqliteStatement {
    private let db: OpaquePointer
    private let stmt: OpaquePointer

    init(db: OpaquePointer, sql: String) throws {
        var stmt: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK, let stmt else {
            throw DatabaseError.last(db, while: "prepare: \(sql)")
        }
        self.db = db
        self.stmt = stmt
    }

    deinit { sqlite3_finalize(stmt) }

    func bind(_ values: [SqliteValue]) throws {
        for (offset, value) in values.enumerated() {
            let index = Int32(offset + 1)
            let rc: Int32
            switch value {
            case .null:
                rc = sqlite3_bind_null(stmt, index)
            case .int(let n):
                rc = sqlite3_bind_int64(stmt, index, n)
            case .real(let d):
                rc = sqlite3_bind_double(stmt, index, d)
            case .text(let s):
                rc = sqlite3_bind_text(stmt, index, s, -1, sqliteTransient)
            case .blob(let data):
                rc = data.withUnsafeBytes { buffer in
                    sqlite3_bind_blob(stmt, index, buffer.baseAddress, Int32(data.count), sqliteTransient)
                }
            }
            guard rc == SQLITE_OK else {
                throw DatabaseError.last(db, while: "bind parameter \(index)")
            }
        }
    }

    /// Steps once; true when a result row is available, false when done.
    func step() throws -> Bool {
        switch sqlite3_step(stmt) {
        case SQLITE_ROW: return true
        case SQLITE_DONE: return false
        default: throw DatabaseError.last(db, while: "step")
        }
    }

    var row: SqliteRow { SqliteRow(stmt: stmt) }
}