diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fc184d7..05a15ee 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -46,9 +46,9 @@ jobs:
- postgres:16
- postgres:14
swift-image:
- - swift:6.0-noble
- swift:6.1-noble
- swift:6.2-noble
+ - swift:6.3-noble
include:
- postgres-image: postgres:18
postgres-auth: scram-sha-256
@@ -75,14 +75,14 @@ jobs:
- name: Run local tests
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
+ uses: vapor/swift-codecov-action@2f478f2fd22e06ca363967d0fb6c6593a8548cee # v0.3.5
with:
codecov_token: ${{ secrets.CODECOV_TOKEN || '' }}
linux-integration:
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
- container: swift:6.2-noble
+ container: swift:6.3-noble
services:
*postgres_host_a:
image: postgres:18
@@ -128,7 +128,7 @@ jobs:
POSTGRES_DB: postgres
steps:
- name: Select latest available Xcode
- uses: maxim-lobanov/setup-xcode@v1
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{ matrix.xcode-version }}
- name: Install Postgres, setup DB and auth, and wait for server start
@@ -144,18 +144,26 @@ jobs:
- name: Run local tests
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
+ uses: vapor/swift-codecov-action@2f478f2fd22e06ca363967d0fb6c6593a8548cee # v0.3.5
with:
codecov_token: ${{ secrets.CODECOV_TOKEN || '' }}
musl:
runs-on: ubuntu-latest
- container: swift:6.2-noble
+ container: swift:6.3-noble
timeout-minutes: 30
steps:
- name: Check out code
uses: actions/checkout@v7
+ - name: Install linux dependencies
+ run: apt-get update && apt-get install -y curl jq
- name: Install SDK
- run: swift sdk install https://download.swift.org/swift-6.2.3-release/static-sdk/swift-6.2.3-RELEASE/swift-6.2.3-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum f30ec724d824ef43b5546e02ca06a8682dafab4b26a99fbb0e858c347e507a2c
+ run: |
+ version_num="$(swiftc --version | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)"
+ tag="swift-${version_num}-RELEASE"
+ sdk_info="$(curl -s https://www.swift.org/api/v1/install/releases.json | jq -r --arg tag "$tag" '.[] | select(.tag == $tag).platforms[] | select(.platform == "static-sdk") | [.version, .checksum] | @tsv')"
+ version="$(echo "$sdk_info" | cut -f1)"
+ checksum="$(echo "$sdk_info" | cut -f2)"
+ swift sdk install "https://download.swift.org/$(echo $tag | tr [A-Z] [a-z])/static-sdk/$tag/${tag}_static-linux-${version}.artifactbundle.tar.gz" --checksum "$checksum"
- name: Build
run: swift build --swift-sdk x86_64-swift-linux-musl
diff --git a/Package.swift b/Package.swift
index 4eb6987..5731a85 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:6.0
+// swift-tools-version:6.1
import PackageDescription
let package = Package(
@@ -13,8 +13,8 @@ let package = Package(
.library(name: "PostgresKit", targets: ["PostgresKit"]),
],
dependencies: [
- .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/postgres-nio.git", from: "1.30.1"),
+ .package(url: "https://github.com/vapor/sql-kit.git", from: "3.34.0"),
.package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"),
],
targets: [
diff --git a/README.md b/README.md
index 70796a9..be6d3bd 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,15 @@
-
+
-
-

-

-

-

-

-

-
+
+[](https://docs.vapor.codes/4.0/)
+[](https://discord.gg/vapor)
+[](./LICENSE)
+[](https://github.com/vapor/postgres-kit/actions/workflows/test.yml)
+[](https://codecov.io/github/vapor/postgres-kit)
+[](https://swift.org)
+
+
diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift
index 66339f4..8d866f0 100644
--- a/Sources/PostgresKit/PostgresDataTranslation.swift
+++ b/Sources/PostgresKit/PostgresDataTranslation.swift
@@ -1,18 +1,10 @@
import Foundation
import PostgresNIO
-
-/// 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.
-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) }
-}
+import SQLKit
extension PostgresCell {
fileprivate var codingKey: any CodingKey {
- PostgresKit.SomeCodingKey(stringValue: !self.columnName.isEmpty ? "\(self.columnName) (\(self.columnIndex))" : "\(self.columnIndex)")
+ SomeCodingKey(stringValue: !self.columnName.isEmpty ? "\(self.columnName) (\(self.columnIndex))" : "\(self.columnIndex)")
}
}
@@ -196,8 +188,12 @@ struct PostgresDataTranslation {
file: String = #fileID,
line: Int = #line
) throws {
- /// Preferred modern fast-path: Direct conformance to `PostgresEncodable`
- if let fastPathValue = value as? any PostgresEncodable {
+ /// Nil bypass-path: Skip the entire machinery for nil optionals.
+ if (value as Optional) == nil {
+ bindings.appendNull()
+ }
+ /// Preferred modern fast-path: Direct conformance to the `PostgresEncodable` family.
+ else if let fastPathValue = value as? any PostgresThrowingDynamicTypeEncodable {
try bindings.append(fastPathValue, context: context)
}
/// Legacy "fast"-path: Direct conformance to `PostgresDataConvertible`; use is deprecated.
@@ -240,8 +236,8 @@ struct PostgresDataTranslation {
case .invalid: throw ArrayAwareBoxWrappingPostgresEncoder.FallbackSentinel()
case .scalar(let scalar): return scalar
case .indexed(let ref):
- let elementType = ref.contents.first?.type ?? .jsonb
- assert(ref.contents.allSatisfy { $0.type == elementType }, "Type \(T.self)/\(type(of: value)) was encoded as a heterogenous array; this is unsupported.")
+ let elementType = (ref.contents.first)??.type ?? .jsonb
+ assert(ref.contents.allSatisfy { $0.map { $0.type == elementType } ?? true }, "Type \(T.self)/\(type(of: value)) was encoded as a heterogenous array; this is unsupported.")
return PostgresData(array: ref.contents, elementType: elementType)
}
} catch is ArrayAwareBoxWrappingPostgresEncoder.FallbackSentinel {
@@ -343,7 +339,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder
final class ArrayRef { var contents: [T] = [] }
case invalid
- case indexed(ArrayRef)
+ case indexed(ArrayRef)
case scalar(PostgresData)
var isValid: Bool { if case .invalid = self { return false }; return true }
@@ -366,7 +362,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder
else { preconditionFailure("Internal error in encoder (requested indexed count from non-indexed state)") }
}
- mutating func store(indexedScalar: PostgresData) {
+ mutating func store(indexedScalar: PostgresData?) {
if case .indexed(let ref) = self { ref.contents.append(indexedScalar) }
else { preconditionFailure("Internal error in encoder (attempted store to indexed in non-indexed state)") }
}
@@ -406,7 +402,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder
let encoder: ArrayAwareBoxWrappingPostgresEncoder
var codingPath: [any CodingKey] { self.encoder.codingPath }
var count: Int { self.encoder.value.indexedCount }
- mutating func encodeNil() throws { self.encoder.value.store(indexedScalar: .null) }
+ mutating func encodeNil() throws { self.encoder.value.store(indexedScalar: nil) }
mutating func encode(_ value: T) throws {
self.encoder.value.store(indexedScalar: try PostgresDataTranslation.encode(
codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo,
diff --git a/Tests/PostgresKitTests/PostgresKitTests.swift b/Tests/PostgresKitTests/PostgresKitTests.swift
index e5ea327..13ae668 100644
--- a/Tests/PostgresKitTests/PostgresKitTests.swift
+++ b/Tests/PostgresKitTests/PostgresKitTests.swift
@@ -226,13 +226,10 @@ struct PostgresKitTests {
#expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default) == url)
}
- /// This test is painful to write before Swift 6.1 due to #expect(throws:) not returning the thrown error.
- ///
/// This test cares that:
///
- /// 1. The Swift type (i.e. `Foo`) is metnioned in the error's debug description.
+ /// 1. The Swift type (i.e. `Foo`) is mentioned in the error's debug description.
/// 2. The underlying error is included.
- #if swift(>=6.1)
@Test
func errorHandlingWhenDecodingNestedDictionary() throws {
struct Foo: Codable {
@@ -250,7 +247,47 @@ struct PostgresKitTests {
let underContext = try #require({ if case .dataCorrupted(let context2) = context.underlyingError as? DecodingError { context2 } else { nil } }())
#expect(underContext.debugDescription == "Dictionary containers must be JSON-encoded")
}
- #endif
+
+ @Test
+ func encodingArraysContainingNilValues() async throws {
+ let encoded1 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: [-1, nil, nil, nil] as [Int?], in: .default, file: #fileID, line: #line)
+ #expect(encoded1.type == .int8Array)
+ #expect(encoded1.array?.count == 4)
+ #expect(encoded1.array?.dropFirst(0).first?.type == .int8)
+ #expect(encoded1.array?.dropFirst(0).first?.int == -1)
+ #expect(encoded1.array?.dropFirst(1).first?.type == .int8)
+ #expect(encoded1.array?.dropFirst(1).first?.value == nil)
+ #expect(encoded1.array?.dropFirst(2).first?.type == .int8)
+ #expect(encoded1.array?.dropFirst(2).first?.value == nil)
+ #expect(encoded1.array?.dropFirst(3).first?.type == .int8)
+ #expect(encoded1.array?.dropFirst(3).first?.value == nil)
+ let encoded2 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: [nil, nil, nil, nil] as [Int?], in: .default, file: #fileID, line: #line)
+ #expect(encoded2.type == .int8Array)
+ #expect(encoded2.array?.count == 4)
+ #expect(encoded2.array?.dropFirst(0).first?.type == .int8)
+ #expect(encoded2.array?.dropFirst(0).first?.value == nil)
+ #expect(encoded2.array?.dropFirst(1).first?.type == .int8)
+ #expect(encoded2.array?.dropFirst(1).first?.value == nil)
+ #expect(encoded2.array?.dropFirst(2).first?.type == .int8)
+ #expect(encoded2.array?.dropFirst(2).first?.value == nil)
+ #expect(encoded2.array?.dropFirst(3).first?.type == .int8)
+ #expect(encoded2.array?.dropFirst(3).first?.value == nil)
+
+ 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([-1, nil, nil, nil] as [Int?])).values(SQLBind([nil, nil, nil, nil] as [Int?])).run()
+ let rows = try await db.select().column("bar").from("foo").all(decodingColumn: "bar", as: [Int?].self)
+ #expect(rows.dropFirst(0).first == [-1, nil, nil, nil])
+ #expect(rows.dropFirst(1).first == [nil, nil, nil, nil])
+ }
+ }
+ try await connection.close()
+ }
var eventLoop: any EventLoop {
MultiThreadedEventLoopGroup.singleton.any()
diff --git a/Tests/PostgresKitTests/Utilities.swift b/Tests/PostgresKitTests/Utilities.swift
index ec2ad58..bc38b9f 100644
--- a/Tests/PostgresKitTests/Utilities.swift
+++ b/Tests/PostgresKitTests/Utilities.swift
@@ -41,8 +41,8 @@ struct QuickLogHandler: LogHandler {
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)")
+ func log(event: LogEvent) {
+ print("\(self.timestamp()) \(event.level) \(self.label):\(self.prettify(event.metadata ?? [:]).map { " \($0)" } ?? "") [\(event.source)] \(event.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: " ")