From 166818955e8394cd24e8e7c07f2ba4fbdff0d87b Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 13 Apr 2025 06:44:24 -0500 Subject: [PATCH 1/4] Bump Swift minimum to 5.10, update README, update CI, remove unneeded explicit AsyncKit dependency (it's exported by PostgresKit), add .editorconfig and .swift-format --- .editorconfig | 7 ++ .github/workflows/test.yml | 26 +++---- .swift-format | 77 +++++++++++++++++++ Package.swift | 4 +- README.md | 8 +- .../Docs.docc/theme-settings.json | 5 +- 6 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 .editorconfig create mode 100644 .swift-format diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fe287d0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.swift] +indent_style = space +indent_size = 4 +tab_width = 4 +insert_final_newline = true +trim_trailing_whitespace = true + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7329336..a795da6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 unlink postgresql@14 || true) && brew install postgresql@17 && brew link --force postgresql@16 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}" diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..e95ade0 --- /dev/null +++ b/.swift-format @@ -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 +} diff --git a/Package.swift b/Package.swift index 0560db0..8035350 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 import PackageDescription let package = Package( @@ -13,7 +13,6 @@ 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"), ], @@ -21,7 +20,6 @@ let package = Package( .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"), diff --git a/README.md b/README.md index 76a07bb..632b114 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@

- - - - FluentPostgresDriver - +FluentPostgresDriver

Documentation @@ -11,7 +7,7 @@ MIT License Continuous Integration -Swift 5.8+ +Swift 5.10+


diff --git a/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json b/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json index 6f0b9d4..4ce4f1c 100644 --- a/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json +++ b/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json @@ -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 }, From add028393a610132500813a42b545db78c43aeef Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 13 Apr 2025 06:46:03 -0500 Subject: [PATCH 2/4] Run swift-format on most of the code --- ...uentPostgresConfiguration+Deprecated.swift | 6 +- .../FluentPostgresConfiguration.swift | 22 +-- .../FluentPostgresDatabase.swift | 69 +++++---- .../FluentPostgresDriver.swift | 10 +- .../PostgresConverterDelegate.swift | 38 ++--- .../PostgresError+Database.swift | 138 +++++++++--------- .../PostgresRow+Database.swift | 38 +++-- .../FluentPostgresDriverTests.swift | 74 ++++++---- ...luentPostgresTransactionControlTests.swift | 23 +-- 9 files changed, 236 insertions(+), 182 deletions(-) diff --git a/Sources/FluentPostgresDriver/Deprecations/FluentPostgresConfiguration+Deprecated.swift b/Sources/FluentPostgresDriver/Deprecations/FluentPostgresConfiguration+Deprecated.swift index 600c52c..5948911 100644 --- a/Sources/FluentPostgresDriver/Deprecations/FluentPostgresConfiguration+Deprecated.swift +++ b/Sources/FluentPostgresDriver/Deprecations/FluentPostgresConfiguration+Deprecated.swift @@ -1,9 +1,7 @@ -import Logging import FluentKit -import AsyncKit -import NIOCore -import NIOSSL import Foundation +import Logging +import NIOCore import PostgresKit import PostgresNIO diff --git a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift index 851e832..5bdf1c1 100644 --- a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift +++ b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift @@ -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 ) } @@ -82,20 +83,21 @@ extension DatabaseConfigurationFactory { sqlLogLevel: Logger.Level = .debug ) -> DatabaseConfigurationFactory { let configuration = FakeSendable(wrappedValue: configuration) - + return .init { FluentPostgresConfiguration( configuration: configuration, maxConnectionsPerEventLoop: maxConnectionsPerEventLoop, connectionPoolTimeout: connectionPoolTimeout, - encodingContext: encodingContext, decodingContext: decodingContext, + encodingContext: encodingContext, + decodingContext: decodingContext, sqlLogLevel: sqlLogLevel ) } } } -fileprivate struct FakeSendable: @unchecked Sendable { let wrappedValue: T } +private struct FakeSendable: @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 @@ -185,7 +187,7 @@ struct FluentPostgresConfiguration { extension _FluentPostgresDatabase: Database { func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) -> EventLoopFuture { 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 { 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(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { 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 { self.raw("BEGIN").run() } - + func commitTransaction() -> EventLoopFuture { self.raw("COMMIT").run() } - + func rollbackTransaction() -> EventLoopFuture { 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 { + + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) -> EventLoopFuture { 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(_ 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 { self.withConnection { $0.send(request, logger: logger) } } - + func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { 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) } } diff --git a/Sources/FluentPostgresDriver/FluentPostgresDriver.swift b/Sources/FluentPostgresDriver/FluentPostgresDriver.swift index 5b4d85c..53ebcb0 100644 --- a/Sources/FluentPostgresDriver/FluentPostgresDriver.swift +++ b/Sources/FluentPostgresDriver/FluentPostgresDriver.swift @@ -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: Da let encodingContext: PostgresEncodingContext let decodingContext: PostgresDecodingContext let sqlLogLevel: Logger.Level - + func makeDatabase(with context: DatabaseContext) -> any Database { _FluentPostgresDatabase( database: self.pool @@ -23,11 +23,11 @@ struct _FluentPostgresDriver: Da inTransaction: false ) } - + func shutdown() { try? self.pool.syncShutdownGracefully() } - + func shutdownAsync() async { try? await self.pool.shutdownAsync() } diff --git a/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift b/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift index 7eb55c4..63e91bd 100644 --- a/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift +++ b/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift @@ -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("[]") diff --git a/Sources/FluentPostgresDriver/PostgresError+Database.swift b/Sources/FluentPostgresDriver/PostgresError+Database.swift index a2435ee..cd6865e 100644 --- a/Sources/FluentPostgresDriver/PostgresError+Database.swift +++ b/Sources/FluentPostgresDriver/PostgresError+Database.swift @@ -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 diff --git a/Sources/FluentPostgresDriver/PostgresRow+Database.swift b/Sources/FluentPostgresDriver/PostgresRow+Database.swift index 898c3ae..beea4b6 100644 --- a/Sources/FluentPostgresDriver/PostgresRow+Database.swift +++ b/Sources/FluentPostgresDriver/PostgresRow+Database.swift @@ -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(_ 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(_ key: FieldKey, as: T.Type) throws -> T { + try self.row.decode(column: self.adjust(key: key).description, as: T.self) + } } diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift index 2501e4a..c268085 100644 --- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift +++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift @@ -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( _ 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( func XCTAssertNoThrowAsync( _ 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 + ) } } diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift index b33a45f..4bdc52f 100644 --- a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift +++ b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift @@ -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") From ea2d7d2a9e365f3a9b629c4284ad28d41e2bbc8d Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 13 Apr 2025 06:46:16 -0500 Subject: [PATCH 3/4] Remove no-longer-needed Sendable workaround --- .../FluentPostgresConfiguration.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift index 5bdf1c1..4055646 100644 --- a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift +++ b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift @@ -82,9 +82,7 @@ extension DatabaseConfigurationFactory { decodingContext: PostgresDecodingContext, sqlLogLevel: Logger.Level = .debug ) -> DatabaseConfigurationFactory { - let configuration = FakeSendable(wrappedValue: configuration) - - return .init { + .init { FluentPostgresConfiguration( configuration: configuration, maxConnectionsPerEventLoop: maxConnectionsPerEventLoop, @@ -97,8 +95,6 @@ extension DatabaseConfigurationFactory { } } -private struct FakeSendable: @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 @@ -172,7 +168,7 @@ extension DatabaseConfigurationFactory { /// The actual concrete configuration type produced by a configuration factory. struct FluentPostgresConfiguration: DatabaseConfiguration { var middleware: [any AnyModelMiddleware] = [] - fileprivate let configuration: FakeSendable + fileprivate let configuration: SQLPostgresConfiguration let maxConnectionsPerEventLoop: Int let connectionPoolTimeout: TimeAmount let encodingContext: PostgresEncodingContext @@ -180,7 +176,7 @@ struct FluentPostgresConfiguration any DatabaseDriver { - let connectionSource = PostgresConnectionSource(sqlConfiguration: self.configuration.wrappedValue) + let connectionSource = PostgresConnectionSource(sqlConfiguration: self.configuration) let elgPool = EventLoopGroupConnectionPool( source: connectionSource, maxConnectionsPerEventLoop: self.maxConnectionsPerEventLoop, From 32c362a498089e3237668bf12722d837652d05a4 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 13 Apr 2025 07:05:01 -0500 Subject: [PATCH 4/4] Fix a couple of CI typos --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a795da6..15b15bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: - postgres-image-a: 'postgres:16' postgres-image-b: 'postgres:17' postgres-auth: 'scram-sha-256' - swift-image: 'swift:6.1.noble' + swift-image: 'swift:6.1-noble' container: ${{ matrix.swift-image }} runs-on: ubuntu-latest services: @@ -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@17 && 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}"