Add unit tests

This commit is contained in:
2025-10-25 18:56:55 +03:00
parent ddc47abdde
commit bbb7f14650
38 changed files with 1051 additions and 526 deletions

View File

@@ -7,7 +7,7 @@ extension Connection {
/// - ``file(path:)``: A database at a specific file path or URI (persistent).
/// - ``inMemory``: An in-memory database that exists only in RAM.
/// - ``temporary``: A temporary on-disk database deleted when the connection closes.
public enum Location {
public enum Location: Sendable {
/// A database stored at a given file path or URI.
///
/// Use this for persistent databases located on disk or referenced via SQLite URI.

View File

@@ -152,7 +152,7 @@ extension Connection: ConnectionProtocol {
sqlite3_key(connection, key.keyValue, key.length)
}
guard status == SQLITE_OK else {
throw SQLiteError(connection)
throw SQLiteError(code: status, message: "")
}
}
@@ -163,13 +163,15 @@ extension Connection: ConnectionProtocol {
sqlite3_rekey(connection, key.keyValue, key.length)
}
guard status == SQLITE_OK else {
throw SQLiteError(connection)
throw SQLiteError(code: status, message: "")
}
}
public func add(delegate: any ConnectionDelegate) {
delegates.append(.init(delegate: delegate))
delegates.removeAll { $0.delegate == nil }
if !delegates.contains(where: { $0.delegate === delegate }) {
delegates.append(.init(delegate: delegate))
delegates.removeAll { $0.delegate == nil }
}
}
public func remove(delegate: any ConnectionDelegate) {
@@ -179,8 +181,10 @@ extension Connection: ConnectionProtocol {
}
public func add(trace delegate: any ConnectionTraceDelegate) {
traceDelegates.append(.init(delegate: delegate))
traceDelegates.removeAll { $0.delegate == nil }
if !traceDelegates.contains(where: { $0.delegate === delegate }) {
traceDelegates.append(.init(delegate: delegate))
traceDelegates.removeAll { $0.delegate == nil }
}
}
public func remove(trace delegate: any ConnectionTraceDelegate) {

View File

@@ -51,6 +51,19 @@ public final class Statement {
// MARK: - StatementProtocol
extension Statement: StatementProtocol {
public var sql: String? {
let cSQL = sqlite3_sql(statement)
guard let cSQL else { return nil }
return String(cString: cSQL)
}
public var expandedSQL: String? {
let cSQL = sqlite3_expanded_sql(statement)
defer { sqlite3_free(cSQL) }
guard let cSQL else { return nil }
return String(cString: cSQL)
}
public func parameterCount() -> Int32 {
sqlite3_bind_parameter_count(statement)
}

View File

@@ -73,12 +73,12 @@ public enum JournalMode: String, SQLiteRepresentable {
/// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode)
public var rawValue: String {
switch self {
case .delete: "DELETE"
case .delete: "DELETE"
case .truncate: "TRUNCATE"
case .persist: "PERSIST"
case .memory: "MEMORY"
case .wal: "WAL"
case .off: "OFF"
case .persist: "PERSIST"
case .memory: "MEMORY"
case .wal: "WAL"
case .off: "OFF"
}
}
@@ -95,13 +95,13 @@ public enum JournalMode: String, SQLiteRepresentable {
/// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode)
public init?(rawValue: String) {
switch rawValue.uppercased() {
case "DELETE": self = .delete
case "TRUNCATE": self = .truncate
case "PERSIST": self = .persist
case "MEMORY": self = .memory
case "WAL": self = .wal
case "OFF": self = .off
default: return nil
case "DELETE": self = .delete
case "TRUNCATE": self = .truncate
case "PERSIST": self = .persist
case "MEMORY": self = .memory
case "WAL": self = .wal
case "OFF": self = .off
default: return nil
}
}
}

View File

@@ -13,24 +13,48 @@ public enum TransactionType: String, CustomStringConvertible {
/// With `BEGIN DEFERRED`, no locks are acquired immediately. If the first statement is a read
/// (`SELECT`), a read transaction begins. If it is a write statement, a write transaction
/// begins instead. Deferred transactions allow greater concurrency and are the default mode.
case deferred = "DEFERRED"
case deferred
/// Starts a write transaction immediately.
///
/// With `BEGIN IMMEDIATE`, a reserved lock is acquired right away to ensure that no other
/// connection can start a conflicting write. The statement may fail with `SQLITE_BUSY` if
/// another write transaction is already active.
case immediate = "IMMEDIATE"
case immediate
/// Starts an exclusive write transaction.
///
/// With `BEGIN EXCLUSIVE`, a write lock is acquired immediately. In rollback journal mode, it
/// also prevents other connections from reading the database while the transaction is active.
/// In WAL mode, it behaves the same as `.immediate`.
case exclusive = "EXCLUSIVE"
case exclusive
/// A textual representation of the transaction type.
public var description: String {
rawValue
}
/// Returns the SQLite keyword that represents the transaction type.
///
/// The value is always uppercased to match the keywords used by SQLite statements.
public var rawValue: String {
switch self {
case .deferred: "DEFERRED"
case .immediate: "IMMEDIATE"
case .exclusive: "EXCLUSIVE"
}
}
/// Creates a transaction type from an SQLite keyword.
///
/// The initializer accepts any ASCII case variant of the keyword (`"deferred"`, `"Deferred"`,
/// etc.). Returns `nil` if the string does not correspond to a supported transaction type.
public init?(rawValue: String) {
switch rawValue.uppercased() {
case "DEFERRED": self = .deferred
case "IMMEDIATE": self = .immediate
case "EXCLUSIVE": self = .exclusive
default: return nil
}
}
}

View File

@@ -2,11 +2,9 @@ import Foundation
/// A delegate that observes connection-level database events.
///
/// Conforming types can monitor row-level updates and transaction lifecycle events. All methods are
/// optional default implementations do nothing.
///
/// This protocol is typically used for debugging, logging, or synchronizing application state with
/// database changes.
/// Conforming types can monitor row-level updates and transaction lifecycle events. This protocol
/// is typically used for debugging, logging, or synchronizing application state with database
/// changes.
///
/// - Important: Delegate methods are invoked synchronously on SQLites internal execution thread.
/// Implementations must be lightweight and non-blocking to avoid slowing down SQL operations.
@@ -43,9 +41,3 @@ public protocol ConnectionDelegate: AnyObject {
/// - Parameter connection: The connection that rolled back.
func connectionDidRollback(_ connection: ConnectionProtocol)
}
public extension ConnectionDelegate {
func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) {}
func connectionWillCommit(_ connection: ConnectionProtocol) throws {}
func connectionDidRollback(_ connection: ConnectionProtocol) {}
}

View File

@@ -12,10 +12,10 @@ import Foundation
///
/// - ``isAutocommit``
/// - ``isReadonly``
/// - ``busyTimeout``
///
/// ### Accessing PRAGMA Values
///
/// - ``busyTimeout``
/// - ``applicationID``
/// - ``foreignKeys``
/// - ``journalMode``
@@ -84,6 +84,8 @@ public protocol ConnectionProtocol: AnyObject {
/// - SeeAlso: [Determine if a database is read-only](https://sqlite.org/c3ref/db_readonly.html)
var isReadonly: Bool { get }
// MARK: - PRAGMA Accessors
/// The busy timeout of the connection, in milliseconds.
///
/// Defines how long SQLite waits for a locked database to become available before returning
@@ -93,8 +95,6 @@ public protocol ConnectionProtocol: AnyObject {
/// - SeeAlso: [Set A Busy Timeout](https://sqlite.org/c3ref/busy_timeout.html)
var busyTimeout: Int32 { get set }
// MARK: - PRAGMA Accessors
/// The application identifier stored in the database header.
///
/// Used to distinguish database files created by different applications or file formats. This

View File

@@ -8,6 +8,11 @@ import Foundation
///
/// ## Topics
///
/// ### Retrieving Statement SQL
///
/// - ``sql``
/// - ``expandedSQL``
///
/// ### Binding Parameters
///
/// - ``parameterCount()``
@@ -34,6 +39,20 @@ import Foundation
/// - ``columnValue(at:)->T?``
/// - ``currentRow()``
public protocol StatementProtocol {
// MARK: - Retrieving Statement SQL
/// The original SQL text used to create this prepared statement.
///
/// Returns the statement text as it was supplied when the statement was prepared. Useful for
/// diagnostics or query introspection.
var sql: String? { get }
/// The SQL text with all parameter values expanded into their literal forms.
///
/// Shows the actual SQL string that would be executed after parameter binding. Useful for
/// debugging and logging queries.
var expandedSQL: String? { get }
// MARK: - Binding Parameters
/// Returns the number of parameters in the prepared SQLite statement.
@@ -101,7 +120,7 @@ public protocol StatementProtocol {
///
/// - Parameters:
/// - value: The ``SQLiteValue`` to bind.
/// - name: The parameter name as written in SQL, including its prefix.
/// - name: The parameter name exactly as written in SQL, including its prefix.
/// - Throws: ``SQLiteError`` if binding fails.
///
/// - SeeAlso: [Binding Values To Prepared Statements](
@@ -130,7 +149,7 @@ public protocol StatementProtocol {
///
/// - Parameters:
/// - value: The value to bind. If `nil`, `NULL` is bound.
/// - name: The parameter name as written in SQL, including its prefix.
/// - name: The parameter name exactly as written in SQL, including its prefix.
/// - Throws: ``SQLiteError`` if binding fails.
///
/// - SeeAlso: [Binding Values To Prepared Statements](

View File

@@ -46,7 +46,7 @@ public struct SQLiteError: Error, Equatable, CustomStringConvertible, Sendable {
/// name (`SQLiteError`), the numeric code, and the corresponding message, making it useful for
/// debugging, logging, or diagnostic displays.
public var description: String {
"\(Self.self) code: \(code) message: \(message)"
"\(Self.self)(\(code)): \(message)"
}
/// Creates a new error instance with the specified result code and message.