diff --git a/.api-breakage/allowlist-branch-update-for-new-pnio.txt b/.api-breakage/allowlist-branch-update-for-new-pnio.txt
deleted file mode 100644
index 3b27dbf..0000000
--- a/.api-breakage/allowlist-branch-update-for-new-pnio.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-API breakage: var PostgresDialect.sharedSelectLockExpression has declared type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
-API breakage: accessor PostgresDialect.sharedSelectLockExpression.Get() has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
-API breakage: var PostgresDialect.exclusiveSelectLockExpression has declared type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
-API breakage: accessor PostgresDialect.exclusiveSelectLockExpression.Get() has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
-API breakage: func PostgresDatabase.sql(encoder:decoder:) has removed default argument from parameter 0
-API breakage: func PostgresDatabase.sql(encoder:decoder:) has removed default argument from parameter 1
-API breakage: func PostgresRow.sql(decoder:) has removed default argument from parameter 0
-API breakage: func PostgresColumnType.==(_:_:) has been removed
-API breakage: import Foundation has been removed
-
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..998a0eb
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ groups:
+ dependencies:
+ patterns:
+ - "*"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2999d9b..4fb24ec 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -25,15 +25,33 @@ env:
jobs:
# Check for API breakage versus main
api-breakage:
- if: ${{ !(github.event.pull_request.draft || false) }}
+ if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
container: swift:jammy
steps:
- - name: Check out code
- uses: actions/checkout@v3
+ - name: Checkout
+ uses: actions/checkout@v4
with: { 'fetch-depth': 0 }
- - name: Run API breakage check action
- uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@main
+ - name: API breaking changes
+ run: |
+ git config --global --add safe.directory "${GITHUB_WORKSPACE}"
+ swift package diagnose-api-breaking-changes origin/main
+
+ dependency-graph:
+ if: ${{ github.event_name == 'push' }}
+ runs-on: ubuntu-latest
+ container: swift:jammy
+ permissions:
+ contents: write
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+ - name: Fix Git configuration
+ run: |
+ git config --global --add safe.directory "${GITHUB_WORKSPACE}"
+ apt-get update && apt-get install -y curl
+ - name: Submit dependency graph
+ uses: vapor-community/swift-dependency-submission@v0.1
code-coverage:
if: ${{ !(github.event.pull_request.draft || false) }}
@@ -41,7 +59,7 @@ jobs:
container: swift:jammy
services:
psql-a:
- image: postgres:15
+ image: postgres:16
env:
POSTGRES_USER: test_username
POSTGRES_DB: test_database
@@ -50,37 +68,38 @@ jobs:
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
steps:
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Run unit tests for coverage data
run: swift test --enable-code-coverage
- name: Upload coverage data
uses: vapor/swift-codecov-action@v0.2
gh-codeql:
- if: ${{ !(github.event.pull_request.draft || false) }}
- strategy:
- fail-fast: false
- matrix:
- runner_os:
- - ubuntu-latest
- - macos-13
- runs-on: ${{ matrix.runner_os }}
- permissions:
- security-events: write
+ if: ${{ false && !(github.event.pull_request.draft || false) }}
+ runs-on: ubuntu-latest
+ container:
+ image: swift:5.9-jammy
+ permissions: { actions: write, contents: read, security-events: write }
+ timeout-minutes: 60
steps:
- - name: Select appropriate Xcode version
- if: ${{ startsWith(matrix.runner_os, 'macos') }}
- uses: maxim-lobanov/setup-xcode@v1
- with: { xcode-version: '~14.3' }
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ - name: Mark repo safe in non-fake global config
+ run: |
+ git config --global --add safe.directory "${GITHUB_WORKSPACE}"
+ - name: Check Swift compatibility
+ id: swift-check
+ uses: vapor/ci/.github/actions/check-compatible-swift@main
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ if: ${{ steps.swift-check.outputs.swift-compatible == 'true' }}
+ uses: github/codeql-action/init@v3
with: { languages: swift }
- name: Perform build
+ if: ${{ steps.swift-check.outputs.swift-compatible == 'true' }}
run: swift build
- name: Run CodeQL analyze
- uses: github/codeql-action/analyze@v2
+ if: ${{ steps.swift-check.outputs.swift-compatible == 'true' }}
+ uses: github/codeql-action/analyze@v3
linux-unit:
if: ${{ !(github.event.pull_request.draft || false) }}
@@ -88,20 +107,21 @@ jobs:
fail-fast: false
matrix:
postgres-image:
- - postgres:15
- - postgres:13
- - postgres:11
+ - postgres:16
+ - postgres:14
+ - postgres:12
swift-image:
- swift:5.7-jammy
- swift:5.8-jammy
- - swiftlang/swift:nightly-5.9-jammy
+ - swift:5.9-jammy
+ - swiftlang/swift:nightly-5.10-jammy
- swiftlang/swift:nightly-main-jammy
include:
- - postgres-image: postgres:15
+ - postgres-image: postgres:16
postgres-auth: scram-sha-256
- - postgres-image: postgres:13
+ - postgres-image: postgres:14
postgres-auth: md5
- - postgres-image: postgres:11
+ - postgres-image: postgres:12
postgres-auth: trust
runs-on: ubuntu-latest
container: ${{ matrix.swift-image }}
@@ -116,17 +136,17 @@ jobs:
POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }}
steps:
- name: Check out package
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Run local tests
run: swift test
linux-integration:
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
- container: swift:5.8-jammy
+ container: swift:5.9-jammy
services:
psql-a:
- image: postgres:15
+ image: postgres:16
env:
POSTGRES_USER: test_username
POSTGRES_DB: test_database
@@ -143,10 +163,10 @@ jobs:
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
steps:
- name: Check out package
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with: { path: 'postgres-kit' }
- name: Check out fluent-postgres-driver dependent
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with: { repository: 'vapor/fluent-postgres-driver', path: 'fluent-postgres-driver' }
- name: Use local package
run: swift package --package-path fluent-postgres-driver edit postgres-kit --path postgres-kit
@@ -158,10 +178,12 @@ jobs:
strategy:
fail-fast: false
matrix:
- xcode-version:
- - '~14.3'
- - '15.0-beta'
- runs-on: macos-13
+ include:
+ - macos-version: macos-13
+ xcode-version: '~14.3'
+ - macos-version: macos-14
+ xcode-version: latest
+ runs-on: ${{ matrix.macos-version }}
env:
POSTGRES_HOSTNAME: 127.0.0.1
POSTGRES_DB: postgres
@@ -173,11 +195,11 @@ jobs:
- name: Install Postgres, setup DB and auth, and wait for server start
run: |
export PATH="$(brew --prefix)/opt/postgresql@14/bin:$PATH" PGDATA=/tmp/vapor-postgres-test
- (brew unlink postgresql || true) && brew install "postgresql@14" && brew link --force "postgresql@14"
+ (brew unlink postgresql || true) && brew install "postgresql@15" && brew link --force "postgresql@15"
initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}")
pg_ctl start --wait
timeout-minutes: 2
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Run local tests
run: swift test
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..271f683
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,49 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let swiftSettings: [SwiftSetting] = [
+ .enableUpcomingFeature("ExistentialAny"),
+ .enableUpcomingFeature("ConciseMagicFile"),
+ .enableUpcomingFeature("ForwardTrailingClosures"),
+ .enableUpcomingFeature("DisableOutwardActorInference"),
+ .enableExperimentalFeature("StrictConcurrency=complete"),
+]
+
+let package = Package(
+ name: "postgres-kit",
+ platforms: [
+ .macOS(.v10_15),
+ .iOS(.v13),
+ .watchOS(.v6),
+ .tvOS(.v13),
+ ],
+ products: [
+ .library(name: "PostgresKit", targets: ["PostgresKit"]),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.20.0"),
+ .package(url: "https://github.com/vapor/sql-kit.git", from: "3.28.0"),
+ .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"),
+ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0")
+ ],
+ targets: [
+ .target(
+ name: "PostgresKit",
+ dependencies: [
+ .product(name: "AsyncKit", package: "async-kit"),
+ .product(name: "PostgresNIO", package: "postgres-nio"),
+ .product(name: "SQLKit", package: "sql-kit"),
+ .product(name: "Atomics", package: "swift-atomics"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "PostgresKitTests",
+ dependencies: [
+ .target(name: "PostgresKit"),
+ .product(name: "SQLKitBenchmark", package: "sql-kit"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ ]
+)
diff --git a/README.md b/README.md
index c103a68..fe0c827 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,19 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
🐘 Non-blocking, event-driven Swift client for PostgreSQL.
@@ -39,7 +37,7 @@ Use the SPM string to easily include the dependendency in your `Package.swift` f
PostgresKit supports the following platforms:
-- Ubuntu 16.04+
+- Ubuntu 20.04+
- macOS 10.15+
## Overview
diff --git a/Sources/PostgresKit/ConnectionPool+Postgres.swift b/Sources/PostgresKit/ConnectionPool+Postgres.swift
index 6dd11f9..547d811 100644
--- a/Sources/PostgresKit/ConnectionPool+Postgres.swift
+++ b/Sources/PostgresKit/ConnectionPool+Postgres.swift
@@ -1,6 +1,6 @@
import NIOCore
import PostgresNIO
-import AsyncKit
+@preconcurrency import AsyncKit
import Logging
extension EventLoopGroupConnectionPool where Source == PostgresConnectionSource {
diff --git a/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift b/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift
index 80c07b5..4a03461 100644
--- a/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift
+++ b/Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift
@@ -34,7 +34,7 @@ extension PostgresConnectionSource {
}
@available(*, deprecated, message: "Use `sqlConfiguration` instead.")
- public var sslContext: Result { .success(self.sqlConfiguration.coreConfiguration.tls.sslContext) }
+ public var sslContext: Result { .success(self.sqlConfiguration.coreConfiguration.tls.sslContext) }
@available(*, deprecated, message: "Use `init(sqlConfiguration:)` instead.")
public init(configuration: PostgresConfiguration) {
diff --git a/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift b/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift
index aafdc1c..057b545 100644
--- a/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift
+++ b/Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift
@@ -9,7 +9,7 @@ public final class PostgresDataEncoder {
self.json = json
}
- public func encode(_ value: Encodable) throws -> PostgresData {
+ public func encode(_ value: any Encodable) throws -> PostgresData {
if let custom = value as? any PostgresDataConvertible, let data = custom.postgresData {
return data
} else {
diff --git a/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg b/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg
new file mode 100644
index 0000000..cdb1a8e
--- /dev/null
+++ b/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg
@@ -0,0 +1,22 @@
+
diff --git a/Sources/PostgresKit/Docs.docc/theme-settings.json b/Sources/PostgresKit/Docs.docc/theme-settings.json
new file mode 100644
index 0000000..b147e23
--- /dev/null
+++ b/Sources/PostgresKit/Docs.docc/theme-settings.json
@@ -0,0 +1,21 @@
+{
+ "theme": {
+ "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" },
+ "border-radius": "0",
+ "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
+ "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
+ "color": {
+ "psqlkit": "#336791",
+ "documentation-intro-fill": "radial-gradient(circle at top, var(--color-psqlkit) 30%, #000 100%)",
+ "documentation-intro-accent": "var(--color-psqlkit)",
+ "logo-base": { "dark": "#fff", "light": "#000" },
+ "logo-shape": { "dark": "#000", "light": "#fff" },
+ "fill": { "dark": "#000", "light": "#fff" }
+ },
+ "icons": { "technology": "/postgreskit/images/vapor-postgreskit-logo.svg" }
+ },
+ "features": {
+ "quickNavigation": { "enable": true },
+ "i18n": { "enable": true }
+ }
+}
diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift
index 1056090..9fbee68 100644
--- a/Sources/PostgresKit/PostgresDataTranslation.swift
+++ b/Sources/PostgresKit/PostgresDataTranslation.swift
@@ -14,6 +14,38 @@ private extension PostgresCell {
var codingKey: any CodingKey { SomeCodingKey(stringValue: !self.columnName.isEmpty ? "\(self.columnName) (\(self.columnIndex))" : "\(self.columnIndex)") }
}
+/// Sidestep problems with URL coding behavior by making it conform directly to Postgres coding.
+extension URL: PostgresNonThrowingEncodable {
+ public static var psqlType: PostgresDataType { String.psqlType }
+ public static var psqlFormat: PostgresFormat { String.psqlFormat }
+
+ @inlinable
+ public func encode(into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext) {
+ self.absoluteString.encode(into: &byteBuffer, context: context)
+ }
+}
+
+/// Sidestep problems with URL coding behavior by making it conform directly to Postgres coding.
+extension URL: PostgresDecodable {
+ @inlinable
+ public init(
+ from buffer: inout ByteBuffer, type: PostgresDataType, format: PostgresFormat,
+ context: PostgresDecodingContext
+ ) throws {
+ let string = try String(from: &buffer, type: type, format: format, context: context)
+
+ if let url = URL(string: string) {
+ self = url
+ }
+ // Also support the broken encoding we were emitting for awhile there.
+ else if string.hasPrefix("\""), string.hasSuffix("\""), let url = URL(string: String(string.dropFirst().dropLast())) {
+ self = url
+ } else {
+ throw PostgresDecodingError.Code.failure
+ }
+ }
+}
+
struct PostgresDataTranslation {
/// This typealias serves to limit the deprecation noise caused by ``PostgresDataConvertible`` to a single
/// warning, down from what would otherwise be a minimum of two. It has no other purpose.
diff --git a/Sources/PostgresKit/PostgresDatabase+SQL.swift b/Sources/PostgresKit/PostgresDatabase+SQL.swift
index f7bb7ac..85a5516 100644
--- a/Sources/PostgresKit/PostgresDatabase+SQL.swift
+++ b/Sources/PostgresKit/PostgresDatabase+SQL.swift
@@ -1,6 +1,13 @@
import PostgresNIO
import Logging
-import SQLKit
+@preconcurrency import SQLKit
+
+// https://github.com/vapor/postgres-nio/pull/450
+#if compiler(>=5.10) && $RetroactiveAttribute
+extension PostgresEncodingContext: @retroactive @unchecked Sendable {}
+#else
+extension PostgresEncodingContext: @unchecked Sendable {}
+#endif
extension PostgresDatabase {
@inlinable
@@ -37,7 +44,7 @@ extension _PostgresSQLDatabase: SQLDatabase, PostgresDatabase {
var version: (any SQLDatabaseReportedVersion)? { nil } // PSQL doesn't send version in wire protocol, must use SQL to read it
var dialect: any SQLDialect { PostgresDialect() }
- func execute(sql query: any SQLExpression, _ onRow: @escaping (any SQLRow) -> ()) -> EventLoopFuture {
+ func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture {
let (sql, binds) = self.serialize(query)
if let queryLogLevel {
diff --git a/Sources/PostgresKit/SQLPostgresConfiguration.swift b/Sources/PostgresKit/SQLPostgresConfiguration.swift
index 5a11c7e..6169f34 100644
--- a/Sources/PostgresKit/SQLPostgresConfiguration.swift
+++ b/Sources/PostgresKit/SQLPostgresConfiguration.swift
@@ -146,7 +146,7 @@ public struct SQLPostgresConfiguration {
/// This is provided for calling code which wants to manage the underlying connection transport on its
/// own, such as when tunneling a connection through SSH.
public init(
- establishedChannel: Channel,
+ establishedChannel: any Channel,
username: String, password: String? = nil,
database: String? = nil
) {
diff --git a/Tests/PostgresKitTests/PostgresKitTests.swift b/Tests/PostgresKitTests/PostgresKitTests.swift
index af1074a..e7ac17f 100644
--- a/Tests/PostgresKitTests/PostgresKitTests.swift
+++ b/Tests/PostgresKitTests/PostgresKitTests.swift
@@ -19,7 +19,7 @@ final class PostgresKitTests: XCTestCase {
let pool = EventLoopGroupConnectionPool(
source: db,
maxConnectionsPerEventLoop: 2,
- on: self.eventLoopGroup
+ on: MultiThreadedEventLoopGroup.singleton
)
defer { pool.shutdown() }
// Postgres seems to take much longer on initial connections when using SCRAM-SHA-256 auth,
@@ -27,13 +27,13 @@ final class PostgresKitTests: XCTestCase {
// Spin the pool a bit before running the measurement to warm it up.
for _ in 1...25 {
_ = try pool.withConnection { conn in
- conn.query("SELECT 1;")
+ conn.query("SELECT 1")
}.wait()
}
self.measure {
for _ in 1...100 {
_ = try! pool.withConnection { conn in
- conn.query("SELECT 1;")
+ conn.query("SELECT 1")
}.wait()
}
}
@@ -125,12 +125,12 @@ final class PostgresKitTests: XCTestCase {
var configuration = SQLPostgresConfiguration.test
configuration.searchPath = ["foo", "bar", "baz"]
let source = PostgresConnectionSource(sqlConfiguration: configuration)
- let pool = EventLoopGroupConnectionPool(source: source, on: self.eventLoopGroup)
+ let pool = EventLoopGroupConnectionPool(source: source, on: MultiThreadedEventLoopGroup.singleton)
defer { pool.shutdown() }
let db = pool.database(logger: .init(label: "test")).sql()
- let rows = try db.raw("SELECT version();").all().wait()
- print(rows)
+ let rows = try db.raw("SELECT version()").all().wait()
+ XCTAssertEqual(rows.count, 1)
}
func testIntegerArrayEncoding() throws {
@@ -224,20 +224,30 @@ final class PostgresKitTests: XCTestCase {
XCTAssertNoThrow(numericValue = try PostgresDataTranslation.decode(Double.self, from: .init(bytes: numericBuffer, dataType: .numeric, format: .binary, columnName: "", columnIndex: -1), in: .default))
XCTAssertEqual(numericValue, Double(Decimal(12345.6789).description))
}
+
+ func testURLWorkaroundDecoding() throws {
+ let url = URL(string: "https://user:pass@www.example.com:8080/path/to/endpoint?query=value#fragment")!
+
+ let encodedNormal = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: url, in: .default, file: #fileID, line: #line)
+ XCTAssertEqual(encodedNormal.value?.getString(at: 0, length: encodedNormal.value?.readableBytes ?? 0), url.absoluteString)
+
+ let encodedBroken = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: "\"\(url.absoluteString)\"", in: .default, file: #fileID, line: #line)
+
+ XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedNormal), in: .default), url)
+ XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default), url)
+ }
- var eventLoop: any EventLoop { self.eventLoopGroup.any() }
- var eventLoopGroup: (any EventLoopGroup)!
+ var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() }
override func setUpWithError() throws {
try super.setUpWithError()
XCTAssertTrue(isLoggingConfigured)
- self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)
}
+}
- override func tearDownWithError() throws {
- try self.eventLoopGroup.syncShutdownGracefully()
- self.eventLoopGroup = nil
- try super.tearDownWithError()
+extension PostgresCell {
+ fileprivate init(with data: PostgresData) {
+ self.init(bytes: data.value, dataType: data.type, format: data.formatCode, columnName: "", columnIndex: -1)
}
}
diff --git a/Tests/PostgresKitTests/Utilities.swift b/Tests/PostgresKitTests/Utilities.swift
index 01e3a54..3c52f2d 100644
--- a/Tests/PostgresKitTests/Utilities.swift
+++ b/Tests/PostgresKitTests/Utilities.swift
@@ -31,7 +31,7 @@ func env(_ name: String) -> String? {
let isLoggingConfigured: Bool = {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
- handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .info
+ handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info
return handler
}
return true