From ff27472af753d1ec64ec3a7c232faa8b34091ad5 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 05:28:16 -0500 Subject: [PATCH 1/3] Bump Swift min to 5.10, update README and API docs, clean up some doc comments, run swift-format, fix warning about retroactive conformance in 6.2. --- .github/workflows/test.yml | 4 +- Package.swift | 9 +- README.md | 29 ++-- Sources/PostgresKit/Docs.docc/PostgresKit.md | 128 ++++++++++++++++-- .../Resources/vapor-postgreskit-logo.svg | 30 ++-- .../PostgresKit/Docs.docc/theme-settings.json | 2 +- .../PostgresKit/PostgresDataTranslation.swift | 109 ++++++++------- .../PostgresKit/PostgresDatabase+SQL.swift | 34 +++-- Sources/PostgresKit/PostgresDialect.swift | 14 +- .../SQLPostgresConfiguration.swift | 4 +- 10 files changed, 242 insertions(+), 121 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61184fb6..72ffa430 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,6 +68,8 @@ jobs: POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }} POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }} steps: + - name: Install curl + run: apt-get update -y -q && apt-get install -y curl - name: Check out package uses: actions/checkout@v5 - name: Run local tests @@ -133,7 +135,7 @@ jobs: run: | brew upgrade || true export PATH="$(brew --prefix)/opt/postgresql@13/bin:$PATH" PGDATA=/tmp/vapor-postgres-test - (brew unlink postgresql@14 || true) && brew install "postgresql@13" && brew link --force "postgresql@13" + brew install "postgresql@17" && brew link --force "postgresql@17" initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}") pg_ctl start --wait timeout-minutes: 15 diff --git a/Package.swift b/Package.swift index b982c5cb..324ba778 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 import PackageDescription let package = Package( @@ -13,9 +13,9 @@ let package = Package( .library(name: "PostgresKit", targets: ["PostgresKit"]), ], dependencies: [ - .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.21.1"), - .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"), - .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), + .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.27.0"), + .package(url: "https://github.com/vapor/sql-kit.git", from: "3.33.1"), + .package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"), ], targets: [ .target( @@ -40,6 +40,7 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("ConciseMagicFile"), .enableUpcomingFeature("ForwardTrailingClosures"), .enableUpcomingFeature("DisableOutwardActorInference"), diff --git a/README.md b/README.md index 46b29a48..709a4b38 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@

- - - - PostgresKit - +PostgresKit

Documentation @@ -11,7 +7,7 @@ MIT License Continuous Integration -Swift 5.9+ +Swift 5.10+


@@ -35,16 +31,11 @@ PostgresKit supports the following platforms: ## Overview -PostgresKit is an [SQLKit] driver for PostgreSQL clients. It supports building and serializing Postgres-dialect SQL queries. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit](https://github.com/vapor/async-kit) is used to provide connection pooling. +PostgresKit is an [SQLKit] driver for PostgreSQL clients. It supports building and serializing Postgres-dialect SQL queries. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. > [!IMPORTANT] > It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. -[SQLKit]: https://github.com/vapor/sql-kit -[PostgresNIO]: https://github.com/vapor/postgres-nio -[AsyncKit]: https://github.com/vapor/async-kit -[PostgresClient]: https://api.vapor.codes/postgresnio/documentation/postgresnio/postgresclient - ### Configuration Database connection options and credentials are specified using a `PostgresConfiguration` struct. @@ -96,7 +87,7 @@ defer { pools.shutdown() } First create a `PostgresConnectionSource` using the configuration struct. This type is responsible for creating new connections to your database server as needed. -Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit SwiftNIO's [documentation](https://apple.github.io/swift-nio/docs/current/NIO/index.html). Make sure to shutdown the connection pool before it deinitializes. +Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit [SwiftNIO's documentation]. Make sure to shutdown the connection pool before it deinitializes. `EventLoopGroupConnectionPool` is a collection of pools for each event loop. When using `EventLoopGroupConnectionPool` directly, random event loops will be chosen as needed. @@ -126,7 +117,7 @@ let postgres = pool.database(logger: ...) // PostgresDatabase let rows = try postgres.simpleQuery("SELECT version();").wait() ``` -Visit [PostgresNIO's docs](https://github.com/vapor/postgres-nio) for more information on using `PostgresDatabase`. +Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`. ### SQLDatabase @@ -137,4 +128,12 @@ let sql = postgres.sql() // SQLDatabase let planets = try sql.select().column("*").from("planets").all().wait() ``` -Visit [SQLKit's docs](https://github.com/vapor/sql-kit) for more information on using `SQLDatabase`. +Visit [SQLKit's docs] for more information on using `SQLDatabase`. + +[SQLKit]: https://github.com/vapor/sql-kit +[SQLKit's docs]: https://api.vapor.codes/sqlkit/documentation/sqlkit +[PostgresNIO]: https://github.com/vapor/postgres-nio +[PostgresNIO's docs]: https://api.vapor.codes/postgresnio/documentation/postgresnio +[AsyncKit]: https://github.com/vapor/async-kit +[PostgresClient]: https://api.vapor.codes/postgresnio/documentation/postgresnio/postgresclient +[SwiftNIO's documentation]: https://swiftpackageindex.com/apple/swift-nio/documentation/nio diff --git a/Sources/PostgresKit/Docs.docc/PostgresKit.md b/Sources/PostgresKit/Docs.docc/PostgresKit.md index 4b86941e..94a7ee2a 100644 --- a/Sources/PostgresKit/Docs.docc/PostgresKit.md +++ b/Sources/PostgresKit/Docs.docc/PostgresKit.md @@ -4,29 +4,127 @@ @TitleHeading(Package) } -PostgresKit is a library providing an SQLKit driver for PostgresNIO. +🐘 Non-blocking, event-driven Swift client for PostgreSQL. + +### Usage + +Use the SPM string to easily include the dependendency in your `Package.swift` file. + +```swift +.package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0") +``` + +### Supported Platforms + +PostgresKit supports the following platforms: + +- Ubuntu 20.04+ +- macOS 10.15+ ## Overview -This package provides the "foundational" level of support for using [Fluent] with PostgreSQL by implementing the requirements of an [SQLKit] driver. It is responsible for: +PostgresKit is an [SQLKit] driver for PostgreSQL clients. It supports building and serializing Postgres-dialect SQL queries. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. + +> Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. + +### Configuration + +Database connection options and credentials are specified using a ``SQLPostgresConfiguration`` struct. + +```swift +import PostgresKit + +let configuration = SQLPostgresConfiguration( + hostname: "localhost", + username: "vapor_username", + password: "vapor_password", + database: "vapor_database" +) +``` + +URL-based configuration is also supported. + +```swift +guard let configuration = SQLPostgresConfiguration(url: "postgres://...") else { + ... +} +``` + +To connect via unix-domain sockets, use ``SQLPostgresConfiguration/init(unixDomainSocketPath:username:password:database:)`` instead of ``SQLPostgresConfiguration/init(hostname:port:username:password:database:tls:)``. + +```swift +let configuration = PostgresConfiguration( + unixDomainSocketPath: "/path/to/socket", + username: "vapor_username", + password: "vapor_password", + database: "vapor_database" +) +``` + +### Connection Pool + +Once you have a ``SQLPostgresConfiguration``, you can use it to create a connection source and pool. + +```swift +let eventLoopGroup: EventLoopGroup = ... +defer { try! eventLoopGroup.syncShutdown() } + +let pools = EventLoopGroupConnectionPool( + source: PostgresConnectionSource(configuration: configuration), + on: eventLoopGroup +) +defer { pools.shutdown() } +``` + +First create a ``PostgresConnectionSource`` using the configuration struct. This type is responsible for creating new connections to your database server as needed. + +Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit [SwiftNIO's documentation]. Make sure to shutdown the connection pool before it deinitializes. + +`EventLoopGroupConnectionPool` is a collection of pools for each event loop. When using `EventLoopGroupConnectionPool` directly, random event loops will be chosen as needed. + +```swift +pools.withConnection { conn + print(conn) // PostgresConnection on randomly chosen event loop +} +``` + +To get a pool for a specific event loop, use `pool(for:)`. This returns an `EventLoopConnectionPool`. + +```swift +let eventLoop: EventLoop = ... +let pool = pools.pool(for: eventLoop) + +pool.withConnection { conn + print(conn) // PostgresConnection on eventLoop +} +``` + +### PostgresDatabase + +Both `EventLoopGroupConnectionPool` and `EventLoopConnectionPool` can be used to create instances of `PostgresDatabase`. -- Managing the underlying PostgreSQL library ([PostgresNIO]), -- Providing a two-way bridge between PostgresNIO and SQLKit's generic data and metadata formats, and -- Presenting an interface for establishing, managing, and interacting with database connections via [AsyncKit]. +```swift +let postgres = pool.database(logger: ...) // PostgresDatabase +let rows = try postgres.simpleQuery("SELECT version();").wait() +``` -> Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit-based support. +Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`. -> Tip: A FluentKit driver for PostgreSQL is provided by the [FluentPostgresDriver] package. +### SQLDatabase -## Version Support +A `PostgresDatabase` can be used to create an instance of `SQLDatabase`. -This package uses [PostgresNIO] for all underlying database interactions. It is compatible with all versions of PostgreSQL and all platforms supported by that package. +```swift +let sql = postgres.sql() // SQLDatabase +let planets = try sql.select().column("*").from("planets").all().wait() +``` -> Caution: There is one exception to the above at the time of this writing: This package requires Swift 5.8 or newer, whereas PostgresNIO continues to support Swift 5.6. +Visit [SQLKit's docs] for more information on using `SQLDatabase`. -[SQLKit]: https://swiftpackageindex.com/vapor/sql-kit -[PostgresNIO]: https://swiftpackageindex.com/vapor/postgres-nio -[Fluent]: https://swiftpackageindex.com/vapor/fluent-kit -[FluentPostgresDriver]: https://swiftpackageindex.com/vapor/fluent-postgres-driver -[AsyncKit]: https://swiftpackageindex.com/vapor/async-kit +[SQLKit]: https://github.com/vapor/sql-kit +[SQLKit's docs]: https://api.vapor.codes/sqlkit/documentation/sqlkit +[PostgresNIO]: https://github.com/vapor/postgres-nio +[PostgresNIO's docs]: https://api.vapor.codes/postgresnio/documentation/postgresnio +[AsyncKit]: https://github.com/vapor/async-kit [PostgresClient]: https://api.vapor.codes/postgresnio/documentation/postgresnio/postgresclient +[SwiftNIO's documentation]: https://swiftpackageindex.com/apple/swift-nio/documentation/nio diff --git a/Sources/PostgresKit/Docs.docc/Resources/vapor-postgreskit-logo.svg b/Sources/PostgresKit/Docs.docc/Resources/vapor-postgreskit-logo.svg index 577997a4..6482a7d0 100644 --- a/Sources/PostgresKit/Docs.docc/Resources/vapor-postgreskit-logo.svg +++ b/Sources/PostgresKit/Docs.docc/Resources/vapor-postgreskit-logo.svg @@ -1,21 +1,25 @@ - - - + + + - - - + + + - - - + + + - - + + diff --git a/Sources/PostgresKit/Docs.docc/theme-settings.json b/Sources/PostgresKit/Docs.docc/theme-settings.json index d78a16fd..52bf00bc 100644 --- a/Sources/PostgresKit/Docs.docc/theme-settings.json +++ b/Sources/PostgresKit/Docs.docc/theme-settings.json @@ -15,7 +15,7 @@ "logo-shape": { "dark": "#000", "light": "#fff" }, "fill": { "dark": "#000", "light": "#fff" } }, - "icons": { "technology": "/postgreskit/images/vapor-postgreskit-logo.svg" } + "icons": { "technology": "/postgreskit/images/PostgresKit/vapor-postgreskit-logo.svg" } }, "features": { "quickNavigation": { "enable": true }, diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift index 3e04d790..eb7053cd 100644 --- a/Sources/PostgresKit/PostgresDataTranslation.swift +++ b/Sources/PostgresKit/PostgresDataTranslation.swift @@ -1,27 +1,27 @@ import Foundation import PostgresNIO -/// Quick and dirty ``CodingKey``, borrowed from FluentKit. If ``CodingKeyRepresentable`` wasn't broken by design +/// Quick and dirty `CodingKey`, borrowed from FluentKit. If `CodingKeyRepresentable` wasn't broken by design /// (specifically, it can't be back-deployed before macOS 12.3 etc., even though it was introduced in Swift 5.6), /// we'd use that instead. -fileprivate struct SomeCodingKey: CodingKey, Hashable { +private struct SomeCodingKey: CodingKey, Hashable { let stringValue: String, intValue: Int? init(stringValue: String) { (self.stringValue, self.intValue) = (stringValue, Int(stringValue)) } init(intValue: Int) { (self.stringValue, self.intValue) = ("\(intValue)", intValue) } } -private extension PostgresCell { - var codingKey: any CodingKey { +extension PostgresCell { + fileprivate var codingKey: any CodingKey { PostgresKit.SomeCodingKey(stringValue: !self.columnName.isEmpty ? "\(self.columnName) (\(self.columnIndex))" : "\(self.columnIndex)") } } /// Sidestep problems with URL coding behavior by making it conform directly to Postgres coding. -extension Foundation.URL: PostgresNIO.PostgresNonThrowingEncodable, PostgresNIO.PostgresDecodable { +extension URL { public static var psqlType: PostgresDataType { String.psqlType } - + public static var psqlFormat: PostgresFormat { String.psqlFormat } @@ -42,7 +42,7 @@ extension Foundation.URL: PostgresNIO.PostgresNonThrowingEncodable, PostgresNIO. context: PostgresDecodingContext ) throws { let string = try String(from: &buffer, type: type, format: format, context: context) - + if let url = URL(string: string) { self = url } @@ -55,16 +55,23 @@ extension Foundation.URL: PostgresNIO.PostgresNonThrowingEncodable, PostgresNIO. } } +#if compiler(>=6.0) +extension URL: @retroactive PostgresNonThrowingEncodable, @retroactive PostgresDecodable {} +#else +extension Foundation.URL: PostgresNIO.PostgresNonThrowingEncodable, PostgresNIO.PostgresDecodable {} +#endif + struct PostgresDataTranslation { - /// This typealias serves to limit the deprecation noise caused by ``PostgresDataConvertible`` to a single + /// This typealias serves to limit the deprecation noise caused by `PostgresDataConvertible` to a single /// warning, down from what would otherwise be a minimum of two. It has no other purpose. fileprivate typealias PostgresLegacyDataConvertible = PostgresDataConvertible - + static func decode( _: T.Type = T.self, from cell: PostgresCell, in context: PostgresDecodingContext, - file: String = #fileID, line: Int = #line + file: String = #fileID, + line: Int = #line ) throws -> T { try self.decode( codingPath: [cell.codingKey], @@ -72,18 +79,21 @@ struct PostgresDataTranslation { T.self, from: cell, in: context, - file: file, line: line + file: file, + line: line ) } - + fileprivate static func decode( - codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], + codingPath: [any CodingKey], + userInfo: [CodingUserInfoKey: Any], _: T.Type = T.self, from cell: PostgresCell, in context: PostgresDecodingContext, - file: String, line: Int + file: String, + line: Int ) throws -> T { - /// Preferred modern fast-path: Direct conformance to ``PostgresDecodable``, let the cell decode. + /// Preferred modern fast-path: Direct conformance to `PostgresDecodable`, let the cell decode. if let fastPathType = T.self as? any PostgresDecodable.Type { let cellToDecode: PostgresCell @@ -127,8 +137,8 @@ struct PostgresDataTranslation { cellToDecode = cell } return try cellToDecode.decode(fastPathType, context: context, file: file, line: line) as! T - - /// Legacy "fast"-path: Direct conformance to ``PostgresDataConvertible``; use is deprecated. + + /// Legacy "fast"-path: Direct conformance to `PostgresDataConvertible`; use is deprecated. } else if let legacyPathType = T.self as? any PostgresLegacyDataConvertible.Type { let legacyData = PostgresData(type: cell.dataType, typeModifier: nil, formatCode: cell.format, value: cell.bytes) @@ -139,8 +149,8 @@ struct PostgresDataTranslation { } return result as! T } - - /// Slow path: Descend through the ``Decodable`` machinery until we fail or find something we can convert. + + /// Slow path: Descend through the `Decodable` machinery until we fail or find something we can convert. else { do { return try T.init(from: ArrayAwareBoxUwrappingDecoder( @@ -172,7 +182,7 @@ struct PostgresDataTranslation { debugDescription: "\(String(reflecting: error))", underlyingError: error ) - + switch error.code { case .typeMismatch: throw DecodingError.typeMismatch(T.self, context) case .missingData: throw DecodingError.valueNotFound(T.self, context) @@ -181,35 +191,38 @@ struct PostgresDataTranslation { } } } - + static func encode( value: T, in context: PostgresEncodingContext, to bindings: inout PostgresBindings, - file: String = #fileID, line: Int = #line + file: String = #fileID, + line: Int = #line ) throws { - /// Preferred modern fast-path: Direct conformance to ``PostgresEncodable`` + /// Preferred modern fast-path: Direct conformance to `PostgresEncodable` if let fastPathValue = value as? any PostgresEncodable { try bindings.append(fastPathValue, context: context) } - /// Legacy "fast"-path: Direct conformance to ``PostgresDataConvertible``; use is deprecated. + /// Legacy "fast"-path: Direct conformance to `PostgresDataConvertible`; use is deprecated. else if let legacyPathValue = value as? any PostgresDataTranslation.PostgresLegacyDataConvertible { guard let legacyData = legacyPathValue.postgresData else { throw EncodingError.invalidValue(value, .init(codingPath: [], debugDescription: "Couldn't get PSQL encoding from value '\(value)'")) } bindings.append(legacyData) } - /// Slow path: Descend through the ``Encodable`` machinery until we fail or find something we can convert. + /// Slow path: Descend through the `Encodable` machinery until we fail or find something we can convert. else { try bindings.append(self.encode(codingPath: [], userInfo: [:], value: value, in: context, file: file, line: line)) } } - + internal /*fileprivate*/ static func encode( - codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], + codingPath: [any CodingKey], + userInfo: [CodingUserInfoKey: Any], value: T, in context: PostgresEncodingContext, - file: String, line: Int + file: String, + line: Int ) throws -> PostgresData { // TODO: Avoid repeating the conformance checks here, or at the very least only repeat them after a second level of nesting... if let fastPathValue = value as? any PostgresEncodable { @@ -256,7 +269,7 @@ private final class ArrayAwareBoxUwrappingDecoder= self.data.count } - + var currentIndex = 0 - + mutating func decodeNil() throws -> Bool { guard self.data[self.currentIndex].value == nil else { return false } self.currentIndex += 1 return true } - + mutating func decode(_: T.Type) throws -> T { // TODO: Don't fake a cell. let data = self.data[self.currentIndex], cell = PostgresCell( @@ -297,17 +310,17 @@ private final class ArrayAwareBoxUwrappingDecoder(keyedBy: K.Type) throws -> KeyedDecodingContainer { throw self.rejectNestingError } mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { throw self.rejectNestingError } mutating func superDecoder() throws -> any Decoder { throw self.rejectNestingError } } - + func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer { throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Dictionary containers must be JSON-encoded")) } - + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { // TODO: Find a better way to figure out arrays guard let array = PostgresData(type: self.cell.dataType, typeModifier: nil, formatCode: self.cell.format, value: self.cell.bytes).array else { @@ -315,11 +328,11 @@ private final class ArrayAwareBoxUwrappingDecoder any SingleValueDecodingContainer { self } - + func decodeNil() -> Bool { self.cell.bytes == nil } - + func decode(_: T.Type) throws -> T { try PostgresDataTranslation.decode( codingPath: self.codingPath + [PostgresKit.SomeCodingKey(stringValue: "(Unwrapping(\(T0.self)))")], userInfo: self.userInfo, @@ -331,7 +344,7 @@ private final class ArrayAwareBoxUwrappingDecoder: Encoder, SingleValueEncodingContainer { enum Value { final class ArrayRef { var contents: [T] = [] } - + case invalid case indexed(ArrayRef) case scalar(PostgresData) @@ -350,7 +363,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder case .indexed(_): break // existing array, adopt it for appending (support for superEncoder()) } } - + var indexedCount: Int { if case .indexed(let ref) = self { return ref.contents.count } else { preconditionFailure("Internal error in encoder (requested indexed count from non-indexed state)") } @@ -361,7 +374,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder else { preconditionFailure("Internal error in encoder (attempted store to indexed in non-indexed state)") } } } - + var codingPath: [any CodingKey] let userInfo: [CodingUserInfoKey: Any] let context: PostgresEncodingContext @@ -376,22 +389,22 @@ private final class ArrayAwareBoxWrappingPostgresEncoder self.line = line self.value = value } - + func container(keyedBy: K.Type) -> KeyedEncodingContainer { precondition(!self.value.isValid, "Requested multiple containers from the same encoder.") return .init(FailureEncoder()) } - + func unkeyedContainer() -> any UnkeyedEncodingContainer { self.value.requestIndexed() return ArrayContainer(encoder: self) } - + func singleValueContainer() -> any SingleValueEncodingContainer { precondition(!self.value.isValid, "Requested multiple containers from the same encoder.") return self } - + struct ArrayContainer: UnkeyedEncodingContainer { let encoder: ArrayAwareBoxWrappingPostgresEncoder var codingPath: [any CodingKey] { self.encoder.codingPath } @@ -420,7 +433,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder codingPath: self.codingPath, userInfo: self.userInfo, value: value, in: self.context, file: self.file, line: self.line )) } - + struct FallbackSentinel: Error {} /// This is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing. diff --git a/Sources/PostgresKit/PostgresDatabase+SQL.swift b/Sources/PostgresKit/PostgresDatabase+SQL.swift index 95d38c72..5be0b5e5 100644 --- a/Sources/PostgresKit/PostgresDatabase+SQL.swift +++ b/Sources/PostgresKit/PostgresDatabase+SQL.swift @@ -7,13 +7,18 @@ extension PostgresDatabase { public func sql(queryLogLevel: Logger.Level? = .debug) -> some SQLDatabase { self.sql(encodingContext: .default, decodingContext: .default, queryLogLevel: queryLogLevel) } - + public func sql( encodingContext: PostgresEncodingContext, decodingContext: PostgresDecodingContext, queryLogLevel: Logger.Level? = .debug ) -> some SQLDatabase { - PostgresSQLDatabase(database: self, encodingContext: encodingContext, decodingContext: decodingContext, queryLogLevel: queryLogLevel) + PostgresSQLDatabase( + database: self, + encodingContext: encodingContext, + decodingContext: decodingContext, + queryLogLevel: queryLogLevel + ) } } @@ -28,22 +33,22 @@ extension PostgresSQLDatabase: SQLDatabase, PostgresDatabase { var logger: Logger { self.database.logger } - + var eventLoop: any EventLoop { self.database.eventLoop } - + var version: (any SQLDatabaseReportedVersion)? { nil // PSQL doesn't send version in wire protocol, must use SQL to read it } - + var dialect: any SQLDialect { PostgresDialect() } - - func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture { + + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) -> EventLoopFuture { let (sql, binds) = self.serialize(query) - + if let queryLogLevel = self.queryLogLevel { self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(binds.map { .string("\($0)") })]) } @@ -61,13 +66,13 @@ extension PostgresSQLDatabase: SQLDatabase, PostgresDatabase { ) } }.map { _ in } } - + func execute( sql query: any SQLExpression, - _ onRow: @escaping @Sendable (any SQLRow) -> () + _ onRow: @escaping @Sendable (any SQLRow) -> Void ) async throws { let (sql, binds) = self.serialize(query) - + if let queryLogLevel = self.queryLogLevel { self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(binds.map { .string("\($0)") })]) } @@ -85,16 +90,15 @@ extension PostgresSQLDatabase: SQLDatabase, PostgresDatabase { ) }.get() } - - + func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture { self.database.send(request, logger: logger) } - + func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { self.database.withConnection(closure) } - + func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { try await self.withConnection { c in c.eventLoop.makeFutureWithTask { diff --git a/Sources/PostgresKit/PostgresDialect.swift b/Sources/PostgresKit/PostgresDialect.swift index 8502ed6f..43a82ca1 100644 --- a/Sources/PostgresKit/PostgresDialect.swift +++ b/Sources/PostgresKit/PostgresDialect.swift @@ -2,7 +2,7 @@ import SQLKit public struct PostgresDialect: SQLDialect { public init() {} - + public var name: String { "postgresql" } @@ -70,7 +70,7 @@ public struct PostgresDialect: SQLDialect { } public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? { - if case let .custom(expr) = dataType, (expr as? SQLRaw)?.sql == "TIMESTAMP" { + if case .custom(let expr) = dataType, (expr as? SQLRaw)?.sql == "TIMESTAMP" { return SQLRaw("TIMESTAMPTZ") } else if case .blob = dataType { return SQLRaw("BYTEA") @@ -85,11 +85,11 @@ public struct PostgresDialect: SQLDialect { public var unionFeatures: SQLUnionFeatures { [ - .union, .unionAll, + .union, .unionAll, .intersect, .intersectAll, - .except, .exceptAll, + .except, .exceptAll, .explicitDistinct, - .parenthesizedSubqueries + .parenthesizedSubqueries, ] } @@ -103,7 +103,7 @@ public struct PostgresDialect: SQLDialect { public func nestedSubpathExpression(in column: any SQLExpression, for path: [String]) -> (any SQLExpression)? { guard !path.isEmpty else { return nil } - + let descender = SQLList( [column] + path.dropLast().map(SQLLiteral.string(_:)), separator: SQLRaw("->") @@ -112,7 +112,7 @@ public struct PostgresDialect: SQLDialect { [descender, SQLLiteral.string(path.last!)], separator: SQLRaw("->>") ) - + return SQLGroupExpression(accessor) } } diff --git a/Sources/PostgresKit/SQLPostgresConfiguration.swift b/Sources/PostgresKit/SQLPostgresConfiguration.swift index e9d9def5..f143dd74 100644 --- a/Sources/PostgresKit/SQLPostgresConfiguration.swift +++ b/Sources/PostgresKit/SQLPostgresConfiguration.swift @@ -76,7 +76,7 @@ public struct SQLPostgresConfiguration: Sendable { /// > additional information and recommendations. /// /// [tlsconfig]: - /// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/tlsconfiguration + /// https://swiftpackageindex.com/apple/swift-nio-ssl/documentation/niossl/tlsconfiguration public init(url: URL) throws { guard let comp = URLComponents(url: url, resolvingAgainstBaseURL: true), let username = comp.user else { throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString]) @@ -141,7 +141,7 @@ public struct SQLPostgresConfiguration: Sendable { } /// Create a ``SQLPostgresConfiguration`` for establishing a connection to a server over a - /// preestablished `NIOCore/Channel`. + /// preestablished `NIOCore.Channel`. /// /// This is provided for calling code which wants to manage the underlying connection transport on its /// own, such as when tunneling a connection through SSH. From e2ba95dfa8eddb39c5ca59bf692f90093c8df0c1 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 13:17:28 -0500 Subject: [PATCH 2/3] Apply PR feedback --- README.md | 10 +++++----- Sources/PostgresKit/Docs.docc/PostgresKit.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 709a4b38..ddaa31df 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ### Usage -Use the SPM string to easily include the dependendency in your `Package.swift` file. +Reference this package in your `Package.swift` to include it in your project. ```swift .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0") @@ -75,14 +75,14 @@ let configuration = PostgresConfiguration( Once you have a `PostgresConfiguration`, you can use it to create a connection source and pool. ```swift -let eventLoopGroup: EventLoopGroup = ... -defer { try! eventLoopGroup.syncShutdown() } - +let eventLoopGroup: EventLoopGroup = NIOSingletons.posixEventLoopGroup let pools = EventLoopGroupConnectionPool( source: PostgresConnectionSource(configuration: configuration), on: eventLoopGroup ) -defer { pools.shutdown() } + +// When you're done: +try await pools.shutdownAsync() ``` First create a `PostgresConnectionSource` using the configuration struct. This type is responsible for creating new connections to your database server as needed. diff --git a/Sources/PostgresKit/Docs.docc/PostgresKit.md b/Sources/PostgresKit/Docs.docc/PostgresKit.md index 94a7ee2a..1b7708a4 100644 --- a/Sources/PostgresKit/Docs.docc/PostgresKit.md +++ b/Sources/PostgresKit/Docs.docc/PostgresKit.md @@ -8,7 +8,7 @@ ### Usage -Use the SPM string to easily include the dependendency in your `Package.swift` file. +Reference this package in your `Package.swift` to include it in your project. ```swift .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0") @@ -66,14 +66,14 @@ let configuration = PostgresConfiguration( Once you have a ``SQLPostgresConfiguration``, you can use it to create a connection source and pool. ```swift -let eventLoopGroup: EventLoopGroup = ... -defer { try! eventLoopGroup.syncShutdown() } - +let eventLoopGroup: EventLoopGroup = NIOSingletons.posixEventLoopGroup let pools = EventLoopGroupConnectionPool( source: PostgresConnectionSource(configuration: configuration), on: eventLoopGroup ) -defer { pools.shutdown() } + +// When you're done: +try await pools.shutdownAsync() ``` First create a ``PostgresConnectionSource`` using the configuration struct. This type is responsible for creating new connections to your database server as needed. From 93cac3b4c51774ac2867adb1f256411287872a01 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 16:10:17 -0500 Subject: [PATCH 3/3] Fixup those darn docs --- README.md | 29 ++++++++++---------- Sources/PostgresKit/Docs.docc/PostgresKit.md | 14 +++++----- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ddaa31df..4861d161 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,13 @@
-🐘 Non-blocking, event-driven Swift client for PostgreSQL. +PostgresKit is an [SQLKit] driver for PostgreSQL clients. + +## Overview + +PostgresKit supports building and serializing Postgres-dialect SQL queries using [SQLKit]'s API. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. + +> Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. ### Usage @@ -29,21 +35,14 @@ PostgresKit supports the following platforms: - Ubuntu 20.04+ - macOS 10.15+ -## Overview - -PostgresKit is an [SQLKit] driver for PostgreSQL clients. It supports building and serializing Postgres-dialect SQL queries. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. - -> [!IMPORTANT] -> It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. - ### Configuration -Database connection options and credentials are specified using a `PostgresConfiguration` struct. +Database connection options and credentials are specified using a ``SQLPostgresConfiguration`` struct. ```swift import PostgresKit -let configuration = PostgresConfiguration( +let configuration = SQLPostgresConfiguration( hostname: "localhost", username: "vapor_username", password: "vapor_password", @@ -51,15 +50,15 @@ let configuration = PostgresConfiguration( ) ``` -URL string based configuration is also supported. +URL-based configuration is also supported. ```swift -guard let configuration = PostgresConfiguration(url: "postgres://...") else { +guard let configuration = SQLPostgresConfiguration(url: "postgres://...") else { ... } ``` -To connect via unix-domain sockets, use `unixDomainSocketPath` instead of `hostname` and `port`. +To connect via unix-domain sockets, use ``SQLPostgresConfiguration/init(unixDomainSocketPath:username:password:database:)`` instead of ``SQLPostgresConfiguration/init(hostname:port:username:password:database:tls:)``. ```swift let configuration = PostgresConfiguration( @@ -72,7 +71,7 @@ let configuration = PostgresConfiguration( ### Connection Pool -Once you have a `PostgresConfiguration`, you can use it to create a connection source and pool. +Once you have a ``SQLPostgresConfiguration``, you can use it to create a connection source and pool. ```swift let eventLoopGroup: EventLoopGroup = NIOSingletons.posixEventLoopGroup @@ -85,7 +84,7 @@ let pools = EventLoopGroupConnectionPool( try await pools.shutdownAsync() ``` -First create a `PostgresConnectionSource` using the configuration struct. This type is responsible for creating new connections to your database server as needed. +First create a ``PostgresConnectionSource`` using the configuration struct. This type is responsible for creating new connections to your database server as needed. Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit [SwiftNIO's documentation]. Make sure to shutdown the connection pool before it deinitializes. diff --git a/Sources/PostgresKit/Docs.docc/PostgresKit.md b/Sources/PostgresKit/Docs.docc/PostgresKit.md index 1b7708a4..47bef7fa 100644 --- a/Sources/PostgresKit/Docs.docc/PostgresKit.md +++ b/Sources/PostgresKit/Docs.docc/PostgresKit.md @@ -4,7 +4,13 @@ @TitleHeading(Package) } -🐘 Non-blocking, event-driven Swift client for PostgreSQL. +PostgresKit is an [SQLKit] driver for PostgreSQL clients. + +## Overview + +PostgresKit supports building and serializing Postgres-dialect SQL queries using [SQLKit]'s API. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. + +> Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. ### Usage @@ -21,12 +27,6 @@ PostgresKit supports the following platforms: - Ubuntu 20.04+ - macOS 10.15+ -## Overview - -PostgresKit is an [SQLKit] driver for PostgreSQL clients. It supports building and serializing Postgres-dialect SQL queries. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. - -> Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. - ### Configuration Database connection options and credentials are specified using a ``SQLPostgresConfiguration`` struct.