Refactor entire codebase and rewrite documentation
This commit is contained in:
@@ -1,29 +1,79 @@
|
||||
import Foundation
|
||||
import DataLiteC
|
||||
|
||||
public final class Connection: ConnectionProtocol {
|
||||
/// A class representing a connection to an SQLite database.
|
||||
///
|
||||
/// The `Connection` class manages the connection to an SQLite database. It provides an interface
|
||||
/// for preparing SQL queries, managing transactions, and handling errors. This class serves as the
|
||||
/// main object for interacting with the database.
|
||||
public final class Connection {
|
||||
// MARK: - Private Properties
|
||||
|
||||
private let connection: OpaquePointer
|
||||
fileprivate var delegates = [DelegateBox]()
|
||||
|
||||
// MARK: - Connection State
|
||||
|
||||
public var isAutocommit: Bool {
|
||||
sqlite3_get_autocommit(connection) != 0
|
||||
fileprivate var delegates = [DelegateBox]() {
|
||||
didSet {
|
||||
switch (oldValue.isEmpty, delegates.isEmpty) {
|
||||
case (true, false):
|
||||
let ctx = Unmanaged.passUnretained(self).toOpaque()
|
||||
sqlite3_update_hook(connection, updateHookCallback(_:_:_:_:_:), ctx)
|
||||
sqlite3_commit_hook(connection, commitHookCallback(_:), ctx)
|
||||
sqlite3_rollback_hook(connection, rollbackHookCallback(_:), ctx)
|
||||
case (false, true):
|
||||
sqlite3_update_hook(connection, nil, nil)
|
||||
sqlite3_commit_hook(connection, nil, nil)
|
||||
sqlite3_rollback_hook(connection, nil, nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isReadonly: Bool {
|
||||
sqlite3_db_readonly(connection, "main") == 1
|
||||
}
|
||||
|
||||
public var busyTimeout: Int32 {
|
||||
get { try! get(pragma: .busyTimeout) ?? 0 }
|
||||
set { try! set(pragma: .busyTimeout, value: newValue) }
|
||||
fileprivate var traceDelegates = [TraceDelegateBox]() {
|
||||
didSet {
|
||||
switch (oldValue.isEmpty, traceDelegates.isEmpty) {
|
||||
case (true, false):
|
||||
let ctx = Unmanaged.passUnretained(self).toOpaque()
|
||||
sqlite3_trace_stmt(connection, traceCallback(_:_:_:_:), ctx)
|
||||
case (false, true):
|
||||
sqlite3_trace_stmt(connection, nil, nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
/// Initializes a new connection to an SQLite database.
|
||||
///
|
||||
/// Opens a connection to the database at the specified `location` using the given `options`.
|
||||
/// If the location represents a file path, this method ensures that the parent directory
|
||||
/// exists, creating intermediate directories if needed.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// let connection = try Connection(
|
||||
/// location: .file(path: "/path/to/sqlite.db"),
|
||||
/// options: .readwrite
|
||||
/// )
|
||||
/// // Use the connection to execute SQL statements
|
||||
/// } catch {
|
||||
/// print("Failed to open database: \\(error)")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - location: The location of the database. Can represent a file path, an in-memory
|
||||
/// database, or a temporary database.
|
||||
/// - options: Connection options that define behavior such as read-only mode, creation
|
||||
/// flags, and cache type.
|
||||
///
|
||||
/// - Throws: ``SQLiteError`` if the connection cannot be opened or initialized due to
|
||||
/// SQLite-related issues such as invalid path, missing permissions, or corruption.
|
||||
/// - Throws: An error if directory creation fails for file-based database locations.
|
||||
public init(location: Location, options: Options) throws {
|
||||
if case let Location.file(path) = location, !path.isEmpty {
|
||||
try FileManager.default.createDirectory(
|
||||
@@ -35,21 +85,43 @@ public final class Connection: ConnectionProtocol {
|
||||
var connection: OpaquePointer! = nil
|
||||
let status = sqlite3_open_v2(location.path, &connection, options.rawValue, nil)
|
||||
|
||||
if status == SQLITE_OK, let connection = connection {
|
||||
self.connection = connection
|
||||
|
||||
let ctx = Unmanaged.passUnretained(self).toOpaque()
|
||||
sqlite3_trace_v2(connection, UInt32(SQLITE_TRACE_STMT), traceCallback(_:_:_:_:), ctx)
|
||||
sqlite3_update_hook(connection, updateHookCallback(_:_:_:_:_:), ctx)
|
||||
sqlite3_commit_hook(connection, commitHookCallback(_:), ctx)
|
||||
sqlite3_rollback_hook(connection, rollbackHookCallback(_:), ctx)
|
||||
} else {
|
||||
let error = Error(connection)
|
||||
guard status == SQLITE_OK, let connection else {
|
||||
let error = SQLiteError(connection)
|
||||
sqlite3_close_v2(connection)
|
||||
throw error
|
||||
}
|
||||
|
||||
self.connection = connection
|
||||
}
|
||||
|
||||
/// Initializes a new connection to an SQLite database using a file path.
|
||||
///
|
||||
/// Opens a connection to the SQLite database located at the specified `path` using the provided
|
||||
/// `options`. Internally, this method calls the designated initializer to perform the actual
|
||||
/// setup and validation.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// let connection = try Connection(
|
||||
/// path: "/path/to/sqlite.db",
|
||||
/// options: .readwrite
|
||||
/// )
|
||||
/// // Use the connection to execute SQL statements
|
||||
/// } catch {
|
||||
/// print("Failed to open database: \\(error)")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The file system path to the SQLite database file. Can be absolute or relative.
|
||||
/// - options: Options that control how the database is opened, such as access mode and
|
||||
/// cache type.
|
||||
///
|
||||
/// - Throws: ``SQLiteError`` if the connection cannot be opened due to SQLite-level errors,
|
||||
/// invalid path, missing permissions, or corruption.
|
||||
/// - Throws: An error if the required directory structure cannot be created.
|
||||
public convenience init(path: String, options: Options) throws {
|
||||
try self.init(location: .file(path: path), options: options)
|
||||
}
|
||||
@@ -57,73 +129,116 @@ public final class Connection: ConnectionProtocol {
|
||||
deinit {
|
||||
sqlite3_close_v2(connection)
|
||||
}
|
||||
|
||||
// MARK: - Delegation
|
||||
|
||||
public func addDelegate(_ delegate: ConnectionDelegate) {
|
||||
delegates.removeAll { $0.delegate == nil }
|
||||
delegates.append(.init(delegate: delegate))
|
||||
}
|
||||
|
||||
// MARK: - ConnectionProtocol
|
||||
|
||||
extension Connection: ConnectionProtocol {
|
||||
public var isAutocommit: Bool {
|
||||
sqlite3_get_autocommit(connection) != 0
|
||||
}
|
||||
|
||||
public func removeDelegate(_ delegate: ConnectionDelegate) {
|
||||
delegates.removeAll { $0.delegate == nil || $0.delegate === delegate }
|
||||
public var isReadonly: Bool {
|
||||
sqlite3_db_readonly(connection, "main") == 1
|
||||
}
|
||||
|
||||
// MARK: - Custom SQL Functions
|
||||
|
||||
public func add(function: Function.Type) throws(Error) {
|
||||
try function.install(db: connection)
|
||||
}
|
||||
|
||||
public func remove(function: Function.Type) throws(Error) {
|
||||
try function.uninstall(db: connection)
|
||||
}
|
||||
|
||||
// MARK: - Statement Preparation
|
||||
|
||||
public func prepare(sql query: String, options: Statement.Options = []) throws(Error) -> Statement {
|
||||
try Statement(db: connection, sql: query, options: options)
|
||||
}
|
||||
|
||||
// MARK: - Script Execution
|
||||
|
||||
public func execute(raw sql: String) throws(Error) {
|
||||
let status = sqlite3_exec(connection, sql, nil, nil, nil)
|
||||
if status != SQLITE_OK {
|
||||
throw Error(connection)
|
||||
public static func initialize() throws(SQLiteError) {
|
||||
let status = sqlite3_initialize()
|
||||
guard status == SQLITE_OK else {
|
||||
throw SQLiteError(code: status, message: "")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encryption Keys
|
||||
public static func shutdown() throws(SQLiteError) {
|
||||
let status = sqlite3_shutdown()
|
||||
guard status == SQLITE_OK else {
|
||||
throw SQLiteError(code: status, message: "")
|
||||
}
|
||||
}
|
||||
|
||||
public func apply(_ key: Key, name: String? = nil) throws(Error) {
|
||||
public func apply(_ key: Key, name: String?) throws(SQLiteError) {
|
||||
let status = if let name {
|
||||
sqlite3_key_v2(connection, name, key.keyValue, key.length)
|
||||
} else {
|
||||
sqlite3_key(connection, key.keyValue, key.length)
|
||||
}
|
||||
if status != SQLITE_OK {
|
||||
throw Error(connection)
|
||||
guard status == SQLITE_OK else {
|
||||
throw SQLiteError(connection)
|
||||
}
|
||||
}
|
||||
|
||||
public func rekey(_ key: Key, name: String? = nil) throws(Error) {
|
||||
public func rekey(_ key: Key, name: String?) throws(SQLiteError) {
|
||||
let status = if let name {
|
||||
sqlite3_rekey_v2(connection, name, key.keyValue, key.length)
|
||||
} else {
|
||||
sqlite3_rekey(connection, key.keyValue, key.length)
|
||||
}
|
||||
if status != SQLITE_OK {
|
||||
throw Error(connection)
|
||||
guard status == SQLITE_OK else {
|
||||
throw SQLiteError(connection)
|
||||
}
|
||||
}
|
||||
|
||||
public func add(delegate: any ConnectionDelegate) {
|
||||
delegates.append(.init(delegate: delegate))
|
||||
delegates.removeAll { $0.delegate == nil }
|
||||
}
|
||||
|
||||
public func remove(delegate: any ConnectionDelegate) {
|
||||
delegates.removeAll {
|
||||
$0.delegate === delegate || $0.delegate == nil
|
||||
}
|
||||
}
|
||||
|
||||
public func add(trace delegate: any ConnectionTraceDelegate) {
|
||||
traceDelegates.append(.init(delegate: delegate))
|
||||
traceDelegates.removeAll { $0.delegate == nil }
|
||||
}
|
||||
|
||||
public func remove(trace delegate: any ConnectionTraceDelegate) {
|
||||
traceDelegates.removeAll {
|
||||
$0.delegate === delegate || $0.delegate == nil
|
||||
}
|
||||
}
|
||||
|
||||
public func add(function: Function.Type) throws(SQLiteError) {
|
||||
try function.install(db: connection)
|
||||
}
|
||||
|
||||
public func remove(function: Function.Type) throws(SQLiteError) {
|
||||
try function.uninstall(db: connection)
|
||||
}
|
||||
|
||||
public func prepare(
|
||||
sql query: String, options: Statement.Options
|
||||
) throws(SQLiteError) -> any StatementProtocol {
|
||||
try Statement(db: connection, sql: query, options: options)
|
||||
}
|
||||
|
||||
public func execute(raw sql: String) throws(SQLiteError) {
|
||||
let status = sqlite3_exec(connection, sql, nil, nil, nil)
|
||||
guard status == SQLITE_OK else { throw SQLiteError(connection) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DelegateBox
|
||||
|
||||
fileprivate extension Connection {
|
||||
class DelegateBox {
|
||||
weak var delegate: ConnectionDelegate?
|
||||
|
||||
init(delegate: ConnectionDelegate? = nil) {
|
||||
init(delegate: ConnectionDelegate) {
|
||||
self.delegate = delegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TraceDelegateBox
|
||||
|
||||
fileprivate extension Connection {
|
||||
class TraceDelegateBox {
|
||||
weak var delegate: ConnectionTraceDelegate?
|
||||
|
||||
init(delegate: ConnectionTraceDelegate) {
|
||||
self.delegate = delegate
|
||||
}
|
||||
}
|
||||
@@ -131,28 +246,60 @@ fileprivate extension Connection {
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
private typealias TraceCallback = @convention(c) (
|
||||
UInt32,
|
||||
UnsafeMutableRawPointer?,
|
||||
UnsafeMutableRawPointer?,
|
||||
UnsafeMutableRawPointer?
|
||||
) -> Int32
|
||||
|
||||
@discardableResult
|
||||
private func sqlite3_trace_stmt(
|
||||
_ db: OpaquePointer!,
|
||||
_ callback: TraceCallback!,
|
||||
_ ctx: UnsafeMutableRawPointer!
|
||||
) -> Int32 {
|
||||
sqlite3_trace_v2(db, SQLITE_TRACE_STMT, callback, ctx)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func sqlite3_trace_v2(
|
||||
_ db: OpaquePointer!,
|
||||
_ mask: Int32,
|
||||
_ callback: TraceCallback!,
|
||||
_ ctx: UnsafeMutableRawPointer!
|
||||
) -> Int32 {
|
||||
sqlite3_trace_v2(db, UInt32(mask), callback, ctx)
|
||||
}
|
||||
|
||||
private func traceCallback(
|
||||
_ flag: UInt32,
|
||||
_ ctx: UnsafeMutableRawPointer?,
|
||||
_ p: UnsafeMutableRawPointer?,
|
||||
_ x: UnsafeMutableRawPointer?
|
||||
) -> Int32 {
|
||||
guard let ctx = ctx else { return SQLITE_OK }
|
||||
guard let ctx,
|
||||
let stmt = OpaquePointer(p)
|
||||
else { return SQLITE_OK }
|
||||
|
||||
let connection = Unmanaged<Connection>
|
||||
.fromOpaque(ctx)
|
||||
.takeUnretainedValue()
|
||||
|
||||
guard !connection.delegates.isEmpty,
|
||||
let stmt = OpaquePointer(p),
|
||||
let pSql = sqlite3_expanded_sql(stmt),
|
||||
let xSql = x?.assumingMemoryBound(to: CChar.self)
|
||||
else { return SQLITE_OK }
|
||||
let xSql = x?.assumingMemoryBound(to: CChar.self)
|
||||
let pSql = sqlite3_expanded_sql(stmt)
|
||||
|
||||
defer { sqlite3_free(pSql) }
|
||||
|
||||
guard let xSql, let pSql else {
|
||||
return SQLITE_OK
|
||||
}
|
||||
|
||||
let pSqlString = String(cString: pSql)
|
||||
let xSqlString = String(cString: xSql)
|
||||
let pSqlString = String(cString: pSql)
|
||||
let trace = (xSqlString, pSqlString)
|
||||
|
||||
for box in connection.delegates {
|
||||
for box in connection.traceDelegates {
|
||||
box.delegate?.connection(connection, trace: trace)
|
||||
}
|
||||
|
||||
@@ -166,37 +313,36 @@ private func updateHookCallback(
|
||||
_ tName: UnsafePointer<CChar>?,
|
||||
_ rowID: sqlite3_int64
|
||||
) {
|
||||
guard let ctx = ctx else { return }
|
||||
guard let ctx else { return }
|
||||
|
||||
let connection = Unmanaged<Connection>
|
||||
.fromOpaque(ctx)
|
||||
.takeUnretainedValue()
|
||||
|
||||
if !connection.delegates.isEmpty {
|
||||
guard let dName = dName, let tName = tName else { return }
|
||||
|
||||
let dbName = String(cString: dName)
|
||||
let tableName = String(cString: tName)
|
||||
let updateAction: SQLiteAction
|
||||
|
||||
switch action {
|
||||
case SQLITE_INSERT:
|
||||
updateAction = .insert(db: dbName, table: tableName, rowID: rowID)
|
||||
case SQLITE_UPDATE:
|
||||
updateAction = .update(db: dbName, table: tableName, rowID: rowID)
|
||||
case SQLITE_DELETE:
|
||||
updateAction = .delete(db: dbName, table: tableName, rowID: rowID)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
for box in connection.delegates {
|
||||
box.delegate?.connection(connection, didUpdate: updateAction)
|
||||
}
|
||||
guard let dName = dName, let tName = tName else { return }
|
||||
|
||||
let dbName = String(cString: dName)
|
||||
let tableName = String(cString: tName)
|
||||
|
||||
let updateAction: SQLiteAction? = switch action {
|
||||
case SQLITE_INSERT: .insert(db: dbName, table: tableName, rowID: rowID)
|
||||
case SQLITE_UPDATE: .update(db: dbName, table: tableName, rowID: rowID)
|
||||
case SQLITE_DELETE: .delete(db: dbName, table: tableName, rowID: rowID)
|
||||
default: nil
|
||||
}
|
||||
|
||||
guard let updateAction else { return }
|
||||
|
||||
for box in connection.delegates {
|
||||
box.delegate?.connection(connection, didUpdate: updateAction)
|
||||
}
|
||||
}
|
||||
|
||||
private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 {
|
||||
private func commitHookCallback(
|
||||
_ ctx: UnsafeMutableRawPointer?
|
||||
) -> Int32 {
|
||||
guard let ctx = ctx else { return SQLITE_OK }
|
||||
|
||||
let connection = Unmanaged<Connection>
|
||||
.fromOpaque(ctx)
|
||||
.takeUnretainedValue()
|
||||
@@ -211,8 +357,11 @@ private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 {
|
||||
}
|
||||
}
|
||||
|
||||
private func rollbackHookCallback(_ ctx: UnsafeMutableRawPointer?) {
|
||||
private func rollbackHookCallback(
|
||||
_ ctx: UnsafeMutableRawPointer?
|
||||
) {
|
||||
guard let ctx = ctx else { return }
|
||||
|
||||
let connection = Unmanaged<Connection>
|
||||
.fromOpaque(ctx)
|
||||
.takeUnretainedValue()
|
||||
|
||||
Reference in New Issue
Block a user