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 {}