diff --git a/.github/contributing.md b/.github/contributing.md
deleted file mode 100644
index 6f9ef50..0000000
--- a/.github/contributing.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Contributing to PostgresKit
-
-👋 Welcome to the Vapor team!
-
-## Docker
-
-This package includes a `docker-compose` file you can use for spinning up test databases with test credentials.
-
-```sh
-$ docker-compose up psql-11
-```
-
-## Testing
-
-Once in Xcode, select the `postgres-kit` scheme and use `CMD+U` to run the tests.
-
-You can also test via the CLI using `swift test`.
-
-If you are fixing a single GitHub issue in particular, you can add a test named `testGH` to ensure
-that your fix is working. This will also help prevent regression.
-
-## SemVer
-
-Vapor follows [SemVer](https://semver.org). This means that any changes to the source code that can cause
-existing code to stop compiling _must_ wait until the next major version to be included.
-
-Code that is only additive and will not break any existing code can be included in the next minor release.
-
-----------
-
-Join us on Discord if you have any questions: [discord.gg/vapor](https://discord.gg/vapor).
-
-— Thanks! 🙌
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4fb24ec..f3306a4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -78,7 +78,7 @@ jobs:
if: ${{ false && !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
container:
- image: swift:5.9-jammy
+ image: swift:5.10-jammy
permissions: { actions: write, contents: read, security-events: write }
timeout-minutes: 60
steps:
@@ -111,10 +111,9 @@ jobs:
- postgres:14
- postgres:12
swift-image:
- - swift:5.7-jammy
- swift:5.8-jammy
- swift:5.9-jammy
- - swiftlang/swift:nightly-5.10-jammy
+ - swift:5.10-jammy
- swiftlang/swift:nightly-main-jammy
include:
- postgres-image: postgres:16
@@ -143,7 +142,7 @@ jobs:
linux-integration:
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
- container: swift:5.9-jammy
+ container: swift:5.10-jammy
services:
psql-a:
image: postgres:16
@@ -194,11 +193,12 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: Install Postgres, setup DB and auth, and wait for server start
run: |
+ brew upgrade || true
export PATH="$(brew --prefix)/opt/postgresql@14/bin:$PATH" PGDATA=/tmp/vapor-postgres-test
- (brew unlink postgresql || true) && brew install "postgresql@15" && brew link --force "postgresql@15"
+ (brew unlink postgresql@14 || 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
+ timeout-minutes: 15
- name: Checkout code
uses: actions/checkout@v4
- name: Run local tests
diff --git a/Package.swift b/Package.swift
index 3133ecb..7e62357 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.7
+// swift-tools-version:5.8
import PackageDescription
let package = Package(
@@ -13,21 +13,38 @@ let package = Package(
.library(name: "PostgresKit", targets: ["PostgresKit"]),
],
dependencies: [
- .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.14.2"),
+ .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.14.0"),
- .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.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")
- ]),
- .testTarget(name: "PostgresKitTests", dependencies: [
- .target(name: "PostgresKit"),
- .product(name: "SQLKitBenchmark", package: "sql-kit"),
- ]),
+ .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
+ ),
]
)
+
+var swiftSettings: [SwiftSetting] { [
+ .enableUpcomingFeature("ExistentialAny"),
+ .enableUpcomingFeature("ConciseMagicFile"),
+ .enableUpcomingFeature("ForwardTrailingClosures"),
+ .enableUpcomingFeature("DisableOutwardActorInference"),
+ .enableUpcomingFeature("StrictConcurrency"),
+ .enableExperimentalFeature("StrictConcurrency=complete"),
+] }
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
deleted file mode 100644
index 271f683..0000000
--- a/Package@swift-5.9.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-// 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 fe0c827..1c5d7db 100644
--- a/README.md
+++ b/README.md
@@ -11,26 +11,19 @@
-
+
🐘 Non-blocking, event-driven Swift client for PostgreSQL.
-### Major Releases
-
-The table below shows a list of PostgresKit major releases alongside their compatible NIO and Swift versions.
-
-|Version|NIO|Swift|SPM|
-|-|-|-|-|
-|2.0|2.0|5.2+|`from: "2.0.0"`|
-|1.0|1.0|4.0+|`from: "1.0.0"`|
+### Usage
Use the SPM string to easily include the dependendency in your `Package.swift` file.
```swift
-.package(url: "https://github.com/vapor/postgres-kit.git", from: ...)
+.package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0")
```
### Supported Platforms
diff --git a/Sources/PostgresKit/ConnectionPool+Postgres.swift b/Sources/PostgresKit/ConnectionPool+Postgres.swift
index 547d811..9d46e03 100644
--- a/Sources/PostgresKit/ConnectionPool+Postgres.swift
+++ b/Sources/PostgresKit/ConnectionPool+Postgres.swift
@@ -15,12 +15,13 @@ extension EventLoopConnectionPool where Source == PostgresConnectionSource {
}
}
-
private struct _EventLoopGroupConnectionPoolPostgresDatabase: PostgresDatabase {
let pool: EventLoopGroupConnectionPool
let logger: Logger
- var eventLoop: any EventLoop { self.pool.eventLoopGroup.any() }
+ var eventLoop: any EventLoop {
+ self.pool.eventLoopGroup.any()
+ }
func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture {
self.pool.withConnection(logger: logger) { $0.send(request, logger: logger) }
@@ -35,7 +36,9 @@ private struct _EventLoopConnectionPoolPostgresDatabase: PostgresDatabase {
let pool: EventLoopConnectionPool
let logger: Logger
- var eventLoop: any EventLoop { self.pool.eventLoop }
+ 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) }
diff --git a/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg b/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg
index cdb1a8e..577997a 100644
--- a/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg
+++ b/Sources/PostgresKit/Docs.docc/images/vapor-postgreskit-logo.svg
@@ -1,22 +1,21 @@
diff --git a/Sources/PostgresKit/Exports.swift b/Sources/PostgresKit/Exports.swift
index 34f0b57..baa0374 100644
--- a/Sources/PostgresKit/Exports.swift
+++ b/Sources/PostgresKit/Exports.swift
@@ -1,15 +1,4 @@
-#if swift(>=5.8)
-
@_documentation(visibility: internal) @_exported import AsyncKit
@_documentation(visibility: internal) @_exported import PostgresNIO
@_documentation(visibility: internal) @_exported import SQLKit
@_documentation(visibility: internal) @_exported import struct Foundation.URL
-
-#else
-
-@_exported import AsyncKit
-@_exported import PostgresNIO
-@_exported import SQLKit
-@_exported import struct Foundation.URL
-
-#endif
diff --git a/Sources/PostgresKit/PostgresConnectionSource.swift b/Sources/PostgresKit/PostgresConnectionSource.swift
index 4bc2a23..6a53cd7 100644
--- a/Sources/PostgresKit/PostgresConnectionSource.swift
+++ b/Sources/PostgresKit/PostgresConnectionSource.swift
@@ -39,4 +39,4 @@ public struct PostgresConnectionSource: ConnectionPoolSource {
}
}
-extension PostgresConnection: ConnectionPoolItem { }
+extension PostgresConnection: ConnectionPoolItem {}
diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift
index 9fbee68..51f5c1c 100644
--- a/Sources/PostgresKit/PostgresDataTranslation.swift
+++ b/Sources/PostgresKit/PostgresDataTranslation.swift
@@ -11,25 +11,34 @@ fileprivate struct SomeCodingKey: CodingKey, Hashable {
}
private extension PostgresCell {
- var codingKey: any CodingKey { SomeCodingKey(stringValue: !self.columnName.isEmpty ? "\(self.columnName) (\(self.columnIndex))" : "\(self.columnIndex)") }
+ 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 {
- public static var psqlType: PostgresDataType { String.psqlType }
- public static var psqlFormat: PostgresFormat { String.psqlFormat }
+extension URL: PostgresNonThrowingEncodable, PostgresDecodable {
+ public static var psqlType: PostgresDataType {
+ String.psqlType
+ }
+
+ public static var psqlFormat: PostgresFormat {
+ String.psqlFormat
+ }
@inlinable
- public func encode(into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext) {
+ 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,
+ from buffer: inout ByteBuffer,
+ type: PostgresDataType,
+ format: PostgresFormat,
context: PostgresDecodingContext
) throws {
let string = try String(from: &buffer, type: type, format: format, context: context)
@@ -57,7 +66,14 @@ struct PostgresDataTranslation {
in context: PostgresDecodingContext,
file: String = #fileID, line: Int = #line
) throws -> T {
- try self.decode(codingPath: [cell.codingKey], userInfo: [:], T.self, from: cell, in: context, file: file, line: line)
+ try self.decode(
+ codingPath: [cell.codingKey],
+ userInfo: [:],
+ T.self,
+ from: cell,
+ in: context,
+ file: file, line: line
+ )
}
fileprivate static func decode(
@@ -70,20 +86,48 @@ struct PostgresDataTranslation {
/// Preferred modern fast-path: Direct conformance to ``PostgresDecodable``, let the cell decode.
if let fastPathType = T.self as? any PostgresDecodable.Type {
let cellToDecode: PostgresCell
- if cell.dataType.isUserDefined && (T.self is String.Type || T.self is String?.Type) { // Workaround cheat for Fluent's enum "support"
- cellToDecode = PostgresCell(bytes: cell.bytes, dataType: .name, format: cell.format, columnName: cell.columnName, columnIndex: cell.columnIndex)
- } else if cell.format == .binary && [.char, .varchar, .text].contains(cell.dataType) && T.self is Decimal.Type { // Workaround cheat for Fluent's assumption that Decimal strings work
- cellToDecode = PostgresCell(bytes: cell.bytes, dataType: .numeric, format: .text, columnName: cell.columnName, columnIndex: cell.columnIndex)
- } else if cell.format == .binary && cell.dataType == .numeric && T.self is Double.Type { // Workaround cheat for Fluent's expectation that Postgres's `numeric/decimal` can be decoded as Double
- // Extremely manual workaround...
+
+ if cell.dataType.isUserDefined && (T.self is String.Type || T.self is String?.Type) {
+ /// Workaround for Fluent's enum "support":
+ ///
+ /// If we're trying to decode a string and the real cell's data type is in the user-defined range,
+ /// assume we're dealing with a Fluent enum and pretend that the cell has a string data type instead.
+ cellToDecode = .init(
+ bytes: cell.bytes,
+ dataType: .name,
+ format: cell.format,
+ columnName: cell.columnName,
+ columnIndex: cell.columnIndex
+ )
+ } else if cell.format == .binary && [.char, .varchar, .text].contains(cell.dataType) && T.self is Decimal.Type {
+ /// Workaround for Fluent's assumption that Decimal strings work:
+ ///
+ /// If the cell's data type is a binary-format string-like, and we're trying to decode a `Decimal`,
+ /// reinterpret the cell as a text-format numeric value so that the `PostgresCodable` conformance of
+ /// `Decimal` will work as written.
+ cellToDecode = .init(
+ bytes: cell.bytes,
+ dataType: .numeric,
+ format: .text,
+ columnName: cell.columnName,
+ columnIndex: cell.columnIndex
+ )
+ } else if cell.format == .binary && cell.dataType == .numeric && T.self is Double.Type {
+ /// Workaround for Fluent's expectation that Postgres's `numeric/decimal` can be decoded as `Double`:
+ ///
+ /// If the cell is a binary-format numeric value and we're trying to decode a `Double`, use
+ /// `PostgresData` to manually interpret the cell as a `PostgresNumeric` and use that result to convert
+ /// to `Double`.
guard let value = PostgresData(type: cell.dataType, formatCode: cell.format, value: cell.bytes).numeric?.double else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Invalid numeric value encoding"))
}
return value as! T
} else {
+ /// No workarounds needed, use the cell as-is.
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.
} else if let legacyPathType = T.self as? any PostgresLegacyDataConvertible.Type {
let legacyData = PostgresData(type: cell.dataType, typeModifier: nil, formatCode: cell.format, value: cell.bytes)
@@ -95,14 +139,24 @@ struct PostgresDataTranslation {
}
return result as! T
}
+
/// Slow path: Descend through the ``Decodable`` machinery until we fail or find something we can convert.
else {
do {
- return try T.init(from: ArrayAwareBoxUwrappingDecoder(codingPath: codingPath, userInfo: userInfo, cell: cell, context: context, file: file, line: line))
+ return try T.init(from: ArrayAwareBoxUwrappingDecoder(
+ codingPath: codingPath,
+ userInfo: userInfo,
+ cell: cell,
+ context: context,
+ file: file, line: line
+ ))
} catch DecodingError.dataCorrupted {
/// Glacial path: Attempt to decode as plain JSON.
guard cell.dataType == .json || cell.dataType == .jsonb else {
- throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Unable to interpret value of PSQL type \(cell.dataType): \(cell.bytes.map { "\($0)" } ?? "null")"))
+ throw DecodingError.dataCorrupted(.init(
+ codingPath: codingPath,
+ debugDescription: "Unable to interpret value of PSQL type \(cell.dataType): \(cell.bytes.map { "\($0)" } ?? "null")"
+ ))
}
if cell.dataType == .jsonb, cell.format == .binary, let buffer = cell.bytes {
// TODO: Un-hardcode this magic knowledge of the JSONB encoding
@@ -113,7 +167,12 @@ struct PostgresDataTranslation {
} catch let error as PostgresDecodingError {
/// We effectively transform PostgresDecodingErrors into plain DecodingErrors here, mostly so the full
/// coding path, which gives us the original type(s) involved, is preserved.
- let context = DecodingError.Context(codingPath: codingPath, debugDescription: "\(String(reflecting: error))", underlyingError: error)
+ let context = DecodingError.Context(
+ codingPath: codingPath,
+ 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)
@@ -183,8 +242,10 @@ struct PostgresDataTranslation {
}
private final class ArrayAwareBoxUwrappingDecoder: Decoder, SingleValueDecodingContainer {
- let codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any]
- let cell: PostgresCell, context: PostgresDecodingContext
+ let codingPath: [any CodingKey]
+ let userInfo: [CodingUserInfoKey: Any]
+ let cell: PostgresCell
+ let context: PostgresDecodingContext
let file: String, line: Int
init(codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], cell: PostgresCell, context: PostgresDecodingContext, file: String, line: Int) {
@@ -197,10 +258,21 @@ private final class ArrayAwareBoxUwrappingDecoder= self.data.count }
+ let data: [PostgresData]
+ let decoder: ArrayAwareBoxUwrappingDecoder
+
+ var codingPath: [any CodingKey] {
+ self.decoder.codingPath
+ }
+
+ var count: Int? {
+ self.data.count
+ }
+
+ var isAtEnd: Bool {
+ self.currentIndex >= self.data.count
+ }
+
var currentIndex = 0
mutating func decodeNil() throws -> Bool {
@@ -217,9 +289,10 @@ private final class ArrayAwareBoxUwrappingDecoder(_: T.Type) throws -> T {
try PostgresDataTranslation.decode(
- codingPath: self.codingPath + [SomeCodingKey(stringValue: "(Unwrapping(\(T0.self)))")], userInfo: self.userInfo,
+ codingPath: self.codingPath + [PostgresKit.SomeCodingKey(stringValue: "(Unwrapping(\(T0.self)))")], userInfo: self.userInfo,
T.self, from: self.cell, in: self.context, file: self.file, line: self.line
)
}
@@ -326,7 +399,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder
mutating func encodeNil() throws { self.encoder.value.store(indexedScalar: .null) }
mutating func encode(_ value: T) throws {
self.encoder.value.store(indexedScalar: try PostgresDataTranslation.encode(
- codingPath: self.codingPath + [SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo,
+ codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo,
value: value, in: self.encoder.context,
file: self.encoder.file, line: self.encoder.line
))
@@ -334,7 +407,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder
mutating func nestedContainer(keyedBy: K.Type) -> KeyedEncodingContainer { self.superEncoder().container(keyedBy: K.self) }
mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { self.superEncoder().unkeyedContainer() }
mutating func superEncoder() -> any Encoder { ArrayAwareBoxWrappingPostgresEncoder(
- codingPath: self.codingPath + [SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo,
+ codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo,
context: self.encoder.context,
file: self.encoder.file, line: self.encoder.line,
value: self.encoder.value
@@ -353,7 +426,7 @@ private final class ArrayAwareBoxWrappingPostgresEncoder
/// This is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing.
struct FailureEncoder: Encoder, KeyedEncodingContainerProtocol, UnkeyedEncodingContainer, SingleValueEncodingContainer {
let codingPath = [any CodingKey](), userInfo = [CodingUserInfoKey: Any](), count = 0
- init() {}; init() where K == SomeCodingKey {}
+ init() {}; init() where K == PostgresKit.SomeCodingKey {}
func encodeNil() throws { throw FallbackSentinel() }
func encodeNil(forKey: K) throws { throw FallbackSentinel() }
func encode(_: T) throws { throw FallbackSentinel() }
diff --git a/Sources/PostgresKit/PostgresDatabase+SQL.swift b/Sources/PostgresKit/PostgresDatabase+SQL.swift
index 85a5516..0d2c070 100644
--- a/Sources/PostgresKit/PostgresDatabase+SQL.swift
+++ b/Sources/PostgresKit/PostgresDatabase+SQL.swift
@@ -1,6 +1,6 @@
import PostgresNIO
import Logging
-@preconcurrency import SQLKit
+import SQLKit
// https://github.com/vapor/postgres-nio/pull/450
#if compiler(>=5.10) && $RetroactiveAttribute
@@ -39,10 +39,21 @@ private struct _PostgresSQLDatabase ()) -> EventLoopFuture {
let (sql, binds) = self.serialize(query)
@@ -65,6 +76,31 @@ extension _PostgresSQLDatabase: SQLDatabase, PostgresDatabase {
} }.map { _ in }
}
+ func execute(
+ sql query: any SQLExpression,
+ _ onRow: @escaping @Sendable (any SQLRow) -> ()
+ ) async throws {
+ let (sql, binds) = self.serialize(query)
+
+ if let queryLogLevel {
+ self.logger.log(level: queryLogLevel, "\(sql) [\(binds)]")
+ }
+
+ var bindings = PostgresBindings(capacity: binds.count)
+ for bind in binds {
+ try PostgresDataTranslation.encode(value: bind, in: self.encodingContext, to: &bindings)
+ }
+
+ _ = try await self.database.withConnection {
+ $0.query(
+ .init(unsafeSQL: sql, binds: bindings),
+ logger: $0.logger,
+ { onRow($0.sql(decodingContext: self.decodingContext)) }
+ )
+ }.get()
+ }
+
+
func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture {
self.database.send(request, logger: logger)
}
@@ -72,4 +108,16 @@ extension _PostgresSQLDatabase: SQLDatabase, PostgresDatabase {
func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture {
self.database.withConnection(closure)
}
+
+ func withSession(_ closure: @escaping (any SQLDatabase) async throws -> R) async throws -> R {
+ try await self.withConnection { c in
+ c.eventLoop.makeFutureWithTask {
+ try await closure(c.sql(
+ encodingContext: self.encodingContext,
+ decodingContext: self.decodingContext,
+ queryLogLevel: self.queryLogLevel
+ ))
+ }
+ }.get()
+ }
}
diff --git a/Sources/PostgresKit/PostgresDialect.swift b/Sources/PostgresKit/PostgresDialect.swift
index c07eaa4..47c0842 100644
--- a/Sources/PostgresKit/PostgresDialect.swift
+++ b/Sources/PostgresKit/PostgresDialect.swift
@@ -3,31 +3,57 @@ import SQLKit
public struct PostgresDialect: SQLDialect {
public init() {}
- public var name: String { "postgresql" }
+ public var name: String {
+ "postgresql"
+ }
- public var identifierQuote: any SQLExpression { SQLRaw(#"""#) }
+ public var identifierQuote: any SQLExpression {
+ SQLRaw(#"""#)
+ }
- public var literalStringQuote: any SQLExpression { SQLRaw("'") }
+ public var literalStringQuote: any SQLExpression {
+ SQLRaw("'")
+ }
- public var supportsAutoIncrement: Bool { true }
+ public var supportsAutoIncrement: Bool {
+ true
+ }
- public var autoIncrementClause: any SQLExpression { SQLRaw("GENERATED BY DEFAULT AS IDENTITY") }
+ public var autoIncrementClause: any SQLExpression {
+ SQLRaw("GENERATED BY DEFAULT AS IDENTITY")
+ }
- public var autoIncrementFunction: (any SQLExpression)? { nil }
+ public var autoIncrementFunction: (any SQLExpression)? {
+ nil
+ }
- public func bindPlaceholder(at position: Int) -> any SQLExpression { SQLRaw("$\(position)") }
+ public func bindPlaceholder(at position: Int) -> any SQLExpression {
+ SQLRaw("$\(position)")
+ }
- public func literalBoolean(_ value: Bool) -> any SQLExpression { SQLRaw("\(value)") }
+ public func literalBoolean(_ value: Bool) -> any SQLExpression {
+ SQLRaw("\(value)")
+ }
- public var literalDefault: any SQLExpression { SQLRaw("DEFAULT") }
+ public var literalDefault: any SQLExpression {
+ SQLRaw("DEFAULT")
+ }
- public var supportsIfExists: Bool { true }
+ public var supportsIfExists: Bool {
+ true
+ }
- public var enumSyntax: SQLEnumSyntax { .typeName }
+ public var enumSyntax: SQLEnumSyntax {
+ .typeName
+ }
- public var supportsDropBehavior: Bool { true }
+ public var supportsDropBehavior: Bool {
+ true
+ }
- public var supportsReturning: Bool { true }
+ public var supportsReturning: Bool {
+ true
+ }
public var triggerSyntax: SQLTriggerSyntax {
.init(
@@ -43,17 +69,35 @@ public struct PostgresDialect: SQLDialect {
)
}
- public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? { nil }
+ public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? {
+ if case let .custom(expr) = dataType, (expr as? SQLRaw)?.sql == "TIMESTAMP" {
+ return SQLRaw("TIMESTAMPTZ")
+ } else {
+ return nil
+ }
+ }
- public var upsertSyntax: SQLUpsertSyntax { .standard }
+ public var upsertSyntax: SQLUpsertSyntax {
+ .standard
+ }
public var unionFeatures: SQLUnionFeatures {
- [.union, .unionAll, .intersect, .intersectAll, .except, .exceptAll, .explicitDistinct, .parenthesizedSubqueries]
+ [
+ .union, .unionAll,
+ .intersect, .intersectAll,
+ .except, .exceptAll,
+ .explicitDistinct,
+ .parenthesizedSubqueries
+ ]
}
- public var sharedSelectLockExpression: (any SQLExpression)? { SQLRaw("FOR SHARE") }
+ public var sharedSelectLockExpression: (any SQLExpression)? {
+ SQLRaw("FOR SHARE")
+ }
- public var exclusiveSelectLockExpression: (any SQLExpression)? { SQLRaw("FOR UPDATE") }
+ public var exclusiveSelectLockExpression: (any SQLExpression)? {
+ SQLRaw("FOR UPDATE")
+ }
public func nestedSubpathExpression(in column: any SQLExpression, for path: [String]) -> (any SQLExpression)? {
guard !path.isEmpty else { return nil }
diff --git a/Tests/PostgresKitTests/PostgresKitTests.swift b/Tests/PostgresKitTests/PostgresKitTests.swift
index e7ac17f..c927a2d 100644
--- a/Tests/PostgresKitTests/PostgresKitTests.swift
+++ b/Tests/PostgresKitTests/PostgresKitTests.swift
@@ -9,9 +9,13 @@ import Foundation
final class PostgresKitTests: XCTestCase {
func testSQLKitBenchmark() throws {
let conn = try PostgresConnection.test(on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
+ defer { try? conn.close().wait() }
let benchmark = SQLBenchmarker(on: conn.sql())
- try benchmark.run()
+ do {
+ try benchmark.run()
+ } catch {
+ XCTFail("Caught error: \(String(reflecting: error))")
+ }
}
func testPerformance() throws {
@@ -56,37 +60,41 @@ final class PostgresKitTests: XCTestCase {
let db = conn.sql()
- 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()
- }
-
- for i in 0..<5_000 {
- let zipcode = Foo(
- id: UUID().uuidString,
- description: "test \(i)",
- latitude: Double.random(in: 0...100),
- longitude: Double.random(in: 0...100),
- created_by: "test",
- created_at: Date(),
- modified_by: "test",
- modified_at: Date()
+ 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
)
- try db.insert(into: "foos")
- .model(zipcode)
- .run().wait()
+ """).run().wait()
+ defer {
+ try? db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run().wait()
+ }
+
+ for i in 0..<5_000 {
+ let zipcode = Foo(
+ id: UUID().uuidString,
+ description: "test \(i)",
+ latitude: Double.random(in: 0...100),
+ longitude: Double.random(in: 0...100),
+ created_by: "test",
+ created_at: Date(),
+ modified_by: "test",
+ modified_at: Date()
+ )
+ try db.insert(into: "foos")
+ .model(zipcode)
+ .run().wait()
+ }
+ } catch {
+ XCTFail("Caught error: \(String(reflecting: error))")
}
}
@@ -149,12 +157,6 @@ final class PostgresKitTests: XCTestCase {
XCTAssertEqual(rows.first?.first?.dataType, Bar.psqlArrayType)
XCTAssertEqual(try rows.first?.first?.decode([Bar].self), [Bar]())
}
-
- func testEnum() throws {
- let connection = try PostgresConnection.test(on: self.eventLoop).wait()
- defer { try! connection.close().wait() }
- try SQLBenchmarker(on: connection.sql()).testEnum()
- }
/// 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
@@ -237,10 +239,11 @@ final class PostgresKitTests: XCTestCase {
XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default), url)
}
- var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() }
+ var eventLoop: any EventLoop {
+ MultiThreadedEventLoopGroup.singleton.any()
+ }
- override func setUpWithError() throws {
- try super.setUpWithError()
+ override class func setUp() {
XCTAssertTrue(isLoggingConfigured)
}
}