diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index 0fea43d3..316a00de 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -3,6 +3,9 @@ on: push: branches: - main +permissions: + contents: read + id-token: write jobs: build-and-deploy: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bc22a82..7191f710 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,167 +5,112 @@ concurrency: on: pull_request: { types: [opened, reopened, synchronize, ready_for_review] } push: { branches: [ main ] } - +permissions: + contents: read env: LOG_LEVEL: info - SWIFT_DETERMINISTIC_HASHING: 1 - POSTGRES_HOSTNAME: 'psql-a' - POSTGRES_HOSTNAME_A: 'psql-a' - POSTGRES_HOSTNAME_B: 'psql-b' - POSTGRES_DB: 'test_database' - POSTGRES_DB_A: 'test_database' - POSTGRES_DB_B: 'test_database' - POSTGRES_USER: 'test_username' - POSTGRES_USER_A: 'test_username' - POSTGRES_USER_B: 'test_username' - POSTGRES_PASSWORD: 'test_password' - POSTGRES_PASSWORD_A: 'test_password' - POSTGRES_PASSWORD_B: 'test_password' + POSTGRES_HOSTNAME_A: &postgres_host_a 'psql-a' + POSTGRES_HOSTNAME_B: &postgres_host_b 'psql-b' + POSTGRES_HOSTNAME: *postgres_host_a + POSTGRES_DB_A: &postgres_db_a 'test_database' + POSTGRES_DB_B: &postgres_db_b 'test_database' + POSTGRES_DB: *postgres_db_a + POSTGRES_USER_A: &postgres_user_a 'test_username' + POSTGRES_USER_B: &postgres_user_b 'test_username' + POSTGRES_USER: *postgres_user_a + POSTGRES_PASSWORD_A: &postgres_pass_a 'test_password' + POSTGRES_PASSWORD_B: &postgres_pass_b 'test_password' + POSTGRES_PASSWORD: *postgres_pass_a jobs: api-breakage: if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:jammy + container: swift:noble steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: { 'fetch-depth': 0 } - 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) }} - runs-on: ubuntu-latest - container: swift:jammy - services: - psql-a: - image: postgres:16 - env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password - POSTGRES_HOST_AUTH_METHOD: scram-sha-256 - POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 - steps: - - name: Check out code - 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.3 - with: - codecov_token: ${{ secrets.CODECOV_TOKEN }} - -# gh-codeql: -# if: ${{ !(github.event.pull_request.draft || false) }} -# runs-on: ubuntu-latest -# container: -# image: swift:5.10-jammy -# permissions: { actions: write, contents: read, security-events: write } -# timeout-minutes: 60 -# steps: -# - name: Check out code -# uses: actions/checkout@v4 -# - name: Mark repo safe in non-fake global config -# run: | -# git config --global --add safe.directory "${GITHUB_WORKSPACE}" -# - name: Initialize CodeQL -# uses: github/codeql-action/init@v3 -# with: { languages: swift } -# - name: Perform build -# run: swift build -# - name: Run CodeQL analyze -# uses: github/codeql-action/analyze@v3 - linux-unit: if: ${{ !(github.event.pull_request.draft || false) }} strategy: fail-fast: false matrix: postgres-image: + - postgres:18 - postgres:16 - postgres:14 - - postgres:12 swift-image: - - swift:5.8-jammy - - swift:5.9-jammy - - swift:5.10-jammy - - swiftlang/swift:nightly-main-jammy + - swift:6.0-noble + - swift:6.1-noble + - swift:6.2-noble include: - - postgres-image: postgres:16 + - postgres-image: postgres:18 postgres-auth: scram-sha-256 - - postgres-image: postgres:14 + - postgres-image: postgres:16 postgres-auth: md5 - - postgres-image: postgres:12 + - postgres-image: postgres:14 postgres-auth: trust runs-on: ubuntu-latest container: ${{ matrix.swift-image }} services: - psql-a: + *postgres_host_a: image: ${{ matrix.postgres-image }} env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password + POSTGRES_USER: *postgres_user_a + POSTGRES_DB: *postgres_db_a + POSTGRES_PASSWORD: *postgres_pass_a 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@v4 + uses: actions/checkout@v5 - name: Run local tests - run: swift test --sanitize=thread + run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable + - name: Upload coverage data + uses: vapor/swift-codecov-action@v0.3 + with: + codecov_token: ${{ secrets.CODECOV_TOKEN || '' }} linux-integration: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:5.10-jammy + container: swift:6.2-noble services: - psql-a: - image: postgres:16 + *postgres_host_a: + image: postgres:18 env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password + POSTGRES_USER: *postgres_user_a + POSTGRES_DB: *postgres_db_a + POSTGRES_PASSWORD: *postgres_pass_a POSTGRES_HOST_AUTH_METHOD: scram-sha-256 POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 - psql-b: - image: postgres:15 + *postgres_host_b: + image: postgres:16 env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password + POSTGRES_USER: *postgres_user_b + POSTGRES_DB: *postgres_db_b + POSTGRES_PASSWORD: *postgres_pass_b POSTGRES_HOST_AUTH_METHOD: scram-sha-256 POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 steps: - name: Check out package - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: { path: 'postgres-kit' } - name: Check out fluent-postgres-driver dependent - uses: actions/checkout@v4 + uses: actions/checkout@v5 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 - name: Run fluent-postgres-kit tests - run: swift test --package-path fluent-postgres-driver --sanitize=thread + run: swift test --package-path fluent-postgres-driver macos-unit: if: ${{ !(github.event.pull_request.draft || false) }} @@ -173,10 +118,10 @@ jobs: fail-fast: false matrix: include: - - macos-version: macos-13 - xcode-version: '~14.3' - - macos-version: macos-14 - xcode-version: latest + - macos-version: macos-15 + xcode-version: latest-stable + - macos-version: macos-26 + xcode-version: latest-stable runs-on: ${{ matrix.macos-version }} env: POSTGRES_HOSTNAME: 127.0.0.1 @@ -189,12 +134,28 @@ jobs: - name: Install Postgres, setup DB and auth, and wait for server start 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" + export PGDATA=/tmp/vapor-postgres-test + brew install "postgresql@18" && brew link --force "postgresql@18" initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}") pg_ctl start --wait timeout-minutes: 15 - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Run local tests - run: swift test --sanitize=thread + run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable + - name: Upload coverage data + uses: vapor/swift-codecov-action@v0.3 + with: + codecov_token: ${{ secrets.CODECOV_TOKEN || '' }} + + musl: + runs-on: ubuntu-latest + container: swift:6.2-noble + timeout-minutes: 30 + steps: + - name: Check out code + uses: actions/checkout@v5 + - name: Install SDK + run: swift sdk install https://download.swift.org/swift-6.2-release/static-sdk/swift-6.2-RELEASE/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum d2225840e592389ca517bbf71652f7003dbf45ac35d1e57d98b9250368769378 + - name: Build + run: swift build --swift-sdk x86_64-swift-linux-musl diff --git a/Package.swift b/Package.swift index 8248f263..4eb69874 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.0 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.2"), + .package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"), ], targets: [ .target( @@ -39,6 +39,10 @@ let package = Package( ) var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("ExistentialAny"), + //.enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("InferIsolatedConformances"), + //.enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("ImmutableWeakCaptures"), ] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift deleted file mode 100644 index 051be68d..00000000 --- a/Package@swift-5.9.swift +++ /dev/null @@ -1,47 +0,0 @@ -// swift-tools-version:5.9 -import PackageDescription - -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.2"), - .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"), - ], - targets: [ - .target( - name: "PostgresKit", - dependencies: [ - .product(name: "AsyncKit", package: "async-kit"), - .product(name: "PostgresNIO", package: "postgres-nio"), - .product(name: "SQLKit", package: "sql-kit"), - ], - swiftSettings: swiftSettings - ), - .testTarget( - name: "PostgresKitTests", - dependencies: [ - .target(name: "PostgresKit"), - .product(name: "SQLKitBenchmark", package: "sql-kit"), - ], - swiftSettings: swiftSettings - ), - ] -) - -var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableExperimentalFeature("StrictConcurrency=complete"), -] } diff --git a/README.md b/README.md index 9d65fa1d..d53e3d44 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,28 @@

- - - - PostgresKit - +PostgresKit

Documentation Team Chat MIT License Continuous Integration - -Swift 5.8+ +Code Coverage +Swift 6.0+


-🐘 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 -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") @@ -33,26 +35,14 @@ PostgresKit supports the following platforms: - Ubuntu 20.04+ - macOS 10.15+ -## Overview - -PostgresKit is an [SQLKit] driver for PostgreSQL cliets. 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. - -> [!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. +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", @@ -60,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( @@ -81,22 +71,22 @@ 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 = ... -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. +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 +116,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 +127,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/ConnectionPool+Postgres.swift b/Sources/PostgresKit/ConnectionPool+Postgres.swift index 9d46e039..080666fd 100644 --- a/Sources/PostgresKit/ConnectionPool+Postgres.swift +++ b/Sources/PostgresKit/ConnectionPool+Postgres.swift @@ -1,7 +1,7 @@ -import NIOCore -import PostgresNIO @preconcurrency import AsyncKit import Logging +import NIOCore +import PostgresNIO extension EventLoopGroupConnectionPool where Source == PostgresConnectionSource { public func database(logger: Logger) -> any PostgresDatabase { @@ -39,11 +39,11 @@ private struct _EventLoopConnectionPoolPostgresDatabase: PostgresDatabase { var eventLoop: any EventLoop { self.pool.eventLoop } - + func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture { self.pool.withConnection(logger: logger) { 0ドル.send(request, logger: logger) } } - + func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { self.pool.withConnection(logger: self.logger, closure) } diff --git a/Sources/PostgresKit/Deprecations/PostgresConfiguration.swift b/Sources/PostgresKit/Deprecations/PostgresConfiguration.swift index edb9d8d8..98446ec1 100644 --- a/Sources/PostgresKit/Deprecations/PostgresConfiguration.swift +++ b/Sources/PostgresKit/Deprecations/PostgresConfiguration.swift @@ -1,6 +1,6 @@ -import NIOSSL import Foundation import NIOCore +import NIOSSL @available(*, deprecated, message: "Use `SQLPostgresConfiguration` instead.") public struct PostgresConfiguration { diff --git a/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift b/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift index 4a03461a..3c366689 100644 --- a/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift +++ b/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift @@ -1,8 +1,8 @@ -import NIOSSL import Atomics import Logging -import PostgresNIO import NIOCore +import NIOSSL +import PostgresNIO extension PostgresConnectionSource { @available(*, deprecated, message: "Use `sqlConfiguration` instead.") diff --git a/Sources/PostgresKit/Deprecations/PostgresDatabase+SQL+Deprecated.swift b/Sources/PostgresKit/Deprecations/PostgresDatabase+SQL+Deprecated.swift index 272b9177..ccc813b1 100644 --- a/Sources/PostgresKit/Deprecations/PostgresDatabase+SQL+Deprecated.swift +++ b/Sources/PostgresKit/Deprecations/PostgresDatabase+SQL+Deprecated.swift @@ -1,5 +1,5 @@ -import PostgresNIO import Foundation +import PostgresNIO import SQLKit @available(*, deprecated, message: "Use `.sql(jsonEncoder:jsonDecoder:)` instead.") diff --git a/Sources/PostgresKit/Docs.docc/PostgresKit.md b/Sources/PostgresKit/Docs.docc/PostgresKit.md index 4b86941e..47bef7fa 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. +PostgresKit is an [SQLKit] driver for PostgreSQL clients. ## 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 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. -- 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]. +> 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. -> 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. +### Usage -> Tip: A FluentKit driver for PostgreSQL is provided by the [FluentPostgresDriver] package. +Reference this package in your `Package.swift` to include it in your project. -## Version Support +```swift +.package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0") +``` -This package uses [PostgresNIO] for all underlying database interactions. It is compatible with all versions of PostgreSQL and all platforms supported by that package. +### Supported Platforms -> 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. +PostgresKit supports the following platforms: -[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 +- Ubuntu 20.04+ +- macOS 10.15+ + +### 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 = NIOSingletons.posixEventLoopGroup +let pools = EventLoopGroupConnectionPool( + source: PostgresConnectionSource(configuration: configuration), + on: eventLoopGroup +) + +// 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. + +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`. + +```swift +let postgres = pool.database(logger: ...) // PostgresDatabase +let rows = try postgres.simpleQuery("SELECT version();").wait() +``` + +Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`. + +### SQLDatabase + +A `PostgresDatabase` can be used to create an instance of `SQLDatabase`. + +```swift +let sql = postgres.sql() // SQLDatabase +let planets = try sql.select().column("*").from("planets").all().wait() +``` + +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/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 de0d11a0..f13ead35 100644 --- a/Sources/PostgresKit/Docs.docc/theme-settings.json +++ b/Sources/PostgresKit/Docs.docc/theme-settings.json @@ -1,6 +1,6 @@ { "theme": { - "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, + "aside": { "border-radius": "16px", "border-width": "3px", "border-style": "double" }, "border-radius": "0", "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, @@ -8,11 +8,14 @@ "psqlkit": "#336791", "documentation-intro-fill": "radial-gradient(circle at top, var(--color-psqlkit) 30%, #000 100%)", "documentation-intro-accent": "var(--color-psqlkit)", + "hero-eyebrow": "white", + "documentation-intro-figure": "white", + "hero-title": "white", "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" } + "icons": { "technology": "/postgreskit/images/PostgresKit/vapor-postgreskit-logo.svg" } }, "features": { "quickNavigation": { "enable": true }, diff --git a/Sources/PostgresKit/PostgresConnectionSource.swift b/Sources/PostgresKit/PostgresConnectionSource.swift index 87ada6c6..37f70541 100644 --- a/Sources/PostgresKit/PostgresConnectionSource.swift +++ b/Sources/PostgresKit/PostgresConnectionSource.swift @@ -1,16 +1,16 @@ -import NIOSSL -import NIOConcurrencyHelpers import AsyncKit import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOSSL import PostgresNIO import SQLKit -import NIOCore public struct PostgresConnectionSource: ConnectionPoolSource { public let sqlConfiguration: SQLPostgresConfiguration - + private static let idGenerator = NIOLockedValueBox(0) - + public init(sqlConfiguration: SQLPostgresConfiguration) { self.sqlConfiguration = sqlConfiguration } @@ -22,10 +22,13 @@ public struct PostgresConnectionSource: ConnectionPoolSource { let connectionFuture = PostgresConnection.connect( on: eventLoop, configuration: self.sqlConfiguration.coreConfiguration, - id: Self.idGenerator.withLockedValue { 0ドル += 1; return 0ドル }, + id: Self.idGenerator.withLockedValue { + 0ドル += 1 + return 0ドル + }, logger: logger ) - + if let searchPath = self.sqlConfiguration.searchPath { return connectionFuture.flatMap { conn in conn.sql(queryLogLevel: nil) @@ -39,4 +42,4 @@ public struct PostgresConnectionSource: ConnectionPoolSource { } } -extension PostgresConnection: ConnectionPoolItem {} +extension PostgresNIO.PostgresConnection: AsyncKit.ConnectionPoolItem {} diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift index 51f5c1c6..6fb9346d 100644 --- a/Sources/PostgresKit/PostgresDataTranslation.swift +++ b/Sources/PostgresKit/PostgresDataTranslation.swift @@ -1,27 +1,27 @@ -import PostgresNIO 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 URL: PostgresNonThrowingEncodable, PostgresDecodable { +extension URL { public static var psqlType: PostgresDataType { String.psqlType } - + public static var psqlFormat: PostgresFormat { String.psqlFormat } @@ -42,7 +42,7 @@ extension URL: PostgresNonThrowingEncodable, PostgresDecodable { 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,19 @@ extension URL: PostgresNonThrowingEncodable, PostgresDecodable { } } +extension URL: @retroactive PostgresNonThrowingEncodable, @retroactive PostgresDecodable {} + 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 +75,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 +133,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 +145,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 +178,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 +187,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 +265,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 +306,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 +324,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 +340,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 +359,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 +370,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 +385,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 +429,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 a3d996bf..5be0b5e5 100644 --- a/Sources/PostgresKit/PostgresDatabase+SQL.swift +++ b/Sources/PostgresKit/PostgresDatabase+SQL.swift @@ -1,5 +1,5 @@ -import PostgresNIO import Logging +import PostgresNIO import SQLKit extension PostgresDatabase { @@ -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,17 +90,16 @@ 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 { + + func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { try await self.withConnection { c in c.eventLoop.makeFutureWithTask { try await closure(c.sql( 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/PostgresRow+SQL.swift b/Sources/PostgresKit/PostgresRow+SQL.swift index 7ac3233d..ab945ac4 100644 --- a/Sources/PostgresKit/PostgresRow+SQL.swift +++ b/Sources/PostgresKit/PostgresRow+SQL.swift @@ -1,5 +1,5 @@ -import PostgresNIO import Foundation +import PostgresNIO import SQLKit extension PostgresRow { @@ -7,7 +7,7 @@ extension PostgresRow { public func sql() -> some SQLRow { self.sql(decodingContext: .default) } - + public func sql(decodingContext: PostgresDecodingContext) -> some SQLRow { _PostgresSQLRow(randomAccessView: self.makeRandomAccess(), decodingContext: decodingContext) } @@ -39,7 +39,7 @@ extension _PostgresSQLRow: SQLRow { guard self.randomAccessView.contains(column) else { throw _Error.missingColumn(column) } - + return try PostgresDataTranslation.decode(T.self, from: self.randomAccessView[column], in: self.decodingContext) } } diff --git a/Sources/PostgresKit/SQLPostgresConfiguration.swift b/Sources/PostgresKit/SQLPostgresConfiguration.swift index 6169f344..f143dd74 100644 --- a/Sources/PostgresKit/SQLPostgresConfiguration.swift +++ b/Sources/PostgresKit/SQLPostgresConfiguration.swift @@ -1,10 +1,10 @@ -import NIOSSL import Foundation import NIOCore +import NIOSSL import PostgresNIO /// Provides configuration paramters for establishing PostgreSQL database connections. -public struct SQLPostgresConfiguration { +public struct SQLPostgresConfiguration: Sendable { /// IANA-assigned port number for PostgreSQL /// `UInt16(getservbyname("postgresql", "tcp").pointee.s_port).byteSwapped` public static var ianaPortNumber: Int { 5432 } @@ -76,7 +76,7 @@ public struct SQLPostgresConfiguration { ///> 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 { } /// 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. diff --git a/Tests/PostgresKitTests/PostgresKitTests.swift b/Tests/PostgresKitTests/PostgresKitTests.swift index c927a2d4..1b16b9fe 100644 --- a/Tests/PostgresKitTests/PostgresKitTests.swift +++ b/Tests/PostgresKitTests/PostgresKitTests.swift @@ -1,49 +1,29 @@ -@testable import PostgresKit -import SQLKitBenchmark -import XCTest +import Foundation import Logging -import PostgresNIO import NIOCore -import Foundation +import PostgresNIO +import SQLKitBenchmark +import Testing +@testable import PostgresKit -final class PostgresKitTests: XCTestCase { - func testSQLKitBenchmark() throws { - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try? conn.close().wait() } - let benchmark = SQLBenchmarker(on: conn.sql()) - do { - try benchmark.run() - } catch { - XCTFail("Caught error: \(String(reflecting: error))") - } - } - - func testPerformance() throws { - let db = PostgresConnectionSource(sqlConfiguration: .test) - let pool = EventLoopGroupConnectionPool( - source: db, - maxConnectionsPerEventLoop: 2, - on: MultiThreadedEventLoopGroup.singleton - ) - defer { pool.shutdown() } - // Postgres seems to take much longer on initial connections when using SCRAM-SHA-256 auth, - // which causes XCTest to bail due to the first measurement having a very high deviation. - // 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") - }.wait() - } - self.measure { - for _ in 1...100 { - _ = try! pool.withConnection { conn in - conn.query("SELECT 1") - }.wait() - } +extension AllSuites { + +@Suite +struct PostgresKitTests { + @Test + func sqlKitBenchmark() async throws { + let conn = try await PostgresConnection.test(on: self.eventLoop) + + await #expect(throws: Never.self) { + let benchmark = SQLBenchmarker(on: conn.sql()) + + try await benchmark.runAllTests() } + try await conn.close() } - - func testLeak() throws { + + @Test + func leak() async throws { struct Foo: Codable { var id: String var description: String? @@ -55,29 +35,22 @@ final class PostgresKitTests: XCTestCase { var modified_at: Date } - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! conn.close().wait() } - + let conn = try await PostgresConnection.test(on: self.eventLoop) let db = conn.sql() - - do { - try db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run().wait() - try db.raw(""" - CREATE TABLE \(ident: "foos") ( - \(ident: "id") TEXT PRIMARY KEY, - \(ident: "description") TEXT, - \(ident: "latitude") DOUBLE PRECISION, - \(ident: "longitude") DOUBLE PRECISION, - \(ident: "created_by") TEXT, - \(ident: "created_at") TIMESTAMPTZ, - \(ident: "modified_by") TEXT, - \(ident: "modified_at") TIMESTAMPTZ - ) - """).run().wait() - defer { - try? db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run().wait() - } - + + await #expect(throws: Never.self) { + try await db.drop(table: "foos").ifExists().run() + try await db.create(table: "foos") + .column("id", type: .text, .primaryKey(autoIncrement: false)) + .column("description", type: .text) + .column("latitude", type: .custom(SQLRaw("DOUBLE PRECISION"))) + .column("longitude", type: .custom(SQLRaw("DOUBLE PRECISION"))) + .column("created_by", type: .text) + .column("created_at", type: .custom(SQLRaw("TIMESTAMPTZ"))) + .column("modified_by", type: .text) + .column("modified_at", type: .custom(SQLRaw("TIMESTAMPTZ"))) + .run() + for i in 0..<5_000 { let zipcode = Foo( id: UUID().uuidString, @@ -89,79 +62,91 @@ final class PostgresKitTests: XCTestCase { modified_by: "test", modified_at: Date() ) - try db.insert(into: "foos") + try await db.insert(into: "foos") .model(zipcode) - .run().wait() + .run() } - } catch { - XCTFail("Caught error: \(String(reflecting: error))") } + try? await db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run() + try await conn.close() } - func testArrayEncoding() throws { - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! conn.close().wait() } - + @Test + func arrayEncoding() async throws { + let conn = try await PostgresConnection.test(on: self.eventLoop) + struct Foo: Codable { var bar: Int } - let foos: [Foo] = [.init(bar: 1), .init(bar: 2)] - try conn.sql().raw("SELECT \(bind: foos)::JSONB[] as \(ident: "foos")") - .run().wait() + + await #expect(throws: Never.self) { + let foos: [Foo] = [.init(bar: 1), .init(bar: 2)] + try await conn.sql().raw("SELECT \(bind: foos)::JSONB[] as \(ident: "foos")").run() + } + try await conn.close() } - func testDecodeModelWithNil() throws { - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! conn.close().wait() } + @Test + func decodeModelWithNil() async throws { + let conn = try await PostgresConnection.test(on: self.eventLoop) - let rows = try conn.sql().raw("SELECT \(literal: "foo")::text as \(ident: "foo"), \(SQLLiteral.null) as \(ident: "bar"), \(literal: "baz")::text as \(ident: "baz")").all().wait() - let row = rows[0] - - struct Test: Codable { - var foo: String - var bar: String? - var baz: String? - } + await #expect(throws: Never.self) { + let rows = try await conn.sql().raw("SELECT \(literal: "foo")::text as \(ident: "foo"), \(SQLLiteral.null) as \(ident: "bar"), \(literal: "baz")::text as \(ident: "baz")").all() + let row = rows[0] - let test = try row.decode(model: Test.self) - XCTAssertEqual(test.foo, "foo") - XCTAssertEqual(test.bar, nil) - XCTAssertEqual(test.baz, "baz") + struct Test: Codable { + var foo: String + var bar: String? + var baz: String? + } + + let test = try row.decode(model: Test.self) + #expect(test.foo == "foo") + #expect(test.bar == nil) + #expect(test.baz == "baz") + } + try await conn.close() } - func testEventLoopGroupSQL() throws { + @Test + func eventLoopGroupSQL() async throws { var configuration = SQLPostgresConfiguration.test configuration.searchPath = ["foo", "bar", "baz"] let source = PostgresConnectionSource(sqlConfiguration: configuration) 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() - XCTAssertEqual(rows.count, 1) + await #expect(throws: Never.self) { + try await #expect(db.raw("SELECT version()").all().count == 1) + } + try await pool.shutdownAsync() } - func testIntegerArrayEncoding() throws { - let connection = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! connection.close().wait() } - let sql = connection.sql() - _ = try sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run().wait() - _ = try sql.raw("CREATE TABLE \(ident: "foo") (\(ident: "bar") bigint[] not null)").run().wait() - defer { - _ = try! sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run().wait() + @Test + func integerArrayEncoding() async throws { + let connection = try await PostgresConnection.test(on: self.eventLoop) + + await #expect(throws: Never.self) { + let sql = connection.sql() + _ = try await sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run() + try await sql.withSession { db in + _ = try await db.create(table: "foo").column("bar", type: .custom(SQLRaw("bigint[]")), .notNull).run() + _ = try await db.insert(into: "foo").columns("bar").values(SQLBind([Bar]())).run() + let rows = try await connection.query("SELECT bar FROM foo", logger: connection.logger).collect() + #expect(rows.count == 1) + #expect(rows.first?.count == 1) + #expect(rows.first?.first?.dataType == Bar.psqlArrayType) + #expect(try rows.first?.first?.decode([Bar].self) == [Bar]()) + } } - _ = try sql.raw("INSERT INTO \(ident: "foo") (\(ident: "bar")) VALUES (\(bind: [Bar]()))").run().wait() - let rows = try connection.query("SELECT bar FROM foo", logger: connection.logger).wait() - XCTAssertEqual(rows.count, 1) - XCTAssertEqual(rows.first?.count, 1) - XCTAssertEqual(rows.first?.first?.dataType, Bar.psqlArrayType) - XCTAssertEqual(try rows.first?.first?.decode([Bar].self), [Bar]()) + try await connection.close() } /// Tests dealing with encoding of values whose `encode(to:)` implementation calls one of the `superEncoder()` /// methods (most notably the implementation of `Codable` for Fluent's `Fields`, which we can't directly test /// at this layer). - func testValuesThatUseSuperEncoder() throws { + @Test + func valuesThatUseSuperEncoder() throws { struct UnusualType: Codable { var prop1: String, prop2: [Bool], prop3: [[Bool]] @@ -201,53 +186,57 @@ final class PostgresKitTests: XCTestCase { let encoded1 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: instance, in: .default, file: #fileID, line: #line) let encoded2 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: [instance, instance], in: .default, file: #fileID, line: #line) - XCTAssertEqual(encoded1.type, .jsonb) - XCTAssertEqual(encoded2.type, .jsonbArray) - + #expect(encoded1.type == .jsonb) + #expect(encoded2.type == .jsonbArray) + let decoded1 = try PostgresDataTranslation.decode(UnusualType.self, from: .init(bytes: encoded1.value, dataType: encoded1.type, format: encoded1.formatCode, columnName: "", columnIndex: -1), in: .default) let decoded2 = try PostgresDataTranslation.decode([UnusualType].self, from: .init(bytes: encoded2.value, dataType: encoded2.type, format: encoded2.formatCode, columnName: "", columnIndex: -1), in: .default) - XCTAssertEqual(decoded1.prop3, instance.prop3) - XCTAssertEqual(decoded2.count, 2) + #expect(decoded1.prop3 == instance.prop3) + #expect(decoded2.count == 2) } - - func testFluentWorkaroundsDecoding() throws { + + @Test + func fluentWorkaroundsDecoding() throws { // SQLKit benchmarks already test enum handling // Text encoding for Decimal let decimalBuffer = ByteBuffer(string: Decimal(12345.6789).description) var decimalValue: Decimal? - XCTAssertNoThrow(decimalValue = try PostgresDataTranslation.decode(Decimal.self, from: .init(bytes: decimalBuffer, dataType: .numeric, format: .text, columnName: "", columnIndex: -1), in: .default)) - XCTAssertEqual(decimalValue, Decimal(12345.6789)) - + #expect(throws: Never.self) { decimalValue = try PostgresDataTranslation.decode(Decimal.self, from: .init(bytes: decimalBuffer, dataType: .numeric, format: .text, columnName: "", columnIndex: -1), in: .default) } + #expect(decimalValue == Decimal(12345.6789)) + // Decoding Double from NUMERIC let numericBuffer = PostgresData(numeric: .init(decimal: 12345.6789)).value var numericValue: Double? - 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)) + #expect(throws: Never.self) { numericValue = try PostgresDataTranslation.decode(Double.self, from: .init(bytes: numericBuffer, dataType: .numeric, format: .binary, columnName: "", columnIndex: -1), in: .default) } + #expect(numericValue == Double(Decimal(12345.6789).description)) } - - func testURLWorkaroundDecoding() throws { + + @Test + func urlWorkaroundDecoding() 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) - + #expect(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) + #expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedNormal), in: .default) == url) + #expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default) == url) } var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() } - override class func setUp() { - XCTAssertTrue(isLoggingConfigured) + init() { + #expect(isLoggingConfigured) } } +} + 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/SQLPostgresConfigurationTests.swift b/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift index 64959ee3..e0648390 100644 --- a/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift +++ b/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift @@ -1,75 +1,83 @@ -@testable import PostgresKit -import XCTest +import PostgresKit +import Testing -final class SQLPostgresConfigurationTests: XCTestCase { - func testURLHandling() throws { +extension AllSuites { +@Suite +struct SQLPostgresConfigurationTests { + @Test + func urlHandling() throws { let config1 = try SQLPostgresConfiguration(url: "postgres+tcp://test_username:test_password@test_hostname:9999/test_database?tlsmode=disable") - XCTAssertEqual(config1.coreConfiguration.database, "test_database") - XCTAssertEqual(config1.coreConfiguration.password, "test_password") - XCTAssertEqual(config1.coreConfiguration.username, "test_username") - XCTAssertEqual(config1.coreConfiguration.host, "test_hostname") - XCTAssertEqual(config1.coreConfiguration.port, 9999) - XCTAssertNil(config1.coreConfiguration.unixSocketPath) - XCTAssertFalse(config1.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config1.coreConfiguration.tls.isEnforced) + #expect(config1.coreConfiguration.database == "test_database") + #expect(config1.coreConfiguration.password == "test_password") + #expect(config1.coreConfiguration.username == "test_username") + #expect(config1.coreConfiguration.host == "test_hostname") + #expect(config1.coreConfiguration.port == 9999) + #expect(config1.coreConfiguration.unixSocketPath == nil) + #expect(!config1.coreConfiguration.tls.isAllowed) + #expect(!config1.coreConfiguration.tls.isEnforced) let config2 = try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname") - XCTAssertNil(config2.coreConfiguration.database) - XCTAssertNil(config2.coreConfiguration.password) - XCTAssertEqual(config2.coreConfiguration.username, "test_username") - XCTAssertEqual(config2.coreConfiguration.host, "test_hostname") - XCTAssertEqual(config2.coreConfiguration.port, SQLPostgresConfiguration.ianaPortNumber) - XCTAssertNil(config2.coreConfiguration.unixSocketPath) - XCTAssertTrue(config2.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config2.coreConfiguration.tls.isEnforced) + #expect(config2.coreConfiguration.database == nil) + #expect(config2.coreConfiguration.password == nil) + #expect(config2.coreConfiguration.username == "test_username") + #expect(config2.coreConfiguration.host == "test_hostname") + #expect(config2.coreConfiguration.port == SQLPostgresConfiguration.ianaPortNumber) + #expect(config2.coreConfiguration.unixSocketPath == nil) + #expect(config2.coreConfiguration.tls.isAllowed) + #expect(!config2.coreConfiguration.tls.isEnforced) let config3 = try SQLPostgresConfiguration(url: "postgres+uds://test_username:test_password@localhost/tmp/postgres.sock?tlsmode=require#test_database") - XCTAssertEqual(config3.coreConfiguration.database, "test_database") - XCTAssertEqual(config3.coreConfiguration.password, "test_password") - XCTAssertEqual(config3.coreConfiguration.username, "test_username") - XCTAssertNil(config3.coreConfiguration.host) - XCTAssertNil(config3.coreConfiguration.port) - XCTAssertEqual(config3.coreConfiguration.unixSocketPath, "/tmp/postgres.sock") - XCTAssertTrue(config3.coreConfiguration.tls.isAllowed) - XCTAssertTrue(config3.coreConfiguration.tls.isEnforced) + #expect(config3.coreConfiguration.database == "test_database") + #expect(config3.coreConfiguration.password == "test_password") + #expect(config3.coreConfiguration.username == "test_username") + #expect(config3.coreConfiguration.host == nil) + #expect(config3.coreConfiguration.port == nil) + #expect(config3.coreConfiguration.unixSocketPath == "/tmp/postgres.sock") + #expect(config3.coreConfiguration.tls.isAllowed) + #expect(config3.coreConfiguration.tls.isEnforced) let config4 = try SQLPostgresConfiguration(url: "postgres+uds://test_username@/tmp/postgres.sock") - XCTAssertNil(config4.coreConfiguration.database) - XCTAssertNil(config4.coreConfiguration.password) - XCTAssertEqual(config4.coreConfiguration.username, "test_username") - XCTAssertNil(config4.coreConfiguration.host) - XCTAssertNil(config4.coreConfiguration.port) - XCTAssertEqual(config4.coreConfiguration.unixSocketPath, "/tmp/postgres.sock") - XCTAssertFalse(config4.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config4.coreConfiguration.tls.isEnforced) - + #expect(config4.coreConfiguration.database == nil) + #expect(config4.coreConfiguration.password == nil) + #expect(config4.coreConfiguration.username == "test_username") + #expect(config4.coreConfiguration.host == nil) + #expect(config4.coreConfiguration.port == nil) + #expect(config4.coreConfiguration.unixSocketPath == "/tmp/postgres.sock") + #expect(!config4.coreConfiguration.tls.isAllowed) + #expect(!config4.coreConfiguration.tls.isEnforced) + for modestr in ["tlsmode=false", "tlsmode=verify-full&tlsmode=disable"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") - XCTAssertFalse(config.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config.coreConfiguration.tls.isEnforced) + #expect(!config.coreConfiguration.tls.isAllowed) + #expect(!config.coreConfiguration.tls.isEnforced) } for modestr in ["tlsmode=prefer", "tlsmode=allow", "tlsmode=true"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") - XCTAssertTrue(config.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config.coreConfiguration.tls.isEnforced) + #expect(config.coreConfiguration.tls.isAllowed) + #expect(!config.coreConfiguration.tls.isEnforced) } for modestr in ["tlsmode=require", "tlsmode=verify-ca", "tlsmode=verify-full", "tls=verify-full", "ssl=verify-full", "tlsmode=prefer&sslmode=verify-full"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") - XCTAssertTrue(config.coreConfiguration.tls.isAllowed) - XCTAssertTrue(config.coreConfiguration.tls.isEnforced) + #expect(config.coreConfiguration.tls.isAllowed) + #expect(config.coreConfiguration.tls.isEnforced) } - XCTAssertNoThrow(try SQLPostgresConfiguration(url: "postgresql://test_username@test_hostname")) - XCTAssertNoThrow(try SQLPostgresConfiguration(url: "postgresql+tcp://test_username@test_hostname")) - XCTAssertNoThrow(try SQLPostgresConfiguration(url: "postgresql+uds://test_username@/tmp/postgres.sock")) - - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+tcp://test_hostname"), "should fail when username missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname?tlsmode=absurd"), "should fail when TLS mode invalid") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds://localhost/tmp/postgres.sock?tlsmode=require"), "should fail when username missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds:///tmp/postgres.sock"), "should fail when authority missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds://username@localhost/"), "should fail when path missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds://username@remotehost/tmp"), "should fail when authority not localhost or empty") + #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql://test_username@test_hostname") } + #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql+tcp://test_username@test_hostname") } + #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql+uds://test_username@/tmp/postgres.sock") } + + #expect(throws: (any Error).self, "should fail when username missing") { try SQLPostgresConfiguration(url: "postgres+tcp://test_hostname") } + #expect(throws: (any Error).self, "should fail when TLS mode invalid") { try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname?tlsmode=absurd") } + #expect(throws: (any Error).self, "should fail when username missing") { try SQLPostgresConfiguration(url: "postgres+uds://localhost/tmp/postgres.sock?tlsmode=require") } + #expect(throws: (any Error).self, "should fail when authority missing") { try SQLPostgresConfiguration(url: "postgres+uds:///tmp/postgres.sock") } + #expect(throws: (any Error).self, "should fail when path missing") { try SQLPostgresConfiguration(url: "postgres+uds://username@localhost/") } + #expect(throws: (any Error).self, "should fail when authority not localhost or empty") { try SQLPostgresConfiguration(url: "postgres+uds://username@remotehost/tmp") } } + + init() { + #expect(isLoggingConfigured) + } +} } diff --git a/Tests/PostgresKitTests/Utilities.swift b/Tests/PostgresKitTests/Utilities.swift index 3c52f2d9..ec2ad580 100644 --- a/Tests/PostgresKitTests/Utilities.swift +++ b/Tests/PostgresKitTests/Utilities.swift @@ -1,13 +1,16 @@ -import XCTest -import PostgresKit -import Logging import Foundation +import Logging import NIOCore +import PostgresKit import PostgresNIO +import Testing extension PostgresConnection { - static func test(on eventLoop: any EventLoop) -> EventLoopFuture { - PostgresConnectionSource(sqlConfiguration: .test).makeConnection(logger: .init(label: "vapor.codes.postgres-kit.test"), on: eventLoop) + static func test(on eventLoop: any EventLoop) async throws -> PostgresConnection { + try await PostgresConnectionSource(sqlConfiguration: .test).makeConnection( + logger: .init(label: "vapor.codes.postgres-kit.test"), + on: eventLoop + ).get() } } @@ -29,10 +32,39 @@ func env(_ name: String) -> String? { } let isLoggingConfigured: Bool = { - LoggingSystem.bootstrap { label in - var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: 0ドル) } ?? .info - return handler - } + LoggingSystem.bootstrap { QuickLogHandler(label: 0,ドル level: env("LOG_LEVEL").flatMap { .init(rawValue: 0ドル) } ?? .info) } return true }() + +struct QuickLogHandler: LogHandler { + private let label: String + var logLevel = Logger.Level.info, metadataProvider = LoggingSystem.metadataProvider, metadata = Logger.Metadata() + subscript(metadataKey key: String) -> Logger.Metadata.Value? { get { self.metadata[key] } set { self.metadata[key] = newValue } } + init(label: String, level: Logger.Level) { (self.label, self.logLevel) = (label, level) } + func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) { + print("\(self.timestamp()) \(level) \(self.label):\(self.prettify(metadata ?? [:]).map { " \(0ドル)" } ?? "") [\(source)] \(message)") + } + private func prettify(_ metadata: Logger.Metadata) -> String? { + self.metadata.merging(self.metadataProvider?.get() ?? [:]) { 1ドル }.merging(metadata) { 1ドル }.sorted { 0ドル.0 < 1ドル.0 }.map { "\(0ドル)=\(1ドル.mvDesc)" }.joined(separator: " ") + } + private func timestamp() -> String { .init(unsafeUninitializedCapacity: 255) { buffer in + var timestamp = time(nil) + return localtime(×tamp).map { strftime(buffer.baseAddress!, buffer.count, "%Y-%m-%dT%H:%M:%S%z", 0ドル) } ?? buffer.initialize(fromContentsOf: "".utf8) + } } +} +extension Logger.MetadataValue { + var mvDesc: String { switch self { + case .dictionary(let dict): "[\(dict.mapValues(\.mvDesc).lazy.sorted { 0ドル.0 < 1ドル.0 }.map { "\(0ドル): \(1ドル)" }.joined(separator: ", "))]" + case .array(let list): "[\(list.map(\.mvDesc).joined(separator: ", "))]" + case .string(let str): #""\#(str)""# + case .stringConvertible(let repr): switch repr { + case let repr as Bool: "\(repr)" + case let repr as any FixedWidthInteger: "\(repr)" + case let repr as any BinaryFloatingPoint: "\(repr)" + default: #""\#(String(describing: repr))""# + } + } } +} + +@Suite(.serialized) +struct AllSuites {}

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /