From dac16b8bdafe039b84832aa50b841747ee9e8519 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Sat, 20 Sep 2025 18:28:40 +0300 Subject: [PATCH] Add multicast delegate --- .../Classes/Connection+Error.swift | 12 ++-- Sources/DataLiteCore/Classes/Connection.swift | 72 ++++++++++++------- .../Classes/Function+Aggregate.swift | 1 + .../Classes/Function+Scalar.swift | 1 + Sources/DataLiteCore/Classes/Statement.swift | 26 ------- .../Docs.docc/Connection/Connection.md | 10 +-- .../Protocols/ConnectionProtocol.swift | 31 ++++---- 7 files changed, 78 insertions(+), 75 deletions(-) diff --git a/Sources/DataLiteCore/Classes/Connection+Error.swift b/Sources/DataLiteCore/Classes/Connection+Error.swift index 7eeb7d9..bf6a6cd 100644 --- a/Sources/DataLiteCore/Classes/Connection+Error.swift +++ b/Sources/DataLiteCore/Classes/Connection+Error.swift @@ -21,26 +21,26 @@ extension Connection { /// - ``init(code:message:)`` public struct Error: Swift.Error, Equatable, CustomStringConvertible { // MARK: - Properties - + /// The database engine error code. /// /// This code indicates the specific error returned by SQLite during an operation. /// For a full list of possible error codes, see: /// [SQLite Result and Error Codes](https://www.sqlite.org/rescode.html). public let code: Int32 - + /// A human-readable error message describing the failure. public let message: String - + /// A textual representation of the error. /// /// Combines the error code and message into a single descriptive string. public var description: String { "Connection.Error code: \(code) message: \(message)" } - + // MARK: - Initialization - + /// Creates an error with the given code and message. /// /// - Parameters: @@ -50,7 +50,7 @@ extension Connection { self.code = code self.message = message } - + /// Creates an error by extracting details from a SQLite connection. /// /// - Parameter connection: A pointer to the SQLite connection. diff --git a/Sources/DataLiteCore/Classes/Connection.swift b/Sources/DataLiteCore/Classes/Connection.swift index 033b149..458b484 100644 --- a/Sources/DataLiteCore/Classes/Connection.swift +++ b/Sources/DataLiteCore/Classes/Connection.swift @@ -5,10 +5,7 @@ public final class Connection: ConnectionProtocol { // MARK: - Private Properties private let connection: OpaquePointer - - // MARK: - Delegation - - public weak var delegate: (any ConnectionDelegate)? + fileprivate var delegates = [DelegateBox]() // MARK: - Connection State @@ -61,6 +58,17 @@ public final class Connection: ConnectionProtocol { sqlite3_close_v2(connection) } + // MARK: - Delegation + + public func addDelegate(_ delegate: ConnectionDelegate) { + delegates.removeAll { $0.delegate == nil } + delegates.append(.init(delegate: delegate)) + } + + public func removeDelegate(_ delegate: ConnectionDelegate) { + delegates.removeAll { $0.delegate == nil || $0.delegate === delegate } + } + // MARK: - Custom SQL Functions public func add(function: Function.Type) throws(Error) { @@ -111,6 +119,16 @@ public final class Connection: ConnectionProtocol { } } +fileprivate extension Connection { + class DelegateBox { + weak var delegate: ConnectionDelegate? + + init(delegate: ConnectionDelegate? = nil) { + self.delegate = delegate + } + } +} + // MARK: - Functions private func traceCallback( @@ -124,16 +142,18 @@ private func traceCallback( .fromOpaque(ctx) .takeUnretainedValue() - if let delegate = connection.delegate { - guard let stmt = OpaquePointer(p), - let pSql = sqlite3_expanded_sql(stmt), - let xSql = x?.assumingMemoryBound(to: CChar.self) - else { return SQLITE_OK } - - let pSqlString = String(cString: pSql) - let xSqlString = String(cString: xSql) - let trace = (xSqlString, pSqlString) - delegate.connection(connection, trace: trace) + 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 pSqlString = String(cString: pSql) + let xSqlString = String(cString: xSql) + let trace = (xSqlString, pSqlString) + + for box in connection.delegates { + box.delegate?.connection(connection, trace: trace) } return SQLITE_OK @@ -151,7 +171,7 @@ private func updateHookCallback( .fromOpaque(ctx) .takeUnretainedValue() - if let delegate = connection.delegate { + if !connection.delegates.isEmpty { guard let dName = dName, let tName = tName else { return } let dbName = String(cString: dName) @@ -169,18 +189,21 @@ private func updateHookCallback( return } - delegate.connection(connection, didUpdate: updateAction) + for box in connection.delegates { + box.delegate?.connection(connection, didUpdate: updateAction) + } } } private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 { + guard let ctx = ctx else { return SQLITE_OK } + let connection = Unmanaged + .fromOpaque(ctx) + .takeUnretainedValue() + do { - guard let ctx = ctx else { return SQLITE_OK } - let connection = Unmanaged - .fromOpaque(ctx) - .takeUnretainedValue() - if let delegate = connection.delegate { - try delegate.connectionDidCommit(connection) + for box in connection.delegates { + try box.delegate?.connectionDidCommit(connection) } return SQLITE_OK } catch { @@ -193,7 +216,8 @@ private func rollbackHookCallback(_ ctx: UnsafeMutableRawPointer?) { let connection = Unmanaged .fromOpaque(ctx) .takeUnretainedValue() - if let delegate = connection.delegate { - delegate.connectionDidRollback(connection) + + for box in connection.delegates { + box.delegate?.connectionDidRollback(connection) } } diff --git a/Sources/DataLiteCore/Classes/Function+Aggregate.swift b/Sources/DataLiteCore/Classes/Function+Aggregate.swift index 0cd17a9..24d15b0 100644 --- a/Sources/DataLiteCore/Classes/Function+Aggregate.swift +++ b/Sources/DataLiteCore/Classes/Function+Aggregate.swift @@ -360,6 +360,7 @@ private func xFinal(_ ctx: OpaquePointer?) { let description = error.localizedDescription let message = "Error executing function '\(name)': \(description)" sqlite3_result_error(ctx, message, -1) + sqlite3_result_error_code(ctx, SQLITE_ERROR) } } diff --git a/Sources/DataLiteCore/Classes/Function+Scalar.swift b/Sources/DataLiteCore/Classes/Function+Scalar.swift index 6589f84..9a4d243 100644 --- a/Sources/DataLiteCore/Classes/Function+Scalar.swift +++ b/Sources/DataLiteCore/Classes/Function+Scalar.swift @@ -201,6 +201,7 @@ private func xFunc( let description = error.localizedDescription let message = "Error executing function '\(name)': \(description)" sqlite3_result_error(ctx, message, -1) + sqlite3_result_error_code(ctx, SQLITE_ERROR) } } diff --git a/Sources/DataLiteCore/Classes/Statement.swift b/Sources/DataLiteCore/Classes/Statement.swift index 5a7928f..99d8912 100644 --- a/Sources/DataLiteCore/Classes/Statement.swift +++ b/Sources/DataLiteCore/Classes/Statement.swift @@ -683,46 +683,20 @@ public final class Statement: Equatable, Hashable { // MARK: - Functions -/// Binds a string to a parameter in an SQL statement. -/// -/// - Parameters: -/// - stmt: A pointer to the prepared SQL statement. -/// - index: The index of the parameter (1-based). -/// - string: The string to be bound to the parameter. -/// - Returns: SQLite error code if binding fails. private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 { sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT) } -/// Binds binary data to a parameter in an SQL statement. -/// -/// - Parameters: -/// - stmt: A pointer to the prepared SQL statement. -/// - index: The index of the parameter (1-based). -/// - data: The `Data` to be bound to the parameter. -/// - Returns: SQLite error code if binding fails. private func sqlite3_bind_blob(_ stmt: OpaquePointer!, _ index: Int32, _ data: Data) -> Int32 { data.withUnsafeBytes { sqlite3_bind_blob(stmt, index, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT) } } -/// Retrieves text data from a result column of an SQL statement. -/// -/// - Parameters: -/// - stmt: A pointer to the prepared SQL statement. -/// - iCol: The column index. -/// - Returns: A `String` containing the text data from the specified column. private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String { String(cString: DataLiteC.sqlite3_column_text(stmt, iCol)) } -/// Retrieves binary data from a result column of an SQL statement. -/// -/// - Parameters: -/// - stmt: A pointer to the prepared SQL statement. -/// - iCol: The column index. -/// - Returns: A `Data` object containing the binary data from the specified column. private func sqlite3_column_blob(_ stmt: OpaquePointer!, _ iCol: Int32) -> Data { Data( bytes: sqlite3_column_blob(stmt, iCol), diff --git a/Sources/DataLiteCore/Docs.docc/Connection/Connection.md b/Sources/DataLiteCore/Docs.docc/Connection/Connection.md index 358b5f1..af55e35 100644 --- a/Sources/DataLiteCore/Docs.docc/Connection/Connection.md +++ b/Sources/DataLiteCore/Docs.docc/Connection/Connection.md @@ -242,11 +242,6 @@ on an attached database. If omitted, they apply to the main database. - ``init(location:options:)`` - ``init(path:options:)`` -### Delegation - -- ``ConnectionDelegate`` -- ``delegate`` - ### Connection State - ``isAutocommit`` @@ -261,6 +256,11 @@ on an attached database. If omitted, they apply to the main database. - ``synchronous`` - ``userVersion`` +### Delegation + +- ``addDelegate(_:)`` +- ``removeDelegate(_:)`` + ### SQLite Lifecycle - ``initialize()`` diff --git a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift index e77ffda..a324c5a 100644 --- a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift +++ b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift @@ -15,11 +15,6 @@ import DataLiteC /// /// ## Topics /// -/// ### Delegation -/// -/// - ``ConnectionDelegate`` -/// - ``delegate`` -/// /// ### Connection State /// /// - ``isAutocommit`` @@ -34,6 +29,11 @@ import DataLiteC /// - ``synchronous`` /// - ``userVersion`` /// +/// ### Delegation +/// +/// - ``addDelegate(_:)`` +/// - ``removeDelegate(_:)`` +/// /// ### SQLite Lifecycle /// /// - ``initialize()`` @@ -70,15 +70,6 @@ import DataLiteC /// - ``apply(_:name:)`` /// - ``rekey(_:name:)`` public protocol ConnectionProtocol: AnyObject { - // MARK: - Delegation - - /// An optional delegate to receive connection-related events and callbacks. - /// - /// The delegate allows external objects to monitor or respond to events - /// occurring during the lifetime of the connection, such as errors, - /// transaction commits, or other significant state changes. - var delegate: ConnectionDelegate? { get set } - // MARK: - Connection State /// Indicates whether the database connection is in autocommit mode. @@ -162,6 +153,18 @@ public protocol ConnectionProtocol: AnyObject { /// - SeeAlso: [PRAGMA user_version](https://www.sqlite.org/pragma.html#pragma_user_version) var userVersion: Int32 { get set } + // MARK: - Delegation + + /// Adds a delegate to receive connection events. + /// + /// - Parameter delegate: The delegate to add. + func addDelegate(_ delegate: ConnectionDelegate) + + /// Removes a delegate from receiving connection events. + /// + /// - Parameter delegate: The delegate to remove. + func removeDelegate(_ delegate: ConnectionDelegate) + // MARK: - SQLite Lifecycle /// Initializes the SQLite library.