From 3fc5bf050c354774bd2e72a0345ac7bbd0ee06d8 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 19:59:45 +0100 Subject: [PATCH 01/11] [wip] started to rework api --- Package.resolved | 6 +- Package.swift | 3 +- .../SQLiteDatabaseClient.swift | 32 ++-- ...n.swift => SQLiteDatabaseConnection.swift} | 26 ++- ...eQuery.swift => SQLiteDatabaseQuery.swift} | 4 +- ...abaseRow.swift => SQLiteDatabaseRow.swift} | 5 +- ....swift => SQLiteDatabaseRowSequence.swift} | 12 +- ...t => SQLiteDatabaseTransactionError.swift} | 12 +- .../{ => SQLiteNIO+Extras}/SQLiteClient.swift | 155 ++++++++---------- .../SQLiteConnectionPool.swift | 26 +-- .../SQLiteDatabaseTestSuite.swift | 6 +- 11 files changed, 142 insertions(+), 145 deletions(-) rename Sources/FeatherSQLiteDatabase/{SQLiteConnection.swift => SQLiteDatabaseConnection.swift} (61%) rename Sources/FeatherSQLiteDatabase/{SQLiteQuery.swift => SQLiteDatabaseQuery.swift} (98%) rename Sources/FeatherSQLiteDatabase/{SQLiteRow+DatabaseRow.swift => SQLiteDatabaseRow.swift} (96%) rename Sources/FeatherSQLiteDatabase/{SQLiteQueryResult.swift => SQLiteDatabaseRowSequence.swift} (82%) rename Sources/FeatherSQLiteDatabase/{SQLiteTransactionError.swift => SQLiteDatabaseTransactionError.swift} (87%) rename Sources/FeatherSQLiteDatabase/{ => SQLiteNIO+Extras}/SQLiteClient.swift (67%) rename Sources/FeatherSQLiteDatabase/{ => SQLiteNIO+Extras}/SQLiteConnectionPool.swift (87%) diff --git a/Package.resolved b/Package.resolved index bc9e65b..3534698 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "816ee700ae5206734d35d787d67b0af726ff97f74c5e84dec48c842bb3679698", + "originHash" : "f2c4b405d7b153afc4f2cd0d4e4cfb0af1ffc9b98bf974d2012f977119229f27", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "147c96803f398c50f5717ce08ebd78a9e3ab7ca7", - "version" : "1.0.0-beta.2" + "branch" : "fix/consumption", + "revision" : "327c474c3874ecbd18b4ae15dadc286a84488814" } }, { diff --git a/Package.swift b/Package.swift index 065ec5a..0276964 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/sqlite-nio", from: "1.12.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/feather-framework/feather-database", branch: "fix/consumption"), +// .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"), // [docc-plugin-placeholder] ], targets: [ diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift index 946be84..71c3893 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift @@ -12,7 +12,10 @@ import SQLiteNIO /// /// Use this client to execute queries and manage transactions on SQLite. public struct SQLiteDatabaseClient: DatabaseClient { - + + public typealias Connection = SQLiteDatabaseConnection + + private let client: SQLiteClient /// Create a SQLite database client backed by a connection pool. @@ -22,13 +25,6 @@ public struct SQLiteDatabaseClient: DatabaseClient { self.client = client } - /// Create a SQLite database client backed by a connection pool. - /// - /// - Parameter configuration: The SQLite client configuration. - public init(configuration: SQLiteClient.Configuration) { - self.client = SQLiteClient(configuration: configuration) - } - /// Pre-open the minimum number of connections. public func run() async throws { try await client.run() @@ -50,11 +46,11 @@ public struct SQLiteDatabaseClient: DatabaseClient { /// - Throws: A `DatabaseError` if the connection fails. /// - Returns: The query result produced by the closure. @discardableResult - public func connection( - isolation: isolated (any Actor)? = #isolation, - _ closure: (SQLiteConnection) async throws -> sending T - ) async throws(DatabaseError) -> sending T { - try await client.connection(isolation: isolation, closure) + public func withConnection( + _ closure: (Connection) async throws -> T + ) async throws(DatabaseError) -> T { + fatalError() +// try await client.connection(isolation: isolation, closure) } /// Execute work inside a SQLite transaction. @@ -66,11 +62,11 @@ public struct SQLiteDatabaseClient: DatabaseClient { /// - Throws: A `DatabaseError` if transaction handling fails. /// - Returns: The query result produced by the closure. @discardableResult - public func transaction( - isolation: isolated (any Actor)? = #isolation, - _ closure: (SQLiteConnection) async throws -> sending T - ) async throws(DatabaseError) -> sending T { - try await client.transaction(isolation: isolation, closure) + public func withTransaction( + _ closure: (Connection) async throws -> T + ) async throws(DatabaseError) -> T { + fatalError() +// try await client.transaction(isolation: isolation, closure) } } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteConnection.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift similarity index 61% rename from Sources/FeatherSQLiteDatabase/SQLiteConnection.swift rename to Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift index 0a3f46f..d4825cc 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteConnection.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift @@ -7,8 +7,15 @@ import FeatherDatabase import SQLiteNIO +import Logging -extension SQLiteConnection: @retroactive DatabaseConnection { +public struct SQLiteDatabaseConnection: DatabaseConnection { + + public typealias Query = SQLiteDatabaseQuery + public typealias RowSequence = SQLiteDatabaseRowSequence + + var connection: SQLiteConnection + public var logger: Logger /// Execute a SQLite query on this connection. /// @@ -17,18 +24,25 @@ extension SQLiteConnection: @retroactive DatabaseConnection { /// - Throws: A `DatabaseError` if the query fails. /// - Returns: A query result containing the returned rows. @discardableResult - public func execute( - query: SQLiteQuery - ) async throws(DatabaseError) -> SQLiteQueryResult { + public func run( + query: Query, + _ handler: (RowSequence) async throws -> T = { _ in } + ) async throws(DatabaseError) -> T { + let maxAttempts = 8 var attempt = 0 while true { do { - let result = try await self.query( + let result = try await connection.query( query.sql, query.bindings ) - return SQLiteQueryResult(elements: result) + return try await handler( + SQLiteDatabaseRowSequence( + elements: result.map { + .init(row: $0) + }) + ) } catch { attempt += 1 diff --git a/Sources/FeatherSQLiteDatabase/SQLiteQuery.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift similarity index 98% rename from Sources/FeatherSQLiteDatabase/SQLiteQuery.swift rename to Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift index 79ed8bc..e873322 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteQuery.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift @@ -11,7 +11,7 @@ import SQLiteNIO /// A SQLite query with SQL text and bound parameters. /// /// Use this type to construct SQLite queries safely. -public struct SQLiteQuery: DatabaseQuery { +public struct SQLiteDatabaseQuery: DatabaseQuery { /// The SQL text to execute. /// /// This is the raw SQL string for the query. @@ -36,7 +36,7 @@ public struct SQLiteQuery: DatabaseQuery { } } -extension SQLiteQuery: ExpressibleByStringInterpolation { +extension SQLiteDatabaseQuery: ExpressibleByStringInterpolation { /// A string interpolation builder for SQLite queries. /// diff --git a/Sources/FeatherSQLiteDatabase/SQLiteRow+DatabaseRow.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift similarity index 96% rename from Sources/FeatherSQLiteDatabase/SQLiteRow+DatabaseRow.swift rename to Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift index f350fac..2d12729 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteRow+DatabaseRow.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift @@ -8,7 +8,8 @@ import FeatherDatabase import SQLiteNIO -extension SQLiteRow: @retroactive DatabaseRow { +public struct SQLiteDatabaseRow: DatabaseRow { + var row: SQLiteRow struct SingleValueDecoder: Decoder, SingleValueDecodingContainer { @@ -91,7 +92,7 @@ extension SQLiteRow: @retroactive DatabaseRow { column: String, as type: T.Type ) throws(DecodingError) -> T { - guard let data = self.column(column) else { + guard let data = row.column(column) else { throw .dataCorrupted( .init( codingPath: [], diff --git a/Sources/FeatherSQLiteDatabase/SQLiteQueryResult.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift similarity index 82% rename from Sources/FeatherSQLiteDatabase/SQLiteQueryResult.swift rename to Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift index 9eb9aef..3313d4e 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteQueryResult.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift @@ -11,21 +11,23 @@ import SQLiteNIO /// A query result backed by SQLite rows. /// /// Use this type to iterate or collect SQLite query results. -public struct SQLiteQueryResult: DatabaseQueryResult { - let elements: [SQLiteRow] +public struct SQLiteDatabaseRowSequence: DatabaseRowSequence { + public typealias Row = SQLiteDatabaseRow + + let elements: [Row] /// An async iterator over SQLite rows. /// /// This iterator traverses the in-memory row list. public struct Iterator: AsyncIteratorProtocol { var index = 0 - let elements: [SQLiteRow] + let elements: [Row] /// Return the next row in the sequence. /// /// This returns `nil` after the last row. /// - Returns: The next `SQLiteRow`, or `nil` when finished. - public mutating func next() async -> SQLiteRow? { + public mutating func next() async -> Row? { guard index < elements.count else { return nil } @@ -47,7 +49,7 @@ public struct SQLiteQueryResult: DatabaseQueryResult { /// This returns the rows held by the result. /// - Throws: An error if collection fails. /// - Returns: An array of `SQLiteRow` values. - public func collect() async throws -> [SQLiteRow] { + public func collect() async throws -> [Row] { elements } } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteTransactionError.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift similarity index 87% rename from Sources/FeatherSQLiteDatabase/SQLiteTransactionError.swift rename to Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift index f250365..b143a9f 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteTransactionError.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift @@ -10,7 +10,7 @@ import FeatherDatabase /// Transaction error details for SQLite operations. /// /// Use this to capture errors from transaction phases. -public struct SQLiteTransactionError: DatabaseTransactionError { +public struct SQLiteDatabaseTransactionError: DatabaseTransactionError { /// The source file where the error was created. /// @@ -24,19 +24,19 @@ public struct SQLiteTransactionError: DatabaseTransactionError { /// The error thrown while beginning the transaction. /// /// Set when the `BEGIN` step fails. - public var beginError: Error? + public internal(set) var beginError: Error? /// The error thrown inside the transaction closure. /// /// Set when the closure fails before commit. - public var closureError: Error? + public internal(set) var closureError: Error? /// The error thrown while committing the transaction. /// /// Set when the `COMMIT` step fails. - public var commitError: Error? + public internal(set) var commitError: Error? /// The error thrown while rolling back the transaction. /// /// Set when the `ROLLBACK` step fails. - public var rollbackError: Error? + public internal(set) var rollbackError: Error? /// Create a SQLite transaction error payload. /// @@ -48,7 +48,7 @@ public struct SQLiteTransactionError: DatabaseTransactionError { /// - closureError: The error thrown inside the transaction closure. /// - commitError: The error thrown while committing the transaction. /// - rollbackError: The error thrown while rolling back the transaction. - public init( + init( file: String = #fileID, line: Int = #line, beginError: Error? = nil, diff --git a/Sources/FeatherSQLiteDatabase/SQLiteClient.swift b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift similarity index 67% rename from Sources/FeatherSQLiteDatabase/SQLiteClient.swift rename to Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift index ccb3364..4eb9111 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteClient.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift @@ -13,7 +13,7 @@ import SQLiteNIO /// /// Use this client to execute queries and transactions concurrently. public final class SQLiteClient: Sendable { - + /// Configuration values for a pooled SQLite client. public struct Configuration: Sendable { @@ -88,12 +88,12 @@ public final class SQLiteClient: Sendable { } } - private let pool: SQLiteConnectionPool + private let pool: SQLiteDatabaseConnectionPool /// Create a SQLite client with a connection pool. /// - Parameter configuration: The client configuration. public init(configuration: Configuration) { - self.pool = SQLiteConnectionPool( + self.pool = SQLiteDatabaseConnectionPool( configuration: configuration ) } @@ -112,25 +112,6 @@ public final class SQLiteClient: Sendable { // MARK: - database api - /// Execute a query using a managed connection. - /// - /// This default implementation executes the query inside `connection(_:)`. - /// Busy errors are retried with an exponential backoff (up to 8 attempts). - /// - Parameters: - /// - isolation: The actor isolation to use for the duration of the call. - /// - query: The query to execute. - /// - Throws: A `DatabaseError` if execution fails. - /// - Returns: The query result. - @discardableResult - public func execute( - isolation: isolated (any Actor)? = #isolation, - query: SQLiteConnection.Query, - ) async throws(DatabaseError) -> SQLiteConnection.Result { - try await connection(isolation: isolation) { connection in - try await connection.execute(query: query) - } - } - /// Execute work using a leased connection. /// /// The connection is returned to the pool when the closure completes. @@ -144,20 +125,21 @@ public final class SQLiteClient: Sendable { isolation: isolated (any Actor)? = #isolation, _ closure: (SQLiteConnection) async throws -> sending T ) async throws(DatabaseError) -> sending T { - let connection = try await leaseConnection() - do { - let result = try await closure(connection) - await pool.releaseConnection(connection) - return result - } - catch let error as DatabaseError { - await pool.releaseConnection(connection) - throw error - } - catch { - await pool.releaseConnection(connection) - throw .connection(error) - } + fatalError() +// let connection = try await leaseConnection() +// do { +// let result = try await closure(connection) +// await pool.releaseConnection(connection) +// return result +// } +// catch let error as DatabaseError { +// await pool.releaseConnection(connection) +// throw error +// } +// catch { +// await pool.releaseConnection(connection) +// throw .connection(error) +// } } /// Execute work inside a SQLite transaction. @@ -174,56 +156,57 @@ public final class SQLiteClient: Sendable { isolation: isolated (any Actor)? = #isolation, _ closure: (SQLiteConnection) async throws -> sending T ) async throws(DatabaseError) -> sending T { - let connection = try await leaseConnection() - do { - try await connection.execute(query: "BEGIN;") - } - catch { - await pool.releaseConnection(connection) - throw DatabaseError.transaction( - SQLiteTransactionError(beginError: error) - ) - } - - var closureHasFinished = false - - do { - let result = try await closure(connection) - closureHasFinished = true - - do { - try await connection.execute(query: "COMMIT;") - } - catch { - await pool.releaseConnection(connection) - throw DatabaseError.transaction( - SQLiteTransactionError(commitError: error) - ) - } - - await pool.releaseConnection(connection) - return result - } - catch { - var txError = SQLiteTransactionError() - - if !closureHasFinished { - txError.closureError = error - - do { - try await connection.execute(query: "ROLLBACK;") - } - catch { - txError.rollbackError = error - } - } - else { - txError.commitError = error - } - - await pool.releaseConnection(connection) - throw DatabaseError.transaction(txError) - } + fatalError() +// let connection = try await leaseConnection() +// do { +// try await connection.execute(query: "BEGIN;") +// } +// catch { +// await pool.releaseConnection(connection) +// throw DatabaseError.transaction( +// SQLiteDatabaseTransactionError(beginError: error) +// ) +// } +// +// var closureHasFinished = false +// +// do { +// let result = try await closure(connection) +// closureHasFinished = true +// +// do { +// try await connection.execute(query: "COMMIT;") +// } +// catch { +// await pool.releaseConnection(connection) +// throw DatabaseError.transaction( +// SQLiteDatabaseTransactionError(commitError: error) +// ) +// } +// +// await pool.releaseConnection(connection) +// return result +// } +// catch { +// var txError = SQLiteDatabaseTransactionError() +// +// if !closureHasFinished { +// txError.closureError = error +// +// do { +// try await connection.execute(query: "ROLLBACK;") +// } +// catch { +// txError.rollbackError = error +// } +// } +// else { +// txError.commitError = error +// } +// +// await pool.releaseConnection(connection) +// throw DatabaseError.transaction(txError) +// } } // MARK: - pool diff --git a/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift similarity index 87% rename from Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift rename to Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift index a83cbab..1c78e97 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift @@ -12,7 +12,7 @@ enum SQLiteConnectionPoolError: Error, Sendable { case shutdown } -actor SQLiteConnectionPool { +actor SQLiteDatabaseConnectionPool { private struct Waiter { let id: Int @@ -134,18 +134,18 @@ actor SQLiteConnectionPool { logger: configuration.logger ) do { - _ = try await connection.execute( - query: - "PRAGMA journal_mode = \(unescaped: configuration.journalMode.rawValue);" - ) - _ = try await connection.execute( - query: - "PRAGMA busy_timeout = \(unescaped: String(configuration.busyTimeoutMilliseconds));" - ) - _ = try await connection.execute( - query: - "PRAGMA foreign_keys = \(unescaped: configuration.foreignKeysMode.rawValue);" - ) +// _ = try await connection.execute( +// query: +// "PRAGMA journal_mode = \(unescaped: configuration.journalMode.rawValue);" +// ) +// _ = try await connection.execute( +// query: +// "PRAGMA busy_timeout = \(unescaped: String(configuration.busyTimeoutMilliseconds));" +// ) +// _ = try await connection.execute( +// query: +// "PRAGMA foreign_keys = \(unescaped: configuration.foreignKeysMode.rawValue);" +// ) } catch { do { diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift index 0b3231c..a2bcd33 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift @@ -254,7 +254,7 @@ struct SQLiteDatabaseTestSuite { """# ) - let insert = SQLiteQuery( + let insert = SQLiteDatabaseQuery( unsafeSQL: #""" INSERT INTO "widgets" ("id", "name") @@ -297,7 +297,7 @@ struct SQLiteDatabaseTestSuite { ) let body: String? = nil - let insert: SQLiteQuery = #""" + let insert: SQLiteDatabaseQuery = #""" INSERT INTO "notes" ("id", "body") VALUES @@ -337,7 +337,7 @@ struct SQLiteDatabaseTestSuite { ) let label: SQLiteData = .text("alpha") - let insert: SQLiteQuery = #""" + let insert: SQLiteDatabaseQuery = #""" INSERT INTO "tags" ("id", "label") VALUES From ea7039906cd5b283fbd411222324b0198b9b07ab Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 20:09:54 +0100 Subject: [PATCH 02/11] [wip] minor lib changes --- .../SQLiteDatabaseClient.swift | 43 +- .../SQLiteNIO+Extras/SQLiteClient.swift | 152 +- .../SQLiteConnectionPool.swift | 25 +- .../SQLiteConnectionPoolError.swift | 11 + .../SQLiteDatabaseTestSuite.swift | 1516 ++++++++--------- 5 files changed, 875 insertions(+), 872 deletions(-) create mode 100644 Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift index 71c3893..74260dc 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift @@ -7,6 +7,7 @@ import FeatherDatabase import SQLiteNIO +import Logging /// A SQLite-backed database client. /// @@ -15,24 +16,18 @@ public struct SQLiteDatabaseClient: DatabaseClient { public typealias Connection = SQLiteDatabaseConnection - - private let client: SQLiteClient + let client: SQLiteClient + var logger: Logger /// Create a SQLite database client backed by a connection pool. /// /// - Parameter client: The SQLite client to use. - public init(client: SQLiteClient) { + public init( + client: SQLiteClient, + logger: Logger + ) { self.client = client - } - - /// Pre-open the minimum number of connections. - public func run() async throws { - try await client.run() - } - - /// Close all pooled connections and refuse new leases. - public func shutdown() async { - await client.shutdown() + self.logger = logger } // MARK: - database api @@ -41,7 +36,6 @@ public struct SQLiteDatabaseClient: DatabaseClient { /// /// The closure is executed with a pooled connection. /// - Parameters: - /// - isolation: The actor isolation to use for the closure. /// - closure: A closure that receives the SQLite connection. /// - Throws: A `DatabaseError` if the connection fails. /// - Returns: The query result produced by the closure. @@ -49,15 +43,20 @@ public struct SQLiteDatabaseClient: DatabaseClient { public func withConnection( _ closure: (Connection) async throws -> T ) async throws(DatabaseError) -> T { - fatalError() -// try await client.connection(isolation: isolation, closure) + try await client.withConnection { connection in + try await closure( + SQLiteDatabaseConnection( + connection: connection, + logger: logger + ) + ) + } } /// Execute work inside a SQLite transaction. /// /// The closure runs between `BEGIN` and `COMMIT` with rollback on failure. /// - Parameters: - /// - isolation: The actor isolation to use for the closure. /// - closure: A closure that receives the SQLite connection. /// - Throws: A `DatabaseError` if transaction handling fails. /// - Returns: The query result produced by the closure. @@ -65,8 +64,14 @@ public struct SQLiteDatabaseClient: DatabaseClient { public func withTransaction( _ closure: (Connection) async throws -> T ) async throws(DatabaseError) -> T { - fatalError() -// try await client.transaction(isolation: isolation, closure) + try await client.withTransaction { connection in + try await closure( + SQLiteDatabaseConnection( + connection: connection, + logger: logger + ) + ) + } } } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift index 4eb9111..6d2dedd 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift @@ -116,30 +116,27 @@ public final class SQLiteClient: Sendable { /// /// The connection is returned to the pool when the closure completes. /// - Parameters: - /// - isolation: The actor isolation to use for the closure. /// - closure: A closure that receives a SQLite connection. /// - Throws: A `DatabaseError` if leasing or execution fails. /// - Returns: The result produced by the closure. @discardableResult - public func connection( - isolation: isolated (any Actor)? = #isolation, - _ closure: (SQLiteConnection) async throws -> sending T - ) async throws(DatabaseError) -> sending T { - fatalError() -// let connection = try await leaseConnection() -// do { -// let result = try await closure(connection) -// await pool.releaseConnection(connection) -// return result -// } -// catch let error as DatabaseError { -// await pool.releaseConnection(connection) -// throw error -// } -// catch { -// await pool.releaseConnection(connection) -// throw .connection(error) -// } + public func withConnection( + _ closure: (SQLiteConnection) async throws -> T + ) async throws(DatabaseError) -> T { + let connection = try await leaseConnection() + do { + let result = try await closure(connection) + await pool.releaseConnection(connection) + return result + } + catch let error as DatabaseError { + await pool.releaseConnection(connection) + throw error + } + catch { + await pool.releaseConnection(connection) + throw .connection(error) + } } /// Execute work inside a SQLite transaction. @@ -147,66 +144,63 @@ public final class SQLiteClient: Sendable { /// The transaction is committed on success and rolled back on failure. /// Busy errors are retried with an exponential backoff (up to 8 attempts). /// - Parameters: - /// - isolation: The actor isolation to use for the closure. /// - closure: A closure that receives a SQLite connection. /// - Throws: A `DatabaseError` if transaction handling fails. /// - Returns: The result produced by the closure. @discardableResult - public func transaction( - isolation: isolated (any Actor)? = #isolation, - _ closure: (SQLiteConnection) async throws -> sending T - ) async throws(DatabaseError) -> sending T { - fatalError() -// let connection = try await leaseConnection() -// do { -// try await connection.execute(query: "BEGIN;") -// } -// catch { -// await pool.releaseConnection(connection) -// throw DatabaseError.transaction( -// SQLiteDatabaseTransactionError(beginError: error) -// ) -// } -// -// var closureHasFinished = false -// -// do { -// let result = try await closure(connection) -// closureHasFinished = true -// -// do { -// try await connection.execute(query: "COMMIT;") -// } -// catch { -// await pool.releaseConnection(connection) -// throw DatabaseError.transaction( -// SQLiteDatabaseTransactionError(commitError: error) -// ) -// } -// -// await pool.releaseConnection(connection) -// return result -// } -// catch { -// var txError = SQLiteDatabaseTransactionError() -// -// if !closureHasFinished { -// txError.closureError = error -// -// do { -// try await connection.execute(query: "ROLLBACK;") -// } -// catch { -// txError.rollbackError = error -// } -// } -// else { -// txError.commitError = error -// } -// -// await pool.releaseConnection(connection) -// throw DatabaseError.transaction(txError) -// } + public func withTransaction( + _ closure: (SQLiteConnection) async throws -> T + ) async throws(DatabaseError) -> T { + let connection = try await leaseConnection() + do { + _ = try await connection.query("BEGIN;") + } + catch { + await pool.releaseConnection(connection) + throw DatabaseError.transaction( + SQLiteDatabaseTransactionError(beginError: error) + ) + } + + var closureHasFinished = false + + do { + let result = try await closure(connection) + closureHasFinished = true + + do { + _ = try await connection.query("COMMIT;") + } + catch { + await pool.releaseConnection(connection) + throw DatabaseError.transaction( + SQLiteDatabaseTransactionError(commitError: error) + ) + } + + await pool.releaseConnection(connection) + return result + } + catch { + var txError = SQLiteDatabaseTransactionError() + + if !closureHasFinished { + txError.closureError = error + + do { + _ = try await connection.query("ROLLBACK;") + } + catch { + txError.rollbackError = error + } + } + else { + txError.commitError = error + } + + await pool.releaseConnection(connection) + throw DatabaseError.transaction(txError) + } } // MARK: - pool @@ -215,9 +209,9 @@ public final class SQLiteClient: Sendable { await pool.connectionCount() } - private func leaseConnection() async throws(DatabaseError) - -> SQLiteConnection - { + private func leaseConnection( + + ) async throws(DatabaseError) -> SQLiteConnection { do { return try await pool.leaseConnection() } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift index 1c78e97..1d7297e 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift @@ -8,10 +8,6 @@ import Logging import SQLiteNIO -enum SQLiteConnectionPoolError: Error, Sendable { - case shutdown -} - actor SQLiteDatabaseConnectionPool { private struct Waiter { @@ -134,18 +130,15 @@ actor SQLiteDatabaseConnectionPool { logger: configuration.logger ) do { -// _ = try await connection.execute( -// query: -// "PRAGMA journal_mode = \(unescaped: configuration.journalMode.rawValue);" -// ) -// _ = try await connection.execute( -// query: -// "PRAGMA busy_timeout = \(unescaped: String(configuration.busyTimeoutMilliseconds));" -// ) -// _ = try await connection.execute( -// query: -// "PRAGMA foreign_keys = \(unescaped: configuration.foreignKeysMode.rawValue);" -// ) + _ = try await connection.query( + "PRAGMA journal_mode = \(configuration.journalMode.rawValue);" + ) + _ = try await connection.query( + "PRAGMA busy_timeout = \(configuration.busyTimeoutMilliseconds);" + ) + _ = try await connection.query( + "PRAGMA foreign_keys = \(configuration.foreignKeysMode.rawValue);" + ) } catch { do { diff --git a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift new file mode 100644 index 0000000..52b8ee2 --- /dev/null +++ b/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift @@ -0,0 +1,11 @@ +// +// File.swift +// feather-sqlite-database +// +// Created by Tibor Bödecs on 2026. 02. 02.. +// + + +enum SQLiteConnectionPoolError: Error { + case shutdown +} diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift index a2bcd33..6c2c23e 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift @@ -1,759 +1,759 @@ +//// +//// SQLiteDatabaseTestSuite.swift +//// feather-sqlite-database +//// +//// Created by Tibor Bödecs on 2026. 01. 10.. +//// // -// SQLiteDatabaseTestSuite.swift -// feather-sqlite-database -// -// Created by Tibor Bödecs on 2026. 01. 10.. -// - -import FeatherDatabase -import Logging -import SQLiteNIO -import Testing - -@testable import FeatherSQLiteDatabase - -@Suite -struct SQLiteDatabaseTestSuite { - - private func runUsingTestDatabaseClient( - _ closure: ((SQLiteDatabaseClient) async throws -> Void) - ) async throws { - var logger = Logger(label: "test") - logger.logLevel = .info - - let configuration = SQLiteClient.Configuration( - storage: .memory, - logger: logger - ) - - let client = SQLiteClient(configuration: configuration) - - let database = SQLiteDatabaseClient(client: client) - - try await client.run() - try await closure(database) - await client.shutdown() - } - - @Test - func foreignKeySupport() async throws { - try await runUsingTestDatabaseClient { database in - - let result = - try await database.execute( - query: "PRAGMA foreign_keys" - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "foreign_keys", as: Int.self) == 1 - ) - } - } - - @Test - func tableCreation() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE IF NOT EXISTS "galaxies" ( - "id" INTEGER PRIMARY KEY, - "name" TEXT - ); - """# - ) - - let results = try await database.execute( - query: #""" - SELECT name - FROM sqlite_master - WHERE type = 'table' - ORDER BY name; - """# - ) - - let resultArray = try await results.collect() - #expect(resultArray.count == 1) - - let item = resultArray[0] - let name = try item.decode(column: "name", as: String.self) - #expect(name == "galaxies") - } - } - - @Test - func tableInsert() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE IF NOT EXISTS "galaxies" ( - "id" INTEGER PRIMARY KEY, - "name" TEXT - ); - """# - ) - - let name1 = "Andromeda" - let name2 = "Milky Way" - - try await database.execute( - query: #""" - INSERT INTO "galaxies" - ("id", "name") - VALUES - (\#(nil), \#(name1)), - (\#(nil), \#(name2)); - """# - ) - - let results = try await database.execute( - query: #""" - SELECT * FROM "galaxies" ORDER BY "name" ASC; - """# - ) - - let resultArray = try await results.collect() - #expect(resultArray.count == 2) - - let item1 = resultArray[0] - let name1result = try item1.decode(column: "name", as: String.self) - #expect(name1result == name1) - - let item2 = resultArray[1] - let name2result = try item2.decode(column: "name", as: String.self) - #expect(name2result == name2) - } - } - - @Test - func rowDecoding() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "foo" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "foo" - ("id", "value") - VALUES - (1, 'abc'), - (2, NULL); - """# - ) - - let result = - try await database.execute( - query: #""" - SELECT "id", "value" - FROM "foo" - ORDER BY "id"; - """# - ) - .collect() - - #expect(result.count == 2) - - let item1 = result[0] - let item2 = result[1] - - #expect(try item1.decode(column: "id", as: Int.self) == 1) - #expect(try item2.decode(column: "id", as: Int.self) == 2) - - #expect(try item1.decode(column: "id", as: Int?.self) == .some(1)) - #expect((try? item1.decode(column: "value", as: Int?.self)) == nil) - - #expect(try item1.decode(column: "value", as: String.self) == "abc") - #expect( - (try? item2.decode(column: "value", as: String.self)) == nil - ) - - #expect( - (try item1.decode(column: "value", as: String?.self)) - == .some("abc") - ) - #expect( - (try item2.decode(column: "value", as: String?.self)) == .none - ) - } - } - - @Test - func queryEncoding() async throws { - try await runUsingTestDatabaseClient { database in - - let tableName = "foo" - let idColumn = "id" - let valueColumn = "value" - let row1: (Int, String?) = (1, "abc") - let row2: (Int, String?) = (2, nil) - - try await database.execute( - query: #""" - CREATE TABLE \#(unescaped: tableName) ( - \#(unescaped: idColumn) INTEGER NOT NULL PRIMARY KEY, - \#(unescaped: valueColumn) TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO \#(unescaped: tableName) - (\#(unescaped: idColumn), \#(unescaped: valueColumn)) - VALUES - (\#(row1.0), \#(row1.1)), - (\#(row2.0), \#(row2.1)); - """# - ) - - let result = - try await database.execute( - query: #""" - SELECT \#(unescaped: idColumn), \#(unescaped: valueColumn) - FROM \#(unescaped: tableName) - ORDER BY \#(unescaped: idColumn) ASC; - """# - ) - .collect() - - #expect(result.count == 2) - - let item1 = result[0] - let item2 = result[1] - - #expect(try item1.decode(column: "id", as: Int.self) == 1) - #expect(try item2.decode(column: "id", as: Int.self) == 2) - - #expect( - try item1.decode(column: "value", as: String?.self) == "abc" - ) - #expect(try item2.decode(column: "value", as: String?.self) == nil) - } - } - - @Test - func unsafeSQLBindings() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "widgets" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - let insert = SQLiteDatabaseQuery( - unsafeSQL: #""" - INSERT INTO "widgets" - ("id", "name") - VALUES - (?, ?); - """#, - bindings: [.integer(1), .text("gizmo")] - ) - - try await database.execute(query: insert) - - let result = - try await database.execute( - query: #""" - SELECT "name" - FROM "widgets" - WHERE "id" = 1; - """# - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "name", as: String.self) == "gizmo" - ) - } - } - - @Test - func optionalStringInterpolationNil() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "notes" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "body" TEXT - ); - """# - ) - - let body: String? = nil - let insert: SQLiteDatabaseQuery = #""" - INSERT INTO "notes" - ("id", "body") - VALUES - (1, \#(body)); - """# - - try await database.execute(query: insert) - - let result = - try await database.execute( - query: #""" - SELECT "body" - FROM "notes" - WHERE "id" = 1; - """# - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "body", as: String?.self) == nil - ) - } - } - - @Test - func sqliteDataInterpolation() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "tags" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "label" TEXT NOT NULL - ); - """# - ) - - let label: SQLiteData = .text("alpha") - let insert: SQLiteDatabaseQuery = #""" - INSERT INTO "tags" - ("id", "label") - VALUES - (1, \#(label)); - """# - - try await database.execute(query: insert) - - let result = - try await database.execute( - query: #""" - SELECT "label" - FROM "tags" - WHERE "id" = 1; - """# - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "label", as: String.self) - == "alpha" - ) - } - } - - @Test - func resultSequenceIterator() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "numbers" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT NOT NULL - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "numbers" - ("id", "value") - VALUES - (1, 'one'), - (2, 'two'); - """# - ) - - let result = try await database.execute( - query: #""" - SELECT "id", "value" - FROM "numbers" - ORDER BY "id"; - """# - ) - - var iterator = result.makeAsyncIterator() - let first = await iterator.next() - let second = await iterator.next() - let third = await iterator.next() - - #expect(first != nil) - #expect(second != nil) - #expect(third == nil) - - if let first { - #expect(try first.decode(column: "id", as: Int.self) == 1) - #expect( - try first.decode(column: "value", as: String.self) == "one" - ) - } - else { - Issue.record("Expected first iterator element to exist.") - } - - if let second { - #expect(try second.decode(column: "id", as: Int.self) == 2) - #expect( - try second.decode(column: "value", as: String.self) == "two" - ) - } - else { - Issue.record("Expected second iterator element to exist.") - } - } - } - - @Test - func collectFirstReturnsFirstRow() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "widgets" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "widgets" - ("id", "name") - VALUES - (1, 'alpha'), - (2, 'beta'); - """# - ) - - let result = try await database.execute( - query: #""" - SELECT "name" - FROM "widgets" - ORDER BY "id" ASC; - """# - ) - - let first = try await result.collectFirst() - - #expect(first != nil) - #expect( - try first?.decode(column: "name", as: String.self) == "alpha" - ) - } - } - - @Test - func transactionSuccess() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "items" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - try await database.transaction { connection in - try await connection.execute( - query: #""" - INSERT INTO "items" - ("id", "name") - VALUES - (1, 'widget'); - """# - ) - } - - let result = - try await database.execute( - query: #""" - SELECT "name" - FROM "items" - WHERE "id" = 1; - """# - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "name", as: String.self) - == "widget" - ) - } - } - - @Test - func transactionFailurePropagates() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "dummy" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - do { - _ = try await database.transaction { connection in - try await connection.execute( - query: #""" - INSERT INTO "dummy" - ("id", "name") - VALUES - (1, 'ok'); - """# - ) - - return try await connection.execute( - query: #""" - INSERT INTO "dummy" - ("id", "name") - VALUES - (2, NULL); - """# - ) - } - Issue.record( - "Expected database transaction error to be thrown." - ) - } - catch DatabaseError.transaction(let error) { - #expect(error.beginError == nil) - #expect(error.closureError != nil) - #expect( - error.closureError.debugDescription.contains( - "NOT NULL constraint failed" - ) - ) - #expect(error.rollbackError == nil) - #expect(error.commitError == nil) - } - catch { - Issue.record( - "Expected database transaction error to be thrown." - ) - } - - let result = - try await database.execute( - query: #""" - SELECT "id" - FROM "dummy"; - """# - ) - .collect() - - #expect(result.isEmpty) - } - } - - @Test - func doubleRoundTrip() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "measurements" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" REAL NOT NULL - ); - """# - ) - - let expected = 1.5 - - try await database.execute( - query: #""" - INSERT INTO "measurements" - ("id", "value") - VALUES - (1, \#(expected)); - """# - ) - - let result = - try await database.execute( - query: #""" - SELECT "value" - FROM "measurements" - WHERE "id" = 1; - """# - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "value", as: Double.self) - == expected - ) - } - } - - @Test - func missingColumnThrows() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "items" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "items" - ("id", "value") - VALUES - (1, 'abc'); - """# - ) - - let result = - try await database.execute( - query: #""" - SELECT "id" - FROM "items"; - """# - ) - .collect() - - #expect(result.count == 1) - - do { - _ = try result[0].decode(column: "value", as: String.self) - Issue.record("Expected decoding a missing column to throw.") - } - catch DecodingError.dataCorrupted { - - } - catch { - Issue.record( - "Expected a dataCorrupted error for missing column." - ) - } - } - } - - @Test - func typeMismatchThrows() async throws { - try await runUsingTestDatabaseClient { database in - - try await database.execute( - query: #""" - CREATE TABLE "items" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "items" - ("id", "value") - VALUES - (1, 'abc'); - """# - ) - - let result = - try await database.execute( - query: #""" - SELECT "value" - FROM "items"; - """# - ) - .collect() - - #expect(result.count == 1) - - do { - _ = try result[0].decode(column: "value", as: Int.self) - Issue.record("Expected decoding a string as Int to throw.") - } - catch DecodingError.typeMismatch { - - } - catch { - Issue.record( - "Expected a typeMismatch error when decoding a string as Int." - ) - } - } - } - - @Test - func queryFailureErrorText() async throws { - try await runUsingTestDatabaseClient { database in - - do { - _ = try await database.execute( - query: #""" - SELECT * - FROM "missing_table"; - """# - ) - Issue.record("Expected query to fail for missing table.") - } - catch DatabaseError.query(let error) { - #expect("\(error)".contains("no such table")) - } - catch { - Issue.record("Expected database query error to be thrown.") - } - } - } - - @Test - func versionCheck() async throws { - try await runUsingTestDatabaseClient { database in - - let result = try await database.execute( - query: #""" - SELECT - sqlite_version() AS "version" - WHERE - 1=\#(1); - """# - ) - - let resultArray = try await result.collect() - #expect(resultArray.count == 1) - - let item = resultArray[0] - let version = try item.decode(column: "version", as: String.self) - #expect(version.split(separator: ".").count == 3) - } - } -} +//import FeatherDatabase +//import Logging +//import SQLiteNIO +//import Testing +// +//@testable import FeatherSQLiteDatabase +// +//@Suite +//struct SQLiteDatabaseTestSuite { +// +// private func runUsingTestDatabaseClient( +// _ closure: ((SQLiteDatabaseClient) async throws -> Void) +// ) async throws { +// var logger = Logger(label: "test") +// logger.logLevel = .info +// +// let configuration = SQLiteClient.Configuration( +// storage: .memory, +// logger: logger +// ) +// +// let client = SQLiteClient(configuration: configuration) +// +// let database = SQLiteDatabaseClient(client: client) +// +// try await client.run() +// try await closure(database) +// await client.shutdown() +// } +// +// @Test +// func foreignKeySupport() async throws { +// try await runUsingTestDatabaseClient { database in +// +// let result = +// try await database.execute( +// query: "PRAGMA foreign_keys" +// ) +// .collect() +// +// #expect(result.count == 1) +// #expect( +// try result[0].decode(column: "foreign_keys", as: Int.self) == 1 +// ) +// } +// } +// +// @Test +// func tableCreation() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE IF NOT EXISTS "galaxies" ( +// "id" INTEGER PRIMARY KEY, +// "name" TEXT +// ); +// """# +// ) +// +// let results = try await database.execute( +// query: #""" +// SELECT name +// FROM sqlite_master +// WHERE type = 'table' +// ORDER BY name; +// """# +// ) +// +// let resultArray = try await results.collect() +// #expect(resultArray.count == 1) +// +// let item = resultArray[0] +// let name = try item.decode(column: "name", as: String.self) +// #expect(name == "galaxies") +// } +// } +// +// @Test +// func tableInsert() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE IF NOT EXISTS "galaxies" ( +// "id" INTEGER PRIMARY KEY, +// "name" TEXT +// ); +// """# +// ) +// +// let name1 = "Andromeda" +// let name2 = "Milky Way" +// +// try await database.execute( +// query: #""" +// INSERT INTO "galaxies" +// ("id", "name") +// VALUES +// (\#(nil), \#(name1)), +// (\#(nil), \#(name2)); +// """# +// ) +// +// let results = try await database.execute( +// query: #""" +// SELECT * FROM "galaxies" ORDER BY "name" ASC; +// """# +// ) +// +// let resultArray = try await results.collect() +// #expect(resultArray.count == 2) +// +// let item1 = resultArray[0] +// let name1result = try item1.decode(column: "name", as: String.self) +// #expect(name1result == name1) +// +// let item2 = resultArray[1] +// let name2result = try item2.decode(column: "name", as: String.self) +// #expect(name2result == name2) +// } +// } +// +// @Test +// func rowDecoding() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "foo" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "value" TEXT +// ); +// """# +// ) +// +// try await database.execute( +// query: #""" +// INSERT INTO "foo" +// ("id", "value") +// VALUES +// (1, 'abc'), +// (2, NULL); +// """# +// ) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "id", "value" +// FROM "foo" +// ORDER BY "id"; +// """# +// ) +// .collect() +// +// #expect(result.count == 2) +// +// let item1 = result[0] +// let item2 = result[1] +// +// #expect(try item1.decode(column: "id", as: Int.self) == 1) +// #expect(try item2.decode(column: "id", as: Int.self) == 2) +// +// #expect(try item1.decode(column: "id", as: Int?.self) == .some(1)) +// #expect((try? item1.decode(column: "value", as: Int?.self)) == nil) +// +// #expect(try item1.decode(column: "value", as: String.self) == "abc") +// #expect( +// (try? item2.decode(column: "value", as: String.self)) == nil +// ) +// +// #expect( +// (try item1.decode(column: "value", as: String?.self)) +// == .some("abc") +// ) +// #expect( +// (try item2.decode(column: "value", as: String?.self)) == .none +// ) +// } +// } +// +// @Test +// func queryEncoding() async throws { +// try await runUsingTestDatabaseClient { database in +// +// let tableName = "foo" +// let idColumn = "id" +// let valueColumn = "value" +// let row1: (Int, String?) = (1, "abc") +// let row2: (Int, String?) = (2, nil) +// +// try await database.execute( +// query: #""" +// CREATE TABLE \#(unescaped: tableName) ( +// \#(unescaped: idColumn) INTEGER NOT NULL PRIMARY KEY, +// \#(unescaped: valueColumn) TEXT +// ); +// """# +// ) +// +// try await database.execute( +// query: #""" +// INSERT INTO \#(unescaped: tableName) +// (\#(unescaped: idColumn), \#(unescaped: valueColumn)) +// VALUES +// (\#(row1.0), \#(row1.1)), +// (\#(row2.0), \#(row2.1)); +// """# +// ) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT \#(unescaped: idColumn), \#(unescaped: valueColumn) +// FROM \#(unescaped: tableName) +// ORDER BY \#(unescaped: idColumn) ASC; +// """# +// ) +// .collect() +// +// #expect(result.count == 2) +// +// let item1 = result[0] +// let item2 = result[1] +// +// #expect(try item1.decode(column: "id", as: Int.self) == 1) +// #expect(try item2.decode(column: "id", as: Int.self) == 2) +// +// #expect( +// try item1.decode(column: "value", as: String?.self) == "abc" +// ) +// #expect(try item2.decode(column: "value", as: String?.self) == nil) +// } +// } +// +// @Test +// func unsafeSQLBindings() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "widgets" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "name" TEXT NOT NULL +// ); +// """# +// ) +// +// let insert = SQLiteDatabaseQuery( +// unsafeSQL: #""" +// INSERT INTO "widgets" +// ("id", "name") +// VALUES +// (?, ?); +// """#, +// bindings: [.integer(1), .text("gizmo")] +// ) +// +// try await database.execute(query: insert) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "name" +// FROM "widgets" +// WHERE "id" = 1; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// #expect( +// try result[0].decode(column: "name", as: String.self) == "gizmo" +// ) +// } +// } +// +// @Test +// func optionalStringInterpolationNil() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "notes" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "body" TEXT +// ); +// """# +// ) +// +// let body: String? = nil +// let insert: SQLiteDatabaseQuery = #""" +// INSERT INTO "notes" +// ("id", "body") +// VALUES +// (1, \#(body)); +// """# +// +// try await database.execute(query: insert) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "body" +// FROM "notes" +// WHERE "id" = 1; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// #expect( +// try result[0].decode(column: "body", as: String?.self) == nil +// ) +// } +// } +// +// @Test +// func sqliteDataInterpolation() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "tags" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "label" TEXT NOT NULL +// ); +// """# +// ) +// +// let label: SQLiteData = .text("alpha") +// let insert: SQLiteDatabaseQuery = #""" +// INSERT INTO "tags" +// ("id", "label") +// VALUES +// (1, \#(label)); +// """# +// +// try await database.execute(query: insert) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "label" +// FROM "tags" +// WHERE "id" = 1; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// #expect( +// try result[0].decode(column: "label", as: String.self) +// == "alpha" +// ) +// } +// } +// +// @Test +// func resultSequenceIterator() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "numbers" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "value" TEXT NOT NULL +// ); +// """# +// ) +// +// try await database.execute( +// query: #""" +// INSERT INTO "numbers" +// ("id", "value") +// VALUES +// (1, 'one'), +// (2, 'two'); +// """# +// ) +// +// let result = try await database.execute( +// query: #""" +// SELECT "id", "value" +// FROM "numbers" +// ORDER BY "id"; +// """# +// ) +// +// var iterator = result.makeAsyncIterator() +// let first = await iterator.next() +// let second = await iterator.next() +// let third = await iterator.next() +// +// #expect(first != nil) +// #expect(second != nil) +// #expect(third == nil) +// +// if let first { +// #expect(try first.decode(column: "id", as: Int.self) == 1) +// #expect( +// try first.decode(column: "value", as: String.self) == "one" +// ) +// } +// else { +// Issue.record("Expected first iterator element to exist.") +// } +// +// if let second { +// #expect(try second.decode(column: "id", as: Int.self) == 2) +// #expect( +// try second.decode(column: "value", as: String.self) == "two" +// ) +// } +// else { +// Issue.record("Expected second iterator element to exist.") +// } +// } +// } +// +// @Test +// func collectFirstReturnsFirstRow() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "widgets" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "name" TEXT NOT NULL +// ); +// """# +// ) +// +// try await database.execute( +// query: #""" +// INSERT INTO "widgets" +// ("id", "name") +// VALUES +// (1, 'alpha'), +// (2, 'beta'); +// """# +// ) +// +// let result = try await database.execute( +// query: #""" +// SELECT "name" +// FROM "widgets" +// ORDER BY "id" ASC; +// """# +// ) +// +// let first = try await result.collectFirst() +// +// #expect(first != nil) +// #expect( +// try first?.decode(column: "name", as: String.self) == "alpha" +// ) +// } +// } +// +// @Test +// func transactionSuccess() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "items" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "name" TEXT NOT NULL +// ); +// """# +// ) +// +// try await database.transaction { connection in +// try await connection.execute( +// query: #""" +// INSERT INTO "items" +// ("id", "name") +// VALUES +// (1, 'widget'); +// """# +// ) +// } +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "name" +// FROM "items" +// WHERE "id" = 1; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// #expect( +// try result[0].decode(column: "name", as: String.self) +// == "widget" +// ) +// } +// } +// +// @Test +// func transactionFailurePropagates() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "dummy" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "name" TEXT NOT NULL +// ); +// """# +// ) +// +// do { +// _ = try await database.transaction { connection in +// try await connection.execute( +// query: #""" +// INSERT INTO "dummy" +// ("id", "name") +// VALUES +// (1, 'ok'); +// """# +// ) +// +// return try await connection.execute( +// query: #""" +// INSERT INTO "dummy" +// ("id", "name") +// VALUES +// (2, NULL); +// """# +// ) +// } +// Issue.record( +// "Expected database transaction error to be thrown." +// ) +// } +// catch DatabaseError.transaction(let error) { +// #expect(error.beginError == nil) +// #expect(error.closureError != nil) +// #expect( +// error.closureError.debugDescription.contains( +// "NOT NULL constraint failed" +// ) +// ) +// #expect(error.rollbackError == nil) +// #expect(error.commitError == nil) +// } +// catch { +// Issue.record( +// "Expected database transaction error to be thrown." +// ) +// } +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "id" +// FROM "dummy"; +// """# +// ) +// .collect() +// +// #expect(result.isEmpty) +// } +// } +// +// @Test +// func doubleRoundTrip() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "measurements" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "value" REAL NOT NULL +// ); +// """# +// ) +// +// let expected = 1.5 +// +// try await database.execute( +// query: #""" +// INSERT INTO "measurements" +// ("id", "value") +// VALUES +// (1, \#(expected)); +// """# +// ) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "value" +// FROM "measurements" +// WHERE "id" = 1; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// #expect( +// try result[0].decode(column: "value", as: Double.self) +// == expected +// ) +// } +// } +// +// @Test +// func missingColumnThrows() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "items" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "value" TEXT +// ); +// """# +// ) +// +// try await database.execute( +// query: #""" +// INSERT INTO "items" +// ("id", "value") +// VALUES +// (1, 'abc'); +// """# +// ) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "id" +// FROM "items"; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// +// do { +// _ = try result[0].decode(column: "value", as: String.self) +// Issue.record("Expected decoding a missing column to throw.") +// } +// catch DecodingError.dataCorrupted { +// +// } +// catch { +// Issue.record( +// "Expected a dataCorrupted error for missing column." +// ) +// } +// } +// } +// +// @Test +// func typeMismatchThrows() async throws { +// try await runUsingTestDatabaseClient { database in +// +// try await database.execute( +// query: #""" +// CREATE TABLE "items" ( +// "id" INTEGER NOT NULL PRIMARY KEY, +// "value" TEXT +// ); +// """# +// ) +// +// try await database.execute( +// query: #""" +// INSERT INTO "items" +// ("id", "value") +// VALUES +// (1, 'abc'); +// """# +// ) +// +// let result = +// try await database.execute( +// query: #""" +// SELECT "value" +// FROM "items"; +// """# +// ) +// .collect() +// +// #expect(result.count == 1) +// +// do { +// _ = try result[0].decode(column: "value", as: Int.self) +// Issue.record("Expected decoding a string as Int to throw.") +// } +// catch DecodingError.typeMismatch { +// +// } +// catch { +// Issue.record( +// "Expected a typeMismatch error when decoding a string as Int." +// ) +// } +// } +// } +// +// @Test +// func queryFailureErrorText() async throws { +// try await runUsingTestDatabaseClient { database in +// +// do { +// _ = try await database.execute( +// query: #""" +// SELECT * +// FROM "missing_table"; +// """# +// ) +// Issue.record("Expected query to fail for missing table.") +// } +// catch DatabaseError.query(let error) { +// #expect("\(error)".contains("no such table")) +// } +// catch { +// Issue.record("Expected database query error to be thrown.") +// } +// } +// } +// +// @Test +// func versionCheck() async throws { +// try await runUsingTestDatabaseClient { database in +// +// let result = try await database.execute( +// query: #""" +// SELECT +// sqlite_version() AS "version" +// WHERE +// 1=\#(1); +// """# +// ) +// +// let resultArray = try await result.collect() +// #expect(resultArray.count == 1) +// +// let item = resultArray[0] +// let version = try item.decode(column: "version", as: String.self) +// #expect(version.split(separator: ".").count == 3) +// } +// } +//} From 2088059a15c1d26fe3f55b3d1fd75a9a1f5f3d9f Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Tue, 3 Feb 2026 13:08:24 +0100 Subject: [PATCH 03/11] new target --- Package.swift | 11 +++- .../SQLiteDatabaseClient.swift | 39 +++++++---- .../SQLiteDatabaseConnection.swift | 11 ++-- .../SQLiteDatabaseRowSequence.swift | 2 +- .../SQLiteDatabaseTransactionError.swift | 40 +++--------- .../SQLiteClient.swift | 28 ++++---- .../SQLiteConnectionPool.swift | 4 +- .../SQLiteConnectionPoolError.swift | 3 +- .../SQLiteTransactionError.swift | 64 +++++++++++++++++++ .../SQLiteClientTestSuite.swift | 1 + .../SQLiteDatabaseTestSuite.swift | 24 +++---- 11 files changed, 144 insertions(+), 83 deletions(-) rename Sources/{FeatherSQLiteDatabase/SQLiteNIO+Extras => SQLiteNIOExtras}/SQLiteClient.swift (90%) rename Sources/{FeatherSQLiteDatabase/SQLiteNIO+Extras => SQLiteNIOExtras}/SQLiteConnectionPool.swift (96%) rename Sources/{FeatherSQLiteDatabase/SQLiteNIO+Extras => SQLiteNIOExtras}/SQLiteConnectionPoolError.swift (80%) create mode 100644 Sources/SQLiteNIOExtras/SQLiteTransactionError.swift diff --git a/Package.swift b/Package.swift index 0276964..610d7ad 100644 --- a/Package.swift +++ b/Package.swift @@ -43,11 +43,20 @@ let package = Package( ], targets: [ .target( - name: "FeatherSQLiteDatabase", + name: "SQLiteNIOExtras", dependencies: [ .product(name: "Logging", package: "swift-log"), .product(name: "SQLiteNIO", package: "sqlite-nio"), + ], + swiftSettings: defaultSwiftSettings + ), + .target( + name: "FeatherSQLiteDatabase", + dependencies: [ +// .product(name: "Logging", package: "swift-log"), +// .product(name: "SQLiteNIO", package: "sqlite-nio"), .product(name: "FeatherDatabase", package: "feather-database"), + .target(name: "SQLiteNIOExtras"), ], swiftSettings: defaultSwiftSettings ), diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift index 74260dc..23c68f0 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift @@ -6,22 +6,25 @@ // import FeatherDatabase -import SQLiteNIO import Logging +import SQLiteNIO +import SQLiteNIOExtras /// A SQLite-backed database client. /// /// Use this client to execute queries and manage transactions on SQLite. public struct SQLiteDatabaseClient: DatabaseClient { - + public typealias Connection = SQLiteDatabaseConnection - + let client: SQLiteClient var logger: Logger /// Create a SQLite database client backed by a connection pool. /// - /// - Parameter client: The SQLite client to use. + /// - Parameters: + /// - client: The SQLite client to use. + /// - logger: The logger to use. public init( client: SQLiteClient, logger: Logger @@ -35,8 +38,7 @@ public struct SQLiteDatabaseClient: DatabaseClient { /// Execute work using a leased connection. /// /// The closure is executed with a pooled connection. - /// - Parameters: - /// - closure: A closure that receives the SQLite connection. + /// - Parameters closure: A closure that receives the SQLite connection. /// - Throws: A `DatabaseError` if the connection fails. /// - Returns: The query result produced by the closure. @discardableResult @@ -56,22 +58,33 @@ public struct SQLiteDatabaseClient: DatabaseClient { /// Execute work inside a SQLite transaction. /// /// The closure runs between `BEGIN` and `COMMIT` with rollback on failure. - /// - Parameters: - /// - closure: A closure that receives the SQLite connection. + /// - Parameters closure: A closure that receives the SQLite connection. /// - Throws: A `DatabaseError` if transaction handling fails. /// - Returns: The query result produced by the closure. @discardableResult public func withTransaction( _ closure: (Connection) async throws -> T ) async throws(DatabaseError) -> T { - try await client.withTransaction { connection in - try await closure( - SQLiteDatabaseConnection( - connection: connection, - logger: logger + do { + return try await client.withTransaction { connection in + try await closure( + SQLiteDatabaseConnection( + connection: connection, + logger: logger + ) + ) + } + } + catch let error as SQLiteTransactionError { + throw .transaction( + SQLiteDatabaseTransactionError( + underlyingError: error ) ) } + catch { + throw .connection(error) + } } } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift index d4825cc..c13f7a8 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift @@ -6,11 +6,11 @@ // import FeatherDatabase -import SQLiteNIO import Logging +import SQLiteNIO public struct SQLiteDatabaseConnection: DatabaseConnection { - + public typealias Query = SQLiteDatabaseQuery public typealias RowSequence = SQLiteDatabaseRowSequence @@ -20,7 +20,9 @@ public struct SQLiteDatabaseConnection: DatabaseConnection { /// Execute a SQLite query on this connection. /// /// This wraps `SQLiteNIO` query execution and maps errors. - /// - Parameter query: The SQLite query to execute. + /// - Parameters: + /// - query: The SQLite query to execute. + /// - handler: The handler to process the result sequence. /// - Throws: A `DatabaseError` if the query fails. /// - Returns: A query result containing the returned rows. @discardableResult @@ -41,7 +43,8 @@ public struct SQLiteDatabaseConnection: DatabaseConnection { SQLiteDatabaseRowSequence( elements: result.map { .init(row: $0) - }) + } + ) ) } catch { diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift index 3313d4e..9fec470 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift @@ -13,7 +13,7 @@ import SQLiteNIO /// Use this type to iterate or collect SQLite query results. public struct SQLiteDatabaseRowSequence: DatabaseRowSequence { public typealias Row = SQLiteDatabaseRow - + let elements: [Row] /// An async iterator over SQLite rows. diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift index b143a9f..877a142 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift @@ -6,61 +6,39 @@ // import FeatherDatabase +import SQLiteNIOExtras /// Transaction error details for SQLite operations. /// /// Use this to capture errors from transaction phases. public struct SQLiteDatabaseTransactionError: DatabaseTransactionError { + var underlyingError: SQLiteTransactionError + /// The source file where the error was created. /// /// This is captured with `#fileID` by default. - public let file: String + public var file: String { underlyingError.file } /// The source line where the error was created. /// /// This is captured with `#line` by default. - public let line: Int + public var line: Int { underlyingError.line } /// The error thrown while beginning the transaction. /// /// Set when the `BEGIN` step fails. - public internal(set) var beginError: Error? + public var beginError: Error? { underlyingError.beginError } /// The error thrown inside the transaction closure. /// /// Set when the closure fails before commit. - public internal(set) var closureError: Error? + public var closureError: Error? { underlyingError.closureError } /// The error thrown while committing the transaction. /// /// Set when the `COMMIT` step fails. - public internal(set) var commitError: Error? + public var commitError: Error? { underlyingError.commitError } /// The error thrown while rolling back the transaction. /// /// Set when the `ROLLBACK` step fails. - public internal(set) var rollbackError: Error? + public var rollbackError: Error? { underlyingError.rollbackError } - /// Create a SQLite transaction error payload. - /// - /// Use this to record the errors that occurred during transaction handling. - /// - Parameters: - /// - file: The source file identifier. - /// - line: The source line number. - /// - beginError: The error thrown while beginning the transaction. - /// - closureError: The error thrown inside the transaction closure. - /// - commitError: The error thrown while committing the transaction. - /// - rollbackError: The error thrown while rolling back the transaction. - init( - file: String = #fileID, - line: Int = #line, - beginError: Error? = nil, - closureError: Error? = nil, - commitError: Error? = nil, - rollbackError: Error? = nil - ) { - self.file = file - self.line = line - self.beginError = beginError - self.closureError = closureError - self.commitError = commitError - self.rollbackError = rollbackError - } } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift b/Sources/SQLiteNIOExtras/SQLiteClient.swift similarity index 90% rename from Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift rename to Sources/SQLiteNIOExtras/SQLiteClient.swift index 6d2dedd..f5ab2c4 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteClient.swift +++ b/Sources/SQLiteNIOExtras/SQLiteClient.swift @@ -13,7 +13,7 @@ import SQLiteNIO /// /// Use this client to execute queries and transactions concurrently. public final class SQLiteClient: Sendable { - + /// Configuration values for a pooled SQLite client. public struct Configuration: Sendable { @@ -115,8 +115,7 @@ public final class SQLiteClient: Sendable { /// Execute work using a leased connection. /// /// The connection is returned to the pool when the closure completes. - /// - Parameters: - /// - closure: A closure that receives a SQLite connection. + /// - Parameter closure: A closure that receives a SQLite connection. /// - Throws: A `DatabaseError` if leasing or execution fails. /// - Returns: The result produced by the closure. @discardableResult @@ -143,23 +142,20 @@ public final class SQLiteClient: Sendable { /// /// The transaction is committed on success and rolled back on failure. /// Busy errors are retried with an exponential backoff (up to 8 attempts). - /// - Parameters: - /// - closure: A closure that receives a SQLite connection. + /// - Parameters closure: A closure that receives a SQLite connection. /// - Throws: A `DatabaseError` if transaction handling fails. /// - Returns: The result produced by the closure. @discardableResult public func withTransaction( _ closure: (SQLiteConnection) async throws -> T - ) async throws(DatabaseError) -> T { + ) async throws -> T { let connection = try await leaseConnection() do { _ = try await connection.query("BEGIN;") } catch { await pool.releaseConnection(connection) - throw DatabaseError.transaction( - SQLiteDatabaseTransactionError(beginError: error) - ) + throw SQLiteTransactionError(beginError: error) } var closureHasFinished = false @@ -173,16 +169,14 @@ public final class SQLiteClient: Sendable { } catch { await pool.releaseConnection(connection) - throw DatabaseError.transaction( - SQLiteDatabaseTransactionError(commitError: error) - ) + throw SQLiteTransactionError(commitError: error) } await pool.releaseConnection(connection) return result } catch { - var txError = SQLiteDatabaseTransactionError() + var txError = SQLiteTransactionError() if !closureHasFinished { txError.closureError = error @@ -199,7 +193,7 @@ public final class SQLiteClient: Sendable { } await pool.releaseConnection(connection) - throw DatabaseError.transaction(txError) + throw txError } } @@ -209,9 +203,9 @@ public final class SQLiteClient: Sendable { await pool.connectionCount() } - private func leaseConnection( - - ) async throws(DatabaseError) -> SQLiteConnection { + private func leaseConnection() async throws(DatabaseError) + -> SQLiteConnection + { do { return try await pool.leaseConnection() } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift similarity index 96% rename from Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift rename to Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift index 1d7297e..29caad3 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPool.swift +++ b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift @@ -134,10 +134,10 @@ actor SQLiteDatabaseConnectionPool { "PRAGMA journal_mode = \(configuration.journalMode.rawValue);" ) _ = try await connection.query( - "PRAGMA busy_timeout = \(configuration.busyTimeoutMilliseconds);" + "PRAGMA busy_timeout = \(configuration.busyTimeoutMilliseconds);" ) _ = try await connection.query( - "PRAGMA foreign_keys = \(configuration.foreignKeysMode.rawValue);" + "PRAGMA foreign_keys = \(configuration.foreignKeysMode.rawValue);" ) } catch { diff --git a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift b/Sources/SQLiteNIOExtras/SQLiteConnectionPoolError.swift similarity index 80% rename from Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift rename to Sources/SQLiteNIOExtras/SQLiteConnectionPoolError.swift index 52b8ee2..6cf2f80 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteNIO+Extras/SQLiteConnectionPoolError.swift +++ b/Sources/SQLiteNIOExtras/SQLiteConnectionPoolError.swift @@ -1,11 +1,10 @@ // -// File.swift +// SQLiteConnectionPoolError.swift // feather-sqlite-database // // Created by Tibor Bödecs on 2026. 02. 02.. // - enum SQLiteConnectionPoolError: Error { case shutdown } diff --git a/Sources/SQLiteNIOExtras/SQLiteTransactionError.swift b/Sources/SQLiteNIOExtras/SQLiteTransactionError.swift new file mode 100644 index 0000000..f0b38b3 --- /dev/null +++ b/Sources/SQLiteNIOExtras/SQLiteTransactionError.swift @@ -0,0 +1,64 @@ +// +// SQLiteTransactionError.swift +// feather-sqlite-database +// +// Created by Tibor Bödecs on 2026. 02. 03.. +// + +/// Transaction error details for SQLite operations. +/// +/// Use this to capture errors from transaction phases. +public struct SQLiteTransactionError: Error { + + /// The source file where the error was created. + /// + /// This is captured with `#fileID` by default. + public let file: String + /// The source line where the error was created. + /// + /// This is captured with `#line` by default. + public let line: Int + + /// The error thrown while beginning the transaction. + /// + /// Set when the `BEGIN` step fails. + public internal(set) var beginError: Error? + /// The error thrown inside the transaction closure. + /// + /// Set when the closure fails before commit. + public internal(set) var closureError: Error? + /// The error thrown while committing the transaction. + /// + /// Set when the `COMMIT` step fails. + public internal(set) var commitError: Error? + /// The error thrown while rolling back the transaction. + /// + /// Set when the `ROLLBACK` step fails. + public internal(set) var rollbackError: Error? + + /// Create a SQLite transaction error payload. + /// + /// Use this to record the errors that occurred during transaction handling. + /// - Parameters: + /// - file: The source file identifier. + /// - line: The source line number. + /// - beginError: The error thrown while beginning the transaction. + /// - closureError: The error thrown inside the transaction closure. + /// - commitError: The error thrown while committing the transaction. + /// - rollbackError: The error thrown while rolling back the transaction. + init( + file: String = #fileID, + line: Int = #line, + beginError: Error? = nil, + closureError: Error? = nil, + commitError: Error? = nil, + rollbackError: Error? = nil + ) { + self.file = file + self.line = line + self.beginError = beginError + self.closureError = closureError + self.commitError = commitError + self.rollbackError = rollbackError + } +} diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift index c2b8ebf..130bc95 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift @@ -8,6 +8,7 @@ import FeatherDatabase import Logging import SQLiteNIO +import SQLiteNIOExtras import Testing @testable import FeatherSQLiteDatabase diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift index 6c2c23e..53f28a6 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift @@ -1,9 +1,9 @@ -//// -//// SQLiteDatabaseTestSuite.swift -//// feather-sqlite-database -//// -//// Created by Tibor Bödecs on 2026. 01. 10.. -//// +// +// SQLiteDatabaseTestSuite.swift +// feather-sqlite-database +// +// Created by Tibor Bödecs on 2026. 01. 10.. +// // //import FeatherDatabase //import Logging @@ -101,7 +101,7 @@ // // try await database.execute( // query: #""" -// INSERT INTO "galaxies" +// INSERT INTO "galaxies" // ("id", "name") // VALUES // (\#(nil), \#(name1)), @@ -143,7 +143,7 @@ // // try await database.execute( // query: #""" -// INSERT INTO "foo" +// INSERT INTO "foo" // ("id", "value") // VALUES // (1, 'abc'), @@ -208,7 +208,7 @@ // // try await database.execute( // query: #""" -// INSERT INTO \#(unescaped: tableName) +// INSERT INTO \#(unescaped: tableName) // (\#(unescaped: idColumn), \#(unescaped: valueColumn)) // VALUES // (\#(row1.0), \#(row1.1)), @@ -741,9 +741,9 @@ // // let result = try await database.execute( // query: #""" -// SELECT -// sqlite_version() AS "version" -// WHERE +// SELECT +// sqlite_version() AS "version" +// WHERE // 1=\#(1); // """# // ) From d2d80121ddd57417cf7280d55189ce87a56b3c21 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Tue, 3 Feb 2026 13:19:22 +0100 Subject: [PATCH 04/11] prep tests --- Package.swift | 7 + .../SQLiteDatabaseClient.swift | 18 +- Sources/SQLiteNIOExtras/SQLiteClient.swift | 20 +- .../SQLiteClientTestSuite.swift | 468 ++--- .../SQLiteDatabaseTestSuite.swift | 1504 ++++++++--------- .../SQLiteNIOExtrasTestSuite.swift | 68 + 6 files changed, 1076 insertions(+), 1009 deletions(-) create mode 100644 Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift diff --git a/Package.swift b/Package.swift index 610d7ad..1daf9a1 100644 --- a/Package.swift +++ b/Package.swift @@ -60,6 +60,13 @@ let package = Package( ], swiftSettings: defaultSwiftSettings ), + .testTarget( + name: "SQLiteNIOExtrasTests", + dependencies: [ + .target(name: "SQLiteNIOExtras"), + ], + swiftSettings: defaultSwiftSettings + ), .testTarget( name: "FeatherSQLiteDatabaseTests", dependencies: [ diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift index 23c68f0..ef17fcd 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseClient.swift @@ -7,7 +7,6 @@ import FeatherDatabase import Logging -import SQLiteNIO import SQLiteNIOExtras /// A SQLite-backed database client. @@ -45,13 +44,18 @@ public struct SQLiteDatabaseClient: DatabaseClient { public func withConnection( _ closure: (Connection) async throws -> T ) async throws(DatabaseError) -> T { - try await client.withConnection { connection in - try await closure( - SQLiteDatabaseConnection( - connection: connection, - logger: logger + do { + return try await client.withConnection { connection in + try await closure( + SQLiteDatabaseConnection( + connection: connection, + logger: logger + ) ) - ) + } + } + catch { + throw .connection(error) } } diff --git a/Sources/SQLiteNIOExtras/SQLiteClient.swift b/Sources/SQLiteNIOExtras/SQLiteClient.swift index f5ab2c4..6392a3a 100644 --- a/Sources/SQLiteNIOExtras/SQLiteClient.swift +++ b/Sources/SQLiteNIOExtras/SQLiteClient.swift @@ -5,7 +5,6 @@ // Created by Tibor Bödecs on 2026. 01. 26.. // -import FeatherDatabase import Logging import SQLiteNIO @@ -121,20 +120,16 @@ public final class SQLiteClient: Sendable { @discardableResult public func withConnection( _ closure: (SQLiteConnection) async throws -> T - ) async throws(DatabaseError) -> T { + ) async throws -> T { let connection = try await leaseConnection() do { let result = try await closure(connection) await pool.releaseConnection(connection) return result } - catch let error as DatabaseError { - await pool.releaseConnection(connection) - throw error - } catch { await pool.releaseConnection(connection) - throw .connection(error) + throw error } } @@ -203,14 +198,7 @@ public final class SQLiteClient: Sendable { await pool.connectionCount() } - private func leaseConnection() async throws(DatabaseError) - -> SQLiteConnection - { - do { - return try await pool.leaseConnection() - } - catch { - throw .connection(error) - } + private func leaseConnection() async throws -> SQLiteConnection { + try await pool.leaseConnection() } } diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift index 130bc95..9dfc655 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift @@ -16,238 +16,238 @@ import Testing @Suite struct SQLiteClientTestSuite { - private func makeTemporaryDatabasePath() -> String { - let suffix = UInt64.random(in: 0...UInt64.max) - return "/tmp/feather-sqlite-\(suffix).sqlite" - } - - private func randomTableSuffix() -> String { - String(UInt64.random(in: 0...UInt64.max)) - } - - private func runUsingTestClient( - _ closure: (SQLiteClient) async throws -> Void - ) async throws { - var logger = Logger(label: "test.sqlite.client") - logger.logLevel = .info - - let configuration = SQLiteClient.Configuration( - storage: .file(path: makeTemporaryDatabasePath()), - logger: logger, - ) - let client = SQLiteClient(configuration: configuration) - - try await client.run() - defer { Task { await client.shutdown() } } - - try await closure(client) - } - - @Test - func concurrentTransactionsUseMultipleConnections() async throws { - var logger = Logger(label: "test.sqlite.client") - logger.logLevel = .info - - let configuration = SQLiteClient.Configuration( - storage: .file(path: makeTemporaryDatabasePath()), - logger: logger - ) - let client = SQLiteClient(configuration: configuration) - - try await client.run() - - try await client.execute( - query: #""" - CREATE TABLE "items" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - async let first: Void = client.transaction { connection in - - try await connection.execute( - query: #""" - INSERT INTO "items" - ("id", "name") - VALUES - (1, 'alpha'); - """# - ) - } - - async let second: Void = client.transaction { connection in - try await connection.execute( - query: #""" - INSERT INTO "items" - ("id", "name") - VALUES - (2, 'beta'); - """# - ) - } - - do { - - _ = try await (first, second) - - let result = try await client.execute( - query: #""" - SELECT COUNT(*) AS "count" - FROM "items"; - """# - ) - let rows = try await result.collect() - - #expect(try rows[0].decode(column: "count", as: Int.self) == 2) - #expect(await client.connectionCount() == 2) - } - catch { - Issue.record(error) - } - - await client.shutdown() - } - - @Test - func concurrentTransactionUpdates() async throws { - try await runUsingTestClient { database in - let suffix = randomTableSuffix() - let table = "sessions_\(suffix)" - let sessionID = "session_\(suffix)" - - enum TestError: Error { - case missingRow - } - - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)"; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" TEXT NOT NULL PRIMARY KEY, - "access_token" TEXT NOT NULL, - "access_expires_at" INTEGER NOT NULL, - "refresh_token" TEXT NOT NULL, - "refresh_count" INTEGER NOT NULL DEFAULT 0 - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "access_token", "access_expires_at", "refresh_token", "refresh_count") - VALUES - ( - \#(sessionID), - 'stale', - (strftime('%s','now') - 300), - 'refresh', - 0 - ); - """# - ) - - func getValidAccessToken(sessionID: String) async throws -> String { - try await database.transaction { connection in - - let updateResult = try await connection.execute( - query: #""" - UPDATE "\#(unescaped: table)" - SET - "refresh_count" = "refresh_count" + 1, - "access_token" = 'token_' || ("refresh_count" + 1), - "access_expires_at" = (strftime('%s','now') + 600) - WHERE - "id" = \#(sessionID) - AND "access_expires_at" - <= (strftime('%s','now') + 60) - RETURNING "access_token"; - """# - ) - let updatedRows = try await updateResult.collect() - if let updatedRow = updatedRows.first { - return try updatedRow.decode( - column: "access_token", - as: String.self - ) - } - - let result = try await connection.execute( - query: #""" - SELECT - "access_token", - "refresh_count", - "access_expires_at" > (strftime('%s','now') + 60) AS "is_valid" - FROM "\#(unescaped: table)" - WHERE "id" = \#(sessionID); - """# - ) - let rows = try await result.collect() - - guard let row = rows.first else { - throw TestError.missingRow - } - - let isValid = try row.decode( - column: "is_valid", - as: Bool.self - ) - #expect(isValid == true) - - return try row.decode( - column: "access_token", - as: String.self - ) - } - } - - let workerCount = 80 - var tokens: [String] = [] - try await withThrowingTaskGroup(of: String.self) { group in - for _ in 0.. strftime('%s','now') AS "is_valid" - FROM "\#(unescaped: table)" - WHERE "id" = \#(sessionID); - """# - ) - .collect() - - #expect(result.count == 1) - #expect( - try result[0].decode(column: "refresh_count", as: Int.self) - == 1 - ) - #expect( - try result[0].decode(column: "access_token", as: String.self) - == "token_1" - ) - #expect( - try result[0].decode(column: "is_valid", as: Bool.self) - == true - ) - } - } + // private func makeTemporaryDatabasePath() -> String { + // let suffix = UInt64.random(in: 0...UInt64.max) + // return "/tmp/feather-sqlite-\(suffix).sqlite" + // } + // + // private func randomTableSuffix() -> String { + // String(UInt64.random(in: 0...UInt64.max)) + // } + // + // private func runUsingTestClient( + // _ closure: (SQLiteClient) async throws -> Void + // ) async throws { + // var logger = Logger(label: "test.sqlite.client") + // logger.logLevel = .info + // + // let configuration = SQLiteClient.Configuration( + // storage: .file(path: makeTemporaryDatabasePath()), + // logger: logger, + // ) + // let client = SQLiteClient(configuration: configuration) + // + // try await client.run() + // defer { Task { await client.shutdown() } } + // + // try await closure(client) + // } + // + // @Test + // func concurrentTransactionsUseMultipleConnections() async throws { + // var logger = Logger(label: "test.sqlite.client") + // logger.logLevel = .info + // + // let configuration = SQLiteClient.Configuration( + // storage: .file(path: makeTemporaryDatabasePath()), + // logger: logger + // ) + // let client = SQLiteClient(configuration: configuration) + // + // try await client.run() + // + // try await client.execute( + // query: #""" + // CREATE TABLE "items" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "name" TEXT NOT NULL + // ); + // """# + // ) + // + // async let first: Void = client.transaction { connection in + // + // try await connection.execute( + // query: #""" + // INSERT INTO "items" + // ("id", "name") + // VALUES + // (1, 'alpha'); + // """# + // ) + // } + // + // async let second: Void = client.transaction { connection in + // try await connection.execute( + // query: #""" + // INSERT INTO "items" + // ("id", "name") + // VALUES + // (2, 'beta'); + // """# + // ) + // } + // + // do { + // + // _ = try await (first, second) + // + // let result = try await client.execute( + // query: #""" + // SELECT COUNT(*) AS "count" + // FROM "items"; + // """# + // ) + // let rows = try await result.collect() + // + // #expect(try rows[0].decode(column: "count", as: Int.self) == 2) + // #expect(await client.connectionCount() == 2) + // } + // catch { + // Issue.record(error) + // } + // + // await client.shutdown() + // } + // + // @Test + // func concurrentTransactionUpdates() async throws { + // try await runUsingTestClient { database in + // let suffix = randomTableSuffix() + // let table = "sessions_\(suffix)" + // let sessionID = "session_\(suffix)" + // + // enum TestError: Error { + // case missingRow + // } + // + // try await database.execute( + // query: #""" + // DROP TABLE IF EXISTS "\#(unescaped: table)"; + // """# + // ) + // try await database.execute( + // query: #""" + // CREATE TABLE "\#(unescaped: table)" ( + // "id" TEXT NOT NULL PRIMARY KEY, + // "access_token" TEXT NOT NULL, + // "access_expires_at" INTEGER NOT NULL, + // "refresh_token" TEXT NOT NULL, + // "refresh_count" INTEGER NOT NULL DEFAULT 0 + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO "\#(unescaped: table)" + // ("id", "access_token", "access_expires_at", "refresh_token", "refresh_count") + // VALUES + // ( + // \#(sessionID), + // 'stale', + // (strftime('%s','now') - 300), + // 'refresh', + // 0 + // ); + // """# + // ) + // + // func getValidAccessToken(sessionID: String) async throws -> String { + // try await database.transaction { connection in + // + // let updateResult = try await connection.execute( + // query: #""" + // UPDATE "\#(unescaped: table)" + // SET + // "refresh_count" = "refresh_count" + 1, + // "access_token" = 'token_' || ("refresh_count" + 1), + // "access_expires_at" = (strftime('%s','now') + 600) + // WHERE + // "id" = \#(sessionID) + // AND "access_expires_at" + // <= (strftime('%s','now') + 60) + // RETURNING "access_token"; + // """# + // ) + // let updatedRows = try await updateResult.collect() + // if let updatedRow = updatedRows.first { + // return try updatedRow.decode( + // column: "access_token", + // as: String.self + // ) + // } + // + // let result = try await connection.execute( + // query: #""" + // SELECT + // "access_token", + // "refresh_count", + // "access_expires_at" > (strftime('%s','now') + 60) AS "is_valid" + // FROM "\#(unescaped: table)" + // WHERE "id" = \#(sessionID); + // """# + // ) + // let rows = try await result.collect() + // + // guard let row = rows.first else { + // throw TestError.missingRow + // } + // + // let isValid = try row.decode( + // column: "is_valid", + // as: Bool.self + // ) + // #expect(isValid == true) + // + // return try row.decode( + // column: "access_token", + // as: String.self + // ) + // } + // } + // + // let workerCount = 80 + // var tokens: [String] = [] + // try await withThrowingTaskGroup(of: String.self) { group in + // for _ in 0.. strftime('%s','now') AS "is_valid" + // FROM "\#(unescaped: table)" + // WHERE "id" = \#(sessionID); + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "refresh_count", as: Int.self) + // == 1 + // ) + // #expect( + // try result[0].decode(column: "access_token", as: String.self) + // == "token_1" + // ) + // #expect( + // try result[0].decode(column: "is_valid", as: Bool.self) + // == true + // ) + // } + // } } diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift index 53f28a6..9b75556 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift @@ -5,755 +5,755 @@ // Created by Tibor Bödecs on 2026. 01. 10.. // // -//import FeatherDatabase -//import Logging -//import SQLiteNIO -//import Testing -// -//@testable import FeatherSQLiteDatabase -// -//@Suite -//struct SQLiteDatabaseTestSuite { -// -// private func runUsingTestDatabaseClient( -// _ closure: ((SQLiteDatabaseClient) async throws -> Void) -// ) async throws { -// var logger = Logger(label: "test") -// logger.logLevel = .info -// -// let configuration = SQLiteClient.Configuration( -// storage: .memory, -// logger: logger -// ) -// -// let client = SQLiteClient(configuration: configuration) -// -// let database = SQLiteDatabaseClient(client: client) -// -// try await client.run() -// try await closure(database) -// await client.shutdown() -// } -// -// @Test -// func foreignKeySupport() async throws { -// try await runUsingTestDatabaseClient { database in -// -// let result = -// try await database.execute( -// query: "PRAGMA foreign_keys" -// ) -// .collect() -// -// #expect(result.count == 1) -// #expect( -// try result[0].decode(column: "foreign_keys", as: Int.self) == 1 -// ) -// } -// } -// -// @Test -// func tableCreation() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE IF NOT EXISTS "galaxies" ( -// "id" INTEGER PRIMARY KEY, -// "name" TEXT -// ); -// """# -// ) -// -// let results = try await database.execute( -// query: #""" -// SELECT name -// FROM sqlite_master -// WHERE type = 'table' -// ORDER BY name; -// """# -// ) -// -// let resultArray = try await results.collect() -// #expect(resultArray.count == 1) -// -// let item = resultArray[0] -// let name = try item.decode(column: "name", as: String.self) -// #expect(name == "galaxies") -// } -// } -// -// @Test -// func tableInsert() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE IF NOT EXISTS "galaxies" ( -// "id" INTEGER PRIMARY KEY, -// "name" TEXT -// ); -// """# -// ) -// -// let name1 = "Andromeda" -// let name2 = "Milky Way" -// -// try await database.execute( -// query: #""" -// INSERT INTO "galaxies" -// ("id", "name") -// VALUES -// (\#(nil), \#(name1)), -// (\#(nil), \#(name2)); -// """# -// ) -// -// let results = try await database.execute( -// query: #""" -// SELECT * FROM "galaxies" ORDER BY "name" ASC; -// """# -// ) -// -// let resultArray = try await results.collect() -// #expect(resultArray.count == 2) -// -// let item1 = resultArray[0] -// let name1result = try item1.decode(column: "name", as: String.self) -// #expect(name1result == name1) -// -// let item2 = resultArray[1] -// let name2result = try item2.decode(column: "name", as: String.self) -// #expect(name2result == name2) -// } -// } -// -// @Test -// func rowDecoding() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "foo" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "value" TEXT -// ); -// """# -// ) -// -// try await database.execute( -// query: #""" -// INSERT INTO "foo" -// ("id", "value") -// VALUES -// (1, 'abc'), -// (2, NULL); -// """# -// ) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "id", "value" -// FROM "foo" -// ORDER BY "id"; -// """# -// ) -// .collect() -// -// #expect(result.count == 2) -// -// let item1 = result[0] -// let item2 = result[1] -// -// #expect(try item1.decode(column: "id", as: Int.self) == 1) -// #expect(try item2.decode(column: "id", as: Int.self) == 2) -// -// #expect(try item1.decode(column: "id", as: Int?.self) == .some(1)) -// #expect((try? item1.decode(column: "value", as: Int?.self)) == nil) -// -// #expect(try item1.decode(column: "value", as: String.self) == "abc") -// #expect( -// (try? item2.decode(column: "value", as: String.self)) == nil -// ) -// -// #expect( -// (try item1.decode(column: "value", as: String?.self)) -// == .some("abc") -// ) -// #expect( -// (try item2.decode(column: "value", as: String?.self)) == .none -// ) -// } -// } -// -// @Test -// func queryEncoding() async throws { -// try await runUsingTestDatabaseClient { database in -// -// let tableName = "foo" -// let idColumn = "id" -// let valueColumn = "value" -// let row1: (Int, String?) = (1, "abc") -// let row2: (Int, String?) = (2, nil) -// -// try await database.execute( -// query: #""" -// CREATE TABLE \#(unescaped: tableName) ( -// \#(unescaped: idColumn) INTEGER NOT NULL PRIMARY KEY, -// \#(unescaped: valueColumn) TEXT -// ); -// """# -// ) -// -// try await database.execute( -// query: #""" -// INSERT INTO \#(unescaped: tableName) -// (\#(unescaped: idColumn), \#(unescaped: valueColumn)) -// VALUES -// (\#(row1.0), \#(row1.1)), -// (\#(row2.0), \#(row2.1)); -// """# -// ) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT \#(unescaped: idColumn), \#(unescaped: valueColumn) -// FROM \#(unescaped: tableName) -// ORDER BY \#(unescaped: idColumn) ASC; -// """# -// ) -// .collect() -// -// #expect(result.count == 2) -// -// let item1 = result[0] -// let item2 = result[1] -// -// #expect(try item1.decode(column: "id", as: Int.self) == 1) -// #expect(try item2.decode(column: "id", as: Int.self) == 2) -// -// #expect( -// try item1.decode(column: "value", as: String?.self) == "abc" -// ) -// #expect(try item2.decode(column: "value", as: String?.self) == nil) -// } -// } -// -// @Test -// func unsafeSQLBindings() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "widgets" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "name" TEXT NOT NULL -// ); -// """# -// ) -// -// let insert = SQLiteDatabaseQuery( -// unsafeSQL: #""" -// INSERT INTO "widgets" -// ("id", "name") -// VALUES -// (?, ?); -// """#, -// bindings: [.integer(1), .text("gizmo")] -// ) -// -// try await database.execute(query: insert) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "name" -// FROM "widgets" -// WHERE "id" = 1; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// #expect( -// try result[0].decode(column: "name", as: String.self) == "gizmo" -// ) -// } -// } -// -// @Test -// func optionalStringInterpolationNil() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "notes" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "body" TEXT -// ); -// """# -// ) -// -// let body: String? = nil -// let insert: SQLiteDatabaseQuery = #""" -// INSERT INTO "notes" -// ("id", "body") -// VALUES -// (1, \#(body)); -// """# -// -// try await database.execute(query: insert) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "body" -// FROM "notes" -// WHERE "id" = 1; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// #expect( -// try result[0].decode(column: "body", as: String?.self) == nil -// ) -// } -// } -// -// @Test -// func sqliteDataInterpolation() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "tags" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "label" TEXT NOT NULL -// ); -// """# -// ) -// -// let label: SQLiteData = .text("alpha") -// let insert: SQLiteDatabaseQuery = #""" -// INSERT INTO "tags" -// ("id", "label") -// VALUES -// (1, \#(label)); -// """# -// -// try await database.execute(query: insert) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "label" -// FROM "tags" -// WHERE "id" = 1; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// #expect( -// try result[0].decode(column: "label", as: String.self) -// == "alpha" -// ) -// } -// } -// -// @Test -// func resultSequenceIterator() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "numbers" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "value" TEXT NOT NULL -// ); -// """# -// ) -// -// try await database.execute( -// query: #""" -// INSERT INTO "numbers" -// ("id", "value") -// VALUES -// (1, 'one'), -// (2, 'two'); -// """# -// ) -// -// let result = try await database.execute( -// query: #""" -// SELECT "id", "value" -// FROM "numbers" -// ORDER BY "id"; -// """# -// ) -// -// var iterator = result.makeAsyncIterator() -// let first = await iterator.next() -// let second = await iterator.next() -// let third = await iterator.next() -// -// #expect(first != nil) -// #expect(second != nil) -// #expect(third == nil) -// -// if let first { -// #expect(try first.decode(column: "id", as: Int.self) == 1) -// #expect( -// try first.decode(column: "value", as: String.self) == "one" -// ) -// } -// else { -// Issue.record("Expected first iterator element to exist.") -// } -// -// if let second { -// #expect(try second.decode(column: "id", as: Int.self) == 2) -// #expect( -// try second.decode(column: "value", as: String.self) == "two" -// ) -// } -// else { -// Issue.record("Expected second iterator element to exist.") -// } -// } -// } -// -// @Test -// func collectFirstReturnsFirstRow() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "widgets" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "name" TEXT NOT NULL -// ); -// """# -// ) -// -// try await database.execute( -// query: #""" -// INSERT INTO "widgets" -// ("id", "name") -// VALUES -// (1, 'alpha'), -// (2, 'beta'); -// """# -// ) -// -// let result = try await database.execute( -// query: #""" -// SELECT "name" -// FROM "widgets" -// ORDER BY "id" ASC; -// """# -// ) -// -// let first = try await result.collectFirst() -// -// #expect(first != nil) -// #expect( -// try first?.decode(column: "name", as: String.self) == "alpha" -// ) -// } -// } -// -// @Test -// func transactionSuccess() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "items" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "name" TEXT NOT NULL -// ); -// """# -// ) -// -// try await database.transaction { connection in -// try await connection.execute( -// query: #""" -// INSERT INTO "items" -// ("id", "name") -// VALUES -// (1, 'widget'); -// """# -// ) -// } -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "name" -// FROM "items" -// WHERE "id" = 1; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// #expect( -// try result[0].decode(column: "name", as: String.self) -// == "widget" -// ) -// } -// } -// -// @Test -// func transactionFailurePropagates() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "dummy" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "name" TEXT NOT NULL -// ); -// """# -// ) -// -// do { -// _ = try await database.transaction { connection in -// try await connection.execute( -// query: #""" -// INSERT INTO "dummy" -// ("id", "name") -// VALUES -// (1, 'ok'); -// """# -// ) -// -// return try await connection.execute( -// query: #""" -// INSERT INTO "dummy" -// ("id", "name") -// VALUES -// (2, NULL); -// """# -// ) -// } -// Issue.record( -// "Expected database transaction error to be thrown." -// ) -// } -// catch DatabaseError.transaction(let error) { -// #expect(error.beginError == nil) -// #expect(error.closureError != nil) -// #expect( -// error.closureError.debugDescription.contains( -// "NOT NULL constraint failed" -// ) -// ) -// #expect(error.rollbackError == nil) -// #expect(error.commitError == nil) -// } -// catch { -// Issue.record( -// "Expected database transaction error to be thrown." -// ) -// } -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "id" -// FROM "dummy"; -// """# -// ) -// .collect() -// -// #expect(result.isEmpty) -// } -// } -// -// @Test -// func doubleRoundTrip() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "measurements" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "value" REAL NOT NULL -// ); -// """# -// ) -// -// let expected = 1.5 -// -// try await database.execute( -// query: #""" -// INSERT INTO "measurements" -// ("id", "value") -// VALUES -// (1, \#(expected)); -// """# -// ) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "value" -// FROM "measurements" -// WHERE "id" = 1; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// #expect( -// try result[0].decode(column: "value", as: Double.self) -// == expected -// ) -// } -// } -// -// @Test -// func missingColumnThrows() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "items" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "value" TEXT -// ); -// """# -// ) -// -// try await database.execute( -// query: #""" -// INSERT INTO "items" -// ("id", "value") -// VALUES -// (1, 'abc'); -// """# -// ) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "id" -// FROM "items"; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// -// do { -// _ = try result[0].decode(column: "value", as: String.self) -// Issue.record("Expected decoding a missing column to throw.") -// } -// catch DecodingError.dataCorrupted { -// -// } -// catch { -// Issue.record( -// "Expected a dataCorrupted error for missing column." -// ) -// } -// } -// } -// -// @Test -// func typeMismatchThrows() async throws { -// try await runUsingTestDatabaseClient { database in -// -// try await database.execute( -// query: #""" -// CREATE TABLE "items" ( -// "id" INTEGER NOT NULL PRIMARY KEY, -// "value" TEXT -// ); -// """# -// ) -// -// try await database.execute( -// query: #""" -// INSERT INTO "items" -// ("id", "value") -// VALUES -// (1, 'abc'); -// """# -// ) -// -// let result = -// try await database.execute( -// query: #""" -// SELECT "value" -// FROM "items"; -// """# -// ) -// .collect() -// -// #expect(result.count == 1) -// -// do { -// _ = try result[0].decode(column: "value", as: Int.self) -// Issue.record("Expected decoding a string as Int to throw.") -// } -// catch DecodingError.typeMismatch { -// -// } -// catch { -// Issue.record( -// "Expected a typeMismatch error when decoding a string as Int." -// ) -// } -// } -// } -// -// @Test -// func queryFailureErrorText() async throws { -// try await runUsingTestDatabaseClient { database in -// -// do { -// _ = try await database.execute( -// query: #""" -// SELECT * -// FROM "missing_table"; -// """# -// ) -// Issue.record("Expected query to fail for missing table.") -// } -// catch DatabaseError.query(let error) { -// #expect("\(error)".contains("no such table")) -// } -// catch { -// Issue.record("Expected database query error to be thrown.") -// } -// } -// } -// -// @Test -// func versionCheck() async throws { -// try await runUsingTestDatabaseClient { database in -// -// let result = try await database.execute( -// query: #""" -// SELECT -// sqlite_version() AS "version" -// WHERE -// 1=\#(1); -// """# -// ) -// -// let resultArray = try await result.collect() -// #expect(resultArray.count == 1) -// -// let item = resultArray[0] -// let version = try item.decode(column: "version", as: String.self) -// #expect(version.split(separator: ".").count == 3) -// } -// } -//} +import FeatherDatabase +import Logging +import SQLiteNIO +import Testing + +@testable import FeatherSQLiteDatabase + +@Suite +struct SQLiteDatabaseTestSuite { + // + // private func runUsingTestDatabaseClient( + // _ closure: ((SQLiteDatabaseClient) async throws -> Void) + // ) async throws { + // var logger = Logger(label: "test") + // logger.logLevel = .info + // + // let configuration = SQLiteClient.Configuration( + // storage: .memory, + // logger: logger + // ) + // + // let client = SQLiteClient(configuration: configuration) + // + // let database = SQLiteDatabaseClient(client: client) + // + // try await client.run() + // try await closure(database) + // await client.shutdown() + // } + // + // @Test + // func foreignKeySupport() async throws { + // try await runUsingTestDatabaseClient { database in + // + // let result = + // try await database.execute( + // query: "PRAGMA foreign_keys" + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "foreign_keys", as: Int.self) == 1 + // ) + // } + // } + // + // @Test + // func tableCreation() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE IF NOT EXISTS "galaxies" ( + // "id" INTEGER PRIMARY KEY, + // "name" TEXT + // ); + // """# + // ) + // + // let results = try await database.execute( + // query: #""" + // SELECT name + // FROM sqlite_master + // WHERE type = 'table' + // ORDER BY name; + // """# + // ) + // + // let resultArray = try await results.collect() + // #expect(resultArray.count == 1) + // + // let item = resultArray[0] + // let name = try item.decode(column: "name", as: String.self) + // #expect(name == "galaxies") + // } + // } + // + // @Test + // func tableInsert() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE IF NOT EXISTS "galaxies" ( + // "id" INTEGER PRIMARY KEY, + // "name" TEXT + // ); + // """# + // ) + // + // let name1 = "Andromeda" + // let name2 = "Milky Way" + // + // try await database.execute( + // query: #""" + // INSERT INTO "galaxies" + // ("id", "name") + // VALUES + // (\#(nil), \#(name1)), + // (\#(nil), \#(name2)); + // """# + // ) + // + // let results = try await database.execute( + // query: #""" + // SELECT * FROM "galaxies" ORDER BY "name" ASC; + // """# + // ) + // + // let resultArray = try await results.collect() + // #expect(resultArray.count == 2) + // + // let item1 = resultArray[0] + // let name1result = try item1.decode(column: "name", as: String.self) + // #expect(name1result == name1) + // + // let item2 = resultArray[1] + // let name2result = try item2.decode(column: "name", as: String.self) + // #expect(name2result == name2) + // } + // } + // + // @Test + // func rowDecoding() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "foo" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "value" TEXT + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO "foo" + // ("id", "value") + // VALUES + // (1, 'abc'), + // (2, NULL); + // """# + // ) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "id", "value" + // FROM "foo" + // ORDER BY "id"; + // """# + // ) + // .collect() + // + // #expect(result.count == 2) + // + // let item1 = result[0] + // let item2 = result[1] + // + // #expect(try item1.decode(column: "id", as: Int.self) == 1) + // #expect(try item2.decode(column: "id", as: Int.self) == 2) + // + // #expect(try item1.decode(column: "id", as: Int?.self) == .some(1)) + // #expect((try? item1.decode(column: "value", as: Int?.self)) == nil) + // + // #expect(try item1.decode(column: "value", as: String.self) == "abc") + // #expect( + // (try? item2.decode(column: "value", as: String.self)) == nil + // ) + // + // #expect( + // (try item1.decode(column: "value", as: String?.self)) + // == .some("abc") + // ) + // #expect( + // (try item2.decode(column: "value", as: String?.self)) == .none + // ) + // } + // } + // + // @Test + // func queryEncoding() async throws { + // try await runUsingTestDatabaseClient { database in + // + // let tableName = "foo" + // let idColumn = "id" + // let valueColumn = "value" + // let row1: (Int, String?) = (1, "abc") + // let row2: (Int, String?) = (2, nil) + // + // try await database.execute( + // query: #""" + // CREATE TABLE \#(unescaped: tableName) ( + // \#(unescaped: idColumn) INTEGER NOT NULL PRIMARY KEY, + // \#(unescaped: valueColumn) TEXT + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO \#(unescaped: tableName) + // (\#(unescaped: idColumn), \#(unescaped: valueColumn)) + // VALUES + // (\#(row1.0), \#(row1.1)), + // (\#(row2.0), \#(row2.1)); + // """# + // ) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT \#(unescaped: idColumn), \#(unescaped: valueColumn) + // FROM \#(unescaped: tableName) + // ORDER BY \#(unescaped: idColumn) ASC; + // """# + // ) + // .collect() + // + // #expect(result.count == 2) + // + // let item1 = result[0] + // let item2 = result[1] + // + // #expect(try item1.decode(column: "id", as: Int.self) == 1) + // #expect(try item2.decode(column: "id", as: Int.self) == 2) + // + // #expect( + // try item1.decode(column: "value", as: String?.self) == "abc" + // ) + // #expect(try item2.decode(column: "value", as: String?.self) == nil) + // } + // } + // + // @Test + // func unsafeSQLBindings() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "widgets" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "name" TEXT NOT NULL + // ); + // """# + // ) + // + // let insert = SQLiteDatabaseQuery( + // unsafeSQL: #""" + // INSERT INTO "widgets" + // ("id", "name") + // VALUES + // (?, ?); + // """#, + // bindings: [.integer(1), .text("gizmo")] + // ) + // + // try await database.execute(query: insert) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "name" + // FROM "widgets" + // WHERE "id" = 1; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "name", as: String.self) == "gizmo" + // ) + // } + // } + // + // @Test + // func optionalStringInterpolationNil() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "notes" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "body" TEXT + // ); + // """# + // ) + // + // let body: String? = nil + // let insert: SQLiteDatabaseQuery = #""" + // INSERT INTO "notes" + // ("id", "body") + // VALUES + // (1, \#(body)); + // """# + // + // try await database.execute(query: insert) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "body" + // FROM "notes" + // WHERE "id" = 1; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "body", as: String?.self) == nil + // ) + // } + // } + // + // @Test + // func sqliteDataInterpolation() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "tags" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "label" TEXT NOT NULL + // ); + // """# + // ) + // + // let label: SQLiteData = .text("alpha") + // let insert: SQLiteDatabaseQuery = #""" + // INSERT INTO "tags" + // ("id", "label") + // VALUES + // (1, \#(label)); + // """# + // + // try await database.execute(query: insert) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "label" + // FROM "tags" + // WHERE "id" = 1; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "label", as: String.self) + // == "alpha" + // ) + // } + // } + // + // @Test + // func resultSequenceIterator() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "numbers" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "value" TEXT NOT NULL + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO "numbers" + // ("id", "value") + // VALUES + // (1, 'one'), + // (2, 'two'); + // """# + // ) + // + // let result = try await database.execute( + // query: #""" + // SELECT "id", "value" + // FROM "numbers" + // ORDER BY "id"; + // """# + // ) + // + // var iterator = result.makeAsyncIterator() + // let first = await iterator.next() + // let second = await iterator.next() + // let third = await iterator.next() + // + // #expect(first != nil) + // #expect(second != nil) + // #expect(third == nil) + // + // if let first { + // #expect(try first.decode(column: "id", as: Int.self) == 1) + // #expect( + // try first.decode(column: "value", as: String.self) == "one" + // ) + // } + // else { + // Issue.record("Expected first iterator element to exist.") + // } + // + // if let second { + // #expect(try second.decode(column: "id", as: Int.self) == 2) + // #expect( + // try second.decode(column: "value", as: String.self) == "two" + // ) + // } + // else { + // Issue.record("Expected second iterator element to exist.") + // } + // } + // } + // + // @Test + // func collectFirstReturnsFirstRow() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "widgets" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "name" TEXT NOT NULL + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO "widgets" + // ("id", "name") + // VALUES + // (1, 'alpha'), + // (2, 'beta'); + // """# + // ) + // + // let result = try await database.execute( + // query: #""" + // SELECT "name" + // FROM "widgets" + // ORDER BY "id" ASC; + // """# + // ) + // + // let first = try await result.collectFirst() + // + // #expect(first != nil) + // #expect( + // try first?.decode(column: "name", as: String.self) == "alpha" + // ) + // } + // } + // + // @Test + // func transactionSuccess() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "items" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "name" TEXT NOT NULL + // ); + // """# + // ) + // + // try await database.transaction { connection in + // try await connection.execute( + // query: #""" + // INSERT INTO "items" + // ("id", "name") + // VALUES + // (1, 'widget'); + // """# + // ) + // } + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "name" + // FROM "items" + // WHERE "id" = 1; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "name", as: String.self) + // == "widget" + // ) + // } + // } + // + // @Test + // func transactionFailurePropagates() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "dummy" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "name" TEXT NOT NULL + // ); + // """# + // ) + // + // do { + // _ = try await database.transaction { connection in + // try await connection.execute( + // query: #""" + // INSERT INTO "dummy" + // ("id", "name") + // VALUES + // (1, 'ok'); + // """# + // ) + // + // return try await connection.execute( + // query: #""" + // INSERT INTO "dummy" + // ("id", "name") + // VALUES + // (2, NULL); + // """# + // ) + // } + // Issue.record( + // "Expected database transaction error to be thrown." + // ) + // } + // catch DatabaseError.transaction(let error) { + // #expect(error.beginError == nil) + // #expect(error.closureError != nil) + // #expect( + // error.closureError.debugDescription.contains( + // "NOT NULL constraint failed" + // ) + // ) + // #expect(error.rollbackError == nil) + // #expect(error.commitError == nil) + // } + // catch { + // Issue.record( + // "Expected database transaction error to be thrown." + // ) + // } + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "id" + // FROM "dummy"; + // """# + // ) + // .collect() + // + // #expect(result.isEmpty) + // } + // } + // + // @Test + // func doubleRoundTrip() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "measurements" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "value" REAL NOT NULL + // ); + // """# + // ) + // + // let expected = 1.5 + // + // try await database.execute( + // query: #""" + // INSERT INTO "measurements" + // ("id", "value") + // VALUES + // (1, \#(expected)); + // """# + // ) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "value" + // FROM "measurements" + // WHERE "id" = 1; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // #expect( + // try result[0].decode(column: "value", as: Double.self) + // == expected + // ) + // } + // } + // + // @Test + // func missingColumnThrows() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "items" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "value" TEXT + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO "items" + // ("id", "value") + // VALUES + // (1, 'abc'); + // """# + // ) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "id" + // FROM "items"; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // + // do { + // _ = try result[0].decode(column: "value", as: String.self) + // Issue.record("Expected decoding a missing column to throw.") + // } + // catch DecodingError.dataCorrupted { + // + // } + // catch { + // Issue.record( + // "Expected a dataCorrupted error for missing column." + // ) + // } + // } + // } + // + // @Test + // func typeMismatchThrows() async throws { + // try await runUsingTestDatabaseClient { database in + // + // try await database.execute( + // query: #""" + // CREATE TABLE "items" ( + // "id" INTEGER NOT NULL PRIMARY KEY, + // "value" TEXT + // ); + // """# + // ) + // + // try await database.execute( + // query: #""" + // INSERT INTO "items" + // ("id", "value") + // VALUES + // (1, 'abc'); + // """# + // ) + // + // let result = + // try await database.execute( + // query: #""" + // SELECT "value" + // FROM "items"; + // """# + // ) + // .collect() + // + // #expect(result.count == 1) + // + // do { + // _ = try result[0].decode(column: "value", as: Int.self) + // Issue.record("Expected decoding a string as Int to throw.") + // } + // catch DecodingError.typeMismatch { + // + // } + // catch { + // Issue.record( + // "Expected a typeMismatch error when decoding a string as Int." + // ) + // } + // } + // } + // + // @Test + // func queryFailureErrorText() async throws { + // try await runUsingTestDatabaseClient { database in + // + // do { + // _ = try await database.execute( + // query: #""" + // SELECT * + // FROM "missing_table"; + // """# + // ) + // Issue.record("Expected query to fail for missing table.") + // } + // catch DatabaseError.query(let error) { + // #expect("\(error)".contains("no such table")) + // } + // catch { + // Issue.record("Expected database query error to be thrown.") + // } + // } + // } + // + // @Test + // func versionCheck() async throws { + // try await runUsingTestDatabaseClient { database in + // + // let result = try await database.execute( + // query: #""" + // SELECT + // sqlite_version() AS "version" + // WHERE + // 1=\#(1); + // """# + // ) + // + // let resultArray = try await result.collect() + // #expect(resultArray.count == 1) + // + // let item = resultArray[0] + // let version = try item.decode(column: "version", as: String.self) + // #expect(version.split(separator: ".").count == 3) + // } + // } +} diff --git a/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift b/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift new file mode 100644 index 0000000..0d5c341 --- /dev/null +++ b/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift @@ -0,0 +1,68 @@ +// +// SQLiteNIOExtrasTestSuite.swift +// feather-sqlite-database +// +// Created by Tibor Bödecs on 2026. 01. 10.. +// +// +import Logging +import SQLiteNIO +import Testing + +@testable import SQLiteNIOExtras + +@Suite +struct SQLiteNIOExtrasTestSuite { + + private func makeTemporaryDatabasePath() -> String { + let suffix = UInt64.random(in: 0...UInt64.max) + return "/tmp/feather-sqlite-\(suffix).sqlite" + } + + private func randomTableSuffix() -> String { + String(UInt64.random(in: 0...UInt64.max)) + } + + private func runUsingTestClient( + _ closure: (SQLiteClient) async throws -> Void + ) async throws { + var logger = Logger(label: "test.sqlite.client") + logger.logLevel = .info + + let configuration = SQLiteClient.Configuration( + storage: .file(path: makeTemporaryDatabasePath()), + logger: logger, + ) + let client = SQLiteClient(configuration: configuration) + + try await client.run() + defer { Task { await client.shutdown() } } + + try await closure(client) + } + + // private func runUsingTestDatabaseClient( + // _ closure: ((SQLiteDatabaseClient) async throws -> Void) + // ) async throws { + // var logger = Logger(label: "test") + // logger.logLevel = .info + // + // let configuration = SQLiteClient.Configuration( + // storage: .memory, + // logger: logger + // ) + // + // let client = SQLiteClient(configuration: configuration) + // + // let database = SQLiteDatabaseClient(client: client) + // + // try await client.run() + // try await closure(database) + // await client.shutdown() + // } + + @Test + func example() async throws { + + } +} From ffa61a2b8ed6d665e535b1ea3d0919827307e1ad Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Tue, 3 Feb 2026 20:21:20 +0100 Subject: [PATCH 05/11] fix all tests --- .../SQLiteClientTestSuite.swift | 253 --- .../SQLiteDatabaseTestSuite.swift | 1502 +++++++++-------- .../SQLiteNIOExtrasTestSuite.swift | 204 ++- 3 files changed, 944 insertions(+), 1015 deletions(-) delete mode 100644 Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift deleted file mode 100644 index 9dfc655..0000000 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift +++ /dev/null @@ -1,253 +0,0 @@ -// -// SQLiteClientTestSuite.swift -// feather-sqlite-database -// -// Created by Tibor Bödecs on 2026. 01. 26.. -// - -import FeatherDatabase -import Logging -import SQLiteNIO -import SQLiteNIOExtras -import Testing - -@testable import FeatherSQLiteDatabase - -@Suite -struct SQLiteClientTestSuite { - - // private func makeTemporaryDatabasePath() -> String { - // let suffix = UInt64.random(in: 0...UInt64.max) - // return "/tmp/feather-sqlite-\(suffix).sqlite" - // } - // - // private func randomTableSuffix() -> String { - // String(UInt64.random(in: 0...UInt64.max)) - // } - // - // private func runUsingTestClient( - // _ closure: (SQLiteClient) async throws -> Void - // ) async throws { - // var logger = Logger(label: "test.sqlite.client") - // logger.logLevel = .info - // - // let configuration = SQLiteClient.Configuration( - // storage: .file(path: makeTemporaryDatabasePath()), - // logger: logger, - // ) - // let client = SQLiteClient(configuration: configuration) - // - // try await client.run() - // defer { Task { await client.shutdown() } } - // - // try await closure(client) - // } - // - // @Test - // func concurrentTransactionsUseMultipleConnections() async throws { - // var logger = Logger(label: "test.sqlite.client") - // logger.logLevel = .info - // - // let configuration = SQLiteClient.Configuration( - // storage: .file(path: makeTemporaryDatabasePath()), - // logger: logger - // ) - // let client = SQLiteClient(configuration: configuration) - // - // try await client.run() - // - // try await client.execute( - // query: #""" - // CREATE TABLE "items" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "name" TEXT NOT NULL - // ); - // """# - // ) - // - // async let first: Void = client.transaction { connection in - // - // try await connection.execute( - // query: #""" - // INSERT INTO "items" - // ("id", "name") - // VALUES - // (1, 'alpha'); - // """# - // ) - // } - // - // async let second: Void = client.transaction { connection in - // try await connection.execute( - // query: #""" - // INSERT INTO "items" - // ("id", "name") - // VALUES - // (2, 'beta'); - // """# - // ) - // } - // - // do { - // - // _ = try await (first, second) - // - // let result = try await client.execute( - // query: #""" - // SELECT COUNT(*) AS "count" - // FROM "items"; - // """# - // ) - // let rows = try await result.collect() - // - // #expect(try rows[0].decode(column: "count", as: Int.self) == 2) - // #expect(await client.connectionCount() == 2) - // } - // catch { - // Issue.record(error) - // } - // - // await client.shutdown() - // } - // - // @Test - // func concurrentTransactionUpdates() async throws { - // try await runUsingTestClient { database in - // let suffix = randomTableSuffix() - // let table = "sessions_\(suffix)" - // let sessionID = "session_\(suffix)" - // - // enum TestError: Error { - // case missingRow - // } - // - // try await database.execute( - // query: #""" - // DROP TABLE IF EXISTS "\#(unescaped: table)"; - // """# - // ) - // try await database.execute( - // query: #""" - // CREATE TABLE "\#(unescaped: table)" ( - // "id" TEXT NOT NULL PRIMARY KEY, - // "access_token" TEXT NOT NULL, - // "access_expires_at" INTEGER NOT NULL, - // "refresh_token" TEXT NOT NULL, - // "refresh_count" INTEGER NOT NULL DEFAULT 0 - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO "\#(unescaped: table)" - // ("id", "access_token", "access_expires_at", "refresh_token", "refresh_count") - // VALUES - // ( - // \#(sessionID), - // 'stale', - // (strftime('%s','now') - 300), - // 'refresh', - // 0 - // ); - // """# - // ) - // - // func getValidAccessToken(sessionID: String) async throws -> String { - // try await database.transaction { connection in - // - // let updateResult = try await connection.execute( - // query: #""" - // UPDATE "\#(unescaped: table)" - // SET - // "refresh_count" = "refresh_count" + 1, - // "access_token" = 'token_' || ("refresh_count" + 1), - // "access_expires_at" = (strftime('%s','now') + 600) - // WHERE - // "id" = \#(sessionID) - // AND "access_expires_at" - // <= (strftime('%s','now') + 60) - // RETURNING "access_token"; - // """# - // ) - // let updatedRows = try await updateResult.collect() - // if let updatedRow = updatedRows.first { - // return try updatedRow.decode( - // column: "access_token", - // as: String.self - // ) - // } - // - // let result = try await connection.execute( - // query: #""" - // SELECT - // "access_token", - // "refresh_count", - // "access_expires_at" > (strftime('%s','now') + 60) AS "is_valid" - // FROM "\#(unescaped: table)" - // WHERE "id" = \#(sessionID); - // """# - // ) - // let rows = try await result.collect() - // - // guard let row = rows.first else { - // throw TestError.missingRow - // } - // - // let isValid = try row.decode( - // column: "is_valid", - // as: Bool.self - // ) - // #expect(isValid == true) - // - // return try row.decode( - // column: "access_token", - // as: String.self - // ) - // } - // } - // - // let workerCount = 80 - // var tokens: [String] = [] - // try await withThrowingTaskGroup(of: String.self) { group in - // for _ in 0.. strftime('%s','now') AS "is_valid" - // FROM "\#(unescaped: table)" - // WHERE "id" = \#(sessionID); - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "refresh_count", as: Int.self) - // == 1 - // ) - // #expect( - // try result[0].decode(column: "access_token", as: String.self) - // == "token_1" - // ) - // #expect( - // try result[0].decode(column: "is_valid", as: Bool.self) - // == true - // ) - // } - // } -} diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift index 9b75556..53a55f7 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift @@ -8,752 +8,770 @@ import FeatherDatabase import Logging import SQLiteNIO +import SQLiteNIOExtras import Testing @testable import FeatherSQLiteDatabase @Suite struct SQLiteDatabaseTestSuite { - // - // private func runUsingTestDatabaseClient( - // _ closure: ((SQLiteDatabaseClient) async throws -> Void) - // ) async throws { - // var logger = Logger(label: "test") - // logger.logLevel = .info - // - // let configuration = SQLiteClient.Configuration( - // storage: .memory, - // logger: logger - // ) - // - // let client = SQLiteClient(configuration: configuration) - // - // let database = SQLiteDatabaseClient(client: client) - // - // try await client.run() - // try await closure(database) - // await client.shutdown() - // } - // - // @Test - // func foreignKeySupport() async throws { - // try await runUsingTestDatabaseClient { database in - // - // let result = - // try await database.execute( - // query: "PRAGMA foreign_keys" - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "foreign_keys", as: Int.self) == 1 - // ) - // } - // } - // - // @Test - // func tableCreation() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE IF NOT EXISTS "galaxies" ( - // "id" INTEGER PRIMARY KEY, - // "name" TEXT - // ); - // """# - // ) - // - // let results = try await database.execute( - // query: #""" - // SELECT name - // FROM sqlite_master - // WHERE type = 'table' - // ORDER BY name; - // """# - // ) - // - // let resultArray = try await results.collect() - // #expect(resultArray.count == 1) - // - // let item = resultArray[0] - // let name = try item.decode(column: "name", as: String.self) - // #expect(name == "galaxies") - // } - // } - // - // @Test - // func tableInsert() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE IF NOT EXISTS "galaxies" ( - // "id" INTEGER PRIMARY KEY, - // "name" TEXT - // ); - // """# - // ) - // - // let name1 = "Andromeda" - // let name2 = "Milky Way" - // - // try await database.execute( - // query: #""" - // INSERT INTO "galaxies" - // ("id", "name") - // VALUES - // (\#(nil), \#(name1)), - // (\#(nil), \#(name2)); - // """# - // ) - // - // let results = try await database.execute( - // query: #""" - // SELECT * FROM "galaxies" ORDER BY "name" ASC; - // """# - // ) - // - // let resultArray = try await results.collect() - // #expect(resultArray.count == 2) - // - // let item1 = resultArray[0] - // let name1result = try item1.decode(column: "name", as: String.self) - // #expect(name1result == name1) - // - // let item2 = resultArray[1] - // let name2result = try item2.decode(column: "name", as: String.self) - // #expect(name2result == name2) - // } - // } - // - // @Test - // func rowDecoding() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "foo" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "value" TEXT - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO "foo" - // ("id", "value") - // VALUES - // (1, 'abc'), - // (2, NULL); - // """# - // ) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "id", "value" - // FROM "foo" - // ORDER BY "id"; - // """# - // ) - // .collect() - // - // #expect(result.count == 2) - // - // let item1 = result[0] - // let item2 = result[1] - // - // #expect(try item1.decode(column: "id", as: Int.self) == 1) - // #expect(try item2.decode(column: "id", as: Int.self) == 2) - // - // #expect(try item1.decode(column: "id", as: Int?.self) == .some(1)) - // #expect((try? item1.decode(column: "value", as: Int?.self)) == nil) - // - // #expect(try item1.decode(column: "value", as: String.self) == "abc") - // #expect( - // (try? item2.decode(column: "value", as: String.self)) == nil - // ) - // - // #expect( - // (try item1.decode(column: "value", as: String?.self)) - // == .some("abc") - // ) - // #expect( - // (try item2.decode(column: "value", as: String?.self)) == .none - // ) - // } - // } - // - // @Test - // func queryEncoding() async throws { - // try await runUsingTestDatabaseClient { database in - // - // let tableName = "foo" - // let idColumn = "id" - // let valueColumn = "value" - // let row1: (Int, String?) = (1, "abc") - // let row2: (Int, String?) = (2, nil) - // - // try await database.execute( - // query: #""" - // CREATE TABLE \#(unescaped: tableName) ( - // \#(unescaped: idColumn) INTEGER NOT NULL PRIMARY KEY, - // \#(unescaped: valueColumn) TEXT - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO \#(unescaped: tableName) - // (\#(unescaped: idColumn), \#(unescaped: valueColumn)) - // VALUES - // (\#(row1.0), \#(row1.1)), - // (\#(row2.0), \#(row2.1)); - // """# - // ) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT \#(unescaped: idColumn), \#(unescaped: valueColumn) - // FROM \#(unescaped: tableName) - // ORDER BY \#(unescaped: idColumn) ASC; - // """# - // ) - // .collect() - // - // #expect(result.count == 2) - // - // let item1 = result[0] - // let item2 = result[1] - // - // #expect(try item1.decode(column: "id", as: Int.self) == 1) - // #expect(try item2.decode(column: "id", as: Int.self) == 2) - // - // #expect( - // try item1.decode(column: "value", as: String?.self) == "abc" - // ) - // #expect(try item2.decode(column: "value", as: String?.self) == nil) - // } - // } - // - // @Test - // func unsafeSQLBindings() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "widgets" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "name" TEXT NOT NULL - // ); - // """# - // ) - // - // let insert = SQLiteDatabaseQuery( - // unsafeSQL: #""" - // INSERT INTO "widgets" - // ("id", "name") - // VALUES - // (?, ?); - // """#, - // bindings: [.integer(1), .text("gizmo")] - // ) - // - // try await database.execute(query: insert) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "name" - // FROM "widgets" - // WHERE "id" = 1; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "name", as: String.self) == "gizmo" - // ) - // } - // } - // - // @Test - // func optionalStringInterpolationNil() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "notes" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "body" TEXT - // ); - // """# - // ) - // - // let body: String? = nil - // let insert: SQLiteDatabaseQuery = #""" - // INSERT INTO "notes" - // ("id", "body") - // VALUES - // (1, \#(body)); - // """# - // - // try await database.execute(query: insert) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "body" - // FROM "notes" - // WHERE "id" = 1; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "body", as: String?.self) == nil - // ) - // } - // } - // - // @Test - // func sqliteDataInterpolation() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "tags" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "label" TEXT NOT NULL - // ); - // """# - // ) - // - // let label: SQLiteData = .text("alpha") - // let insert: SQLiteDatabaseQuery = #""" - // INSERT INTO "tags" - // ("id", "label") - // VALUES - // (1, \#(label)); - // """# - // - // try await database.execute(query: insert) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "label" - // FROM "tags" - // WHERE "id" = 1; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "label", as: String.self) - // == "alpha" - // ) - // } - // } - // - // @Test - // func resultSequenceIterator() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "numbers" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "value" TEXT NOT NULL - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO "numbers" - // ("id", "value") - // VALUES - // (1, 'one'), - // (2, 'two'); - // """# - // ) - // - // let result = try await database.execute( - // query: #""" - // SELECT "id", "value" - // FROM "numbers" - // ORDER BY "id"; - // """# - // ) - // - // var iterator = result.makeAsyncIterator() - // let first = await iterator.next() - // let second = await iterator.next() - // let third = await iterator.next() - // - // #expect(first != nil) - // #expect(second != nil) - // #expect(third == nil) - // - // if let first { - // #expect(try first.decode(column: "id", as: Int.self) == 1) - // #expect( - // try first.decode(column: "value", as: String.self) == "one" - // ) - // } - // else { - // Issue.record("Expected first iterator element to exist.") - // } - // - // if let second { - // #expect(try second.decode(column: "id", as: Int.self) == 2) - // #expect( - // try second.decode(column: "value", as: String.self) == "two" - // ) - // } - // else { - // Issue.record("Expected second iterator element to exist.") - // } - // } - // } - // - // @Test - // func collectFirstReturnsFirstRow() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "widgets" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "name" TEXT NOT NULL - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO "widgets" - // ("id", "name") - // VALUES - // (1, 'alpha'), - // (2, 'beta'); - // """# - // ) - // - // let result = try await database.execute( - // query: #""" - // SELECT "name" - // FROM "widgets" - // ORDER BY "id" ASC; - // """# - // ) - // - // let first = try await result.collectFirst() - // - // #expect(first != nil) - // #expect( - // try first?.decode(column: "name", as: String.self) == "alpha" - // ) - // } - // } - // - // @Test - // func transactionSuccess() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "items" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "name" TEXT NOT NULL - // ); - // """# - // ) - // - // try await database.transaction { connection in - // try await connection.execute( - // query: #""" - // INSERT INTO "items" - // ("id", "name") - // VALUES - // (1, 'widget'); - // """# - // ) - // } - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "name" - // FROM "items" - // WHERE "id" = 1; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "name", as: String.self) - // == "widget" - // ) - // } - // } - // - // @Test - // func transactionFailurePropagates() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "dummy" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "name" TEXT NOT NULL - // ); - // """# - // ) - // - // do { - // _ = try await database.transaction { connection in - // try await connection.execute( - // query: #""" - // INSERT INTO "dummy" - // ("id", "name") - // VALUES - // (1, 'ok'); - // """# - // ) - // - // return try await connection.execute( - // query: #""" - // INSERT INTO "dummy" - // ("id", "name") - // VALUES - // (2, NULL); - // """# - // ) - // } - // Issue.record( - // "Expected database transaction error to be thrown." - // ) - // } - // catch DatabaseError.transaction(let error) { - // #expect(error.beginError == nil) - // #expect(error.closureError != nil) - // #expect( - // error.closureError.debugDescription.contains( - // "NOT NULL constraint failed" - // ) - // ) - // #expect(error.rollbackError == nil) - // #expect(error.commitError == nil) - // } - // catch { - // Issue.record( - // "Expected database transaction error to be thrown." - // ) - // } - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "id" - // FROM "dummy"; - // """# - // ) - // .collect() - // - // #expect(result.isEmpty) - // } - // } - // - // @Test - // func doubleRoundTrip() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "measurements" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "value" REAL NOT NULL - // ); - // """# - // ) - // - // let expected = 1.5 - // - // try await database.execute( - // query: #""" - // INSERT INTO "measurements" - // ("id", "value") - // VALUES - // (1, \#(expected)); - // """# - // ) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "value" - // FROM "measurements" - // WHERE "id" = 1; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // #expect( - // try result[0].decode(column: "value", as: Double.self) - // == expected - // ) - // } - // } - // - // @Test - // func missingColumnThrows() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "items" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "value" TEXT - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO "items" - // ("id", "value") - // VALUES - // (1, 'abc'); - // """# - // ) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "id" - // FROM "items"; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // - // do { - // _ = try result[0].decode(column: "value", as: String.self) - // Issue.record("Expected decoding a missing column to throw.") - // } - // catch DecodingError.dataCorrupted { - // - // } - // catch { - // Issue.record( - // "Expected a dataCorrupted error for missing column." - // ) - // } - // } - // } - // - // @Test - // func typeMismatchThrows() async throws { - // try await runUsingTestDatabaseClient { database in - // - // try await database.execute( - // query: #""" - // CREATE TABLE "items" ( - // "id" INTEGER NOT NULL PRIMARY KEY, - // "value" TEXT - // ); - // """# - // ) - // - // try await database.execute( - // query: #""" - // INSERT INTO "items" - // ("id", "value") - // VALUES - // (1, 'abc'); - // """# - // ) - // - // let result = - // try await database.execute( - // query: #""" - // SELECT "value" - // FROM "items"; - // """# - // ) - // .collect() - // - // #expect(result.count == 1) - // - // do { - // _ = try result[0].decode(column: "value", as: Int.self) - // Issue.record("Expected decoding a string as Int to throw.") - // } - // catch DecodingError.typeMismatch { - // - // } - // catch { - // Issue.record( - // "Expected a typeMismatch error when decoding a string as Int." - // ) - // } - // } - // } - // - // @Test - // func queryFailureErrorText() async throws { - // try await runUsingTestDatabaseClient { database in - // - // do { - // _ = try await database.execute( - // query: #""" - // SELECT * - // FROM "missing_table"; - // """# - // ) - // Issue.record("Expected query to fail for missing table.") - // } - // catch DatabaseError.query(let error) { - // #expect("\(error)".contains("no such table")) - // } - // catch { - // Issue.record("Expected database query error to be thrown.") - // } - // } - // } - // - // @Test - // func versionCheck() async throws { - // try await runUsingTestDatabaseClient { database in - // - // let result = try await database.execute( - // query: #""" - // SELECT - // sqlite_version() AS "version" - // WHERE - // 1=\#(1); - // """# - // ) - // - // let resultArray = try await result.collect() - // #expect(resultArray.count == 1) - // - // let item = resultArray[0] - // let version = try item.decode(column: "version", as: String.self) - // #expect(version.split(separator: ".").count == 3) - // } - // } + + private func runUsingTestDatabaseClient( + _ closure: ((SQLiteDatabaseClient) async throws -> Void) + ) async throws { + var logger = Logger(label: "test") + logger.logLevel = .info + + let configuration = SQLiteClient.Configuration( + storage: .memory, + logger: logger + ) + + let client = SQLiteClient(configuration: configuration) + + let database = SQLiteDatabaseClient( + client: client, + logger: logger + ) + + try await client.run() + try await closure(database) + await client.shutdown() + } + + @Test + func foreignKeySupport() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + let result = try await connection.run( + query: "PRAGMA foreign_keys" + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "foreign_keys", as: Int.self) + == 1 + ) + } + } + } + + @Test + func tableCreation() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE IF NOT EXISTS "galaxies" ( + "id" INTEGER PRIMARY KEY, + "name" TEXT + ); + """# + ) + + let results = try await connection.run( + query: #""" + SELECT name + FROM sqlite_master + WHERE type = 'table' + ORDER BY name; + """# + ) { try await $0.collect() } + + #expect(results.count == 1) + + let item = results[0] + let name = try item.decode(column: "name", as: String.self) + #expect(name == "galaxies") + } + } + } + + @Test + func tableInsert() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE IF NOT EXISTS "galaxies" ( + "id" INTEGER PRIMARY KEY, + "name" TEXT + ); + """# + ) + + let name1 = "Andromeda" + let name2 = "Milky Way" + + try await connection.run( + query: #""" + INSERT INTO "galaxies" + ("id", "name") + VALUES + (\#(nil), \#(name1)), + (\#(nil), \#(name2)); + """# + ) + + let results = try await connection.run( + query: #""" + SELECT * FROM "galaxies" ORDER BY "name" ASC; + """# + ) { try await $0.collect() } + + #expect(results.count == 2) + + let item1 = results[0] + let name1result = try item1.decode( + column: "name", + as: String.self + ) + #expect(name1result == name1) + + let item2 = results[1] + let name2result = try item2.decode( + column: "name", + as: String.self + ) + #expect(name2result == name2) + } + } + } + + @Test + func rowDecoding() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "foo" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "foo" + ("id", "value") + VALUES + (1, 'abc'), + (2, NULL); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "id", "value" + FROM "foo" + ORDER BY "id"; + """# + ) { try await $0.collect() } + + #expect(result.count == 2) + + let item1 = result[0] + let item2 = result[1] + + #expect(try item1.decode(column: "id", as: Int.self) == 1) + #expect(try item2.decode(column: "id", as: Int.self) == 2) + + #expect( + try item1.decode(column: "id", as: Int?.self) == .some(1) + ) + #expect( + (try? item1.decode(column: "value", as: Int?.self)) == nil + ) + + #expect( + try item1.decode(column: "value", as: String.self) == "abc" + ) + #expect( + (try? item2.decode(column: "value", as: String.self)) == nil + ) + + #expect( + (try item1.decode(column: "value", as: String?.self)) + == .some("abc") + ) + #expect( + (try item2.decode(column: "value", as: String?.self)) + == .none + ) + } + } + } + + @Test + func queryEncoding() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + let tableName = "foo" + let idColumn = "id" + let valueColumn = "value" + let row1: (Int, String?) = (1, "abc") + let row2: (Int, String?) = (2, nil) + + try await connection.run( + query: #""" + CREATE TABLE \#(unescaped: tableName) ( + \#(unescaped: idColumn) INTEGER NOT NULL PRIMARY KEY, + \#(unescaped: valueColumn) TEXT + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO \#(unescaped: tableName) + (\#(unescaped: idColumn), \#(unescaped: valueColumn)) + VALUES + (\#(row1.0), \#(row1.1)), + (\#(row2.0), \#(row2.1)); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT \#(unescaped: idColumn), \#(unescaped: valueColumn) + FROM \#(unescaped: tableName) + ORDER BY \#(unescaped: idColumn) ASC; + """# + ) { try await $0.collect() } + + #expect(result.count == 2) + + let item1 = result[0] + let item2 = result[1] + + #expect(try item1.decode(column: "id", as: Int.self) == 1) + #expect(try item2.decode(column: "id", as: Int.self) == 2) + + #expect( + try item1.decode(column: "value", as: String?.self) == "abc" + ) + #expect( + try item2.decode(column: "value", as: String?.self) == nil + ) + } + } + } + + @Test + func unsafeSQLBindings() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "widgets" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + + let insert = SQLiteDatabaseQuery( + unsafeSQL: #""" + INSERT INTO "widgets" + ("id", "name") + VALUES + (?, ?); + """#, + bindings: [.integer(1), .text("gizmo")] + ) + + try await connection.run(query: insert) + + let result = + try await connection.run( + query: #""" + SELECT "name" + FROM "widgets" + WHERE "id" = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "gizmo" + ) + } + } + } + + @Test + func optionalStringInterpolationNil() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "notes" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "body" TEXT + ); + """# + ) + + let body: String? = nil + let insert: SQLiteDatabaseQuery = #""" + INSERT INTO "notes" + ("id", "body") + VALUES + (1, \#(body)); + """# + + try await connection.run(query: insert) + + let result = + try await connection.run( + query: #""" + SELECT "body" + FROM "notes" + WHERE "id" = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "body", as: String?.self) + == nil + ) + } + } + } + + @Test + func sqliteDataInterpolation() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "tags" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "label" TEXT NOT NULL + ); + """# + ) + + let label: SQLiteData = .text("alpha") + let insert: SQLiteDatabaseQuery = #""" + INSERT INTO "tags" + ("id", "label") + VALUES + (1, \#(label)); + """# + + try await connection.run(query: insert) + + let result = + try await connection.run( + query: #""" + SELECT "label" + FROM "tags" + WHERE "id" = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "label", as: String.self) + == "alpha" + ) + } + } + } + + @Test + func resultSequenceIterator() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "numbers" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "numbers" + ("id", "value") + VALUES + (1, 'one'), + (2, 'two'); + """# + ) + + let result = try await connection.run( + query: #""" + SELECT "id", "value" + FROM "numbers" + ORDER BY "id"; + """# + ) { try await $0.collect() } + + #expect(result.count == 2) + let first = result[0] + let second = result[1] + + #expect(try first.decode(column: "id", as: Int.self) == 1) + #expect( + try first.decode(column: "value", as: String.self) == "one" + ) + + #expect(try second.decode(column: "id", as: Int.self) == 2) + #expect( + try second.decode(column: "value", as: String.self) == "two" + ) + } + } + } + + @Test + func collectFirstReturnsFirstRow() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "widgets" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "widgets" + ("id", "name") + VALUES + (1, 'alpha'), + (2, 'beta'); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "name" + FROM "widgets" + ORDER BY "id" ASC; + """# + ) { try await $0.collect() } + .first + + #expect(result != nil) + #expect( + try result?.decode(column: "name", as: String.self) + == "alpha" + ) + } + } + } + + @Test + func transactionSuccess() async throws { + try await runUsingTestDatabaseClient { database in + + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "items" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + } + + try await database.withTransaction { connection in + try await connection.run( + query: #""" + INSERT INTO "items" + ("id", "name") + VALUES + (1, 'widget'); + """# + ) + } + + try await database.withConnection { connection in + + let result = try await connection.run( + query: #""" + SELECT "name" + FROM "items" + WHERE "id" = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "widget" + ) + } + } + } + + @Test + func transactionFailurePropagates() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "dummy" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + } + + do { + try await database.withTransaction { connection in + try await connection.run( + query: #""" + INSERT INTO "dummy" + ("id", "name") + VALUES + (1, 'ok'); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "dummy" + ("id", "name") + VALUES + (2, NULL); + """# + ) + } + Issue.record( + "Expected database transaction error to be thrown." + ) + } + catch DatabaseError.transaction(let error) { + #expect(error.beginError == nil) + #expect(error.closureError != nil) + #expect( + error.closureError.debugDescription.contains( + "NOT NULL constraint failed" + ) + ) + #expect(error.rollbackError == nil) + #expect(error.commitError == nil) + } + catch { + Issue.record( + "Expected database transaction error to be thrown." + ) + } + + try await database.withConnection { connection in + let result = + try await connection.run( + query: #""" + SELECT "id" + FROM "dummy"; + """# + ) { try await $0.collect() } + + #expect(result.isEmpty) + } + } + } + + @Test + func doubleRoundTrip() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "measurements" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" REAL NOT NULL + ); + """# + ) + + let expected = 1.5 + + try await connection.run( + query: #""" + INSERT INTO "measurements" + ("id", "value") + VALUES + (1, \#(expected)); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "measurements" + WHERE "id" = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "value", as: Double.self) + == expected + ) + } + } + } + + @Test + func missingColumnThrows() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "items" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "items" + ("id", "value") + VALUES + (1, 'abc'); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "id" + FROM "items"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + + do { + _ = try result[0].decode(column: "value", as: String.self) + Issue.record("Expected decoding a missing column to throw.") + } + catch DecodingError.dataCorrupted { + + } + catch { + Issue.record( + "Expected a dataCorrupted error for missing column." + ) + } + } + } + } + + @Test + func typeMismatchThrows() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "items" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "items" + ("id", "value") + VALUES + (1, 'abc'); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "items"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + + do { + _ = try result[0].decode(column: "value", as: Int.self) + Issue.record("Expected decoding a string as Int to throw.") + } + catch DecodingError.typeMismatch { + + } + catch { + Issue.record( + "Expected a typeMismatch error when decoding a string as Int." + ) + } + } + } + } + + @Test + func queryFailureErrorText() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + do { + _ = try await connection.run( + query: #""" + SELECT * + FROM "missing_table"; + """# + ) + Issue.record("Expected query to fail for missing table.") + } + catch DatabaseError.query(let error) { + #expect("\(error)".contains("no such table")) + } + catch { + Issue.record("Expected database query error to be thrown.") + } + } + } + } + + @Test + func versionCheck() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + let result = try await connection.run( + query: #""" + SELECT + sqlite_version() AS "version" + WHERE + 1=\#(1); + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + + let item = result[0] + let version = try item.decode( + column: "version", + as: String.self + ) + #expect(version.split(separator: ".").count == 3) + } + } + } } diff --git a/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift b/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift index 0d5c341..6999413 100644 --- a/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift +++ b/Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift @@ -41,28 +41,192 @@ struct SQLiteNIOExtrasTestSuite { try await closure(client) } - // private func runUsingTestDatabaseClient( - // _ closure: ((SQLiteDatabaseClient) async throws -> Void) - // ) async throws { - // var logger = Logger(label: "test") - // logger.logLevel = .info - // - // let configuration = SQLiteClient.Configuration( - // storage: .memory, - // logger: logger - // ) - // - // let client = SQLiteClient(configuration: configuration) - // - // let database = SQLiteDatabaseClient(client: client) - // - // try await client.run() - // try await closure(database) - // await client.shutdown() - // } + @Test + func concurrentTransactionsUseMultipleConnections() async throws { + try await runUsingTestClient { client in + + try await client.withConnection { connection in + + try await connection.query( + #""" + CREATE TABLE "items" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + } + + async let first = client.withTransaction { connection in + try await connection.query( + #""" + INSERT INTO "items" + ("id", "name") + VALUES + (1, 'alpha'); + """# + ) + } + + async let second = client.withTransaction { connection in + try await connection.query( + #""" + INSERT INTO "items" + ("id", "name") + VALUES + (2, 'beta'); + """# + ) + } + + do { + _ = try await (first, second) + + let rows = try await client.withConnection { connection in + try await connection.query( + #""" + SELECT COUNT(*) AS "count" + FROM "items"; + """# + ) + } + #expect(rows.count == 1) + #expect(rows[0].column("count")?.integer == 2) + + #expect(await client.connectionCount() == 2) + } + catch { + Issue.record(error) + } + + } + } @Test - func example() async throws { + func concurrentTransactionUpdates() async throws { + try await runUsingTestClient { client in + let suffix = randomTableSuffix() + let table = "sessions_\(suffix)" + let sessionID = "session_\(suffix)" + + enum TestError: Error { + case missingRow + } + + try await client.withConnection { connection in + + _ = try await connection.query( + #""" + DROP TABLE IF EXISTS "\#(table)"; + """# + ) + _ = try await connection.query( + #""" + CREATE TABLE "\#(table)" ( + "id" TEXT NOT NULL PRIMARY KEY, + "access_token" TEXT NOT NULL, + "access_expires_at" INTEGER NOT NULL, + "refresh_token" TEXT NOT NULL, + "refresh_count" INTEGER NOT NULL DEFAULT 0 + ); + """# + ) + + _ = try await connection.query( + #""" + INSERT INTO "\#(table)" + ("id", "access_token", "access_expires_at", "refresh_token", "refresh_count") + VALUES + ( + '\#(sessionID)', + 'stale', + (strftime('%s','now') - 300), + 'refresh', + 0 + ); + """# + ) + } + + func getValidAccessToken( + sessionID: String + ) async throws -> String { + try await client.withTransaction { connection in + + let updatedRows = try await connection.query( + #""" + UPDATE "\#(table)" + SET + "refresh_count" = "refresh_count" + 1, + "access_token" = 'token_' || ("refresh_count" + 1), + "access_expires_at" = (strftime('%s','now') + 600) + WHERE + "id" = '\#(sessionID)' + AND "access_expires_at" + <= (strftime('%s','now') + 60) + RETURNING "access_token"; + """# + ) + + if let updatedRow = updatedRows.first { + return updatedRow.column("access_token")?.string ?? "" + } + + let rows = try await connection.query( + #""" + SELECT + "access_token", + "refresh_count", + "access_expires_at" > (strftime('%s','now') + 60) AS "is_valid" + FROM "\#(table)" + WHERE "id" = '\#(sessionID)'; + """# + ) + + guard let row = rows.first else { + throw TestError.missingRow + } + + let isValid = row.column("is_valid")?.bool ?? false + + #expect(isValid == true) + + return row.column("access_token")?.string ?? "" + } + } + + let workerCount = 80 + var tokens: [String] = [] + try await withThrowingTaskGroup(of: String.self) { group in + for _ in 0.. strftime('%s','now') AS "is_valid" + FROM "\#(table)" + WHERE "id" = '\#(sessionID)'; + """# + ) + } + #expect(result.count == 1) + #expect(result[0].column("refresh_count")?.integer == 1) + #expect(result[0].column("access_token")?.string == "token_1") + #expect(result[0].column("is_valid")?.bool == true) + } } } From 9262050dc1678707f550c784e3fcde49d80aa24b Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Tue, 3 Feb 2026 20:24:00 +0100 Subject: [PATCH 06/11] remove unnecessary retry logic --- .../SQLiteDatabaseConnection.swift | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift index c13f7a8..72d1136 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift @@ -30,36 +30,21 @@ public struct SQLiteDatabaseConnection: DatabaseConnection { query: Query, _ handler: (RowSequence) async throws -> T = { _ in } ) async throws(DatabaseError) -> T { - - let maxAttempts = 8 - var attempt = 0 - while true { - do { - let result = try await connection.query( - query.sql, - query.bindings - ) - return try await handler( - SQLiteDatabaseRowSequence( - elements: result.map { - .init(row: $0) - } - ) + do { + let result = try await connection.query( + query.sql, + query.bindings + ) + return try await handler( + SQLiteDatabaseRowSequence( + elements: result.map { + .init(row: $0) + } ) - } - catch { - attempt += 1 - if attempt >= maxAttempts { - throw .query(error) - } - let delayMilliseconds = min(1000, 25 << (attempt - 1)) - do { - try await Task.sleep(for: .milliseconds(delayMilliseconds)) - } - catch { - throw .query(error) - } - } + ) + } + catch { + throw .query(error) } } } From be3e4a229acdb53607246d1fb46643745fb03d84 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Wed, 4 Feb 2026 12:29:06 +0100 Subject: [PATCH 07/11] prep for beta3 --- Package.resolved | 6 +-- Package.swift | 5 +-- README.md | 39 +++++++++++-------- .../SQLiteDatabaseConnection.swift | 2 +- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/Package.resolved b/Package.resolved index 3534698..146de01 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "f2c4b405d7b153afc4f2cd0d4e4cfb0af1ffc9b98bf974d2012f977119229f27", + "originHash" : "ec031c0e73c03866bdfcc8adfdfda67f96b863f8ba73e708298a520ec0daf502", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "branch" : "fix/consumption", - "revision" : "327c474c3874ecbd18b4ae15dadc286a84488814" + "revision" : "4ef69e67018c4bdf843858e8976c13b97c3afe4c", + "version" : "1.0.0-beta.3" } }, { diff --git a/Package.swift b/Package.swift index 1daf9a1..f856435 100644 --- a/Package.swift +++ b/Package.swift @@ -37,8 +37,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/sqlite-nio", from: "1.12.0"), - .package(url: "https://github.com/feather-framework/feather-database", branch: "fix/consumption"), -// .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.3"), // [docc-plugin-placeholder] ], targets: [ @@ -53,8 +52,6 @@ let package = Package( .target( name: "FeatherSQLiteDatabase", dependencies: [ -// .product(name: "Logging", package: "swift-log"), -// .product(name: "SQLiteNIO", package: "sqlite-nio"), .product(name: "FeatherDatabase", package: "feather-database"), .target(name: "SQLiteNIOExtras"), ], diff --git a/README.md b/README.md index 6bd28f5..4368041 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ SQLite driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. [ - ![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138) + ![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138) ]( - https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.2 + https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.3 ) ## Features @@ -37,7 +37,7 @@ SQLite driver implementation for the abstract [Feather Database](https://github. Add the dependency to your `Package.swift`: ```swift -.package(url: "https://github.com/feather-framework/feather-sqlite-database", exact: "1.0.0-beta.2"), +.package(url: "https://github.com/feather-framework/feather-sqlite-database", exact: "1.0.0-beta.3"), ``` Then add `FeatherSQLiteDatabase` to your target dependencies: @@ -49,16 +49,15 @@ Then add `FeatherSQLiteDatabase` to your target dependencies: ## Usage +API documentation is available at the link below: + [ ![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) ]( https://feather-framework.github.io/feather-sqlite-database/documentation/feathersqlitedatabase/ ) -API documentation is available at the following link. - -> [!TIP] -> Avoid calling `database.execute` while in a transaction; use the transaction `connection` instead. +Here is a brief example: ```swift import Logging @@ -75,19 +74,25 @@ let configuration = SQLiteClient.Configuration( ) let client = SQLiteClient(configuration: configuration) -try await client.run() -let database = SQLiteDatabaseClient(client: client) - -let result = try await database.execute( - query: #""" - SELECT - sqlite_version() AS "version" - WHERE - 1=\#(1); - """# +let database = SQLiteDatabaseClient( + client: client, + logger: logger ) +try await client.run() + +try await database.withConnection { connection in + try await connection.run( + query: #""" + SELECT + sqlite_version() AS "version" + WHERE + 1=\#(1); + """# + ) +} + for try await item in result { let version = try item.decode(column: "version", as: String.self) print(version) diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift index 72d1136..91bc84f 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift @@ -28,7 +28,7 @@ public struct SQLiteDatabaseConnection: DatabaseConnection { @discardableResult public func run( query: Query, - _ handler: (RowSequence) async throws -> T = { _ in } + _ handler: (RowSequence) async throws -> T = { $0 } ) async throws(DatabaseError) -> T { do { let result = try await connection.query( From 5f3b111cee100ddbac7e28843a1327dc756e8a3a Mon Sep 17 00:00:00 2001 From: GErP83 Date: Wed, 4 Feb 2026 15:40:41 +0100 Subject: [PATCH 08/11] change makefile, fix headers --- Makefile | 5 +- README.md | 53 ++++++++----------- .../SQLiteDatabaseConnection.swift | 4 +- .../SQLiteDatabaseQuery.swift | 4 +- .../SQLiteDatabaseRow.swift | 4 +- .../SQLiteDatabaseRowSequence.swift | 4 +- .../SQLiteDatabaseTransactionError.swift | 4 +- 7 files changed, 35 insertions(+), 43 deletions(-) diff --git a/Makefile b/Makefile index 2340c99..043a479 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,10 @@ SHELL=/bin/bash baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts -check: symlinks language deps lint headers +check: symlinks language deps lint headers docc-warnings package + +package: + curl -s $(baseUrl)/check-swift-package.sh | bash symlinks: curl -s $(baseUrl)/check-broken-symlinks.sh | bash diff --git a/README.md b/README.md index 4368041..1748c5f 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,30 @@ SQLite driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. -[ - ![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138) -]( - https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.3 -) +[![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138)](https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.3) ## Features -- 🤝 SQLite driver for Feather Database -- 😱 Automatic query parameter escaping via Swift string interpolation. -- 🔄 Async sequence query results with `Decodable` row support. -- 🧵 Designed for modern Swift concurrency -- 📚 DocC-based API Documentation -- ✅ Unit tests and code coverage +- SQLite driver for Feather Database +- Automatic query parameter escaping via Swift string interpolation. +- Async sequence query results with `Decodable` row support. +- Designed for modern Swift concurrency +- DocC-based API Documentation +- Unit tests and code coverage ## Requirements ![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138) ![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138) - -- Swift 6.1+ -- Platforms: - - Linux - - macOS 15+ - - iOS 18+ - - tvOS 18+ - - watchOS 11+ - - visionOS 2+ +- Swift 6.1+ +- Platforms: + - Linux + - macOS 15+ + - iOS 18+ + - tvOS 18+ + - watchOS 11+ + - visionOS 2+ ## Installation @@ -46,16 +41,11 @@ Then add `FeatherSQLiteDatabase` to your target dependencies: .product(name: "FeatherSQLiteDatabase", package: "feather-sqlite-database"), ``` - ## Usage - + API documentation is available at the link below: -[ - ![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) -]( - https://feather-framework.github.io/feather-sqlite-database/documentation/feathersqlitedatabase/ -) +[![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)](https://feather-framework.github.io/feather-sqlite-database/) Here is a brief example: @@ -104,7 +94,6 @@ await client.shutdown() > [!WARNING] > This repository is a work in progress, things can break until it reaches v1.0.0. - ## Other database drivers The following database driver implementations are available for use: @@ -115,12 +104,12 @@ The following database driver implementations are available for use: ## Development - Build: `swift build` -- Test: - - local: `swift test` - - using Docker: `make docker-test` +- Test: + - local: `swift test` + - using Docker: `make docker-test` - Format: `make format` - Check: `make check` ## Contributing -[Pull requests](https://github.com/feather-framework/feather-sqlite-database/pulls) are welcome. Please keep changes focused and include tests for new logic. 🙏 +[Pull requests](https://github.com/feather-framework/feather-sqlite-database/pulls) are welcome. Please keep changes focused and include tests for new logic. diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift index 91bc84f..1db5ffa 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift @@ -1,8 +1,8 @@ // -// SQLiteConnection.swift +// SQLiteDatabaseConnection.swift // feather-sqlite-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift index e873322..e845f76 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift @@ -1,8 +1,8 @@ // -// SQLiteQuery.swift +// SQLiteDatabaseQuery.swift // feather-sqlite-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift index 2d12729..3203bf5 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRow.swift @@ -1,8 +1,8 @@ // -// SQLiteRow+DatabaseRow.swift +// SQLiteDatabaseRow.swift // feather-sqlite-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift index 9fec470..a18a6a3 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseRowSequence.swift @@ -1,8 +1,8 @@ // -// SQLiteQueryResult.swift +// SQLiteDatabaseRowSequence.swift // feather-sqlite-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift index 877a142..926137f 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseTransactionError.swift @@ -1,8 +1,8 @@ // -// SQLiteTransactionError.swift +// SQLiteDatabaseTransactionError.swift // feather-sqlite-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase From 0cb2622d9dc449fefdc4dd3f878d06139de0e847 Mon Sep 17 00:00:00 2001 From: GErP83 Date: Wed, 4 Feb 2026 17:17:14 +0100 Subject: [PATCH 09/11] change workflows --- .github/workflows/deployment.yml | 1 + .github/workflows/testing.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 680623b..672e272 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -5,6 +5,7 @@ on: tags: - 'v*' - '[0-9]*' + workflow_dispatch: jobs: diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 145a8b9..86e1f80 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -15,7 +15,7 @@ jobs: format_check_enabled : true broken_symlink_check_enabled : true unacceptable_language_check_enabled : true - shell_check_enabled : true + shell_check_enabled : false docs_check_enabled : false api_breakage_check_enabled : false license_header_check_enabled : false @@ -28,7 +28,7 @@ jobs: uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main with: local_swift_dependencies_check_enabled : true - headers_check_enabled : true + headers_check_enabled : false docc_warnings_check_enabled : true swiftlang_tests: From ad91a41fc7d0d34e0cd6ecb9adbe7938b606b2e7 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Wed, 4 Feb 2026 19:02:39 +0100 Subject: [PATCH 10/11] trying to fix cp issues --- Sources/SQLiteNIOExtras/SQLiteClient.swift | 19 +++--- .../SQLiteConnectionPool.swift | 61 +++++++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Sources/SQLiteNIOExtras/SQLiteClient.swift b/Sources/SQLiteNIOExtras/SQLiteClient.swift index 6392a3a..d245ca7 100644 --- a/Sources/SQLiteNIOExtras/SQLiteClient.swift +++ b/Sources/SQLiteNIOExtras/SQLiteClient.swift @@ -145,10 +145,18 @@ public final class SQLiteClient: Sendable { _ closure: (SQLiteConnection) async throws -> T ) async throws -> T { let connection = try await leaseConnection() + do { + try await pool.leaseTransactionPermit() + } + catch { + await pool.releaseConnection(connection) + throw error + } do { _ = try await connection.query("BEGIN;") } catch { + await pool.releaseTransactionPermit() await pool.releaseConnection(connection) throw SQLiteTransactionError(beginError: error) } @@ -159,14 +167,8 @@ public final class SQLiteClient: Sendable { let result = try await closure(connection) closureHasFinished = true - do { - _ = try await connection.query("COMMIT;") - } - catch { - await pool.releaseConnection(connection) - throw SQLiteTransactionError(commitError: error) - } - + _ = try await connection.query("COMMIT;") + await pool.releaseTransactionPermit() await pool.releaseConnection(connection) return result } @@ -187,6 +189,7 @@ public final class SQLiteClient: Sendable { txError.commitError = error } + await pool.releaseTransactionPermit() await pool.releaseConnection(connection) throw txError } diff --git a/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift index 29caad3..cc35de1 100644 --- a/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift +++ b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift @@ -15,12 +15,20 @@ actor SQLiteDatabaseConnectionPool { let continuation: CheckedContinuation } + private struct TransactionWaiter { + let id: Int + let continuation: CheckedContinuation + } + private let configuration: SQLiteClient.Configuration private var availableConnections: [SQLiteConnection] = [] private var waiters: [Waiter] = [] private var totalConnections = 0 private var nextWaiterID = 0 private var isShutdown = false + private var transactionInUse = false + private var transactionWaiters: [TransactionWaiter] = [] + private var nextTransactionWaiterID = 0 init( configuration: SQLiteClient.Configuration @@ -108,12 +116,54 @@ actor SQLiteDatabaseConnectionPool { ) } waiters.removeAll(keepingCapacity: false) + + for waiter in transactionWaiters { + waiter.continuation.resume( + throwing: SQLiteConnectionPoolError.shutdown + ) + } + transactionWaiters.removeAll(keepingCapacity: false) + transactionInUse = false } func connectionCount() -> Int { totalConnections } + func leaseTransactionPermit() async throws { + guard !isShutdown else { + throw SQLiteConnectionPoolError.shutdown + } + + if !transactionInUse { + transactionInUse = true + return + } + + let waiterID = nextTransactionWaiterID + nextTransactionWaiterID += 1 + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + transactionWaiters.append( + TransactionWaiter(id: waiterID, continuation: continuation) + ) + } + } onCancel: { + Task { await self.cancelTransactionWaiter(id: waiterID) } + } + } + + func releaseTransactionPermit() { + if transactionWaiters.isEmpty { + transactionInUse = false + return + } + + let waiter = transactionWaiters.removeFirst() + waiter.continuation.resume() + } + private func cancelWaiter( id: Int ) { @@ -124,6 +174,17 @@ actor SQLiteDatabaseConnectionPool { waiter.continuation.resume(throwing: CancellationError()) } + private func cancelTransactionWaiter( + id: Int + ) { + guard let index = transactionWaiters.firstIndex(where: { $0.id == id }) + else { + return + } + let waiter = transactionWaiters.remove(at: index) + waiter.continuation.resume(throwing: CancellationError()) + } + private func makeConnection() async throws -> SQLiteConnection { let connection = try await SQLiteConnection.open( storage: configuration.storage, From d359c7f7d0893814b744e04f62b569e7a5cb5757 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Wed, 4 Feb 2026 19:09:09 +0100 Subject: [PATCH 11/11] rename connection pool actor --- Sources/SQLiteNIOExtras/SQLiteClient.swift | 4 ++-- Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SQLiteNIOExtras/SQLiteClient.swift b/Sources/SQLiteNIOExtras/SQLiteClient.swift index d245ca7..d0c8c57 100644 --- a/Sources/SQLiteNIOExtras/SQLiteClient.swift +++ b/Sources/SQLiteNIOExtras/SQLiteClient.swift @@ -87,12 +87,12 @@ public final class SQLiteClient: Sendable { } } - private let pool: SQLiteDatabaseConnectionPool + private let pool: SQLiteConnectionPool /// Create a SQLite client with a connection pool. /// - Parameter configuration: The client configuration. public init(configuration: Configuration) { - self.pool = SQLiteDatabaseConnectionPool( + self.pool = SQLiteConnectionPool( configuration: configuration ) } diff --git a/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift index cc35de1..b4ca9b1 100644 --- a/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift +++ b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift @@ -8,7 +8,7 @@ import Logging import SQLiteNIO -actor SQLiteDatabaseConnectionPool { +actor SQLiteConnectionPool { private struct Waiter { let id: Int