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 @@ MIT License Continuous Integration -Swift 5.7+ +Swift 5.8+


🐘 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) } }