Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:6.0
// swift-tools-version:6.1
import PackageDescription

let package = Package(
Expand All @@ -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: [
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<p align="center">
<div align="center">
<img src="https://design.vapor.codes/images/vapor-postgreskit.svg" height="96" alt="PostgresKit">
<br>
<br>
<a href="https://docs.vapor.codes/4.0/"><img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation"></a>
<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a>
<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a>
<a href="https://github.com/vapor/postgres-kit/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/postgres-kit/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration"></a>
<a href="https://codecov.io/github/vapor/postgres-kit"><img src="https://img.shields.io/codecov/c/github/vapor/postgres-kit?style=plastic&logo=codecov&label=codecov" alt="Code Coverage"></a>
<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift60up.svg" alt="Swift 6.0+"></a>
</p>

[![Documentation](https://design.vapor.codes/images/readthedocs.svg)](https://docs.vapor.codes/4.0/)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can point to the API docs if that's better

[![Team Chat](https://design.vapor.codes/images/discordchat.svg)](https://discord.gg/vapor)
[![MIT License](https://design.vapor.codes/images/mitlicense.svg)](./LICENSE)
[![Continuous Integration](https://img.shields.io/github/actions/workflow/status/vapor/postgres-kit/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=ccc)](https://github.com/vapor/postgres-kit/actions/workflows/test.yml)
[![Code Coverage](https://img.shields.io/codecov/c/github/vapor/postgres-kit?style=plastic&logo=codecov&label=codecov)](https://codecov.io/github/vapor/postgres-kit)
[![Swift 6.1+](https://design.vapor.codes/images/swift61up.svg)](https://swift.org)

</div>

<br>

Expand Down
30 changes: 13 additions & 17 deletions Sources/PostgresKit/PostgresDataTranslation.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}

Expand Down Expand Up @@ -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<Any>) == 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.
Expand Down Expand Up @@ -240,8 +236,8 @@ struct PostgresDataTranslation {
case .invalid: throw ArrayAwareBoxWrappingPostgresEncoder<E>.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<E>.FallbackSentinel {
Expand Down Expand Up @@ -343,7 +339,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder<E: PostgresJSONEncoder>
final class ArrayRef<T> { var contents: [T] = [] }

case invalid
case indexed(ArrayRef<PostgresData>)
case indexed(ArrayRef<PostgresData?>)
case scalar(PostgresData)

var isValid: Bool { if case .invalid = self { return false }; return true }
Expand All @@ -366,7 +362,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder<E: PostgresJSONEncoder>
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)") }
}
Expand Down Expand Up @@ -406,7 +402,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder<E: PostgresJSONEncoder>
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<T: Encodable>(_ value: T) throws {
self.encoder.value.store(indexedScalar: try PostgresDataTranslation.encode(
codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo,
Expand Down
47 changes: 42 additions & 5 deletions Tests/PostgresKitTests/PostgresKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions Tests/PostgresKitTests/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: " ")
Expand Down
Loading