diff --git a/.api-breakage/allowlist-branch-update-for-new-pnio.txt b/.api-breakage/allowlist-branch-update-for-new-pnio.txt deleted file mode 100644 index 3b27dbf..0000000 --- a/.api-breakage/allowlist-branch-update-for-new-pnio.txt +++ /dev/null @@ -1,10 +0,0 @@ -API breakage: var PostgresDialect.sharedSelectLockExpression has declared type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)? -API breakage: accessor PostgresDialect.sharedSelectLockExpression.Get() has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)? -API breakage: var PostgresDialect.exclusiveSelectLockExpression has declared type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)? -API breakage: accessor PostgresDialect.exclusiveSelectLockExpression.Get() has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)? -API breakage: func PostgresDatabase.sql(encoder:decoder:) has removed default argument from parameter 0 -API breakage: func PostgresDatabase.sql(encoder:decoder:) has removed default argument from parameter 1 -API breakage: func PostgresRow.sql(decoder:) has removed default argument from parameter 0 -API breakage: func PostgresColumnType.==(_:_:) has been removed -API breakage: import Foundation has been removed - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..998a0eb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + groups: + dependencies: + patterns: + - "*" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2999d9b..4fb24ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,15 +25,33 @@ env: jobs: # Check for API breakage versus main api-breakage: - if: ${{ !(github.event.pull_request.draft || false) }} + if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest container: swift:jammy steps: - - name: Check out code - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: { 'fetch-depth': 0 } - - name: Run API breakage check action - uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@main + - name: API breaking changes + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + swift package diagnose-api-breaking-changes origin/main + + dependency-graph: + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + container: swift:jammy + permissions: + contents: write + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Fix Git configuration + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + apt-get update && apt-get install -y curl + - name: Submit dependency graph + uses: vapor-community/swift-dependency-submission@v0.1 code-coverage: if: ${{ !(github.event.pull_request.draft || false) }} @@ -41,7 +59,7 @@ jobs: container: swift:jammy services: psql-a: - image: postgres:15 + image: postgres:16 env: POSTGRES_USER: test_username POSTGRES_DB: test_database @@ -50,37 +68,38 @@ jobs: POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run unit tests for coverage data run: swift test --enable-code-coverage - name: Upload coverage data uses: vapor/swift-codecov-action@v0.2 gh-codeql: - if: ${{ !(github.event.pull_request.draft || false) }} - strategy: - fail-fast: false - matrix: - runner_os: - - ubuntu-latest - - macos-13 - runs-on: ${{ matrix.runner_os }} - permissions: - security-events: write + if: ${{ false && !(github.event.pull_request.draft || false) }} + runs-on: ubuntu-latest + container: + image: swift:5.9-jammy + permissions: { actions: write, contents: read, security-events: write } + timeout-minutes: 60 steps: - - name: Select appropriate Xcode version - if: ${{ startsWith(matrix.runner_os, 'macos') }} - uses: maxim-lobanov/setup-xcode@v1 - with: { xcode-version: '~14.3' } - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Mark repo safe in non-fake global config + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + - name: Check Swift compatibility + id: swift-check + uses: vapor/ci/.github/actions/check-compatible-swift@main - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + if: ${{ steps.swift-check.outputs.swift-compatible == 'true' }} + uses: github/codeql-action/init@v3 with: { languages: swift } - name: Perform build + if: ${{ steps.swift-check.outputs.swift-compatible == 'true' }} run: swift build - name: Run CodeQL analyze - uses: github/codeql-action/analyze@v2 + if: ${{ steps.swift-check.outputs.swift-compatible == 'true' }} + uses: github/codeql-action/analyze@v3 linux-unit: if: ${{ !(github.event.pull_request.draft || false) }} @@ -88,20 +107,21 @@ jobs: fail-fast: false matrix: postgres-image: - - postgres:15 - - postgres:13 - - postgres:11 + - postgres:16 + - postgres:14 + - postgres:12 swift-image: - swift:5.7-jammy - swift:5.8-jammy - - swiftlang/swift:nightly-5.9-jammy + - swift:5.9-jammy + - swiftlang/swift:nightly-5.10-jammy - swiftlang/swift:nightly-main-jammy include: - - postgres-image: postgres:15 + - postgres-image: postgres:16 postgres-auth: scram-sha-256 - - postgres-image: postgres:13 + - postgres-image: postgres:14 postgres-auth: md5 - - postgres-image: postgres:11 + - postgres-image: postgres:12 postgres-auth: trust runs-on: ubuntu-latest container: ${{ matrix.swift-image }} @@ -116,17 +136,17 @@ jobs: POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }} steps: - name: Check out package - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run local tests run: swift test linux-integration: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:5.8-jammy + container: swift:5.9-jammy services: psql-a: - image: postgres:15 + image: postgres:16 env: POSTGRES_USER: test_username POSTGRES_DB: test_database @@ -143,10 +163,10 @@ jobs: POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 steps: - name: Check out package - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: { path: 'postgres-kit' } - name: Check out fluent-postgres-driver dependent - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: { repository: 'vapor/fluent-postgres-driver', path: 'fluent-postgres-driver' } - name: Use local package run: swift package --package-path fluent-postgres-driver edit postgres-kit --path postgres-kit @@ -158,10 +178,12 @@ jobs: strategy: fail-fast: false matrix: - xcode-version: - - '~14.3' - - '15.0-beta' - runs-on: macos-13 + include: + - macos-version: macos-13 + xcode-version: '~14.3' + - macos-version: macos-14 + xcode-version: latest + runs-on: ${{ matrix.macos-version }} env: POSTGRES_HOSTNAME: 127.0.0.1 POSTGRES_DB: postgres @@ -173,11 +195,11 @@ jobs: - name: Install Postgres, setup DB and auth, and wait for server start run: | export PATH="$(brew --prefix)/opt/postgresql@14/bin:$PATH" PGDATA=/tmp/vapor-postgres-test - (brew unlink postgresql || true) && brew install "postgresql@14" && brew link --force "postgresql@14" + (brew unlink postgresql || true) && brew install "postgresql@15" && brew link --force "postgresql@15" initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}") pg_ctl start --wait timeout-minutes: 2 - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run local tests run: swift test diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000..271f683 --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,49 @@ +// swift-tools-version:5.9 +import PackageDescription + +let swiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] + +let package = Package( + name: "postgres-kit", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + ], + products: [ + .library(name: "PostgresKit", targets: ["PostgresKit"]), + ], + dependencies: [ + .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.20.0"), + .package(url: "https://github.com/vapor/sql-kit.git", from: "3.28.0"), + .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0") + ], + targets: [ + .target( + name: "PostgresKit", + dependencies: [ + .product(name: "AsyncKit", package: "async-kit"), + .product(name: "PostgresNIO", package: "postgres-nio"), + .product(name: "SQLKit", package: "sql-kit"), + .product(name: "Atomics", package: "swift-atomics"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "PostgresKitTests", + dependencies: [ + .target(name: "PostgresKit"), + .product(name: "SQLKitBenchmark", package: "sql-kit"), + ], + swiftSettings: swiftSettings + ), + ] +) diff --git a/README.md b/README.md index c103a68..fe0c827 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,19 @@ -PostgresKit +

+ + + + PostgresKit +
- - Documentation - - - Team Chat - - - MIT License - - - Continuous Integration - - - Swift 5.2 -
+Documentation +Team Chat +MIT License +Continuous Integration + +Swift 5.7+ +

+
🐘 Non-blocking, event-driven Swift client for PostgreSQL. @@ -39,7 +37,7 @@ Use the SPM string to easily include the dependendency in your `Package.swift` f PostgresKit supports the following platforms: -- Ubuntu 16.04+ +- Ubuntu 20.04+ - macOS 10.15+ ## Overview diff --git a/Sources/PostgresKit/ConnectionPool+Postgres.swift b/Sources/PostgresKit/ConnectionPool+Postgres.swift index 6dd11f9..547d811 100644 --- a/Sources/PostgresKit/ConnectionPool+Postgres.swift +++ b/Sources/PostgresKit/ConnectionPool+Postgres.swift @@ -1,6 +1,6 @@ import NIOCore import PostgresNIO -import AsyncKit +@preconcurrency import AsyncKit import Logging extension EventLoopGroupConnectionPool where Source == PostgresConnectionSource { diff --git a/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift b/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift index 80c07b5..4a03461 100644 --- a/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift +++ b/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift @@ -34,7 +34,7 @@ extension PostgresConnectionSource { } @available(*, deprecated, message: "Use `sqlConfiguration` instead.") - public var sslContext: Result { .success(self.sqlConfiguration.coreConfiguration.tls.sslContext) } + public var sslContext: Result { .success(self.sqlConfiguration.coreConfiguration.tls.sslContext) } @available(*, deprecated, message: "Use `init(sqlConfiguration:)` instead.") public init(configuration: PostgresConfiguration) { diff --git a/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift b/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift index aafdc1c..057b545 100644 --- a/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift +++ b/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift @@ -9,7 +9,7 @@ public final class PostgresDataEncoder { self.json = json } - public func encode(_ value: Encodable) throws -> PostgresData { + public func encode(_ value: any Encodable) throws -> PostgresData { if let custom = value as? any PostgresDataConvertible, let data = custom.postgresData { return data } else { diff --git a/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg b/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg new file mode 100644 index 0000000..cdb1a8e --- /dev/null +++ b/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/Sources/PostgresKit/Docs.docc/theme-settings.json b/Sources/PostgresKit/Docs.docc/theme-settings.json new file mode 100644 index 0000000..b147e23 --- /dev/null +++ b/Sources/PostgresKit/Docs.docc/theme-settings.json @@ -0,0 +1,21 @@ +{ + "theme": { + "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" }, + "border-radius": "0", + "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, + "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, + "color": { + "psqlkit": "#336791", + "documentation-intro-fill": "radial-gradient(circle at top, var(--color-psqlkit) 30%, #000 100%)", + "documentation-intro-accent": "var(--color-psqlkit)", + "logo-base": { "dark": "#fff", "light": "#000" }, + "logo-shape": { "dark": "#000", "light": "#fff" }, + "fill": { "dark": "#000", "light": "#fff" } + }, + "icons": { "technology": "/postgreskit/images/vapor-postgreskit-logo.svg" } + }, + "features": { + "quickNavigation": { "enable": true }, + "i18n": { "enable": true } + } +} diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift index 1056090..9fbee68 100644 --- a/Sources/PostgresKit/PostgresDataTranslation.swift +++ b/Sources/PostgresKit/PostgresDataTranslation.swift @@ -14,6 +14,38 @@ private extension PostgresCell { var codingKey: any CodingKey { 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 URL: PostgresNonThrowingEncodable { + public static var psqlType: PostgresDataType { String.psqlType } + public static var psqlFormat: PostgresFormat { String.psqlFormat } + + @inlinable + public func encode(into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext) { + self.absoluteString.encode(into: &byteBuffer, context: context) + } +} + +/// Sidestep problems with URL coding behavior by making it conform directly to Postgres coding. +extension URL: PostgresDecodable { + @inlinable + public init( + from buffer: inout ByteBuffer, type: PostgresDataType, format: PostgresFormat, + context: PostgresDecodingContext + ) throws { + let string = try String(from: &buffer, type: type, format: format, context: context) + + if let url = URL(string: string) { + self = url + } + // Also support the broken encoding we were emitting for awhile there. + else if string.hasPrefix("\""), string.hasSuffix("\""), let url = URL(string: String(string.dropFirst().dropLast())) { + self = url + } else { + throw PostgresDecodingError.Code.failure + } + } +} + struct PostgresDataTranslation { /// 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. diff --git a/Sources/PostgresKit/PostgresDatabase+SQL.swift b/Sources/PostgresKit/PostgresDatabase+SQL.swift index f7bb7ac..85a5516 100644 --- a/Sources/PostgresKit/PostgresDatabase+SQL.swift +++ b/Sources/PostgresKit/PostgresDatabase+SQL.swift @@ -1,6 +1,13 @@ import PostgresNIO import Logging -import SQLKit +@preconcurrency import SQLKit + +// https://github.com/vapor/postgres-nio/pull/450 +#if compiler(>=5.10) && $RetroactiveAttribute +extension PostgresEncodingContext: @retroactive @unchecked Sendable {} +#else +extension PostgresEncodingContext: @unchecked Sendable {} +#endif extension PostgresDatabase { @inlinable @@ -37,7 +44,7 @@ extension _PostgresSQLDatabase: SQLDatabase, PostgresDatabase { 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 (any SQLRow) -> ()) -> EventLoopFuture { + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture { let (sql, binds) = self.serialize(query) if let queryLogLevel { diff --git a/Sources/PostgresKit/SQLPostgresConfiguration.swift b/Sources/PostgresKit/SQLPostgresConfiguration.swift index 5a11c7e..6169f34 100644 --- a/Sources/PostgresKit/SQLPostgresConfiguration.swift +++ b/Sources/PostgresKit/SQLPostgresConfiguration.swift @@ -146,7 +146,7 @@ public struct SQLPostgresConfiguration { /// 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. public init( - establishedChannel: Channel, + establishedChannel: any Channel, username: String, password: String? = nil, database: String? = nil ) { diff --git a/Tests/PostgresKitTests/PostgresKitTests.swift b/Tests/PostgresKitTests/PostgresKitTests.swift index af1074a..e7ac17f 100644 --- a/Tests/PostgresKitTests/PostgresKitTests.swift +++ b/Tests/PostgresKitTests/PostgresKitTests.swift @@ -19,7 +19,7 @@ final class PostgresKitTests: XCTestCase { let pool = EventLoopGroupConnectionPool( source: db, maxConnectionsPerEventLoop: 2, - on: self.eventLoopGroup + on: MultiThreadedEventLoopGroup.singleton ) defer { pool.shutdown() } // Postgres seems to take much longer on initial connections when using SCRAM-SHA-256 auth, @@ -27,13 +27,13 @@ final class PostgresKitTests: XCTestCase { // Spin the pool a bit before running the measurement to warm it up. for _ in 1...25 { _ = try pool.withConnection { conn in - conn.query("SELECT 1;") + conn.query("SELECT 1") }.wait() } self.measure { for _ in 1...100 { _ = try! pool.withConnection { conn in - conn.query("SELECT 1;") + conn.query("SELECT 1") }.wait() } } @@ -125,12 +125,12 @@ final class PostgresKitTests: XCTestCase { var configuration = SQLPostgresConfiguration.test configuration.searchPath = ["foo", "bar", "baz"] let source = PostgresConnectionSource(sqlConfiguration: configuration) - let pool = EventLoopGroupConnectionPool(source: source, on: self.eventLoopGroup) + let pool = EventLoopGroupConnectionPool(source: source, on: MultiThreadedEventLoopGroup.singleton) defer { pool.shutdown() } let db = pool.database(logger: .init(label: "test")).sql() - let rows = try db.raw("SELECT version();").all().wait() - print(rows) + let rows = try db.raw("SELECT version()").all().wait() + XCTAssertEqual(rows.count, 1) } func testIntegerArrayEncoding() throws { @@ -224,20 +224,30 @@ final class PostgresKitTests: XCTestCase { XCTAssertNoThrow(numericValue = try PostgresDataTranslation.decode(Double.self, from: .init(bytes: numericBuffer, dataType: .numeric, format: .binary, columnName: "", columnIndex: -1), in: .default)) XCTAssertEqual(numericValue, Double(Decimal(12345.6789).description)) } + + func testURLWorkaroundDecoding() throws { + let url = URL(string: "https://user:pass@www.example.com:8080/path/to/endpoint?query=value#fragment")! + + let encodedNormal = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: url, in: .default, file: #fileID, line: #line) + XCTAssertEqual(encodedNormal.value?.getString(at: 0, length: encodedNormal.value?.readableBytes ?? 0), url.absoluteString) + + let encodedBroken = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: "\"\(url.absoluteString)\"", in: .default, file: #fileID, line: #line) + + XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedNormal), in: .default), url) + XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default), url) + } - var eventLoop: any EventLoop { self.eventLoopGroup.any() } - var eventLoopGroup: (any EventLoopGroup)! + var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() } override func setUpWithError() throws { try super.setUpWithError() XCTAssertTrue(isLoggingConfigured) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2) } +} - override func tearDownWithError() throws { - try self.eventLoopGroup.syncShutdownGracefully() - self.eventLoopGroup = nil - try super.tearDownWithError() +extension PostgresCell { + fileprivate init(with data: PostgresData) { + self.init(bytes: data.value, dataType: data.type, format: data.formatCode, columnName: "", columnIndex: -1) } } diff --git a/Tests/PostgresKitTests/Utilities.swift b/Tests/PostgresKitTests/Utilities.swift index 01e3a54..3c52f2d 100644 --- a/Tests/PostgresKitTests/Utilities.swift +++ b/Tests/PostgresKitTests/Utilities.swift @@ -31,7 +31,7 @@ func env(_ name: String) -> String? { let isLoggingConfigured: Bool = { LoggingSystem.bootstrap { label in var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .info + handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info return handler } return true