From ba734647095ec51c5ba5a7bb9ed0abdf963bba3c Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Thu, 29 Jan 2026 21:27:01 +0100 Subject: [PATCH 1/4] add service lifecycle support via trait --- Package.resolved | 20 +++++++++- Package.swift | 14 +++++++ .../SQLiteClientService.swift | 37 +++++++++++++++++++ .../SQLiteConnectionPool.swift | 14 +++++++ .../SQLiteClientTestSuite.swift | 19 +++++++--- 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 Sources/FeatherSQLiteDatabase/SQLiteClientService.swift diff --git a/Package.resolved b/Package.resolved index bc9e65b..a9400b9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "816ee700ae5206734d35d787d67b0af726ff97f74c5e84dec48c842bb3679698", + "originHash" : "9e5cc89ae333fceaad6d740b6118c69e46dfe3412fd91c6be5f1a01bc8e84585", "pins" : [ { "identity" : "feather-database", @@ -19,6 +19,15 @@ "version" : "1.12.2" } }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version" : "1.1.1" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -55,6 +64,15 @@ "version" : "2.93.0" } }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 065ec5a..87db569 100644 --- a/Package.swift +++ b/Package.swift @@ -34,10 +34,19 @@ let package = Package( products: [ .library(name: "FeatherSQLiteDatabase", targets: ["FeatherSQLiteDatabase"]), ], + traits: [ + "ServiceLifecycleSupport", + .default( + enabledTraits: [ + "ServiceLifecycleSupport" + ] + ), + ], 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/swift-server/swift-service-lifecycle", from: "2.8.0"), // [docc-plugin-placeholder] ], targets: [ @@ -47,6 +56,11 @@ let package = Package( .product(name: "Logging", package: "swift-log"), .product(name: "SQLiteNIO", package: "sqlite-nio"), .product(name: "FeatherDatabase", package: "feather-database"), + .product( + name: "ServiceLifecycle", + package: "swift-service-lifecycle", + condition: .when(traits: ["ServiceLifecycleSupport"]) + ), ], swiftSettings: defaultSwiftSettings ), diff --git a/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift b/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift new file mode 100644 index 0000000..e221d60 --- /dev/null +++ b/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift @@ -0,0 +1,37 @@ +// +// SQLiteClientService.swift +// feather-sqlite-database +// +// Created by Tibor Bödecs on 2026. 01. 29.. +// + +#if ServiceLifecycleSupport +import ServiceLifecycle + +/// A `Service` wrapper around an `SQLiteClient`. +public struct SQLiteClientService: Service { + + /// The underlying SQLite client instance. + public var sqliteClient: SQLiteClient + + /// Creates a new SQLite client service. + /// + /// - Parameter sqliteClient: The SQLite client to manage for the service lifecycle. + public init(sqliteClient: SQLiteClient) { + self.sqliteClient = sqliteClient + } + + /// Runs the SQLite client service. + /// + /// This method starts the SQLite client, waits for a graceful shutdown + /// signal, and then shuts down the client in an orderly manner. + /// + /// - Throws: Rethrows any error produced while starting the SQLite client. + public func run() async throws { + try await sqliteClient.run() + try? await gracefulShutdown() + await sqliteClient.shutdown() + } + +} +#endif diff --git a/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift b/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift index a83cbab..a4ec42f 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift @@ -168,6 +168,20 @@ actor SQLiteConnectionPool { do { try await connection.close() } + catch is CancellationError { + let logger = configuration.logger + Task.detached { + do { + try await connection.close() + } + catch { + logger.warning( + "Failed to close SQLite connection after cancellation", + metadata: ["error": "\(error)"] + ) + } + } + } catch { configuration.logger.warning( "Failed to close SQLite connection", diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift index c2b8ebf..1329467 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift @@ -25,7 +25,8 @@ struct SQLiteClientTestSuite { } private func runUsingTestClient( - _ closure: (SQLiteClient) async throws -> Void + _ closure: + @escaping (@Sendable (SQLiteDatabaseClient) async throws -> Void) ) async throws { var logger = Logger(label: "test.sqlite.client") logger.logLevel = .info @@ -35,11 +36,19 @@ struct SQLiteClientTestSuite { logger: logger, ) let client = SQLiteClient(configuration: configuration) + let service = SQLiteClientService(sqliteClient: client) + let database = SQLiteDatabaseClient(client: client) - try await client.run() - defer { Task { await client.shutdown() } } - - try await closure(client) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await service.run() + } + group.addTask { + try await closure(database) + } + try await group.next() + group.cancelAll() + } } @Test From 5accedd0f279301dae9da21cf7947af675ae730a Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Thu, 29 Jan 2026 23:15:43 +0100 Subject: [PATCH 2/4] update readme --- README.md | 22 +++++- .../SQLiteClientService.swift | 4 +- .../SQLiteConnectionPool.swift | 22 ++---- .../SQLiteClientTestSuite.swift | 72 ++++++++++++++++--- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 6bd28f5..ea73250 100644 --- a/README.md +++ b/README.md @@ -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: @@ -46,6 +46,26 @@ Then add `FeatherSQLiteDatabase` to your target dependencies: .product(name: "FeatherSQLiteDatabase", package: "feather-sqlite-database"), ``` +### Package traits + +This package offers additional integrations you can enable using [package traits](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/addingdependencies#Packages-with-Traits). +To enable an additional trait on the package, update the package dependency: + +```diff +.package( + url: "https://github.com/feather-framework/feather-sqlite-database", + exact: "1.0.0-beta.3", ++ traits: [ ++ .defaults, ++ "ServiceLifecycleSupport", ++ ] +) +``` + +Available traits: + +- `ServiceLifecycleSupport` (default): Adds support for `SQLiteClientService`, a `ServiceLifecycle.Service` implementation for managing SQLite clients. + ## Usage diff --git a/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift b/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift index e221d60..8ed4b5a 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteClientService.swift @@ -17,7 +17,9 @@ public struct SQLiteClientService: Service { /// Creates a new SQLite client service. /// /// - Parameter sqliteClient: The SQLite client to manage for the service lifecycle. - public init(sqliteClient: SQLiteClient) { + public init( + _ sqliteClient: SQLiteClient + ) { self.sqliteClient = sqliteClient } diff --git a/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift b/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift index a4ec42f..69b320e 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteConnectionPool.swift @@ -154,7 +154,9 @@ actor SQLiteConnectionPool { catch { configuration.logger.warning( "Failed to close SQLite connection after setup error", - metadata: ["error": "\(error)"] + metadata: [ + "error": "\(error)" + ] ) } throw error @@ -168,24 +170,12 @@ actor SQLiteConnectionPool { do { try await connection.close() } - catch is CancellationError { - let logger = configuration.logger - Task.detached { - do { - try await connection.close() - } - catch { - logger.warning( - "Failed to close SQLite connection after cancellation", - metadata: ["error": "\(error)"] - ) - } - } - } catch { configuration.logger.warning( "Failed to close SQLite connection", - metadata: ["error": "\(error)"] + metadata: [ + "error": "\(error)" + ] ) } } diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift index 1329467..5899efb 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/SQLiteClientTestSuite.swift @@ -36,19 +36,11 @@ struct SQLiteClientTestSuite { logger: logger, ) let client = SQLiteClient(configuration: configuration) - let service = SQLiteClientService(sqliteClient: client) let database = SQLiteDatabaseClient(client: client) - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await service.run() - } - group.addTask { - try await closure(database) - } - try await group.next() - group.cancelAll() - } + try await client.run() + try await closure(database) + await client.shutdown() } @Test @@ -259,3 +251,61 @@ struct SQLiteClientTestSuite { } } } + +#if ServiceLifecycleSupport + +import ServiceLifecycle + +extension SQLiteClientTestSuite { + + @Test + func serviceLifecycleSupport() 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) + let service = SQLiteClientService(client) + let database = SQLiteDatabaseClient(client: client) + + let serviceGroup = ServiceGroup( + services: [service], + logger: logger + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + group.addTask { + 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) + } + try await group.next() + + try await Task.sleep(for: .milliseconds(100)) + + await serviceGroup.triggerGracefulShutdown() + } + } +} +#endif From 8e03b9ac6a3b0626af4222ebcc9327a735ccdda9 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Thu, 5 Feb 2026 15:00:15 +0100 Subject: [PATCH 3/4] fix format --- Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift index 3d630c5..ad45165 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift @@ -16,7 +16,7 @@ public struct SQLiteDatabaseService: Service { /// Creates a new SQLite client service. /// - /// - Parameter sqliteClient: The SQLite client to manage for the service lifecycle. + /// - Parameter client: The SQLite client to manage for the service lifecycle. public init( _ client: SQLiteClient ) { From 28007db4812c47fbd6f62688f6d274a9e95249f6 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Thu, 5 Feb 2026 15:16:19 +0100 Subject: [PATCH 4/4] fix traits --- Package.resolved | 20 +++++- Package.swift | 16 ++++- .../SQLiteDatabaseService.swift | 4 +- ...t => FeatherSQLiteDatabaseTestSuite.swift} | 63 ++++++++++++++++++- 4 files changed, 98 insertions(+), 5 deletions(-) rename Tests/FeatherSQLiteDatabaseTests/{SQLiteDatabaseTestSuite.swift => FeatherSQLiteDatabaseTestSuite.swift} (93%) diff --git a/Package.resolved b/Package.resolved index a033d5d..33959b4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9b7a1b4d3d7682d16d34353960396d98809c7d26e380a87f292f8227a1222532", + "originHash" : "89777d929ec26bbed6f64b140dce168ca0fbc69d9273ed1edcfaf674a6bb8eb4", "pins" : [ { "identity" : "feather-database", @@ -19,6 +19,15 @@ "version" : "1.12.3" } }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version" : "1.1.1" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -55,6 +64,15 @@ "version" : "2.94.0" } }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle", + "state" : { + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index f856435..016c569 100644 --- a/Package.swift +++ b/Package.swift @@ -34,10 +34,19 @@ let package = Package( products: [ .library(name: "FeatherSQLiteDatabase", targets: ["FeatherSQLiteDatabase"]), ], + traits: [ + "ServiceLifecycleSupport", + .default( + enabledTraits: [ + "ServiceLifecycleSupport", + ] + ), + ], 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.3"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle", from: "2.8.0"), // [docc-plugin-placeholder] ], targets: [ @@ -52,8 +61,13 @@ let package = Package( .target( name: "FeatherSQLiteDatabase", dependencies: [ - .product(name: "FeatherDatabase", package: "feather-database"), .target(name: "SQLiteNIOExtras"), + .product(name: "FeatherDatabase", package: "feather-database"), + .product( + name: "ServiceLifecycle", + package: "swift-service-lifecycle", + condition: .when(traits: ["ServiceLifecycleSupport"]) + ), ], swiftSettings: defaultSwiftSettings ), diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift index ad45165..8e21b83 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift @@ -6,6 +6,8 @@ // #if ServiceLifecycleSupport + +import SQLiteNIOExtras import ServiceLifecycle /// A `Service` wrapper around an `SQLiteClient`. @@ -20,7 +22,7 @@ public struct SQLiteDatabaseService: Service { public init( _ client: SQLiteClient ) { - self.client = sqliteClient + self.client = client } /// Runs the SQLite client service. diff --git a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift similarity index 93% rename from Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift rename to Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift index 53a55f7..0b5b853 100644 --- a/Tests/FeatherSQLiteDatabaseTests/SQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift @@ -1,5 +1,5 @@ // -// SQLiteDatabaseTestSuite.swift +// FeatherSQLiteDatabaseTestSuite.swift // feather-sqlite-database // // Created by Tibor Bödecs on 2026. 01. 10.. @@ -14,7 +14,7 @@ import Testing @testable import FeatherSQLiteDatabase @Suite -struct SQLiteDatabaseTestSuite { +struct FeatherSQLiteDatabaseTestSuite { private func runUsingTestDatabaseClient( _ closure: ((SQLiteDatabaseClient) async throws -> Void) @@ -775,3 +775,62 @@ struct SQLiteDatabaseTestSuite { } } } + +#if ServiceLifecycleSupport +import ServiceLifecycle + +extension FeatherSQLiteDatabaseTestSuite { + + @Test + func serviceLifecycleSupport() 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) + let service = SQLiteDatabaseService(client) + + let serviceGroup = ServiceGroup( + services: [service], + logger: logger + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + group.addTask { + let result = try await database.withConnection { connection in + try await connection.run( + 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) + } + try await group.next() + + try await Task.sleep(for: .milliseconds(100)) + + await serviceGroup.triggerGracefulShutdown() + } + } +} +#endif