From b577af64946825b796c5af0e87e998a4eaf8e367 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 26 Jan 2026 13:51:16 +0100 Subject: [PATCH 1/4] wip transaction lock test --- Package.resolved | 10 +- .../FeatherPostgresDatabaseTestSuite.swift | 168 ++++++++++++++++++ 2 files changed, 173 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index a44b7e2..ffc8e3c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -67,10 +67,10 @@ { "identity" : "swift-log", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", + "location" : "https://github.com/apple/swift-log", "state" : { - "revision" : "7ee16e465622412764b0ff0c1301801dc71b8f61", - "version" : "1.9.0" + "revision" : "2778fd4e5a12a8aaa30a3ee8285f4ce54c5f3181", + "version" : "1.9.1" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "4a9a97111099376854a7f8f0f9f88b9d61f52eff", - "version" : "2.92.2" + "revision" : "233f61bc2cfbb22d0edeb2594da27a20d2ce514e", + "version" : "2.93.0" } }, { diff --git a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift index 07e1692..6c03ad4 100644 --- a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift +++ b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift @@ -6,6 +6,7 @@ // import FeatherDatabase +import FeatherPostgresDatabase import Logging import NIOSSL import PostgresNIO @@ -745,6 +746,150 @@ struct FeatherPostgresDatabaseTestSuite { } } + @Test + func concurrentTransactionUpdates() async throws { + try await runUsingTestDatabaseClient { 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)" CASCADE; + """# + ) + try await database.execute( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" TEXT NOT NULL PRIMARY KEY, + "access_token" TEXT NOT NULL, + "access_expires_at" TIMESTAMPTZ NOT NULL, + "refresh_token" TEXT NOT NULL, + "refresh_count" INTEGER NOT NULL DEFAULT 0 + ); + """# + ) + + // set an expired token + try await database.execute( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "access_token", "access_expires_at", "refresh_token", "refresh_count") + VALUES + ( + \#(sessionID), + 'stale', + NOW() - INTERVAL '5 minutes', + 'refresh', + 0 + ); + """# + ) + + func getValidAccessToken(sessionID: String) async throws -> String { + try await database.transactionGeneric { connection in + let result = try await connection.execute( + query: #""" + SELECT + "access_token", + "refresh_count", + "access_expires_at" > NOW() + INTERVAL '60 seconds' AS "is_valid" + FROM "\#(unescaped: table)" + WHERE "id" = \#(sessionID) + FOR UPDATE; + """# + ) + 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 + ) + if isValid { + // token was valid, must be called X times + return try row.decode( + column: "access_token", + as: String.self + ) + } + + // refresh, this branch can only be called 1 time + let refreshCount = try row.decode( + column: "refresh_count", + as: Int.self + ) + let newRefreshCount = refreshCount + 1 + let newToken = "token_\(newRefreshCount)" + + try await Task.sleep(for: .milliseconds(40)) + + _ = try await connection.execute( + query: #""" + UPDATE "\#(unescaped: table)" + SET + "access_token" = \#(newToken), + "access_expires_at" = NOW() + INTERVAL '10 minutes', + "refresh_count" = \#(newRefreshCount) + WHERE "id" = \#(sessionID); + """# + ) + + return newToken + } + } + + let workerCount = 80 + var tokens: [String] = [] + try await withThrowingTaskGroup(of: String.self) { group in + for _ in 0.. 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 + ) + } + } + @Test func doubleRoundTrip() async throws { try await runUsingTestDatabaseClient { database in @@ -951,3 +1096,26 @@ struct FeatherPostgresDatabaseTestSuite { } } } + +extension PostgresDatabaseClient { + + @discardableResult + public func transactionGeneric( + isolation: isolated (any Actor)? = #isolation, + _ closure: ((PostgresConnection) async throws -> sending T), + ) async throws(DatabaseError) -> sending T { + do { + return try await client.withTransaction( + logger: logger, + isolation: isolation, + closure + ) + } + catch let error as PostgresTransactionError { + throw .transaction(error) + } + catch { + throw .connection(error) + } + } +} From 0b33aebf3ce357e6fa8213daff928bf6e3032b8f Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 26 Jan 2026 17:14:50 +0100 Subject: [PATCH 2/4] beta.2 updates --- Package.resolved | 6 ++--- Package.swift | 2 +- .../PostgresDatabaseClient.swift | 15 +++++------ .../FeatherPostgresDatabaseTestSuite.swift | 26 +------------------ 4 files changed, 11 insertions(+), 38 deletions(-) diff --git a/Package.resolved b/Package.resolved index ffc8e3c..babac96 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "ea3b79b78be18e85af3a26baa51616c380e4cd43f77304dd1688f0bcc6cbe204", + "originHash" : "27bb6664d476306e8f9d8e8d349100d725e33f2fbb8059fd08732ada339d15eb", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "34b05e9ca725bf857c9bc6e29603a4e457f9969a", - "version" : "1.0.0-beta.1" + "revision" : "147c96803f398c50f5717ce08ebd78a9e3ab7ca7", + "version" : "1.0.0-beta.2" } }, { diff --git a/Package.swift b/Package.swift index 416bba3..f82fd63 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/postgres-nio", from: "1.27.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"), ], targets: [ .target( diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift index bfd7645..cee9781 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift @@ -47,11 +47,10 @@ public struct PostgresDatabaseClient: DatabaseClient { /// - Throws: A `DatabaseError` if connection handling fails. /// - Returns: The query result produced by the closure. @discardableResult - public func connection( + public func connection( isolation: isolated (any Actor)? = #isolation, - _ closure: (PostgresConnection) async throws -> - sending PostgresQueryResult, - ) async throws(DatabaseError) -> sending PostgresQueryResult { + _ closure: (PostgresConnection) async throws -> sending T, + ) async throws(DatabaseError) -> sending T { do { return try await client.withConnection(closure) } @@ -72,12 +71,10 @@ public struct PostgresDatabaseClient: DatabaseClient { /// - Throws: A `DatabaseError` if the transaction fails. /// - Returns: The query result produced by the closure. @discardableResult - public func transaction( + public func transaction( isolation: isolated (any Actor)? = #isolation, - _ closure: ( - (PostgresConnection) async throws -> sending PostgresQueryResult - ), - ) async throws(DatabaseError) -> sending PostgresQueryResult { + _ closure: ((PostgresConnection) async throws -> sending T), + ) async throws(DatabaseError) -> sending T { do { return try await client.withTransaction( logger: logger, diff --git a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift index 6c03ad4..8c53f60 100644 --- a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift +++ b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift @@ -6,7 +6,6 @@ // import FeatherDatabase -import FeatherPostgresDatabase import Logging import NIOSSL import PostgresNIO @@ -791,7 +790,7 @@ struct FeatherPostgresDatabaseTestSuite { ) func getValidAccessToken(sessionID: String) async throws -> String { - try await database.transactionGeneric { connection in + try await database.transaction { connection in let result = try await connection.execute( query: #""" SELECT @@ -1096,26 +1095,3 @@ struct FeatherPostgresDatabaseTestSuite { } } } - -extension PostgresDatabaseClient { - - @discardableResult - public func transactionGeneric( - isolation: isolated (any Actor)? = #isolation, - _ closure: ((PostgresConnection) async throws -> sending T), - ) async throws(DatabaseError) -> sending T { - do { - return try await client.withTransaction( - logger: logger, - isolation: isolation, - closure - ) - } - catch let error as PostgresTransactionError { - throw .transaction(error) - } - catch { - throw .connection(error) - } - } -} From 5899105ea06a548611a56931b9bd3338e4930261 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 26 Jan 2026 17:24:21 +0100 Subject: [PATCH 3/4] readme updates --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6492c41..d087ec5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ Postgres driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. -![Release: 1.0.0-beta.1](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E1-F05138) +[ + ![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138) +]( + https://github.com/feather-framework/feather-postgres-database/releases/tag/1.0.0-beta.2 +) ## Features @@ -33,7 +37,7 @@ Postgres driver implementation for the abstract [Feather Database](https://githu Add the dependency to your `Package.swift`: ```swift -.package(url: "https://github.com/feather-framework/feather-postgres-database", exact: "1.0.0-beta.1"), +.package(url: "https://github.com/feather-framework/feather-postgres-database", exact: "1.0.0-beta.2"), ``` Then add `FeatherPostgresDatabase` to your target dependencies: @@ -45,7 +49,11 @@ Then add `FeatherPostgresDatabase` to your target dependencies: ## Usage -![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) +[ + ![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) +]( + https://feather-framework.github.io/feather-postgres-database/documentation/featherpostgresdatabase/ +) API documentation is available at the following link. @@ -127,7 +135,7 @@ The following database driver implementations are available for use: - Build: `swift build` - Test: - local: `swift test` - - using Docker: `swift docker-test` + - using Docker: `make docker-test` - Format: `make format` - Check: `make check` From 5504684183eed931c449462b5adfa92031c23e2a Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 26 Jan 2026 18:09:05 +0100 Subject: [PATCH 4/4] docc plugin placeholder --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index f82fd63..5a252eb 100644 --- a/Package.swift +++ b/Package.swift @@ -38,6 +38,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/postgres-nio", from: "1.27.0"), .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"), + // [docc-plugin-placeholder] ], targets: [ .target(