Skip to content

Some minor cleanup #224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 13, 2025
Merged
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
7 changes: 7 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[*.swift]
indent_style = space
indent_size = 4
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true

26 changes: 13 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ jobs:
api-breakage:
if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
container: swift:jammy
container: swift:noble
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -42,18 +42,18 @@ jobs:
fail-fast: false
matrix:
include:
- postgres-image-a: 'postgres:13'
postgres-image-b: 'postgres:14'
- postgres-image-a: 'postgres:12'
postgres-image-b: 'postgres:13'
postgres-auth: 'trust'
swift-image: 'swift:5.9-focal'
- postgres-image-a: 'postgres:15'
postgres-image-b: 'postgres:16'
postgres-auth: 'md5'
swift-image: 'swift:5.10-jammy'
- postgres-image-a: 'postgres:15'
postgres-image-b: 'postgres:16'
- postgres-image-a: 'postgres:14'
postgres-image-b: 'postgres:15'
postgres-auth: 'md5'
swift-image: 'swift:6.0-noble'
- postgres-image-a: 'postgres:16'
postgres-image-b: 'postgres:17'
postgres-auth: 'scram-sha-256'
swift-image: 'swift:6.0-jammy'
swift-image: 'swift:6.1-noble'
container: ${{ matrix.swift-image }}
runs-on: ubuntu-latest
services:
@@ -89,8 +89,8 @@ jobs:
fail-fast: false
matrix:
include:
- macos-version: macos-14
xcode-version: latest
- macos-version: macos-15
xcode-version: latest-stable
runs-on: ${{ matrix.macos-version }}
env:
LOG_LEVEL: debug
@@ -106,7 +106,7 @@ jobs:
run: |
brew upgrade || true
export PATH="$(brew --prefix)/opt/postgresql@16/bin:$PATH" PGDATA=/tmp/vapor-postgres-test PGUSER="${POSTGRES_USER_A}"
(brew unlink postgresql@14 || true) && brew install postgresql@16 && brew link --force postgresql@16
brew install postgresql@17 && brew link --force postgresql@17
initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER_A}" --pwfile=<(echo "${POSTGRES_PASSWORD_A}")
pg_ctl start --wait
PGPASSWORD="${POSTGRES_PASSWORD_A}" createdb -w -O "${POSTGRES_USER_A}" "${POSTGRES_DB_A}"
77 changes: 77 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"indentConditionalCompilationBlocks" : false,
"indentSwitchCaseLabels" : false,
"indentation" : {
"spaces" : 4
},
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : false,
"lineBreakBeforeEachGenericRequirement" : false,
"lineBreakBetweenDeclarationAttributes" : false,
"lineLength" : 150,
"maximumBlankLines" : 1,
"multiElementCollectionTrailingCommas" : true,
"noAssignmentInExpressions" : {
"allowedFunctions" : [
]
},
"prioritizeKeepingFunctionOutputTogether" : false,
"reflowMultilineStringLiterals" : {
"never" : {
}
},
"respectsExistingLineBreaks" : true,
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLiteralForEmptyCollectionInit" : true,
"AlwaysUseLowerCamelCase" : true,
"AmbiguousTrailingClosureOverload" : true,
"AvoidRetroactiveConformances" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoAssignmentInExpressions" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyLinesOpeningClosingBraces" : false,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoPlaygroundLiterals" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OmitExplicitReturns" : false,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : true,
"ReplaceForEachWithForLoop" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"TypeNamesShouldBeCapitalized" : true,
"UseEarlyExits" : true,
"UseExplicitNilCheckInConditions" : true,
"UseLetInEveryBoundCaseVariable" : true,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : true,
"UseSynthesizedInitializer" : true,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
},
"spacesAroundRangeFormationOperators" : true,
"spacesBeforeEndOfLineComments" : 1,
"tabWidth" : 4,
"version" : 1
}
4 changes: 1 addition & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:5.10
import PackageDescription

let package = Package(
@@ -13,15 +13,13 @@ let package = Package(
.library(name: "FluentPostgresDriver", targets: ["FluentPostgresDriver"]),
],
dependencies: [
.package(url: "https://github.com/vapor/async-kit.git", from: "1.20.0"),
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.49.0"),
.package(url: "https://github.com/vapor/postgres-kit.git", from: "2.13.4"),
],
targets: [
.target(
name: "FluentPostgresDriver",
dependencies: [
.product(name: "AsyncKit", package: "async-kit"),
.product(name: "FluentKit", package: "fluent-kit"),
.product(name: "FluentSQL", package: "fluent-kit"),
.product(name: "PostgresKit", package: "postgres-kit"),
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/vapor/fluent-postgres-driver/assets/1130717/c2350b70-aaf1-43e1-ab79-86fc88ba8da4">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/vapor/fluent-postgres-driver/assets/1130717/dfc94dc2-281b-4e54-be86-549813496373">
<img src="https://github.com/vapor/fluent-postgres-driver/assets/1130717/dfc94dc2-281b-4e54-be86-549813496373" height="96" alt="FluentPostgresDriver">
</picture>
<img src="https://design.vapor.codes/images/vapor-fluentpostgresdriver.svg" height="96" alt="FluentPostgresDriver">
<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/fluent-postgres-driver/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/fluent-postgres-driver/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration"></a>
<a href="https://codecov.io/github/vapor/fluent-postgres-driver"><img src="https://img.shields.io/codecov/c/github/vapor/fluent-postgres-driver?style=plastic&logo=codecov&label=codecov"></a>
<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift58up.svg" alt="Swift 5.8+"></a>
<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift510up.svg" alt="Swift 5.10+"></a>
</p>

<br>
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Logging
import FluentKit
import AsyncKit
import NIOCore
import NIOSSL
import Foundation
import Logging
import NIOCore
import PostgresKit
import PostgresNIO

Original file line number Diff line number Diff line change
@@ -8,11 +8,14 @@
"fluentpsqldriver": "#336791",
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentpsqldriver) 30%, #000 100%)",
"documentation-intro-accent": "var(--color-fluentpsqldriver)",
"documentation-intro-eyebrow": "white",
"documentation-intro-figure": "white",
"documentation-intro-title": "white",
"logo-base": { "dark": "#fff", "light": "#000" },
"logo-shape": { "dark": "#000", "light": "#fff" },
"fill": { "dark": "#000", "light": "#fff" }
},
"icons": { "technology": "/fluentpostgresdriver/images/vapor-fluentpostgresdriver-logo.svg" }
"icons": { "technology": "/fluentpostgresdriver/images/FluentPostgresDriver/vapor-fluentpostgresdriver-logo.svg" }
},
"features": {
"quickNavigation": { "enable": true },
28 changes: 13 additions & 15 deletions Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Logging
import FluentKit
import AsyncKit
import NIOCore
import NIOSSL
import FluentKit
import Foundation
import Logging
import NIOCore
import PostgresKit
import PostgresNIO

@@ -31,7 +30,8 @@ extension DatabaseConfigurationFactory {
configuration: try .init(url: urlString),
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
encodingContext: encodingContext, decodingContext: decodingContext,
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
)
}
@@ -59,7 +59,8 @@ extension DatabaseConfigurationFactory {
configuration: try .init(url: url),
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
encodingContext: encodingContext, decodingContext: decodingContext,
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
)
}
@@ -81,22 +82,19 @@ extension DatabaseConfigurationFactory {
decodingContext: PostgresDecodingContext<some PostgresJSONDecoder>,
sqlLogLevel: Logger.Level = .debug
) -> DatabaseConfigurationFactory {
let configuration = FakeSendable(wrappedValue: configuration)

return .init {
.init {
FluentPostgresConfiguration(
configuration: configuration,
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
encodingContext: encodingContext, decodingContext: decodingContext,
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
)
}
}
}

fileprivate struct FakeSendable<T>: @unchecked Sendable { let wrappedValue: T }

/// We'd like to just default the context parameters of the "actual" method. Unfortunately, there are a few
/// cases involving the UNIX domain socket initalizer where usage can resolve to either the new
/// `SQLPostgresConfiguration`-based method or the deprecated `PostgresConfiguration`-based method, with no
@@ -170,22 +168,22 @@ extension DatabaseConfigurationFactory {
/// The actual concrete configuration type produced by a configuration factory.
struct FluentPostgresConfiguration<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseConfiguration {
var middleware: [any AnyModelMiddleware] = []
fileprivate let configuration: FakeSendable<SQLPostgresConfiguration>
fileprivate let configuration: SQLPostgresConfiguration
let maxConnectionsPerEventLoop: Int
let connectionPoolTimeout: TimeAmount
let encodingContext: PostgresEncodingContext<E>
let decodingContext: PostgresDecodingContext<D>
let sqlLogLevel: Logger.Level

func makeDriver(for databases: Databases) -> any DatabaseDriver {
let connectionSource = PostgresConnectionSource(sqlConfiguration: self.configuration.wrappedValue)
let connectionSource = PostgresConnectionSource(sqlConfiguration: self.configuration)
let elgPool = EventLoopGroupConnectionPool(
source: connectionSource,
maxConnectionsPerEventLoop: self.maxConnectionsPerEventLoop,
requestTimeout: self.connectionPoolTimeout,
on: databases.eventLoopGroup
)

return _FluentPostgresDriver(
pool: elgPool,
encodingContext: self.encodingContext,
69 changes: 41 additions & 28 deletions Sources/FluentPostgresDriver/FluentPostgresDatabase.swift
Original file line number Diff line number Diff line change
@@ -16,23 +16,24 @@ struct _FluentPostgresDatabase<E: PostgresJSONEncoder, D: PostgresJSONDecoder> {
extension _FluentPostgresDatabase: Database {
func execute(
query: DatabaseQuery,
onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
onOutput: @escaping @Sendable (any DatabaseOutput) -> Void
) -> EventLoopFuture<Void> {
var expression = SQLQueryConverter(delegate: PostgresConverterDelegate()).convert(query)

/// For `.create` query actions, we want to return the generated IDs, unless the `customIDKey` is the
/// empty string, which we use as a very hacky signal for "we don't implement this for composite IDs yet".
if case .create = query.action, query.customIDKey != .some(.string("")) {
expression = SQLKit.SQLList([expression, SQLReturning(.init((query.customIDKey ?? .id).description))], separator: SQLRaw(" "))
}

return self.execute(sql: expression, { onOutput($0.databaseOutput()) })
}

func execute(schema: DatabaseSchema) -> EventLoopFuture<Void> {
let expression = SQLSchemaConverter(delegate: PostgresConverterDelegate()).convert(schema)

return self.execute(sql: expression,
return self.execute(
sql: expression,
// N.B.: Don't fatalError() here; what're users supposed to do about it?
{ self.logger.debug("Unexpected row returned from schema query: \($0)") }
)
@@ -50,9 +51,11 @@ extension _FluentPostgresDatabase: Database {
return self.eventLoop.makeSucceededFuture(())
}

return self.eventLoop.flatten(e.createCases.map { create in
self.alter(enum: e.name).add(value: create).run()
})
return self.eventLoop.flatten(
e.createCases.map { create in
self.alter(enum: e.name).add(value: create).run()
}
)
case .delete:
return self.drop(enum: e.name).run()
}
@@ -64,10 +67,12 @@ extension _FluentPostgresDatabase: Database {
}
return self.withConnection { conn in
guard let sqlConn = conn as? any SQLDatabase else {
fatalError("""
fatalError(
"""
Connection yielded by a Fluent+Postgres database is not also an SQLDatabase.
This is a bug in Fluent; please report it at https://github.com/vapor/fluent-postgres-driver/issues
""")
"""
)
}
return sqlConn.raw("BEGIN").run().flatMap {
closure(conn).flatMap { result in
@@ -78,16 +83,22 @@ extension _FluentPostgresDatabase: Database {
}
}
}

func withConnection<T>(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> {
self.withConnection { (underlying: any PostgresDatabase) in
closure(_FluentPostgresDatabase(
database: underlying.sql(encodingContext: self.encodingContext, decodingContext: self.decodingContext, queryLogLevel: self.database.queryLogLevel),
context: self.context,
encodingContext: self.encodingContext,
decodingContext: self.decodingContext,
inTransaction: true
))
closure(
_FluentPostgresDatabase(
database: underlying.sql(
encodingContext: self.encodingContext,
decodingContext: self.decodingContext,
queryLogLevel: self.database.queryLogLevel
),
context: self.context,
encodingContext: self.encodingContext,
decodingContext: self.decodingContext,
inTransaction: true
)
)
}
}
}
@@ -96,11 +107,11 @@ extension _FluentPostgresDatabase: TransactionControlDatabase {
func beginTransaction() -> EventLoopFuture<Void> {
self.raw("BEGIN").run()
}

func commitTransaction() -> EventLoopFuture<Void> {
self.raw("COMMIT").run()
}

func rollbackTransaction() -> EventLoopFuture<Void> {
self.raw("ROLLBACK").run()
}
@@ -110,15 +121,15 @@ extension _FluentPostgresDatabase: SQLDatabase {
var version: (any SQLDatabaseReportedVersion)? { self.database.version }
var dialect: any SQLDialect { self.database.dialect }
var queryLogLevel: Logger.Level? { self.database.queryLogLevel }
func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture<Void> {

func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) -> EventLoopFuture<Void> {
self.database.execute(sql: query, onRow)
}
func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) async throws {

func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) async throws {
try await self.database.execute(sql: query, onRow)
}

func withSession<R>(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R {
try await self.database.withSession(closure)
}
@@ -128,15 +139,17 @@ extension _FluentPostgresDatabase: PostgresDatabase {
func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture<Void> {
self.withConnection { $0.send(request, logger: logger) }
}

func withConnection<T>(_ closure: @escaping (PostgresConnection) -> EventLoopFuture<T>) -> EventLoopFuture<T> {
guard let psqlDb: any PostgresDatabase = self.database as? any PostgresDatabase else {
fatalError("""
fatalError(
"""
Connection yielded by a Fluent+Postgres database is not also a PostgresDatabase.
This is a bug in Fluent; please report it at https://github.com/vapor/fluent-postgres-driver/issues
""")
"""
)
}

return psqlDb.withConnection(closure)
}
}
10 changes: 5 additions & 5 deletions Sources/FluentPostgresDriver/FluentPostgresDriver.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import AsyncKit
import NIOCore
import Logging
import FluentKit
import Logging
import NIOCore
import PostgresKit

/// Marked `@unchecked Sendable` to silence warning about `PostgresConnectionSource`
@@ -10,7 +10,7 @@ struct _FluentPostgresDriver<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: Da
let encodingContext: PostgresEncodingContext<E>
let decodingContext: PostgresDecodingContext<D>
let sqlLogLevel: Logger.Level

func makeDatabase(with context: DatabaseContext) -> any Database {
_FluentPostgresDatabase(
database: self.pool
@@ -23,11 +23,11 @@ struct _FluentPostgresDriver<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: Da
inTransaction: false
)
}

func shutdown() {
try? self.pool.syncShutdownGracefully()
}

func shutdownAsync() async {
try? await self.pool.shutdownAsync()
}
38 changes: 19 additions & 19 deletions Sources/FluentPostgresDriver/PostgresConverterDelegate.swift
Original file line number Diff line number Diff line change
@@ -6,50 +6,50 @@ struct PostgresConverterDelegate: SQLConverterDelegate {
func customDataType(_ dataType: DatabaseSchema.DataType) -> (any SQLExpression)? {
switch dataType {
case .uuid:
return SQLRaw("UUID")
SQLRaw("UUID")
case .bool:
return SQLRaw("BOOL")
SQLRaw("BOOL")
case .data:
return SQLRaw("BYTEA")
SQLRaw("BYTEA")
case .date:
return SQLRaw("DATE")
SQLRaw("DATE")
case .datetime:
return SQLRaw("TIMESTAMPTZ")
SQLRaw("TIMESTAMPTZ")
case .double:
return SQLRaw("DOUBLE PRECISION")
SQLRaw("DOUBLE PRECISION")
case .dictionary:
return SQLRaw("JSONB")
SQLRaw("JSONB")
case .array(of: let type):
if let type = type, let dataType = self.customDataType(type) {
return SQLArrayDataType(dataType: dataType)
SQLArrayDataType(dataType: dataType)
} else {
return SQLRaw("JSONB")
SQLRaw("JSONB")
}
case .enum(let value):
return SQLIdentifier(value.name)
SQLIdentifier(value.name)
case .int8, .uint8:
return SQLIdentifier("char")
SQLIdentifier("char")
case .int16, .uint16:
return SQLRaw("SMALLINT")
SQLRaw("SMALLINT")
case .int32, .uint32:
return SQLRaw("INT")
SQLRaw("INT")
case .int64, .uint64:
return SQLRaw("BIGINT")
SQLRaw("BIGINT")
case .string:
return SQLRaw("TEXT")
SQLRaw("TEXT")
case .time:
return SQLRaw("TIME")
SQLRaw("TIME")
case .float:
return SQLRaw("FLOAT")
SQLRaw("FLOAT")
case .custom:
return nil
nil
}
}
}

private struct SQLArrayDataType: SQLExpression {
let dataType: any SQLExpression

func serialize(to serializer: inout SQLSerializer) {
self.dataType.serialize(to: &serializer)
serializer.write("[]")
138 changes: 69 additions & 69 deletions Sources/FluentPostgresDriver/PostgresError+Database.swift
Original file line number Diff line number Diff line change
@@ -3,70 +3,70 @@ import FluentSQL
import PostgresKit
import PostgresNIO

fileprivate extension PostgresError.Code {
var isSyntaxError: Bool {
extension PostgresError.Code {
fileprivate var isSyntaxError: Bool {
switch self {
case .syntaxErrorOrAccessRuleViolation,
.syntaxError,
.insufficientPrivilege,
.cannotCoerce,
.groupingError,
.windowingError,
.invalidRecursion,
.invalidForeignKey,
.invalidName,
.nameTooLong,
.reservedName,
.datatypeMismatch,
.indeterminateDatatype,
.collationMismatch,
.indeterminateCollation,
.wrongObjectType,
.undefinedColumn,
.undefinedFunction,
.undefinedTable,
.undefinedParameter,
.undefinedObject,
.duplicateColumn,
.duplicateCursor,
.duplicateDatabase,
.duplicateFunction,
.duplicatePreparedStatement,
.duplicateSchema,
.duplicateTable,
.duplicateAlias,
.duplicateObject,
.ambiguousColumn,
.ambiguousFunction,
.ambiguousParameter,
.ambiguousAlias,
.invalidColumnReference,
.invalidColumnDefinition,
.invalidCursorDefinition,
.invalidDatabaseDefinition,
.invalidFunctionDefinition,
.invalidPreparedStatementDefinition,
.invalidSchemaDefinition,
.invalidTableDefinition,
.invalidObjectDefinition:
return true
.syntaxError,
.insufficientPrivilege,
.cannotCoerce,
.groupingError,
.windowingError,
.invalidRecursion,
.invalidForeignKey,
.invalidName,
.nameTooLong,
.reservedName,
.datatypeMismatch,
.indeterminateDatatype,
.collationMismatch,
.indeterminateCollation,
.wrongObjectType,
.undefinedColumn,
.undefinedFunction,
.undefinedTable,
.undefinedParameter,
.undefinedObject,
.duplicateColumn,
.duplicateCursor,
.duplicateDatabase,
.duplicateFunction,
.duplicatePreparedStatement,
.duplicateSchema,
.duplicateTable,
.duplicateAlias,
.duplicateObject,
.ambiguousColumn,
.ambiguousFunction,
.ambiguousParameter,
.ambiguousAlias,
.invalidColumnReference,
.invalidColumnDefinition,
.invalidCursorDefinition,
.invalidDatabaseDefinition,
.invalidFunctionDefinition,
.invalidPreparedStatementDefinition,
.invalidSchemaDefinition,
.invalidTableDefinition,
.invalidObjectDefinition:
true
default:
return false
false
}
}

var isConstraintFailure: Bool {
fileprivate var isConstraintFailure: Bool {
switch self {
case .integrityConstraintViolation,
.restrictViolation,
.notNullViolation,
.foreignKeyViolation,
.uniqueViolation,
.checkViolation,
.exclusionViolation:
return true
.restrictViolation,
.notNullViolation,
.foreignKeyViolation,
.uniqueViolation,
.checkViolation,
.exclusionViolation:
true
default:
return false
false
}
}
}
@@ -76,8 +76,8 @@ extension PostgresError {
public var isSyntaxError: Bool { self.code.isSyntaxError }
public var isConnectionClosed: Bool {
switch self {
case .connectionClosed: return true
default: return false
case .connectionClosed: true
default: false
}
}
public var isConstraintFailure: Bool { self.code.isConstraintFailure }
@@ -87,30 +87,30 @@ extension PostgresError {
extension PSQLError {
public var isSyntaxError: Bool {
switch self.code {
case .server: return self.serverInfo?[.sqlState].map { PostgresError.Code(raw: $0).isSyntaxError } ?? false
default: return false
case .server: self.serverInfo?[.sqlState].map { PostgresError.Code(raw: $0).isSyntaxError } ?? false
default: false
}
}

public var isConnectionClosed: Bool {
switch self.code {
case .serverClosedConnection, .clientClosedConnection: return true
default: return false
case .serverClosedConnection, .clientClosedConnection: true
default: false
}
}

public var isConstraintFailure: Bool {
switch self.code {
case .server: return self.serverInfo?[.sqlState].map { PostgresError.Code(raw: $0).isConstraintFailure } ?? false
default: return false
case .server: self.serverInfo?[.sqlState].map { PostgresError.Code(raw: $0).isConstraintFailure } ?? false
default: false
}
}
}

#if compiler(<6)
extension PostgresError: DatabaseError { }
extension PSQLError: DatabaseError { }
extension PostgresError: DatabaseError {}
extension PSQLError: DatabaseError {}
#else
extension PostgresError: @retroactive DatabaseError { }
extension PSQLError: @retroactive DatabaseError { }
extension PostgresError: @retroactive DatabaseError {}
extension PSQLError: @retroactive DatabaseError {}
#endif
38 changes: 29 additions & 9 deletions Sources/FluentPostgresDriver/PostgresRow+Database.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
import PostgresNIO
import PostgresKit
import FluentKit
import PostgresKit
import PostgresNIO
import SQLKit

extension SQLRow {
internal func databaseOutput() -> some DatabaseOutput { _PostgresDatabaseOutput(row: self, schema: nil) }
func databaseOutput() -> some DatabaseOutput {
_PostgresDatabaseOutput(row: self, schema: nil)
}
}

private struct _PostgresDatabaseOutput: DatabaseOutput {
let row: any SQLRow
let schema: String?
var description: String { String(describing: self.row) }
private func adjust(key: FieldKey) -> FieldKey { self.schema.map { .prefix(.prefix(.string($0), "_"), key) } ?? key }
func schema(_ schema: String) -> any DatabaseOutput { _PostgresDatabaseOutput(row: self.row, schema: schema) }
func contains(_ key: FieldKey) -> Bool { self.row.contains(column: self.adjust(key: key).description) }
func decodeNil(_ key: FieldKey) throws -> Bool { try self.row.decodeNil(column: self.adjust(key: key).description) }
func decode<T: Decodable>(_ key: FieldKey, as: T.Type) throws -> T { try self.row.decode(column: self.adjust(key: key).description, as: T.self) }

var description: String {
String(describing: self.row)
}

private func adjust(key: FieldKey) -> FieldKey {
self.schema.map { .prefix(.prefix(.string($0), "_"), key) } ?? key
}

func schema(_ schema: String) -> any DatabaseOutput {
_PostgresDatabaseOutput(row: self.row, schema: schema)
}

func contains(_ key: FieldKey) -> Bool {
self.row.contains(column: self.adjust(key: key).description)
}

func decodeNil(_ key: FieldKey) throws -> Bool {
try self.row.decodeNil(column: self.adjust(key: key).description)
}

func decode<T: Decodable>(_ key: FieldKey, as: T.Type) throws -> T {
try self.row.decode(column: self.adjust(key: key).description, as: T.self)
}
}
74 changes: 46 additions & 28 deletions Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import Logging
import FluentKit
import FluentBenchmark
import FluentKit
import FluentPostgresDriver
import XCTest
import Logging
import PostgresKit
import SQLKit
import XCTest

func XCTAssertThrowsErrorAsync<T>(
_ expression: @autoclosure () async throws -> T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath, line: UInt = #line,
file: StaticString = #filePath,
line: UInt = #line,
_ callback: (any Error) -> Void = { _ in }
) async {
do {
@@ -23,7 +24,8 @@ func XCTAssertThrowsErrorAsync<T>(
func XCTAssertNoThrowAsync<T>(
_ expression: @autoclosure () async throws -> T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath, line: UInt = #line
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
_ = try await expression()
@@ -73,7 +75,7 @@ final class FluentPostgresDriverTests: XCTestCase {
XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))")
XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))")
}

let sql2 = (self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any())!) as! any SQLDatabase
try await sql2.drop(table: "foo").ifExists().run()
try await sql2.create(table: "foo").column("name", type: .text, .unique).run()
@@ -83,7 +85,7 @@ final class FluentPostgresDriverTests: XCTestCase {
XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))")
XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))")
}

// Disabled until we figure out why it hangs instead of throwing an error.
//let postgres = (self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any())!) as! any PostgresDatabase
//await XCTAssertThrowsErrorAsync(try await postgres.withConnection { conn in
@@ -96,7 +98,7 @@ final class FluentPostgresDriverTests: XCTestCase {
// XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))")
//}
}

func testBlob() async throws {
struct CreateFoo: AsyncMigration {
func prepare(on database: any Database) async throws {
@@ -156,10 +158,14 @@ final class FluentPostgresDriverTests: XCTestCase {
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601

self.dbs.use(.testPostgres(subconfig: "A",
encodingContext: .init(jsonEncoder: jsonEncoder),
decodingContext: .init(jsonDecoder: jsonDecoder)
), as: .iso8601)
self.dbs.use(
.testPostgres(
subconfig: "A",
encodingContext: .init(jsonEncoder: jsonEncoder),
decodingContext: .init(jsonDecoder: jsonDecoder)
),
as: .iso8601
)
let db = self.dbs.database(
.iso8601,
logger: .init(label: "test"),
@@ -210,30 +216,38 @@ final class FluentPostgresDriverTests: XCTestCase {
throw error
}
}

func testEncodingArrayOfModels() async throws {
final class Elem: Model, ExpressibleByIntegerLiteral, @unchecked Sendable {
static let schema = ""
@ID(custom: .id) var id: Int?
init() {}; init(integerLiteral l: Int) { self.id = l }
init() {}
init(integerLiteral l: Int) { self.id = l }
}
final class Seq: Model, ExpressibleByNilLiteral, ExpressibleByArrayLiteral, @unchecked Sendable {
static let schema = "seqs"
@ID(custom: .id) var id: Int?; @OptionalField(key: "list") var list: [Elem]?
init() {}; init(nilLiteral: ()) { self.list = nil }; init(arrayLiteral el: Elem...) { self.list = el }
@ID(custom: .id) var id: Int?
@OptionalField(key: "list") var list: [Elem]?
init() {}
init(nilLiteral: ()) { self.list = nil }
init(arrayLiteral el: Elem...) { self.list = el }
}
do {
try await self.db.schema(Seq.schema).field(.id, .int, .identifier(auto: true)).field("list", .sql(embed: "JSONB[]")).create()

let s1: Seq = [1, 2], s2: Seq = nil; try [s1, s2].forEach { try $0.create(on: self.db).wait() }


let s1: Seq = [1, 2]
let s2: Seq = nil
try [s1, s2].forEach { try $0.create(on: self.db).wait() }

// Make sure it went into the DB as "array of jsonb" rather than as "array of one jsonb containing array" or such.
let raws = try await (self.db as! any SQLDatabase).raw("SELECT array_to_json(list)::text t FROM seqs").all().map { try $0.decode(column: "t", as: String?.self) }
let raws = try await (self.db as! any SQLDatabase).raw("SELECT array_to_json(list)::text t FROM seqs").all().map {
try $0.decode(column: "t", as: String?.self)
}
XCTAssertEqual(raws, [#"[{"id": 1},{"id": 2}]"#, nil])

// Make sure it round-trips through Fluent.
let seqs = try await Seq.query(on: self.db).all()

XCTAssertEqual(seqs.count, 2)
XCTAssertEqual(seqs.dropFirst(0).first?.id, s1.id)
XCTAssertEqual(seqs.dropFirst(0).first?.list?.map(\.id), s1.list?.map(\.id))
@@ -245,17 +259,16 @@ final class FluentPostgresDriverTests: XCTestCase {
try await db.schema(Seq.schema).delete()
}


var benchmarker: FluentBenchmarker { .init(databases: self.dbs) }
var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton }
var threadPool: NIOThreadPool { NIOThreadPool.singleton }
var dbs: Databases!
var db: (any Database)!
var postgres: any PostgresDatabase { self.db as! any PostgresDatabase }

override func setUp() async throws {
try await super.setUp()

XCTAssert(isLoggingConfigured)
self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup)

@@ -271,7 +284,7 @@ final class FluentPostgresDriverTests: XCTestCase {
_ = try await (b as! any PostgresDatabase).query("create schema public").get()

self.db = a
}
}

override func tearDown() async throws {
await self.dbs.shutdownAsync()
@@ -293,8 +306,13 @@ extension DatabaseConfigurationFactory {
database: env("POSTGRES_DB_\(subconfig)") ?? "test_database",
tls: try! .prefer(.init(configuration: .makeClientConfiguration()))
)

return .postgres(configuration: baseSubconfig, connectionPoolTimeout: .seconds(30), encodingContext: encodingContext, decodingContext: decodingContext)

return .postgres(
configuration: baseSubconfig,
connectionPoolTimeout: .seconds(30),
encodingContext: encodingContext,
decodingContext: decodingContext
)
}
}

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Logging
import FluentKit
import FluentBenchmark
import FluentKit
import FluentPostgresDriver
import XCTest
import Logging
import PostgresKit
import XCTest

final class FluentPostgresTransactionControlTests: XCTestCase {
func testRollback() async throws {
@@ -33,15 +33,15 @@ final class FluentPostgresTransactionControlTests: XCTestCase {
let count2 = try await Todo.query(on: self.db).count()
XCTAssertEqual(count2, 0)
}

var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton }
var threadPool: NIOThreadPool { NIOThreadPool.singleton }
var dbs: Databases!
var db: (any Database)!

override func setUp() async throws {
try await super.setUp()

XCTAssert(isLoggingConfigured)
self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup)

@@ -50,7 +50,7 @@ final class FluentPostgresTransactionControlTests: XCTestCase {
self.db = self.dbs.database(.a, logger: Logger(label: "test.fluent.a"), on: self.eventLoopGroup.any())
_ = try await (self.db as! any PostgresDatabase).query("drop schema public cascade").get()
_ = try await (self.db as! any PostgresDatabase).query("create schema public").get()

try await CreateTodo().prepare(on: self.db)
}

@@ -59,7 +59,7 @@ final class FluentPostgresTransactionControlTests: XCTestCase {
await self.dbs.shutdownAsync()
try await super.tearDown()
}

final class Todo: Model, @unchecked Sendable {
static let schema = "todos"

@@ -70,9 +70,12 @@ final class FluentPostgresTransactionControlTests: XCTestCase {
var title: String

init() {}
init(title: String) { self.title = title; id = nil }
init(title: String) {
self.title = title
id = nil
}
}

struct CreateTodo: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema("todos")