From cb91bf94fceedc6756e5b022ab394f6862154c34 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Tue, 14 May 2024 09:15:32 -0500 Subject: [PATCH] Tidy up FluentKit (#609) * Bump dependency version requirements, add missing target deps, remove useless Swift feature flags * Code style cleanups * Add missing async versions of methods and missing SQLDatabase declarations, remove unneeded uses of UnsafeTransfer, use SQLKit's version of SQLQUalifiedTable * Rename the various versions of `.sql(raw:)` to `.sql(unsafeRaw:)`, deprecating the old name. * Don't reimplement `FieldKey.description` in SQLSchemaConverter, just use it * Run the SQLKit benchmarks as part of running the Fluent benchmarks. * Update tests for deprecations --- Package.swift | 14 ++-- Package@swift-5.9.swift | 12 ++- Sources/FluentBenchmark/Exports.swift | 9 --- .../FluentBenchmark/FluentBenchmarker.swift | 1 + .../FluentBenchmark/Tests/ChildTests.swift | 4 +- .../FluentBenchmark/Tests/FilterTests.swift | 2 +- Sources/FluentBenchmark/Tests/SQLTests.swift | 5 ++ .../FluentBenchmark/Tests/SchemaTests.swift | 6 +- .../Concurrency/Database+Concurrency.swift | 23 +++++- .../FluentKit/Database/Database+Logging.swift | 8 +- Sources/FluentKit/Database/DatabaseID.swift | 1 + .../Database/TransactionControlDatabase.swift | 10 +-- Sources/FluentKit/FluentError.swift | 4 +- .../Middleware/ModelMiddleware.swift | 14 ++-- .../FluentKit/Middleware/ModelResponder.swift | 12 +-- Sources/FluentKit/Migration/Migration.swift | 10 +-- Sources/FluentKit/Migration/Migrator.swift | 58 +++++++------- .../Query/Builder/QueryBuilder.swift | 18 ++--- Sources/FluentKit/Schema/DatabaseSchema.swift | 4 +- Sources/FluentSQL/DatabaseQuery+SQL.swift | 37 +++++++-- Sources/FluentSQL/DatabaseSchema+SQL.swift | 56 ++++++++++++-- ...ift => SQLJSONColumnPath+Deprecated.swift} | 0 .../SQLQualifiedTable+Deprecated.swift | 6 ++ Sources/FluentSQL/SQLQualifiedTable.swift | 23 ------ Sources/FluentSQL/SQLQueryConverter.swift | 75 +++++++++++-------- Sources/FluentSQL/SQLSchemaConverter.swift | 24 +++--- 26 files changed, 254 insertions(+), 182 deletions(-) rename Sources/FluentSQL/{SQLJSONColumnPath.swift => SQLJSONColumnPath+Deprecated.swift} (100%) create mode 100644 Sources/FluentSQL/SQLQualifiedTable+Deprecated.swift delete mode 100644 Sources/FluentSQL/SQLQualifiedTable.swift diff --git a/Package.swift b/Package.swift index 1cc12715..768b58b5 100644 --- a/Package.swift +++ b/Package.swift @@ -16,10 +16,10 @@ let package = Package( .library(name: "XCTFluent", targets: ["XCTFluent"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.2"), - .package(url: "https://github.com/vapor/async-kit.git", from: "1.17.0"), + .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"), + .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), ], targets: [ .target( @@ -38,6 +38,8 @@ let package = Package( dependencies: [ .target(name: "FluentKit"), .target(name: "FluentSQL"), + .product(name: "SQLKit", package: "sql-kit"), + .product(name: "SQLKitBenchmark", package: "sql-kit"), ], swiftSettings: swiftSettings ), @@ -72,10 +74,4 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ConciseMagicFile"), .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("ImportObjcForwardDeclarations"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("IsolatedDefaultValues"), - .enableUpcomingFeature("GlobalConcurrency"), - .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("StrictConcurrency=complete"), ] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index dcb6db3b..a88e2929 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -16,10 +16,10 @@ let package = Package( .library(name: "XCTFluent", targets: ["XCTFluent"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.2"), - .package(url: "https://github.com/vapor/async-kit.git", from: "1.17.0"), + .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"), + .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), ], targets: [ .target( @@ -38,6 +38,8 @@ let package = Package( dependencies: [ .target(name: "FluentKit"), .target(name: "FluentSQL"), + .product(name: "SQLKit", package: "sql-kit"), + .product(name: "SQLKitBenchmark", package: "sql-kit"), ], swiftSettings: swiftSettings ), @@ -73,10 +75,6 @@ var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("ConciseMagicFile"), .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("ImportObjcForwardDeclarations"), .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("IsolatedDefaultValues"), - .enableUpcomingFeature("GlobalConcurrency"), - .enableUpcomingFeature("StrictConcurrency"), .enableExperimentalFeature("StrictConcurrency=complete"), ] } diff --git a/Sources/FluentBenchmark/Exports.swift b/Sources/FluentBenchmark/Exports.swift index 83531ee0..15785820 100644 --- a/Sources/FluentBenchmark/Exports.swift +++ b/Sources/FluentBenchmark/Exports.swift @@ -1,11 +1,2 @@ -#if swift(>=5.8) - @_documentation(visibility: internal) @_exported import FluentKit @_documentation(visibility: internal) @_exported import XCTest - -#else - -@_exported import FluentKit -@_exported import XCTest - -#endif diff --git a/Sources/FluentBenchmark/FluentBenchmarker.swift b/Sources/FluentBenchmark/FluentBenchmarker.swift index 50b0b275..4282acf9 100644 --- a/Sources/FluentBenchmark/FluentBenchmarker.swift +++ b/Sources/FluentBenchmark/FluentBenchmarker.swift @@ -8,6 +8,7 @@ public final class FluentBenchmarker { public init(databases: Databases) { precondition(databases.ids().count >= 2, "FluentBenchmarker Databases instance must have 2 or more registered databases") + self.databases = databases self.database = self.databases.database( logger: .init(label: "codes.vapor.fluent.benchmarker"), diff --git a/Sources/FluentBenchmark/Tests/ChildTests.swift b/Sources/FluentBenchmark/Tests/ChildTests.swift index 166e18f7..cfda2c1e 100644 --- a/Sources/FluentBenchmark/Tests/ChildTests.swift +++ b/Sources/FluentBenchmark/Tests/ChildTests.swift @@ -267,7 +267,9 @@ private final class Player: Model, @unchecked Sendable { init( id: Int? = nil, - name: String, gameID: Game.IDValue) { + name: String, + gameID: Game.IDValue + ) { self.id = id self.name = name self.$game.id = gameID diff --git a/Sources/FluentBenchmark/Tests/FilterTests.swift b/Sources/FluentBenchmark/Tests/FilterTests.swift index 94f73006..fa8eb7fd 100644 --- a/Sources/FluentBenchmark/Tests/FilterTests.swift +++ b/Sources/FluentBenchmark/Tests/FilterTests.swift @@ -45,7 +45,7 @@ extension FluentBenchmarker { SolarSystem() ]) { let moon = try Moon.query(on: self.database) - .filter(\.$name == .sql(raw: "'Moon'")) + .filter(\.$name == .sql(unsafeRaw: "'Moon'")) .first() .wait() diff --git a/Sources/FluentBenchmark/Tests/SQLTests.swift b/Sources/FluentBenchmark/Tests/SQLTests.swift index 0c982c02..0d7b415f 100644 --- a/Sources/FluentBenchmark/Tests/SQLTests.swift +++ b/Sources/FluentBenchmark/Tests/SQLTests.swift @@ -1,8 +1,10 @@ import FluentKit import Foundation import NIOCore +import NIOPosix import XCTest import SQLKit +import SQLKitBenchmark extension FluentBenchmarker { public func testSQL() throws { @@ -10,6 +12,9 @@ extension FluentBenchmarker { return } try self.testSQL_rawDecode(sql) + try MultiThreadedEventLoopGroup.singleton.any().makeFutureWithTask { + try await SQLBenchmarker(on: sql).runAllTests() + }.wait() } private func testSQL_rawDecode(_ sql: any SQLDatabase) throws { diff --git a/Sources/FluentBenchmark/Tests/SchemaTests.swift b/Sources/FluentBenchmark/Tests/SchemaTests.swift index 388b6dc3..09062e62 100644 --- a/Sources/FluentBenchmark/Tests/SchemaTests.swift +++ b/Sources/FluentBenchmark/Tests/SchemaTests.swift @@ -96,7 +96,7 @@ extension FluentBenchmarker { .constraint(.sql(embed: "CONSTRAINT \(normalized1) UNIQUE (\(ident: "id"))")) // Test raw SQL for table constraint definitions (but not names): - .constraint(.constraint(.sql(raw: "UNIQUE (id)"), name: "id_unq_2")) + .constraint(.constraint(.sql(unsafeRaw: "UNIQUE (id)"), name: "id_unq_2")) .constraint(.constraint(.sql(embed: "UNIQUE (\(ident: "id"))"), name: "id_unq_3")) .create().wait() @@ -104,7 +104,7 @@ extension FluentBenchmarker { if (self.database as! any SQLDatabase).dialect.alterTableSyntax.allowsBatch { try self.database.schema("custom_constraints") // Test raw SQL for dropping constraints: - .deleteConstraint(.sql(embed: "\(SQLDropTypedConstraint(name: SQLIdentifier("id_unq_1"), algorithm: .sql(raw: "")))")) + .deleteConstraint(.sql(embed: "\(SQLDropTypedConstraint(name: SQLIdentifier("id_unq_1"), algorithm: .sql(unsafeRaw: "")))")) .update().wait() } } @@ -127,7 +127,7 @@ extension FluentBenchmarker { .field("neverbeid", .string, .sql(embed: "NOT NULL")) // Test raw SQL for entire field definitions: - .field(.sql(raw: "idnah INTEGER NOT NULL")) + .field(.sql(unsafeRaw: "idnah INTEGER NOT NULL")) .field(.sql(embed: "\(ident: "notid") INTEGER")) .create().wait() diff --git a/Sources/FluentKit/Concurrency/Database+Concurrency.swift b/Sources/FluentKit/Concurrency/Database+Concurrency.swift index 676d31bc..8820a337 100644 --- a/Sources/FluentKit/Concurrency/Database+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Database+Concurrency.swift @@ -1,8 +1,27 @@ import NIOCore public extension Database { + func execute( + query: DatabaseQuery, + onOutput: @escaping @Sendable (any DatabaseOutput) -> () + ) async throws { + try await self.execute(query: query, onOutput: onOutput).get() + } + + func execute( + schema: DatabaseSchema + ) async throws { + try await self.execute(schema: schema).get() + } + + func execute( + enum: DatabaseEnum + ) async throws { + try await self.execute(enum: `enum`).get() + } + func transaction(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { - try await self.transaction { db -> EventLoopFuture in + try await self.transaction { db in self.eventLoop.makeFutureWithTask { try await closure(db) } @@ -10,7 +29,7 @@ public extension Database { } func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { - try await self.withConnection { db -> EventLoopFuture in + try await self.withConnection { db in self.eventLoop.makeFutureWithTask { try await closure(db) } diff --git a/Sources/FluentKit/Database/Database+Logging.swift b/Sources/FluentKit/Database/Database+Logging.swift index c81e6c82..2840a62a 100644 --- a/Sources/FluentKit/Database/Database+Logging.swift +++ b/Sources/FluentKit/Database/Database+Logging.swift @@ -33,7 +33,6 @@ extension LoggingOverrideDatabase: Database { func execute( schema: DatabaseSchema ) -> EventLoopFuture { - self.database.execute(schema: schema) } @@ -60,6 +59,13 @@ extension LoggingOverrideDatabase: SQLDatabase where D: SQLDatabase { func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture { self.database.execute(sql: query, onRow) } + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) 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) + } var dialect: any SQLDialect { self.database.dialect } var version: (any SQLDatabaseReportedVersion)? { self.database.version } + var queryLogLevel: Logger.Level? { self.database.queryLogLevel } } diff --git a/Sources/FluentKit/Database/DatabaseID.swift b/Sources/FluentKit/Database/DatabaseID.swift index 3d73fe1a..b13be1d1 100644 --- a/Sources/FluentKit/Database/DatabaseID.swift +++ b/Sources/FluentKit/Database/DatabaseID.swift @@ -1,5 +1,6 @@ public struct DatabaseID: Hashable, Codable, Sendable { public let string: String + public init(string: String) { self.string = string } diff --git a/Sources/FluentKit/Database/TransactionControlDatabase.swift b/Sources/FluentKit/Database/TransactionControlDatabase.swift index ff6cee1c..ec2143bb 100644 --- a/Sources/FluentKit/Database/TransactionControlDatabase.swift +++ b/Sources/FluentKit/Database/TransactionControlDatabase.swift @@ -3,12 +3,12 @@ import NIOCore /// Protocol for describing a database that allows fine-grained control over transcactions /// when you need more control than provided by ``Database/transaction(_:)-1x3ds`` /// -/// ⚠️ **WARNING**: it is the developer's responsiblity to get hold of a ``Database``, -/// execute the transaction functions on that connection, and ensure that the functions aren't called across -/// different conenctions. You are also responsible for ensuring that you commit or rollback queries -/// when you're ready. +/// > Warning: ⚠️ It is the developer's responsiblity to get hold of a ``Database``, execute the +/// > transaction functions on that connection, and ensure that the functions aren't called across +/// > different conenctions. You are also responsible for ensuring that you commit or rollback +/// > queries when you're ready. /// -/// Do not mix these functions and `Database.transaction(_:)`. +/// Do not mix these functions and ``Database/transaction(_:)-1x3ds``. public protocol TransactionControlDatabase: Database { /// Start the transaction on the current connection. This is equivalent to an SQL `BEGIN` /// - Returns: future `Void` when the transaction has been started diff --git a/Sources/FluentKit/FluentError.swift b/Sources/FluentKit/FluentError.swift index dd3224d1..9da1c563 100644 --- a/Sources/FluentKit/FluentError.swift +++ b/Sources/FluentKit/FluentError.swift @@ -96,8 +96,8 @@ extension FluentError { /// An error describing a failure during an an operation on an ``SiblingsProperty``. /// /// > Note: This should just be another case on ``FluentError``, not a separate error type, but at the time -/// of this writing, non-frozen enums are still not available to non-stdlib packages, so to avoid source -/// breakage we chose this as the least annoying of the several annoying workarounds. +/// > of this writing, non-frozen enums are still not available to non-stdlib packages, so to avoid source +/// > breakage we chose this as the least annoying of the several annoying workarounds. public enum SiblingsPropertyError: Error, LocalizedError, CustomStringConvertible, CustomDebugStringConvertible { /// An attempt was made to query, attach to, or detach from a siblings property whose owning model's ID /// is not currently known (usually because that model has not yet been saved to the database). diff --git a/Sources/FluentKit/Middleware/ModelMiddleware.swift b/Sources/FluentKit/Middleware/ModelMiddleware.swift index d717ce02..1a75d709 100644 --- a/Sources/FluentKit/Middleware/ModelMiddleware.swift +++ b/Sources/FluentKit/Middleware/ModelMiddleware.swift @@ -40,29 +40,29 @@ extension ModelMiddleware { } public func create(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { - return next.create(model, on: db) + next.create(model, on: db) } public func update(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { - return next.update(model, on: db) + next.update(model, on: db) } public func delete(model: Model, force: Bool, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { - return next.delete(model, force: force, on: db) + next.delete(model, force: force, on: db) } public func softDelete(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { - return next.softDelete(model, on: db) + next.softDelete(model, on: db) } public func restore(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { - return next.restore(model, on: db) + next.restore(model, on: db) } } extension AnyModelMiddleware { func makeResponder(chainingTo responder: any AnyModelResponder) -> any AnyModelResponder { - return ModelMiddlewareResponder(middleware: self, responder: responder) + ModelMiddlewareResponder(middleware: self, responder: responder) } } @@ -84,7 +84,7 @@ private struct ModelMiddlewareResponder: AnyModelResponder { var responder: any AnyModelResponder func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture { - return self.middleware.handle(event, model, on: db, chainingTo: responder) + self.middleware.handle(event, model, on: db, chainingTo: responder) } } diff --git a/Sources/FluentKit/Middleware/ModelResponder.swift b/Sources/FluentKit/Middleware/ModelResponder.swift index cfb49733..e612d4e7 100644 --- a/Sources/FluentKit/Middleware/ModelResponder.swift +++ b/Sources/FluentKit/Middleware/ModelResponder.swift @@ -10,23 +10,23 @@ public protocol AnyModelResponder: Sendable { extension AnyModelResponder { public func create(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { - return handle(.create, model, on: db) + self.handle(.create, model, on: db) } public func update(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { - return handle(.update, model, on: db) + self.handle(.update, model, on: db) } public func restore(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { - return handle(.restore, model, on: db) + self.handle(.restore, model, on: db) } public func softDelete(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { - return handle(.softDelete, model, on: db) + self.handle(.softDelete, model, on: db) } public func delete(_ model: any AnyModel, force: Bool, on db: any Database) -> EventLoopFuture { - return handle(.delete(force), model, on: db) + self.handle(.delete(force), model, on: db) } } @@ -39,7 +39,7 @@ internal struct BasicModelResponder: AnyModelResponder where Model: Fluen } do { - return try _handle(event, modelType, db) + return try self._handle(event, modelType, db) } catch { return db.eventLoop.makeFailedFuture(error) } diff --git a/Sources/FluentKit/Migration/Migration.swift b/Sources/FluentKit/Migration/Migration.swift index 4cab0881..68ac7533 100644 --- a/Sources/FluentKit/Migration/Migration.swift +++ b/Sources/FluentKit/Migration/Migration.swift @@ -28,7 +28,7 @@ extension Migration { } internal var defaultName: String { -#if compiler(>=5.3) && compiler(<6) + #if compiler(<6) /// `String.init(reflecting:)` creates a `Mirror` unconditionally, but /// when the parameter is a metatype (such as is the case here), that /// mirror is never actually used for anything. Unfortunately, just @@ -39,9 +39,9 @@ extension Migration { /// runtime function directly instead of taking the huge speed hit just /// because the leading underscore makes it harder to ignore the /// fragility of the usage. - return Swift._typeName(Self.self, qualified: true) -#else - return String(reflecting: Self.self) -#endif + Swift._typeName(Self.self, qualified: true) + #else + String(reflecting: Self.self) + #endif } } diff --git a/Sources/FluentKit/Migration/Migrator.swift b/Sources/FluentKit/Migration/Migrator.swift index f40ac046..11091ab9 100644 --- a/Sources/FluentKit/Migration/Migrator.swift +++ b/Sources/FluentKit/Migration/Migrator.swift @@ -41,72 +41,60 @@ public struct Migrator: Sendable { // MARK: Setup public func setupIfNeeded() -> EventLoopFuture { - return self.migrators() { $0.setupIfNeeded() }.transform(to: ()) + self.migrators() { $0.setupIfNeeded() }.transform(to: ()) } // MARK: Prepare public func prepareBatch() -> EventLoopFuture { - return self.migrators() { $0.prepareBatch() }.transform(to: ()) + self.migrators() { $0.prepareBatch() }.transform(to: ()) } // MARK: Revert public func revertLastBatch() -> EventLoopFuture { - return self.migrators() { $0.revertLastBatch() }.transform(to: ()) + self.migrators() { $0.revertLastBatch() }.transform(to: ()) } public func revertBatch(number: Int) -> EventLoopFuture { - return self.migrators() { $0.revertBatch(number: number) }.transform(to: ()) + self.migrators() { $0.revertBatch(number: number) }.transform(to: ()) } public func revertAllBatches() -> EventLoopFuture { - return self.migrators() { $0.revertAllBatches() }.transform(to: ()) + self.migrators() { $0.revertAllBatches() }.transform(to: ()) } // MARK: Preview public func previewPrepareBatch() -> EventLoopFuture<[(any Migration, DatabaseID?)]> { - return self.migrators() { migrator in - return migrator.previewPrepareBatch().and(value: migrator.id) - }.map { items in - return items.reduce(into: []) { result, batch in - let pairs = batch.0.map { ($0, batch.1) } - result.append(contentsOf: pairs) - } + self.migrators() { migrator in + migrator.previewPrepareBatch().and(value: migrator.id) + }.map { + $0.flatMap { migrations, id in migrations.map { ($0, id) } } } } public func previewRevertLastBatch() -> EventLoopFuture<[(any Migration, DatabaseID?)]> { - return self.migrators() { migrator in - return migrator.previewRevertLastBatch().and(value: migrator.id) - }.map { items in - return items.reduce(into: []) { result, batch in - let pairs = batch.0.map { ($0, batch.1) } - result.append(contentsOf: pairs) - } + self.migrators() { migrator in + migrator.previewRevertLastBatch().and(value: migrator.id) + }.map { + $0.flatMap { migrations, id in migrations.map { ($0, id) } } } } public func previewRevertBatch() -> EventLoopFuture<[(any Migration, DatabaseID?)]> { - return self.migrators() { migrator in + self.migrators() { migrator in return migrator.previewPrepareBatch().and(value: migrator.id) - }.map { items in - return items.reduce(into: []) { result, batch in - let pairs = batch.0.map { ($0, batch.1) } - result.append(contentsOf: pairs) - } + }.map { + $0.flatMap { migrations, id in migrations.map { ($0, id) } } } } public func previewRevertAllBatches() -> EventLoopFuture<[(any Migration, DatabaseID?)]> { - return self.migrators() { migrator in - return migrator.previewRevertAllBatches().and(value: migrator.id) - }.map { items in - return items.reduce(into: []) { result, batch in - let pairs = batch.0.map { ($0, batch.1) } - result.append(contentsOf: pairs) - } + self.migrators() { migrator in + migrator.previewRevertAllBatches().and(value: migrator.id) + }.map { + $0.flatMap { migrations, id in migrations.map { ($0, id) } } } } @@ -203,22 +191,28 @@ private final class DatabaseMigrator: Sendable { private func prepare(_ migration: any Migration, batch: Int) -> EventLoopFuture { self.database.logger.log(level: self.migrationLogLevel, "[Migrator] Starting prepare", metadata: ["migration": .string(migration.name)]) + return migration.prepare(on: self.database).flatMap { self.database.logger.log(level: self.migrationLogLevel, "[Migrator] Finished prepare", metadata: ["migration": .string(migration.name)]) + return MigrationLog(name: migration.name, batch: batch).save(on: self.database) }.flatMapErrorThrowing { self.database.logger.error("[Migrator] Failed prepare: \(String(reflecting: $0))", metadata: ["migration": .string(migration.name)]) + throw $0 } } private func revert(_ migration: any Migration) -> EventLoopFuture { self.database.logger.log(level: self.migrationLogLevel, "[Migrator] Starting revert", metadata: ["migration": .string(migration.name)]) + return migration.revert(on: self.database).flatMap { self.database.logger.log(level: self.migrationLogLevel, "[Migrator] Finished revert", metadata: ["migration": .string(migration.name)]) + return MigrationLog.query(on: self.database).filter(\.$name == migration.name).delete() }.flatMapErrorThrowing { self.database.logger.error("[Migrator] Failed revert: \(String(reflecting: $0))", metadata: ["migration": .string(migration.name)]) + throw $0 } } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder.swift b/Sources/FluentKit/Query/Builder/QueryBuilder.swift index c83dfb71..251ccb9b 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder.swift @@ -225,11 +225,11 @@ public final class QueryBuilder .all { models.wrappedValue.append($0) } .flatMapThrowing { try models.wrappedValue.map { try $0.get() } } #else - nonisolated(unsafe) var models: [Result, any Error>] = [] + nonisolated(unsafe) var models: [Result] = [] return self - .all { models.append($0.map { .init(wrappedValue: $0) }) } - .flatMapThrowing { try models.map { try $0.get().wrappedValue } } + .all { models.append($0) } + .flatMapThrowing { try models.map { try $0.get() } } #endif } @@ -239,15 +239,15 @@ public final class QueryBuilder #if swift(<5.10) private final class AllWrapper: @unchecked Sendable { - var all: [UnsafeTransfer] = [] + var all: [Model] = [] var isEmpty: Bool { self.all.isEmpty } - func append(_ value: UnsafeTransfer) { self.all.append(value) } + func append(_ value: Model) { self.all.append(value) } } #endif public func all(_ onOutput: @escaping @Sendable (Result) -> ()) -> EventLoopFuture { #if swift(>=5.10) - nonisolated(unsafe) var all: [UnsafeTransfer] = [] + nonisolated(unsafe) var all: [Model] = [] #else let all: AllWrapper = .init() #endif @@ -256,7 +256,7 @@ public final class QueryBuilder onOutput(.init(catching: { let model = Model() try model.output(from: output.qualifiedSchema(space: Model.spaceIfNotAliased, Model.schemaOrAlias)) - all.append(.init(wrappedValue: model)) + all.append(model) return model })) } @@ -274,9 +274,9 @@ public final class QueryBuilder // run eager loads return loaders.sequencedFlatMapEach(on: $1) { loader in #if swift(>=5.10) - loader.anyRun(models: all.map { $0.wrappedValue }, on: db) + loader.anyRun(models: all.map { $0 }, on: db) #else - loader.anyRun(models: all.all.map { $0.wrappedValue }, on: db) + loader.anyRun(models: all.all.map { $0 }, on: db) #endif } } diff --git a/Sources/FluentKit/Schema/DatabaseSchema.swift b/Sources/FluentKit/Schema/DatabaseSchema.swift index e994435a..3a6ad271 100644 --- a/Sources/FluentKit/Schema/DatabaseSchema.swift +++ b/Sources/FluentKit/Schema/DatabaseSchema.swift @@ -10,7 +10,7 @@ public struct DatabaseSchema: Sendable { public indirect enum DataType: Sendable { public static var int: DataType { - return .int64 + .int64 } case int8 case int16 @@ -18,7 +18,7 @@ public struct DatabaseSchema: Sendable { case int64 public static var uint: DataType { - return .uint64 + .uint64 } case uint8 case uint16 diff --git a/Sources/FluentSQL/DatabaseQuery+SQL.swift b/Sources/FluentSQL/DatabaseQuery+SQL.swift index 0731f4c3..50bf660b 100644 --- a/Sources/FluentSQL/DatabaseQuery+SQL.swift +++ b/Sources/FluentSQL/DatabaseQuery+SQL.swift @@ -2,10 +2,15 @@ import FluentKit import SQLKit extension DatabaseQuery.Value { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) } - + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) + } + public static func sql(embed: SQLQueryString) -> Self { .sql(embed) } @@ -16,8 +21,13 @@ extension DatabaseQuery.Value { } extension DatabaseQuery.Field { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(_ identifier: String) -> Self { @@ -34,8 +44,13 @@ extension DatabaseQuery.Field { } extension DatabaseQuery.Filter { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql( @@ -72,8 +87,13 @@ extension DatabaseQuery.Filter { } extension DatabaseQuery.Join { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(embed: SQLQueryString) -> Self { @@ -86,8 +106,13 @@ extension DatabaseQuery.Join { } extension DatabaseQuery.Sort { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql( diff --git a/Sources/FluentSQL/DatabaseSchema+SQL.swift b/Sources/FluentSQL/DatabaseSchema+SQL.swift index 22a522c3..3000e1a4 100644 --- a/Sources/FluentSQL/DatabaseSchema+SQL.swift +++ b/Sources/FluentSQL/DatabaseSchema+SQL.swift @@ -2,8 +2,13 @@ import FluentKit import SQLKit extension DatabaseSchema.DataType { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(_ dataType: SQLDataType) -> Self { @@ -20,8 +25,13 @@ extension DatabaseSchema.DataType { } extension DatabaseSchema.Constraint { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(_ constraint: SQLTableConstraintAlgorithm) -> Self { @@ -38,8 +48,13 @@ extension DatabaseSchema.Constraint { } extension DatabaseSchema.ConstraintAlgorithm { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(embed: SQLQueryString) -> Self { @@ -52,8 +67,13 @@ extension DatabaseSchema.ConstraintAlgorithm { } extension DatabaseSchema.FieldConstraint { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(_ constraint: SQLColumnConstraintAlgorithm) -> Self { @@ -70,8 +90,13 @@ extension DatabaseSchema.FieldConstraint { } extension DatabaseSchema.FieldDefinition { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(embed: SQLQueryString) -> Self { @@ -84,8 +109,13 @@ extension DatabaseSchema.FieldDefinition { } extension DatabaseSchema.FieldUpdate { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(embed: SQLQueryString) -> Self { @@ -98,8 +128,13 @@ extension DatabaseSchema.FieldUpdate { } extension DatabaseSchema.FieldName { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(embed: SQLQueryString) -> Self { @@ -112,8 +147,13 @@ extension DatabaseSchema.FieldName { } extension DatabaseSchema.ConstraintDelete { + @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") public static func sql(raw: String) -> Self { - .sql(SQLRaw(raw)) + .sql(unsafeRaw: raw) + } + + public static func sql(unsafeRaw: String) -> Self { + .sql(SQLRaw(unsafeRaw)) } public static func sql(embed: SQLQueryString) -> Self { diff --git a/Sources/FluentSQL/SQLJSONColumnPath.swift b/Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift similarity index 100% rename from Sources/FluentSQL/SQLJSONColumnPath.swift rename to Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift diff --git a/Sources/FluentSQL/SQLQualifiedTable+Deprecated.swift b/Sources/FluentSQL/SQLQualifiedTable+Deprecated.swift new file mode 100644 index 00000000..548a384b --- /dev/null +++ b/Sources/FluentSQL/SQLQualifiedTable+Deprecated.swift @@ -0,0 +1,6 @@ +import SQLKit + +/// Formerly a complete implementation of a qualified table SQL expression, now an alias for the equivalent +/// type available in SQLKit. +@available(*, deprecated, message: "This type is now provided by SQLKit; use that module's version.") +public typealias SQLQualifiedTable = SQLKit.SQLQualifiedTable diff --git a/Sources/FluentSQL/SQLQualifiedTable.swift b/Sources/FluentSQL/SQLQualifiedTable.swift deleted file mode 100644 index 79d55368..00000000 --- a/Sources/FluentSQL/SQLQualifiedTable.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SQLKit - -public struct SQLQualifiedTable: SQLExpression { - public var table: any SQLExpression - public var space: (any SQLExpression)? - - public init(_ table: String, space: String? = nil) { - self.init(SQLIdentifier(table), space: space.flatMap(SQLIdentifier.init(_:))) - } - - public init(_ table: any SQLExpression, space: (any SQLExpression)? = nil) { - self.table = table - self.space = space - } - - public func serialize(to serializer: inout SQLSerializer) { - if let space = self.space { - space.serialize(to: &serializer) - serializer.write(".") - } - self.table.serialize(to: &serializer) - } -} diff --git a/Sources/FluentSQL/SQLQueryConverter.swift b/Sources/FluentSQL/SQLQueryConverter.swift index 3a741021..6b1d25fa 100644 --- a/Sources/FluentSQL/SQLQueryConverter.swift +++ b/Sources/FluentSQL/SQLQueryConverter.swift @@ -9,13 +9,14 @@ public struct SQLQueryConverter { public func convert(_ fluent: DatabaseQuery) -> any SQLExpression { let sql: any SQLExpression + switch fluent.action { - case .read, .aggregate: sql = self.select(fluent) - case .create: sql = self.insert(fluent) - case .update: sql = self.update(fluent) - case .delete: sql = self.delete(fluent) - case .custom(let any): - return custom(any) + case .read: sql = self.select(fluent) + case .aggregate: sql = self.select(fluent) + case .create: sql = self.insert(fluent) + case .update: sql = self.update(fluent) + case .delete: sql = self.delete(fluent) + case .custom(let any): sql = custom(any) } return sql } @@ -23,24 +24,32 @@ public struct SQLQueryConverter { // MARK: Private private func delete(_ query: DatabaseQuery) -> any SQLExpression { - var delete = SQLDelete(table: SQLQualifiedTable(query.schema, space: query.space)) + var delete = SQLDelete(table: SQLKit.SQLQualifiedTable(query.schema, space: query.space)) + delete.predicate = self.filters(query.filters) return delete } private func update(_ query: DatabaseQuery) -> any SQLExpression { - var update = SQLUpdate(table: SQLQualifiedTable(query.schema, space: query.space)) + var update = SQLUpdate(table: SQLKit.SQLQualifiedTable(query.schema, space: query.space)) + guard case .dictionary(let values) = query.input.first else { fatalError("Missing query input generating update query") } update.values = query.fields.compactMap { field -> (any SQLExpression)? in let key: FieldKey + switch field { - case let .path(path, schema) where schema == query.schema: key = path[0] - case let .extendedPath(path, schema, space) where schema == query.schema && space == query.space: key = path[0] - default: return nil + case let .path(path, schema) where schema == query.schema: + key = path[0] + case let .extendedPath(path, schema, space) where schema == query.schema && space == query.space: + key = path[0] + default: + return nil + } + guard let value = values[key] else { + return nil } - guard let value = values[key] else { return nil } return SQLColumnAssignment(setting: SQLColumn(self.key(key)), to: self.value(value)) } update.predicate = self.filters(query.filters) @@ -49,7 +58,8 @@ public struct SQLQueryConverter { private func select(_ query: DatabaseQuery) -> any SQLExpression { var select = SQLSelect() - select.tables.append(SQLQualifiedTable(query.schema, space: query.space)) + + select.tables.append(SQLKit.SQLQualifiedTable(query.schema, space: query.space)) switch query.action { case .read: select.isDistinct = query.isUnique @@ -81,7 +91,7 @@ public struct SQLQueryConverter { } private func insert(_ query: DatabaseQuery) -> any SQLExpression { - var insert = SQLInsert(table: SQLQualifiedTable(query.schema, space: query.space)) + var insert = SQLInsert(table: SQLKit.SQLQualifiedTable(query.schema, space: query.space)) // 1. Load the first set of inputs to the query, used as a basis to validate uniformity of all inputs. guard let firstInput = query.input.first, case let .dictionary(firstValues) = firstInput else { @@ -91,20 +101,26 @@ public struct SQLQueryConverter { // 2. Translate the list of fields from the query, which are given in a meaningful, deterministic order, into // column designators. let keys = query.fields.compactMap { field -> FieldKey? in switch field { - case let .path(path, schema) where schema == query.schema: return path[0] - case let .extendedPath(path, schema, space) where schema == query.schema && space == query.space: return path[0] - default: return nil + case let .path(path, schema) where schema == query.schema: + return path[0] + case let .extendedPath(path, schema, space) where schema == query.schema && space == query.space: + return path[0] + default: + return nil } } // 3. Filter the list of columns so that only those actually provided are specified to the insert query, since // often a query will insert only some of a model's fields while still listing all of them. - let usedKeys = keys.filter { firstValues.keys.contains($0) } + let usedKeys = keys.filter { + firstValues.keys.contains($0) + } // 4. Validate each set of inputs, making sure it provides exactly the keys as the first, and convert the sets // to their underlying SQL representations. let dictionaries = query.input.map { input -> [FieldKey: any SQLExpression] in guard case let .dictionary(value) = input else { fatalError("Unexpected query input: \(input)") } guard Set(value.keys).symmetricDifference(usedKeys).isEmpty else { fatalError("Non-uniform query input: \(query.input)") } + return value.mapValues(self.value(_:)) } @@ -166,8 +182,10 @@ public struct SQLQueryConverter { method: DatabaseQuery.Join.Method, filters: [DatabaseQuery.Filter] ) -> any SQLExpression { - let table: any SQLExpression = alias.map { SQLAlias(SQLQualifiedTable(schema, space: space), as: SQLIdentifier($0)) } ?? - SQLQualifiedTable(schema, space: space) + let table: any SQLExpression = alias.map { + SQLAlias(SQLKit.SQLQualifiedTable(schema, space: space), as: SQLIdentifier($0)) + } ?? + SQLKit.SQLQualifiedTable(schema, space: space) return SQLJoin(method: self.joinMethod(method), table: table, expression: self.filters(filters) ?? SQLLiteral.boolean(true)) } @@ -197,7 +215,7 @@ public struct SQLQueryConverter { switch path.count { case 1: - field = SQLColumn(SQLIdentifier(self.key(path[0])), table: SQLQualifiedTable(schema, space: space)) + field = SQLColumn(SQLIdentifier(self.key(path[0])), table: SQLKit.SQLQualifiedTable(schema, space: space)) case 2...: field = self.delegate.nestedFieldExpression(self.key(path[0]), path[1...].map(self.key)) default: @@ -217,6 +235,7 @@ public struct SQLQueryConverter { return any as! any SQLExpression case .field(let field, let method): let name: String + switch method { case .average: name = "AVG" case .count: name = "COUNT" @@ -225,6 +244,7 @@ public struct SQLQueryConverter { case .minimum: name = "MIN" case .custom(let custom): name = custom as! String } + return SQLAlias( SQLFunction( name, @@ -344,17 +364,9 @@ public struct SQLQueryConverter { private func method(_ method: DatabaseQuery.Filter.Method) -> any SQLExpression { switch method { case .equality(let inverse): - if inverse { - return SQLBinaryOperator.notEqual - } else { - return SQLBinaryOperator.equal - } + return inverse ? SQLBinaryOperator.notEqual : SQLBinaryOperator.equal case .subset(let inverse): - if inverse { - return SQLBinaryOperator.notIn - } else { - return SQLBinaryOperator.in - } + return inverse ? SQLBinaryOperator.notIn : SQLBinaryOperator.in case .order(let inverse, let equality): switch (inverse, equality) { case (false, false): @@ -382,6 +394,7 @@ private struct EncodableDatabaseInput: Encodable { func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: SomeCodingKey.self) + for (key, value) in self.input { try container.encode(EncodableDatabaseValue(value: value), forKey: SomeCodingKey(stringValue: key.description)) } diff --git a/Sources/FluentSQL/SQLSchemaConverter.swift b/Sources/FluentSQL/SQLSchemaConverter.swift index 633e20ed..f259a040 100644 --- a/Sources/FluentSQL/SQLSchemaConverter.swift +++ b/Sources/FluentSQL/SQLSchemaConverter.swift @@ -19,12 +19,14 @@ extension SQLConverterDelegate { public struct SQLSchemaConverter { let delegate: any SQLConverterDelegate + public init(delegate: any SQLConverterDelegate) { self.delegate = delegate } public func convert(_ schema: DatabaseSchema) -> any SQLExpression { let schema = self.delegate.beforeConvert(schema) + switch schema.action { case .create: return self.create(schema) @@ -39,6 +41,7 @@ public struct SQLSchemaConverter { private func update(_ schema: DatabaseSchema) -> any SQLExpression { var update = SQLAlterTable(name: self.name(schema.schema, space: schema.space)) + update.addColumns = schema.createFields.map(self.fieldDefinition) update.dropColumns = schema.deleteFields.map(self.fieldName) update.modifyColumns = schema.updateFields.map(self.fieldUpdate) @@ -53,11 +56,13 @@ public struct SQLSchemaConverter { private func delete(_ schema: DatabaseSchema) -> any SQLExpression { let delete = SQLDropTable(table: self.name(schema.schema, space: schema.space)) + return delete } private func create(_ schema: DatabaseSchema) -> any SQLExpression { var create = SQLCreateTable(name: self.name(schema.schema, space: schema.space)) + create.columns = schema.createFields.map(self.fieldDefinition) create.tableConstraints = schema.createConstraints.map { self.constraint($0, table: schema.schema) @@ -69,7 +74,7 @@ public struct SQLSchemaConverter { } private func name(_ string: String, space: String? = nil) -> any SQLExpression { - return SQLQualifiedTable(string, space: space) + SQLKit.SQLQualifiedTable(string, space: space) } private func constraint(_ constraint: DatabaseSchema.Constraint, table: String) -> any SQLExpression { @@ -144,6 +149,7 @@ public struct SQLSchemaConverter { return "\(table).\(self.key(key))" } }.joined(separator: "+") + return "\(prefix):\(fieldsString)" } @@ -264,18 +270,8 @@ public struct SQLSchemaConverter { } } - private func key(_ key: FieldKey) -> String { - switch key { - case .id: - return "id" - case .string(let name): - return name - case .aggregate: - return key.description - case .prefix(let prefix, let key): - return self.key(prefix) + self.key(key) - } - } + @inline(__always) + private func key(_ key: FieldKey) -> String { key.description } } /// SQL drop constraint expression with awareness of foreign keys (for MySQL's broken sake). @@ -332,7 +328,9 @@ public struct SQLDropConstraint: SQLExpression { } else { serializer.write("CONSTRAINT ") } + let normalizedName = serializer.dialect.normalizeSQLConstraint(identifier: name) + normalizedName.serialize(to: &serializer) } }