From b7863dbf767f84f211ccb5e632ea3027d361aeb8 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 13:22:19 +0100 Subject: [PATCH 01/10] attempt to fix sequence consumption --- Package.resolved | 6 +- Package.swift | 3 +- .../PostgresConnection.swift | 44 +- .../PostgresDatabaseClient.swift | 9 +- .../PostgresQueryResult.swift | 13 - .../FeatherPostgresDatabaseTestSuite.swift | 1463 +++++++++-------- 6 files changed, 813 insertions(+), 725 deletions(-) diff --git a/Package.resolved b/Package.resolved index babac96..5637287 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "27bb6664d476306e8f9d8e8d349100d725e33f2fbb8059fd08732ada339d15eb", + "originHash" : "2f4544e2a879160ae7b4aeb6bf035e1be0da0d50d90af93ea54e0a32b7528d62", "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" : "8de8b87ec6bf10a67d201b9c4b14796a0711a155" } }, { diff --git a/Package.swift b/Package.swift index 5a252eb..8b879ec 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/postgres-nio", from: "1.27.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", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/feather-framework/feather-database", branch: "fix/consumption"), // [docc-plugin-placeholder] ], targets: [ diff --git a/Sources/FeatherPostgresDatabase/PostgresConnection.swift b/Sources/FeatherPostgresDatabase/PostgresConnection.swift index d712f90..de8749b 100644 --- a/Sources/FeatherPostgresDatabase/PostgresConnection.swift +++ b/Sources/FeatherPostgresDatabase/PostgresConnection.swift @@ -9,6 +9,8 @@ import FeatherDatabase import PostgresNIO extension PostgresConnection: @retroactive DatabaseConnection { + public typealias Query = PostgresQuery + public typealias Result = PostgresQueryResult /// Execute a Postgres query on this connection. /// @@ -17,21 +19,51 @@ extension PostgresConnection: @retroactive DatabaseConnection { /// - Throws: A `DatabaseError` if the query fails. /// - Returns: A query result containing the returned rows. @discardableResult - public func execute( - query: PostgresQuery - ) async throws(DatabaseError) -> PostgresQueryResult { + public func run( + query: Query, + _ handler: (Result.Row) async throws -> T = { $0 } + ) async throws(DatabaseError) -> [T] { do { - let result = try await self.query( + let resultSequence = try await self.query( .init( unsafeSQL: query.sql, binds: query.bindings ), logger: logger ) - return PostgresQueryResult(backingSequence: result) + + var result: [T] = [] + for try await item in resultSequence { + result.append(try await handler(item)) + } + return result + } + catch { + throw .query(error) + } + + } + + public func run( + query: Query, + _ handler: (Result.Row) async throws -> Void = { _ in } + ) async throws(DatabaseError) { + do { + let resultSequence = try await self.query( + .init( + unsafeSQL: query.sql, + binds: query.bindings + ), + logger: logger + ) + + for try await item in resultSequence { + try await handler(item) + } } catch { - throw DatabaseError.query(error) + throw .query(error) } } + } diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift index cee9781..a7da9b6 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift @@ -18,6 +18,7 @@ extension PostgresTransactionError: @retroactive DatabaseTransactionError {} /// /// Use this client to execute queries and manage transactions on Postgres. public struct PostgresDatabaseClient: DatabaseClient { + public typealias Connection = PostgresConnection var client: PostgresClient var logger: Logger @@ -47,9 +48,9 @@ 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 withConnection( isolation: isolated (any Actor)? = #isolation, - _ closure: (PostgresConnection) async throws -> sending T, + _ closure: (Connection) async throws -> sending T, ) async throws(DatabaseError) -> sending T { do { return try await client.withConnection(closure) @@ -71,9 +72,9 @@ 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 withTransaction( isolation: isolated (any Actor)? = #isolation, - _ closure: ((PostgresConnection) async throws -> sending T), + _ closure: (Connection) async throws -> sending T, ) async throws(DatabaseError) -> sending T { do { return try await client.withTransaction( diff --git a/Sources/FeatherPostgresDatabase/PostgresQueryResult.swift b/Sources/FeatherPostgresDatabase/PostgresQueryResult.swift index f0b4dc9..a6eb03d 100644 --- a/Sources/FeatherPostgresDatabase/PostgresQueryResult.swift +++ b/Sources/FeatherPostgresDatabase/PostgresQueryResult.swift @@ -59,17 +59,4 @@ public struct PostgresQueryResult: DatabaseQueryResult { backingIterator: backingSequence.makeAsyncIterator(), ) } - - /// Collect all rows into an array. - /// - /// This consumes the sequence and returns all rows. - /// - Throws: An error if iteration fails. - /// - Returns: An array of `PostgresRow` values. - public func collect() async throws -> [PostgresRow] { - var items: [PostgresRow] = [] - for try await item in self { - items.append(item) - } - return items - } } diff --git a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift index 8c53f60..93b35e3 100644 --- a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift +++ b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift @@ -92,54 +92,57 @@ struct FeatherPostgresDatabaseTestSuite { let planetsTable = "planets_\(suffix)" let moonsTable = "moons_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: moonsTable)" CASCADE; - """# - ) - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: planetsTable)" CASCADE; - """# - ) - - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: planetsTable)" ( - "id" INTEGER PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: moonsTable)" ( - "id" INTEGER PRIMARY KEY, - "planet_id" INTEGER NOT NULL - REFERENCES "\#(unescaped: planetsTable)" ("id") - ); - """# - ) - - do { - _ = try await database.execute( + try await database.withTransaction { connection in + + try await connection.run( query: #""" - INSERT INTO "\#(unescaped: moonsTable)" - ("id", "planet_id") - VALUES - (1, 999); + DROP TABLE IF EXISTS "\#(unescaped: moonsTable)" CASCADE; """# ) - Issue.record("Expected foreign key constraint violation.") - } - catch DatabaseError.query(let error) { - #expect( - String(reflecting: error) - .contains("violates foreign key constraint") + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: planetsTable)" CASCADE; + """# ) - } - catch { - Issue.record("Expected database query error to be thrown.") + + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: planetsTable)" ( + "id" INTEGER PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: moonsTable)" ( + "id" INTEGER PRIMARY KEY, + "planet_id" INTEGER NOT NULL + REFERENCES "\#(unescaped: planetsTable)" ("id") + ); + """# + ) + + do { + _ = try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: moonsTable)" + ("id", "planet_id") + VALUES + (1, 999); + """# + ) + Issue.record("Expected foreign key constraint violation.") + } + catch DatabaseError.query(let error) { + #expect( + String(reflecting: error) + .contains("violates foreign key constraint") + ) + } + catch { + Issue.record("Expected database query error to be thrown.") + } } } } @@ -150,37 +153,36 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "galaxies_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - - try await database.execute( - query: #""" - CREATE TABLE IF NOT EXISTS "\#(unescaped: table)" ( - "id" INTEGER PRIMARY KEY, - "name" TEXT - ); - """# - ) - - let results = try await database.execute( - query: #""" - SELECT "tablename" - FROM "pg_tables" - WHERE "schemaname" = 'public' - AND "tablename" = '\#(unescaped: table)' - ORDER BY "tablename"; - """# - ) - - let resultArray = try await results.collect() - #expect(resultArray.count == 1) - - let item = resultArray[0] - let name = try item.decode(column: "tablename", as: String.self) - #expect(name == table) + try await database.withTransaction { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + + try await connection.run( + query: #""" + CREATE TABLE IF NOT EXISTS "\#(unescaped: table)" ( + "id" INTEGER PRIMARY KEY, + "name" TEXT + ); + """# + ) + + let results = try await connection.run( + query: #""" + SELECT "tablename" + FROM "pg_tables" + WHERE "schemaname" = 'public' + AND "tablename" = '\#(unescaped: table)' + ORDER BY "tablename"; + """# + ) { try $0.decode(column: "tablename", as: String.self) } + + #expect(results.count == 1) + #expect(results[0] == table) + } } } @@ -190,49 +192,58 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "galaxies_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE IF NOT EXISTS "\#(unescaped: table)" ( - "id" INTEGER PRIMARY KEY, - "name" TEXT - ); - """# - ) - - let name1 = "Andromeda" - let name2 = "Milky Way" - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "name") - VALUES - (\#(1), \#(name1)), - (\#(2), \#(name2)); - """# - ) - - let results = try await database.execute( - query: #""" - SELECT * FROM "\#(unescaped: table)" 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) + try await database.withConnection { connection in + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE IF NOT EXISTS "\#(unescaped: table)" ( + "id" INTEGER PRIMARY KEY, + "name" TEXT + ); + """# + ) + + let name1 = "Andromeda" + let name2 = "Milky Way" + + struct GalaxyRow: Codable, Sendable { + let id: Int + let name: String + + init(_ row: PostgresRow) throws { + self.id = try row.decode(column: "id", as: Int.self) + self.name = try row.decode( + column: "name", + as: String.self + ) + } + } + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "name") + VALUES + (\#(1), \#(name1)), + (\#(2), \#(name2)); + """# + ) + + let results = try await connection.run( + query: #""" + SELECT * FROM "\#(unescaped: table)" ORDER BY "name" ASC; + """# + ) { try GalaxyRow($0) } + + #expect(results.count == 2) + #expect(results[0].name == name1) + #expect(results[1].name == name2) + } + } } @@ -242,63 +253,83 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "foo_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "value") - VALUES - (1, 'abc'), - (2, NULL); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, 'abc'), + (2, NULL); + """# + ) + + struct FooRow: Codable, Sendable { + let id: Int + let value: String + + init(_ row: PostgresRow) throws { + self.id = try row.decode(column: "id", as: Int.self) + self.value = try row.decode( + column: "value", + as: String.self + ) + } + } + + let result = try await connection.run( query: #""" SELECT "id", "value" FROM "\#(unescaped: table)" ORDER BY "id"; """# - ) - .collect() + ) { $0 } - #expect(result.count == 2) + #expect(result.count == 2) - let item1 = result[0] - let item2 = result[1] + 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) == 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: "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) == "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 - ) + #expect( + (try item1.decode(column: "value", as: String?.self)) + == .some("abc") + ) + #expect( + (try item2.decode(column: "value", as: String?.self)) + == .none + ) + } } } @@ -308,56 +339,59 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "foo_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - - let row1: (Int, String?) = (1, "abc") - let row2: (Int, String?) = (2, nil) - - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "value") - VALUES - (\#(row1.0), \#(row1.1)), - (\#(row2.0), \#(row2.1)); - """# - ) - - let result = - try await database.execute( + try await database.withTransaction { connection in + try await connection.run( query: #""" - SELECT "id", "value" - FROM "\#(unescaped: table)" - ORDER BY "id" ASC; + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; """# ) - .collect() - #expect(result.count == 2) + let row1: (Int, String?) = (1, "abc") + let row2: (Int, String?) = (2, nil) - let item1 = result[0] - let item2 = result[1] + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (\#(row1.0), \#(row1.1)), + (\#(row2.0), \#(row2.1)); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "id", "value" + FROM "\#(unescaped: table)" + ORDER BY "id" ASC; + """# + ) { $0 } + + #expect(result.count == 2) - #expect(try item1.decode(column: "id", as: Int.self) == 1) - #expect(try item2.decode(column: "id", as: Int.self) == 2) + let item1 = result[0] + let item2 = result[1] - #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: "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 + ) + } } } @@ -367,45 +401,47 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "widgets_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - let name = "gizmo" - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "name") - VALUES - (\#(1), \#(name)); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + try await connection.run( query: #""" - SELECT "name" - FROM "\#(unescaped: table)" - WHERE "id" = 1; + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) + + let name = "gizmo" + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "name") + VALUES + (\#(1), \#(name)); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "name", as: String.self) == "gizmo" - ) + let result = + try await connection.run( + query: #""" + SELECT "name" + FROM "\#(unescaped: table)" + WHERE "id" = 1; + """# + ) { $0 } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "gizmo" + ) + } } } @@ -415,45 +451,48 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "notes_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "body" TEXT - ); - """# - ) - - let body: String? = nil - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "body") - VALUES - (1, \#(body)); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + + try await connection.run( query: #""" - SELECT "body" - FROM "\#(unescaped: table)" - WHERE "id" = 1; + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "body" TEXT + ); + """# + ) + + let body: String? = nil + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "body") + VALUES + (1, \#(body)); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "body", as: String?.self) == nil - ) + let result = + try await connection.run( + query: #""" + SELECT "body" + FROM "\#(unescaped: table)" + WHERE "id" = 1; + """# + ) { $0 } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "body", as: String?.self) + == nil + ) + } } } @@ -463,46 +502,48 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "tags_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "label" TEXT NOT NULL - ); - """# - ) - - let label = "alpha" - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "label") - VALUES - (1, \#(label)); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + + try await connection.run( query: #""" - SELECT "label" - FROM "\#(unescaped: table)" - WHERE "id" = 1; + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "label" TEXT NOT NULL + ); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "label", as: String.self) - == "alpha" - ) + let label = "alpha" + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "label") + VALUES + (1, \#(label)); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "label" + FROM "\#(unescaped: table)" + WHERE "id" = 1; + """# + ) { $0 } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "label", as: String.self) + == "alpha" + ) + } } } @@ -512,66 +553,53 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "numbers_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT NOT NULL - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "value") - VALUES - (1, 'one'), - (2, 'two'); - """# - ) - - let result = try await database.execute( - query: #""" - SELECT "id", "value" - FROM "\#(unescaped: table)" - ORDER BY "id"; - """# - ) - - var iterator = result.makeAsyncIterator() - let first = try await iterator.next() - let second = try await iterator.next() - let third = try await iterator.next() - - #expect(first != nil) - #expect(second != nil) - #expect(third == nil) - - if let first { + try await database.withConnection { connection in + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, 'one'), + (2, 'two'); + """# + ) + + let result = try await connection.run( + query: #""" + SELECT "id", "value" + FROM "\#(unescaped: table)" + ORDER BY "id"; + """# + ) { $0 } + + #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" ) - } - 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.") - } } } @@ -581,167 +609,176 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "widgets_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" SERIAL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("name") - VALUES - ('alpha'), - ('beta'); - """# - ) - - let result = try await database.execute( - query: #""" - SELECT "name" - FROM "\#(unescaped: table)" - ORDER BY "id" ASC; - """# - ) - - let first = try await result.collectFirst() - - #expect(first != nil) - #expect( - try first?.decode(column: "name", as: String.self) == "alpha" - ) - } - } + try await database.withConnection { connection in - @Test - func transactionSuccess() async throws { - try await runUsingTestDatabaseClient { database in - let suffix = randomTableSuffix() - let table = "items_\(suffix)" - - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - try await database.transaction { connection in - try await connection.execute( + try await connection.run( query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "name") - VALUES - (1, 'widget'); + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" SERIAL PRIMARY KEY, + "name" TEXT NOT NULL + ); """# ) - } - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT "name" - FROM "\#(unescaped: table)" - WHERE "id" = 1; + INSERT INTO "\#(unescaped: table)" + ("name") + VALUES + ('alpha'), + ('beta'); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "name", as: String.self) - == "widget" - ) + let result = + try await connection.run( + query: #""" + SELECT "name" + FROM "\#(unescaped: table)" + ORDER BY "id" ASC; + """# + ) { $0 } + .first + + #expect(result != nil) + #expect( + try result?.decode(column: "name", as: String.self) + == "alpha" + ) + } } } @Test - func transactionFailurePropagates() async throws { + func transactionSuccess() async throws { try await runUsingTestDatabaseClient { database in let suffix = randomTableSuffix() - let table = "dummy_\(suffix)" + let table = "items_\(suffix)" + + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); + """# + ) - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL - ); - """# - ) - - do { - _ = try await database.transaction { connection in - try await connection.execute( + try await database.withTransaction { connection in + try await connection.run( query: #""" INSERT INTO "\#(unescaped: table)" ("id", "name") VALUES - (1, 'ok'); + (1, 'widget'); """# ) + } - return try await connection.execute( + let result = + try await connection.run( query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "name") - VALUES - (2, NULL); + SELECT "name" + FROM "\#(unescaped: table)" + WHERE "id" = 1; """# - ) - } - Issue.record( - "Expected database transaction error to be thrown." - ) - } - catch DatabaseError.transaction(let error) { - #expect(error.beginError == nil) - #expect(error.closureError != nil) + ) { $0 } + + #expect(result.count == 1) #expect( - error.closureError.debugDescription.contains( - "null value in column" - ) - ) - #expect(error.rollbackError == nil) - #expect(error.commitError == nil) - } - catch { - Issue.record( - "Expected database transaction error to be thrown." + try result[0].decode(column: "name", as: String.self) + == "widget" ) } + } + } - let result = - try await database.execute( + @Test + func transactionFailurePropagates() async throws { + try await runUsingTestDatabaseClient { database in + + let suffix = randomTableSuffix() + let table = "dummy_\(suffix)" + + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( query: #""" - SELECT "id" - FROM "\#(unescaped: table)"; + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL + ); """# ) - .collect() - #expect(result.isEmpty) + do { + _ = try await database.withTransaction { connection in + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "name") + VALUES + (1, 'ok'); + """# + ) + + return try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("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( + "null value in column" + ) + ) + #expect(error.rollbackError == nil) + #expect(error.commitError == nil) + } + catch { + Issue.record( + "Expected database transaction error to be thrown." + ) + } + + let result = + try await connection.run( + query: #""" + SELECT "id" + FROM "\#(unescaped: table)"; + """# + ) { $0 } + + #expect(result.isEmpty) + } } } @@ -756,42 +793,45 @@ struct FeatherPostgresDatabaseTestSuite { 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 + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + 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 connection.run( + 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.transaction { connection in - let result = try await connection.execute( + try await database.withTransaction { connection in + let rows = try await connection.run( query: #""" SELECT "access_token", @@ -801,8 +841,7 @@ struct FeatherPostgresDatabaseTestSuite { WHERE "id" = \#(sessionID) FOR UPDATE; """# - ) - let rows = try await result.collect() + ) { $0 } guard let row = rows.first else { throw TestError.missingRow @@ -830,7 +869,7 @@ struct FeatherPostgresDatabaseTestSuite { try await Task.sleep(for: .milliseconds(40)) - _ = try await connection.execute( + try await connection.run( query: #""" UPDATE "\#(unescaped: table)" SET @@ -860,32 +899,46 @@ struct FeatherPostgresDatabaseTestSuite { #expect(Set(tokens).count == 1) - let result = - try await database.execute( - query: #""" - SELECT - "access_token", - "refresh_count", - "access_expires_at" > 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 - ) + struct SessionRow: Codable, Sendable { + let refreshCount: Int + let accessToken: String + let isValid: Bool + + init(_ row: PostgresRow) throws { + self.refreshCount = try row.decode( + column: "refresh_count", + as: Int.self + ) + self.accessToken = try row.decode( + column: "access_token", + as: String.self + ) + self.isValid = try row.decode( + column: "is_valid", + as: Bool.self + ) + } + } + + try await database.withConnection { connection in + + let result = + try await connection.run( + query: #""" + SELECT + "access_token", + "refresh_count", + "access_expires_at" > NOW() AS "is_valid" + FROM "\#(unescaped: table)" + WHERE "id" = \#(sessionID); + """#, + ) { try SessionRow($0) } + + #expect(result.count == 1) + #expect(result[0].refreshCount == 1) + #expect(result[0].accessToken == "token_1") + #expect(result[0].isValid) + } } } @@ -895,46 +948,48 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "measurements_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" DOUBLE PRECISION NOT NULL - ); - """# - ) - - let expected = 1.5 - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "value") - VALUES - (1, \#(expected)); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + + try await connection.run( query: #""" - SELECT "value" - FROM "\#(unescaped: table)" - WHERE "id" = 1; + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" DOUBLE PRECISION NOT NULL + ); + """# + ) + + let expected = 1.5 + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, \#(expected)); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "value", as: Double.self) - == expected - ) + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "\#(unescaped: table)" + WHERE "id" = 1; + """# + ) { $0 } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "value", as: Double.self) + == expected + ) + } } } @@ -944,51 +999,53 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "items_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "value") - VALUES - (1, 'abc'); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + + try await connection.run( query: #""" - SELECT "id" - FROM "\#(unescaped: table)"; + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); """# ) - .collect() - #expect(result.count == 1) + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, 'abc'); + """# + ) - do { - _ = try result[0].decode(column: "value", as: String.self) - Issue.record("Expected decoding a missing column to throw.") - } - catch DecodingError.dataCorrupted { + let result = + try await connection.run( + query: #""" + SELECT "id" + FROM "\#(unescaped: table)"; + """# + ) { $0 } - } - catch { - Issue.record( - "Expected a dataCorrupted error for missing column." - ) + #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." + ) + } } } } @@ -999,51 +1056,53 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "items_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE "\#(unescaped: table)" ( - "id" INTEGER NOT NULL PRIMARY KEY, - "value" TEXT - ); - """# - ) - - try await database.execute( - query: #""" - INSERT INTO "\#(unescaped: table)" - ("id", "value") - VALUES - (1, 'abc'); - """# - ) - - let result = - try await database.execute( + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT + ); + """# + ) + + try await connection.run( query: #""" - SELECT "value" - FROM "\#(unescaped: table)"; + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, 'abc'); """# ) - .collect() - #expect(result.count == 1) + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "\#(unescaped: table)"; + """# + ) { $0 } - do { - _ = try result[0].decode(column: "value", as: Int.self) - Issue.record("Expected decoding a string as Int to throw.") - } - catch DecodingError.typeMismatch { + #expect(result.count == 1) - } - catch { - Issue.record( - "Expected a typeMismatch error when decoding a string as Int." - ) + 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." + ) + } } } } @@ -1054,22 +1113,25 @@ struct FeatherPostgresDatabaseTestSuite { let suffix = randomTableSuffix() let table = "missing_table_\(suffix)" - do { - _ = try await database.execute( - query: #""" - SELECT * - FROM "\#(unescaped: table)"; - """# - ) - Issue.record("Expected query to fail for missing table.") - } - catch DatabaseError.query(let error) { - #expect( - String(reflecting: error).contains("does not exist") - ) - } - catch { - Issue.record("Expected database query error to be thrown.") + try await database.withConnection { connection in + + do { + _ = try await connection.run( + query: #""" + SELECT * + FROM "\#(unescaped: table)"; + """# + ) + Issue.record("Expected query to fail for missing table.") + } + catch DatabaseError.query(let error) { + #expect( + String(reflecting: error).contains("does not exist") + ) + } + catch { + Issue.record("Expected database query error to be thrown.") + } } } } @@ -1077,21 +1139,26 @@ struct FeatherPostgresDatabaseTestSuite { @Test func versionCheck() async throws { try await runUsingTestDatabaseClient { database in - let result = try await database.execute( - query: #""" - SELECT - 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.contains("PostgreSQL")) + try await database.withConnection { connection in + + let result = try await connection.run( + query: #""" + SELECT + version() AS "version" + WHERE + 1=\#(1); + """# + ) { $0 } + + #expect(result.count == 1) + + let item = result[0] + let version = try item.decode( + column: "version", + as: String.self + ) + #expect(version.contains("PostgreSQL")) + } } } } From 6c58db08dd23ecf0bdc4ac6d4768a21e244c1c92 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 17:04:59 +0100 Subject: [PATCH 02/10] API changes based on feedbacks, new pg db types --- Package.resolved | 2 +- .../PostgresConnection.swift | 69 ------------------- .../PostgresDatabaseClient.swift | 49 +++++++------ .../PostgresDatabaseConnection.swift | 51 ++++++++++++++ .../PostgresDatabaseError.swift | 14 ++++ ...uery.swift => PostgresDatabaseQuery.swift} | 0 ...aseRow.swift => PostgresDatabaseRow.swift} | 6 +- ...wift => PostgresDatabaseRowSequence.swift} | 19 ++++- .../FeatherPostgresDatabaseTestSuite.swift | 55 +++++++++------ 9 files changed, 145 insertions(+), 120 deletions(-) delete mode 100644 Sources/FeatherPostgresDatabase/PostgresConnection.swift create mode 100644 Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift create mode 100644 Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift rename Sources/FeatherPostgresDatabase/{PostgresQuery.swift => PostgresDatabaseQuery.swift} (100%) rename Sources/FeatherPostgresDatabase/{PostgresRow+DatabaseRow.swift => PostgresDatabaseRow.swift} (94%) rename Sources/FeatherPostgresDatabase/{PostgresQueryResult.swift => PostgresDatabaseRowSequence.swift} (74%) diff --git a/Package.resolved b/Package.resolved index 5637287..e875c33 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/feather-framework/feather-database", "state" : { "branch" : "fix/consumption", - "revision" : "8de8b87ec6bf10a67d201b9c4b14796a0711a155" + "revision" : "2e7c7461fadae813f6d8faad63ef8c3ba4b121ec" } }, { diff --git a/Sources/FeatherPostgresDatabase/PostgresConnection.swift b/Sources/FeatherPostgresDatabase/PostgresConnection.swift deleted file mode 100644 index de8749b..0000000 --- a/Sources/FeatherPostgresDatabase/PostgresConnection.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// PostgresConnection.swift -// feather-postgres-database -// -// Created by Tibor Bödecs on 2026. 01. 10.. -// - -import FeatherDatabase -import PostgresNIO - -extension PostgresConnection: @retroactive DatabaseConnection { - public typealias Query = PostgresQuery - public typealias Result = PostgresQueryResult - - /// Execute a Postgres query on this connection. - /// - /// This wraps `PostgresNIO` query execution and maps errors. - /// - Parameter query: The Postgres query to execute. - /// - Throws: A `DatabaseError` if the query fails. - /// - Returns: A query result containing the returned rows. - @discardableResult - public func run( - query: Query, - _ handler: (Result.Row) async throws -> T = { $0 } - ) async throws(DatabaseError) -> [T] { - do { - let resultSequence = try await self.query( - .init( - unsafeSQL: query.sql, - binds: query.bindings - ), - logger: logger - ) - - var result: [T] = [] - for try await item in resultSequence { - result.append(try await handler(item)) - } - return result - } - catch { - throw .query(error) - } - - } - - public func run( - query: Query, - _ handler: (Result.Row) async throws -> Void = { _ in } - ) async throws(DatabaseError) { - do { - let resultSequence = try await self.query( - .init( - unsafeSQL: query.sql, - binds: query.bindings - ), - logger: logger - ) - - for try await item in resultSequence { - try await handler(item) - } - } - catch { - throw .query(error) - } - } - -} diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift index a7da9b6..5cecac6 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift @@ -9,18 +9,13 @@ import FeatherDatabase import Logging import PostgresNIO -/// Make Postgres transaction errors conform to `DatabaseTransactionError`. -/// -/// This allows Postgres errors to flow through `DatabaseError`. -extension PostgresTransactionError: @retroactive DatabaseTransactionError {} - /// A Postgres-backed database client. /// /// Use this client to execute queries and manage transactions on Postgres. public struct PostgresDatabaseClient: DatabaseClient { - public typealias Connection = PostgresConnection + public typealias Connection = PostgresDatabaseConnection - var client: PostgresClient + var client: PostgresNIO.PostgresClient var logger: Logger /// Create a Postgres database client. @@ -30,7 +25,7 @@ public struct PostgresDatabaseClient: DatabaseClient { /// - client: The underlying Postgres client. /// - logger: The logger for database operations. public init( - client: PostgresClient, + client: PostgresNIO.PostgresClient, logger: Logger ) { self.client = client @@ -42,18 +37,21 @@ public struct PostgresDatabaseClient: DatabaseClient { /// Execute work using a managed Postgres connection. /// /// The closure receives a Postgres connection for the duration of the call. - /// - Parameters: - /// - isolation: The actor isolation for the operation. - /// - closure: A closure that receives the connection. + /// - Parameter: closure: A closure that receives the connection. /// - Throws: A `DatabaseError` if connection handling fails. /// - Returns: The query result produced by the closure. @discardableResult public func withConnection( - isolation: isolated (any Actor)? = #isolation, - _ closure: (Connection) async throws -> sending T, - ) async throws(DatabaseError) -> sending T { + _ closure: (Connection) async throws -> T, + ) async throws(DatabaseError) -> T { do { - return try await client.withConnection(closure) + return try await client.withConnection { connection in + let databaseConnection = PostgresDatabaseConnection( + connection: connection, + logger: logger + ) + return try await closure(databaseConnection) + } } catch let error as DatabaseError { throw error @@ -66,22 +64,23 @@ public struct PostgresDatabaseClient: DatabaseClient { /// Execute work inside a Postgres transaction. /// /// The closure is wrapped in a transactional scope. - /// - Parameters: - /// - isolation: The actor isolation for the operation. - /// - closure: A closure that receives the connection. + /// - Parameter: closure: A closure that receives the connection. /// - Throws: A `DatabaseError` if the transaction fails. /// - Returns: The query result produced by the closure. @discardableResult public func withTransaction( - isolation: isolated (any Actor)? = #isolation, - _ closure: (Connection) async throws -> sending T, - ) async throws(DatabaseError) -> sending T { + _ closure: (Connection) async throws -> T, + ) async throws(DatabaseError) -> T { do { return try await client.withTransaction( - logger: logger, - isolation: isolation, - closure - ) + logger: logger + ) { connection in + let databaseConnection = PostgresDatabaseConnection( + connection: connection, + logger: logger + ) + return try await closure(databaseConnection) + } } catch let error as PostgresTransactionError { throw .transaction(error) diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift new file mode 100644 index 0000000..a3036b3 --- /dev/null +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift @@ -0,0 +1,51 @@ +// +// PostgresConnection.swift +// feather-postgres-database +// +// Created by Tibor Bödecs on 2026. 01. 10.. +// + +import FeatherDatabase +import PostgresNIO + +public struct PostgresDatabaseConnection: DatabaseConnection { + + public typealias Query = PostgresQuery + public typealias RowSequence = PostgresDatabaseRowSequence + + var connection: PostgresConnection + public var logger: Logger + + /// Execute a Postgres query on this connection. + /// + /// This wraps `PostgresNIO` query execution and maps errors. + /// - Parameters: + /// - query: The Postgres query to execute. + /// - handler: A closure that receives the RowSequence result. + /// - Throws: A `DatabaseError` if the query fails. + /// - Returns: A query result containing the returned rows. + @discardableResult + public func run( + query: Query, + _ handler: (RowSequence) async throws -> T = { _ in } + ) async throws(DatabaseError) -> T { + do { + let sequence = try await connection.query( + .init( + unsafeSQL: query.sql, + binds: query.bindings + ), + logger: logger + ) + + return try await handler( + PostgresDatabaseRowSequence( + backingSequence: sequence + ) + ) + } + catch { + throw .query(error) + } + } +} diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift new file mode 100644 index 0000000..2946a47 --- /dev/null +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift @@ -0,0 +1,14 @@ +// +// File.swift +// feather-postgres-database +// +// Created by Tibor Bödecs on 2026. 02. 02.. +// + +import FeatherDatabase +import PostgresNIO + +/// Make Postgres transaction errors conform to `DatabaseTransactionError`. +/// +/// This allows Postgres errors to flow through `DatabaseError`. +extension PostgresTransactionError: @retroactive DatabaseTransactionError {} diff --git a/Sources/FeatherPostgresDatabase/PostgresQuery.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift similarity index 100% rename from Sources/FeatherPostgresDatabase/PostgresQuery.swift rename to Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift diff --git a/Sources/FeatherPostgresDatabase/PostgresRow+DatabaseRow.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift similarity index 94% rename from Sources/FeatherPostgresDatabase/PostgresRow+DatabaseRow.swift rename to Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift index 9204e01..49abfc8 100644 --- a/Sources/FeatherPostgresDatabase/PostgresRow+DatabaseRow.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift @@ -8,7 +8,9 @@ import FeatherDatabase import PostgresNIO -extension PostgresRow: @retroactive DatabaseRow { +public struct PostgresDatabaseRow: DatabaseRow { + + var row: PostgresRow /// Decode a column value as the given type. /// @@ -22,7 +24,7 @@ extension PostgresRow: @retroactive DatabaseRow { column: String, as type: T.Type ) throws(DecodingError) -> T { - let row = makeRandomAccess() + let row = row.makeRandomAccess() guard row.contains(column) else { throw .dataCorrupted( .init( diff --git a/Sources/FeatherPostgresDatabase/PostgresQueryResult.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift similarity index 74% rename from Sources/FeatherPostgresDatabase/PostgresQueryResult.swift rename to Sources/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift index a6eb03d..650a19d 100644 --- a/Sources/FeatherPostgresDatabase/PostgresQueryResult.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift @@ -11,7 +11,7 @@ import PostgresNIO /// A query result backed by a Postgres row sequence. /// /// Use this type to iterate or collect Postgres query results. -public struct PostgresQueryResult: DatabaseQueryResult { +public struct PostgresDatabaseRowSequence: DatabaseRowSequence { var backingSequence: PostgresRowSequence @@ -28,14 +28,14 @@ public struct PostgresQueryResult: DatabaseQueryResult { /// - Returns: The next `PostgresRow`, or `nil` when finished. #if compiler(>=6.2) @concurrent - public mutating func next() async throws -> PostgresRow? { + public mutating func next() async throws -> PostgresDatabaseRow? { guard !Task.isCancelled else { return nil } guard let postgresRow = try await backingIterator.next() else { return nil } - return postgresRow + return .init(row: postgresRow) } #else public mutating func next() async throws -> PostgresRow? { @@ -59,4 +59,17 @@ public struct PostgresQueryResult: DatabaseQueryResult { backingIterator: backingSequence.makeAsyncIterator(), ) } + + /// Collect all rows into an array. + /// + /// This consumes the sequence and returns all rows. + /// - Throws: An error if iteration fails. + /// - Returns: An array of `PostgresRow` values. + public func collect() async throws -> [PostgresDatabaseRow] { + var items: [PostgresDatabaseRow] = [] + for try await item in self { + items.append(item) + } + return items + } } diff --git a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift index 93b35e3..594ea52 100644 --- a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift +++ b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift @@ -178,7 +178,12 @@ struct FeatherPostgresDatabaseTestSuite { AND "tablename" = '\#(unescaped: table)' ORDER BY "tablename"; """# - ) { try $0.decode(column: "tablename", as: String.self) } + ) { + try await $0.collect() + .map { + try $0.decode(column: "tablename", as: String.self) + } + } #expect(results.count == 1) #expect(results[0] == table) @@ -214,7 +219,7 @@ struct FeatherPostgresDatabaseTestSuite { let id: Int let name: String - init(_ row: PostgresRow) throws { + init(_ row: DatabaseRow) throws { self.id = try row.decode(column: "id", as: Int.self) self.name = try row.decode( column: "name", @@ -237,7 +242,12 @@ struct FeatherPostgresDatabaseTestSuite { query: #""" SELECT * FROM "\#(unescaped: table)" ORDER BY "name" ASC; """# - ) { try GalaxyRow($0) } + ) { + try await $0.collect() + .map { + try GalaxyRow($0) + } + } #expect(results.count == 2) #expect(results[0].name == name1) @@ -282,7 +292,7 @@ struct FeatherPostgresDatabaseTestSuite { let id: Int let value: String - init(_ row: PostgresRow) throws { + init(_ row: DatabaseRow) throws { self.id = try row.decode(column: "id", as: Int.self) self.value = try row.decode( column: "value", @@ -297,7 +307,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" ORDER BY "id"; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 2) @@ -375,7 +385,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" ORDER BY "id" ASC; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 2) @@ -434,7 +444,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" WHERE "id" = 1; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) #expect( @@ -485,7 +495,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" WHERE "id" = 1; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) #expect( @@ -536,7 +546,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" WHERE "id" = 1; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) #expect( @@ -584,7 +594,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" ORDER BY "id"; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 2) let first = result[0] @@ -642,7 +652,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" ORDER BY "id" ASC; """# - ) { $0 } + ) { try await $0.collect() } .first #expect(result != nil) @@ -694,7 +704,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" WHERE "id" = 1; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) #expect( @@ -775,7 +785,7 @@ struct FeatherPostgresDatabaseTestSuite { SELECT "id" FROM "\#(unescaped: table)"; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.isEmpty) } @@ -841,7 +851,7 @@ struct FeatherPostgresDatabaseTestSuite { WHERE "id" = \#(sessionID) FOR UPDATE; """# - ) { $0 } + ) { try await $0.collect() } guard let row = rows.first else { throw TestError.missingRow @@ -904,7 +914,7 @@ struct FeatherPostgresDatabaseTestSuite { let accessToken: String let isValid: Bool - init(_ row: PostgresRow) throws { + init(_ row: DatabaseRow) throws { self.refreshCount = try row.decode( column: "refresh_count", as: Int.self @@ -932,7 +942,12 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" WHERE "id" = \#(sessionID); """#, - ) { try SessionRow($0) } + ) { + try await $0.collect() + .map { + try SessionRow($0) + } + } #expect(result.count == 1) #expect(result[0].refreshCount == 1) @@ -982,7 +997,7 @@ struct FeatherPostgresDatabaseTestSuite { FROM "\#(unescaped: table)" WHERE "id" = 1; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) #expect( @@ -1030,7 +1045,7 @@ struct FeatherPostgresDatabaseTestSuite { SELECT "id" FROM "\#(unescaped: table)"; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) @@ -1087,7 +1102,7 @@ struct FeatherPostgresDatabaseTestSuite { SELECT "value" FROM "\#(unescaped: table)"; """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) @@ -1148,7 +1163,7 @@ struct FeatherPostgresDatabaseTestSuite { WHERE 1=\#(1); """# - ) { $0 } + ) { try await $0.collect() } #expect(result.count == 1) From 97587e1b751230ccc6b5e16bc6238bf858366e05 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 17:13:19 +0100 Subject: [PATCH 03/10] new db query object --- .../PostgresDatabaseConnection.swift | 2 +- .../PostgresDatabaseQuery.swift | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift index a3036b3..274b384 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift @@ -10,7 +10,7 @@ import PostgresNIO public struct PostgresDatabaseConnection: DatabaseConnection { - public typealias Query = PostgresQuery + public typealias Query = PostgresDatabaseQuery public typealias RowSequence = PostgresDatabaseRowSequence var connection: PostgresConnection diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift index 8a609d6..db4f345 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift @@ -8,14 +8,41 @@ import FeatherDatabase import PostgresNIO -extension PostgresQuery: @retroactive DatabaseQuery { +public struct PostgresDatabaseQuery: DatabaseQuery { /// The bindings type for Postgres queries. /// /// This type represents parameter bindings for PostgresNIO. public typealias Bindings = PostgresBindings + /// The SQL text to execute. + /// + /// This is the raw SQL string for the query. + public var sql: String { + query.sql + } + /// The bound parameters for the SQL text. /// /// This exposes the underlying `binds` storage. - public var bindings: PostgresBindings { binds } + public var bindings: PostgresBindings { + query.binds + } + + var query: PostgresQuery + +} + +extension PostgresDatabaseQuery: ExpressibleByStringInterpolation { + + public init( + stringLiteral value: String + ) { + self.init(query: .init(stringLiteral: value)) + } + + public init( + stringInterpolation value: PostgresQuery.StringInterpolation + ) { + self.init(query: .init(stringInterpolation: value)) + } } From ca4a2944cc24b284c1280d1253e343a4ccbdeabe Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 17:18:00 +0100 Subject: [PATCH 04/10] custom transaction error type --- Package.resolved | 2 +- .../PostgresDatabaseClient.swift | 6 ++- .../PostgresDatabaseError.swift | 14 ------- .../PostgresDatabaseTransactionError.swift | 38 +++++++++++++++++++ 4 files changed, 44 insertions(+), 16 deletions(-) delete mode 100644 Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift create mode 100644 Sources/FeatherPostgresDatabase/PostgresDatabaseTransactionError.swift diff --git a/Package.resolved b/Package.resolved index e875c33..b922d44 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/feather-framework/feather-database", "state" : { "branch" : "fix/consumption", - "revision" : "2e7c7461fadae813f6d8faad63ef8c3ba4b121ec" + "revision" : "327c474c3874ecbd18b4ae15dadc286a84488814" } }, { diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift index 5cecac6..8004f38 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift @@ -83,7 +83,11 @@ public struct PostgresDatabaseClient: DatabaseClient { } } catch let error as PostgresTransactionError { - throw .transaction(error) + throw .transaction( + PostgresDatabaseTransactionError( + underlyingError: error + ) + ) } catch { throw .connection(error) diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift deleted file mode 100644 index 2946a47..0000000 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseError.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// File.swift -// feather-postgres-database -// -// Created by Tibor Bödecs on 2026. 02. 02.. -// - -import FeatherDatabase -import PostgresNIO - -/// Make Postgres transaction errors conform to `DatabaseTransactionError`. -/// -/// This allows Postgres errors to flow through `DatabaseError`. -extension PostgresTransactionError: @retroactive DatabaseTransactionError {} diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseTransactionError.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseTransactionError.swift new file mode 100644 index 0000000..a4caf93 --- /dev/null +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseTransactionError.swift @@ -0,0 +1,38 @@ +// +// PostgresDatabaseTransactionError.swift +// feather-postgres-database +// +// Created by Tibor Bödecs on 2026. 02. 02.. +// + +import FeatherDatabase +import PostgresNIO + +public struct PostgresDatabaseTransactionError: DatabaseTransactionError { + + var underlyingError: PostgresTransactionError + + public var file: String { + underlyingError.file + } + + public var line: Int { + underlyingError.line + } + + public var beginError: (any Error)? { + underlyingError.beginError + } + + public var closureError: (any Error)? { + underlyingError.closureError + } + + public var commitError: (any Error)? { + underlyingError.commitError + } + + public var rollbackError: (any Error)? { + underlyingError.rollbackError + } +} From 6337021114e5a8237d1433a77e3a2c087d9ca028 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 2 Feb 2026 17:24:50 +0100 Subject: [PATCH 05/10] custom query string interpolation with unescaped int support --- .../PostgresDatabaseQuery.swift | 115 +++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift index db4f345..9968541 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift @@ -41,8 +41,119 @@ extension PostgresDatabaseQuery: ExpressibleByStringInterpolation { } public init( - stringInterpolation value: PostgresQuery.StringInterpolation + stringInterpolation value: StringInterpolation ) { - self.init(query: .init(stringInterpolation: value)) + self.init( + query: .init( + unsafeSQL: value.sql, + binds: value.binds + ) + ) + } +} + +extension PostgresDatabaseQuery { + + // NOTE: source derived from postgres-nio + public struct StringInterpolation: StringInterpolationProtocol, Sendable { + public typealias StringLiteralType = String + + @usableFromInline + var sql: String + + @usableFromInline + var binds: PostgresBindings + + public init( + literalCapacity: Int, + interpolationCount: Int + ) { + self.sql = "" + self.binds = PostgresBindings(capacity: interpolationCount) + } + + public mutating func appendLiteral( + _ literal: String + ) { + self.sql.append(contentsOf: literal) + } + + @inlinable + public mutating func appendInterpolation< + Value: PostgresThrowingDynamicTypeEncodable + >( + _ value: Value + ) throws { + try self.binds.append(value, context: .default) + self.sql.append(contentsOf: "$\(self.binds.count)") + } + + @inlinable + public mutating func appendInterpolation< + Value: PostgresThrowingDynamicTypeEncodable + >( + _ value: Value? + ) throws { + switch value { + case .none: + self.binds.appendNull() + case .some(let value): + try self.binds.append(value, context: .default) + } + + self.sql.append(contentsOf: "$\(self.binds.count)") + } + + @inlinable + public mutating func appendInterpolation< + Value: PostgresDynamicTypeEncodable + >( + _ value: Value + ) { + self.binds.append(value, context: .default) + self.sql.append(contentsOf: "$\(self.binds.count)") + } + + @inlinable + public mutating func appendInterpolation< + Value: PostgresDynamicTypeEncodable + >( + _ value: Value? + ) { + switch value { + case .none: + self.binds.appendNull() + case .some(let value): + self.binds.append(value, context: .default) + } + + self.sql.append(contentsOf: "$\(self.binds.count)") + } + + @inlinable + public mutating func appendInterpolation< + Value: PostgresThrowingDynamicTypeEncodable, + JSONEncoder: PostgresJSONEncoder + >( + _ value: Value, + context: PostgresEncodingContext + ) throws { + try self.binds.append(value, context: context) + self.sql.append(contentsOf: "$\(self.binds.count)") + } + + @inlinable + public mutating func appendInterpolation( + unescaped interpolated: String + ) { + self.sql.append(contentsOf: interpolated) + } + + @inlinable + public mutating func appendInterpolation( + unescaped interpolated: Int + ) { + self.sql.append(contentsOf: String(interpolated)) + } } } From 3074aafb53f7c1a52c1f2ccb00f1787e07024e79 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Wed, 4 Feb 2026 12:22:35 +0100 Subject: [PATCH 06/10] prep for beta2 --- Package.resolved | 2 +- README.md | 24 ++++++++++--------- .../PostgresDatabaseConnection.swift | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Package.resolved b/Package.resolved index b922d44..f1f671d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/feather-framework/feather-database", "state" : { "branch" : "fix/consumption", - "revision" : "327c474c3874ecbd18b4ae15dadc286a84488814" + "revision" : "2d2e5f46f286599defa83086806da42dc7727961" } }, { diff --git a/README.md b/README.md index d087ec5..24e9a5a 100644 --- a/README.md +++ b/README.md @@ -49,16 +49,16 @@ Then add `FeatherPostgresDatabase` 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-postgres-database/documentation/featherpostgresdatabase/ ) -API documentation is available at the following link. +Here is a brief example: -> [!TIP] -> Avoid calling `database.execute` while in a transaction; use the transaction `connection` instead. ```swift import Logging @@ -100,14 +100,16 @@ try await withThrowingTaskGroup(of: Void.self) { group in } // execute some query group.addTask { - let result = try await database.execute( - query: #""" - SELECT - version() AS "version" - WHERE - 1=\#(1); - """# - ) + let result = try await database.withConnection { connection in + try await connection.run( + query: #""" + SELECT + version() AS "version" + WHERE + 1=\#(1); + """# + ) + } for try await item in result { let version = try item.decode(column: "version", as: String.self) diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift index 274b384..b0a3c07 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift @@ -27,7 +27,7 @@ public struct PostgresDatabaseConnection: 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 sequence = try await connection.query( From 5fbbbdd18c10c469e3fc7259713fe5b127f323df Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Wed, 4 Feb 2026 12:25:28 +0100 Subject: [PATCH 07/10] fix dep --- Package.resolved | 6 +++--- Package.swift | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index f1f671d..e0da6c8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "2f4544e2a879160ae7b4aeb6bf035e1be0da0d50d90af93ea54e0a32b7528d62", + "originHash" : "fa52841b94ca393999caeb6b5bae49eec5f6dc7f48c7920b5cbfe47915ddb293", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "branch" : "fix/consumption", - "revision" : "2d2e5f46f286599defa83086806da42dc7727961" + "revision" : "4ef69e67018c4bdf843858e8976c13b97c3afe4c", + "version" : "1.0.0-beta.3" } }, { diff --git a/Package.swift b/Package.swift index 8b879ec..80e44c6 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/postgres-nio", from: "1.27.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.3"), // [docc-plugin-placeholder] ], targets: [ From b86f29e9ac8e0a67d1c0425980727ce38f0aad4b Mon Sep 17 00:00:00 2001 From: GErP83 Date: Wed, 4 Feb 2026 15:21:31 +0100 Subject: [PATCH 08/10] change makefile --- Makefile | 5 ++++- README.md | 55 ++++++++++++++++++++++--------------------------------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index e165a15..ad60015 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 24e9a5a..bed4523 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,31 @@ Postgres 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) -]( - https://github.com/feather-framework/feather-postgres-database/releases/tag/1.0.0-beta.2 -) +[![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 -- 🤝 Postgres 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 +- Postgres 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+ +- Platforms: + - Linux + - macOS 15+ + - iOS 18+ + - tvOS 18+ + - watchOS 11+ + - visionOS 2+ ## Installation @@ -46,19 +42,13 @@ Then add `FeatherPostgresDatabase` to your target dependencies: .product(name: "FeatherPostgresDatabase", package: "feather-postgres-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-postgres-database/documentation/featherpostgresdatabase/ -) -Here is a brief example: +[![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)](https://feather-framework.github.io/feather-postgres-database/) +Here is a brief example: ```swift import Logging @@ -124,7 +114,6 @@ try await withThrowingTaskGroup(of: Void.self) { group in > [!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: @@ -135,12 +124,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-postgres-database/pulls) are welcome. Please keep changes focused and include tests for new logic. 🙏 +[Pull requests](https://github.com/feather-framework/feather-postgres-database/pulls) are welcome. Please keep changes focused and include tests for new logic. From d8bfed53ddfb5030ea25ba4947dc881807c28f4a Mon Sep 17 00:00:00 2001 From: GErP83 Date: Wed, 4 Feb 2026 15:23:40 +0100 Subject: [PATCH 09/10] fix headers --- .../FeatherPostgresDatabase/PostgresDatabaseConnection.swift | 4 ++-- Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift | 4 ++-- Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift | 4 ++-- .../FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift index b0a3c07..b255861 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift @@ -1,8 +1,8 @@ // -// PostgresConnection.swift +// PostgresDatabaseConnection.swift // feather-postgres-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/FeatherPostgresDatabase/PostgresDatabaseQuery.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift index 9968541..04e6cff 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseQuery.swift @@ -1,8 +1,8 @@ // -// PostgresQuery.swift +// PostgresDatabaseQuery.swift // feather-postgres-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/FeatherPostgresDatabase/PostgresDatabaseRow.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift index 49abfc8..48faf91 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseRow.swift @@ -1,8 +1,8 @@ // -// PostgresRow+DatabaseRow.swift +// PostgresDatabaseRow.swift // feather-postgres-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/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift index 650a19d..7212077 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseRowSequence.swift @@ -1,8 +1,8 @@ // -// PostgresQueryResult.swift +// PostgresDatabaseRowSequence.swift // feather-postgres-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase From fc808715d3d2eeae70eb38bacd33cdbe1798aeab Mon Sep 17 00:00:00 2001 From: GErP83 Date: Wed, 4 Feb 2026 17:15:30 +0100 Subject: [PATCH 10/10] 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 a9773ae..d810b04 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 @@ -27,7 +27,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: