From e22fe81ba7dae8bc6ad4f2f4c2de98ae378d64d7 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 3 Dec 2025 10:50:05 +0100 Subject: [PATCH 1/5] Drop Swift 5.10 --- Package.swift | 10 +++---- .../Query/Builder/QueryBuilder.swift | 28 +------------------ 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/Package.swift b/Package.swift index afe50bb5..b0f24750 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -73,9 +73,9 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), + // .enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableUpcomingFeature("InferIsolatedConformances"), + // .enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("ImmutableWeakCaptures"), ] } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder.swift b/Sources/FluentKit/Query/Builder/QueryBuilder.swift index 2c360859..99831640 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder.swift @@ -146,21 +146,6 @@ public final class QueryBuilder // MARK: Fetch public func chunk(max: Int, closure: @escaping @Sendable ([Result]) -> ()) -> EventLoopFuture { - #if swift(<5.10) - let partial: UnsafeMutableTransferBox<[Result]> = .init([]) - partial.wrappedValue.reserveCapacity(max) - return self.all { row in - partial.wrappedValue.append(row) - if partial.wrappedValue.count >= max { - closure(partial.wrappedValue) - partial.wrappedValue.removeAll(keepingCapacity: true) - } - }.flatMapThrowing { - if !partial.wrappedValue.isEmpty { - closure(partial.wrappedValue) - } - } - #else nonisolated(unsafe) var partial: [Result] = [] partial.reserveCapacity(max) @@ -175,7 +160,6 @@ public final class QueryBuilder closure(partial) } } - #endif } public func first() -> EventLoopFuture { @@ -217,19 +201,11 @@ public final class QueryBuilder } public func all() -> EventLoopFuture<[Model]> { - #if swift(<5.10) - let models: UnsafeMutableTransferBox<[Result]> = .init([]) - - return self - .all { models.wrappedValue.append($0) } - .flatMapThrowing { try models.wrappedValue.map { try $0.get() } } - #else nonisolated(unsafe) var models: [Result] = [] return self .all { models.append($0) } .flatMapThrowing { try models.map { try $0.get() } } - #endif } public func run() -> EventLoopFuture { @@ -352,6 +328,4 @@ public final class QueryBuilder } } -#if swift(<6) || !$InferSendableFromCaptures -extension Swift.KeyPath: @unchecked Swift.Sendable {} -#endif +extension KeyPath: @unchecked @retroactive Sendable {} From 3348c2aabb75ba3b13ac0a2ed43046818be70da4 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 3 Dec 2025 10:52:34 +0100 Subject: [PATCH 2/5] Update workflows --- .github/workflows/api-docs.yml | 4 ++++ .github/workflows/test.yml | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index f9461c7c..7ba4f101 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -4,6 +4,10 @@ on: branches: - main +permissions: + contents: read + id-token: write + jobs: build-and-deploy: uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c44e0d3..13961d74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: env: LOG_LEVEL: info - SWIFT_DETERMINISTIC_HASHING: 1 POSTGRES_HOSTNAME: 'psql-a' POSTGRES_HOSTNAME_A: 'psql-a' POSTGRES_HOSTNAME_B: 'psql-b' @@ -37,12 +36,15 @@ env: MONGO_HOSTNAME_A: 'mongo-a' MONGO_HOSTNAME_B: 'mongo-b' +permissions: + contents: read + jobs: linux-integration-sqlite: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:6.1-noble + container: swift:6.2-noble steps: - name: Check out package uses: actions/checkout@v4 @@ -58,7 +60,7 @@ jobs: linux-integration-mysql: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:6.1-noble + container: swift:6.2-noble services: mysql-a: { image: 'mysql:9', env: { MYSQL_ALLOW_EMPTY_PASSWORD: true, MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database } } mysql-b: { image: 'mysql:9', env: { MYSQL_ALLOW_EMPTY_PASSWORD: true, MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database } } @@ -77,7 +79,7 @@ jobs: linux-integration-psql: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:6.1-noble + container: swift:6.2-noble services: psql-a: { image: 'postgres:17', env: { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database, POSTGRES_HOST_AUTH_METHOD: scram-sha-256, POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 } } psql-b: { image: 'postgres:17', env: { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database, POSTGRES_HOST_AUTH_METHOD: scram-sha-256, POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 } } @@ -96,7 +98,7 @@ jobs: linux-integration-mongo: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:6.1-noble + container: swift:6.2-noble services: mongo-a: { image: 'mongo:6' } mongo-b: { image: 'mongo:6' } From 759763c120676c474ee3e63a9fd14f5bc94b2e46 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 3 Dec 2025 11:02:58 +0100 Subject: [PATCH 3/5] Adopt swift-format --- Package.swift | 18 +- .../FluentBenchmark/FluentBenchmarker.swift | 20 +- .../SolarSystem/GalacticJurisdiction.swift | 44 +- .../FluentBenchmark/SolarSystem/Galaxy.swift | 6 +- .../SolarSystem/Governor.swift | 29 +- .../SolarSystem/Jurisdiction.swift | 18 +- .../FluentBenchmark/SolarSystem/Moon.swift | 69 +-- .../FluentBenchmark/SolarSystem/Planet.swift | 55 +- .../SolarSystem/PlanetTag.swift | 41 +- .../FluentBenchmark/SolarSystem/Star.swift | 8 +- Sources/FluentBenchmark/SolarSystem/Tag.swift | 19 +- .../Tests/AggregateTests.swift | 26 +- .../FluentBenchmark/Tests/ArrayTests.swift | 33 +- .../FluentBenchmark/Tests/BatchTests.swift | 37 +- Sources/FluentBenchmark/Tests/CRUDTests.swift | 63 +- .../FluentBenchmark/Tests/ChildTests.swift | 70 +-- .../FluentBenchmark/Tests/ChildrenTests.swift | 20 +- .../FluentBenchmark/Tests/ChunkTests.swift | 11 +- .../FluentBenchmark/Tests/CodableTests.swift | 18 +- .../Tests/CompositeIDTests.swift | 256 +++++---- .../Tests/CompositeRelationTests.swift | 290 ++++++---- .../Tests/EagerLoadTests.swift | 208 ++++--- Sources/FluentBenchmark/Tests/EnumTests.swift | 227 ++++---- .../FluentBenchmark/Tests/FilterTests.swift | 126 ++-- .../FluentBenchmark/Tests/GroupTests.swift | 101 ++-- Sources/FluentBenchmark/Tests/IDTests.swift | 45 +- Sources/FluentBenchmark/Tests/JoinTests.swift | 102 ++-- .../Tests/MiddlewareTests.swift | 43 +- .../FluentBenchmark/Tests/MigratorTests.swift | 15 +- .../FluentBenchmark/Tests/ModelTests.swift | 112 ++-- .../Tests/OptionalParentTests.swift | 31 +- .../Tests/PaginationTests.swift | 18 +- .../FluentBenchmark/Tests/ParentTests.swift | 36 +- .../Tests/PerformanceTests+Siblings.swift | 149 +++-- .../Tests/PerformanceTests.swift | 116 ++-- .../FluentBenchmark/Tests/RangeTests.swift | 11 +- Sources/FluentBenchmark/Tests/SQLTests.swift | 15 +- .../FluentBenchmark/Tests/SchemaTests.swift | 113 ++-- Sources/FluentBenchmark/Tests/SetTests.swift | 30 +- .../FluentBenchmark/Tests/SiblingsTests.swift | 79 +-- .../Tests/SoftDeleteTests.swift | 70 ++- Sources/FluentBenchmark/Tests/SortTests.swift | 29 +- .../Tests/TimestampTests.swift | 95 +-- .../Tests/TransactionTests.swift | 53 +- .../FluentBenchmark/Tests/UniqueTests.swift | 22 +- .../Concurrency/AsyncMigration.swift | 8 +- .../Concurrency/AsyncModelMiddleware.swift | 12 +- .../Concurrency/Children+Concurrency.swift | 16 +- .../Concurrency/Database+Concurrency.swift | 14 +- .../Concurrency/EnumBuilder+Concurrency.swift | 16 +- .../Concurrency/Model+Concurrency.swift | 32 +- .../ModelResponder+Concurrency.swift | 8 +- .../OptionalChild+Concurrency.swift | 12 +- .../OptionalParent+Concurrency.swift | 8 +- .../Concurrency/Parent+Concurrency.swift | 8 +- .../QueryBuilder+Concurrency.swift | 210 +++---- .../Concurrency/Relation+Concurrency.swift | 4 +- .../SchemaBuilder+Concurrency.swift | 12 +- .../Concurrency/Siblings+Concurrency.swift | 60 +- .../FluentKit/Database/Database+Logging.swift | 12 +- Sources/FluentKit/Database/Database.swift | 21 +- Sources/FluentKit/Database/DatabaseID.swift | 2 +- .../FluentKit/Database/DatabaseInput.swift | 14 +- .../FluentKit/Database/DatabaseOutput.swift | 16 +- Sources/FluentKit/Database/Databases.swift | 27 +- .../Database/KeyPrefixingStrategy.swift | 20 +- Sources/FluentKit/Enum/EnumBuilder.swift | 30 +- Sources/FluentKit/Enum/EnumMetadata.swift | 2 +- Sources/FluentKit/Enum/EnumProperty.swift | 18 +- .../FluentKit/Enum/OptionalEnumProperty.swift | 19 +- Sources/FluentKit/Exports.swift | 7 +- Sources/FluentKit/FluentError.swift | 15 +- .../Middleware/ModelMiddleware.swift | 20 +- .../FluentKit/Middleware/ModelResponder.swift | 15 +- Sources/FluentKit/Migration/Migration.swift | 32 +- .../FluentKit/Migration/MigrationLog.swift | 2 +- Sources/FluentKit/Migration/Migrations.swift | 6 +- Sources/FluentKit/Migration/Migrator.swift | 96 ++-- Sources/FluentKit/Model/AnyModel.swift | 19 +- Sources/FluentKit/Model/EagerLoad.swift | 6 +- Sources/FluentKit/Model/Fields+Codable.swift | 32 +- Sources/FluentKit/Model/Fields.swift | 32 +- Sources/FluentKit/Model/MirrorBypass.swift | 130 ++--- Sources/FluentKit/Model/Model+CRUD.swift | 75 +-- Sources/FluentKit/Model/Model.swift | 2 +- Sources/FluentKit/Model/ModelAlias.swift | 14 +- Sources/FluentKit/Model/Schema.swift | 4 +- .../FluentKit/Operators/FieldOperators.swift | 141 ++--- .../Operators/ValueOperators+Array.swift | 50 +- .../Operators/ValueOperators+String.swift | 216 +++---- .../FluentKit/Operators/ValueOperators.swift | 68 +-- Sources/FluentKit/Properties/Boolean.swift | 5 +- .../Properties/BooleanPropertyFormat.swift | 29 +- Sources/FluentKit/Properties/Children.swift | 37 +- .../Properties/CompositeChildren.swift | 39 +- .../FluentKit/Properties/CompositeID.swift | 12 +- .../Properties/CompositeOptionalChild.swift | 47 +- .../Properties/CompositeOptionalParent.swift | 82 ++- .../Properties/CompositeParent.swift | 47 +- Sources/FluentKit/Properties/Field.swift | 11 +- Sources/FluentKit/Properties/FieldKey.swift | 4 +- Sources/FluentKit/Properties/Group.swift | 46 +- Sources/FluentKit/Properties/ID.swift | 19 +- .../Properties/OptionalBoolean.swift | 5 +- .../FluentKit/Properties/OptionalChild.swift | 31 +- .../FluentKit/Properties/OptionalField.swift | 12 +- .../FluentKit/Properties/OptionalParent.swift | 29 +- Sources/FluentKit/Properties/Parent.swift | 30 +- Sources/FluentKit/Properties/Property.swift | 8 +- Sources/FluentKit/Properties/Relation.swift | 31 +- Sources/FluentKit/Properties/Siblings.swift | 52 +- Sources/FluentKit/Properties/Timestamp.swift | 51 +- .../Properties/TimestampFormat.swift | 16 +- .../Builder/QueryBuilder+Aggregate.swift | 72 +-- .../Builder/QueryBuilder+EagerLoad.swift | 34 +- .../Query/Builder/QueryBuilder+Filter.swift | 47 +- .../Query/Builder/QueryBuilder+Group.swift | 2 +- .../QueryBuilder+Join+DirectRelations.swift | 20 +- .../Query/Builder/QueryBuilder+Join.swift | 134 +++-- .../Query/Builder/QueryBuilder+Paginate.swift | 11 +- .../Query/Builder/QueryBuilder+Set.swift | 10 +- .../Query/Builder/QueryBuilder+Sort.swift | 27 +- .../Query/Builder/QueryBuilder.swift | 65 ++- .../Query/Database/DatabaseQuery+Filter.swift | 2 +- .../Query/Database/DatabaseQuery+Join.swift | 33 +- .../Query/Database/DatabaseQuery.swift | 2 +- Sources/FluentKit/Query/QueryHistory.swift | 4 +- Sources/FluentKit/Schema/DatabaseSchema.swift | 24 +- Sources/FluentKit/Schema/SchemaBuilder.swift | 81 +-- .../Utilities/UnsafeMutableTransferBox.swift | 2 +- Sources/FluentSQL/DatabaseQuery+SQL.swift | 20 +- Sources/FluentSQL/DatabaseSchema+SQL.swift | 34 +- .../SQLDatabase+Model+Concurrency.swift | 6 +- Sources/FluentSQL/SQLDatabase+Model.swift | 43 +- .../SQLJSONColumnPath+Deprecated.swift | 6 +- Sources/FluentSQL/SQLList+Deprecated.swift | 2 +- Sources/FluentSQL/SQLQueryConverter.swift | 94 +-- Sources/FluentSQL/SQLSchemaConverter.swift | 44 +- Sources/XCTFluent/DummyDatabase.swift | 129 +++-- Sources/XCTFluent/TestDatabase.swift | 41 +- .../AsyncTests/AsyncFilterQueryTests.swift | 66 ++- .../AsyncTests/AsyncFluentKitTests.swift | 84 ++- .../AsyncTests/AsyncQueryBuilderTests.swift | 61 +- Tests/FluentKitTests/CompositeIDTests.swift | 479 ++++++++++----- .../DummyDatabaseForTestSQLSerializer.swift | 32 +- Tests/FluentKitTests/FilterQueryTests.swift | 72 ++- Tests/FluentKitTests/FluentKitTests.swift | 543 +++++++++++------- .../OptionalEnumQueryTests.swift | 28 +- .../OptionalFieldQueryTests.swift | 20 +- Tests/FluentKitTests/QueryBuilderTests.swift | 52 +- Tests/FluentKitTests/SQLTests.swift | 78 ++- Tests/FluentKitTests/TestUtilities.swift | 4 +- 152 files changed, 4284 insertions(+), 3447 deletions(-) diff --git a/Package.swift b/Package.swift index b0f24750..5aabdba4 100644 --- a/Package.swift +++ b/Package.swift @@ -71,11 +71,13 @@ let package = Package( ] ) -var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("ExistentialAny"), - // .enableUpcomingFeature("InternalImportsByDefault"), - .enableUpcomingFeature("MemberImportVisibility"), - .enableUpcomingFeature("InferIsolatedConformances"), - // .enableUpcomingFeature("NonisolatedNonsendingByDefault"), - .enableUpcomingFeature("ImmutableWeakCaptures"), -] } +var swiftSettings: [SwiftSetting] { + [ + .enableUpcomingFeature("ExistentialAny"), + // .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("InferIsolatedConformances"), + // .enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("ImmutableWeakCaptures"), + ] +} diff --git a/Sources/FluentBenchmark/FluentBenchmarker.swift b/Sources/FluentBenchmark/FluentBenchmarker.swift index b1536a24..6399042e 100644 --- a/Sources/FluentBenchmark/FluentBenchmarker.swift +++ b/Sources/FluentBenchmark/FluentBenchmarker.swift @@ -8,7 +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"), @@ -54,17 +54,17 @@ public final class FluentBenchmarker { // MARK: Utilities func runTest( - _ name: String, - _ migrations: [any Migration], - _ test: () throws -> () + _ name: String, + _ migrations: [any Migration], + _ test: () throws -> Void ) throws { try self.runTest(name, migrations, { _ in try test() }) } - + func runTest( _ name: String, _ migrations: [any Migration], - _ test: (any Database) throws -> () + _ test: (any Database) throws -> Void ) throws { // This re-initialization is required to make the middleware tests work thanks to ridiculous design flaws self.database = self.databases.database( @@ -73,12 +73,12 @@ public final class FluentBenchmarker { )! try self.runTest(name, migrations, on: self.database, test) } - + func runTest( _ name: String, _ migrations: [any Migration], on database: any Database, - _ test: (any Database) throws -> () + _ test: (any Database) throws -> Void ) throws { database.logger.notice("Running \(name)...") @@ -91,7 +91,7 @@ public final class FluentBenchmarker { database.logger.error("\(name): Error: \(String(reflecting: error))") throw error } - + let result = Result { try test(database) } // Revert migrations @@ -106,7 +106,7 @@ public final class FluentBenchmarker { throw error } } - + if case .failure(let error) = result { database.logger.error("\(name): Error: \(String(reflecting: error))") throw error diff --git a/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift b/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift index 46677bfa..111286bc 100644 --- a/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift +++ b/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift @@ -4,45 +4,45 @@ import NIOCore public final class GalacticJurisdiction: Model, @unchecked Sendable { public static let schema = "galaxy_jurisdictions" - + public final class IDValue: Fields, Hashable, @unchecked Sendable { @Parent(key: "galaxy_id") public var galaxy: Galaxy - + @Parent(key: "jurisdiction_id") public var jurisdiction: Jurisdiction - + @Field(key: "rank") public var rank: Int - + public init() {} - + public convenience init(galaxy: Galaxy, jurisdiction: Jurisdiction, rank: Int) throws { try self.init(galaxyId: galaxy.requireID(), jurisdictionId: jurisdiction.requireID(), rank: rank) } - + public init(galaxyId: Galaxy.IDValue, jurisdictionId: Jurisdiction.IDValue, rank: Int) { self.$galaxy.id = galaxyId self.$jurisdiction.id = jurisdictionId self.rank = rank } - + public static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.$galaxy.id == rhs.$galaxy.id && lhs.$jurisdiction.id == rhs.$jurisdiction.id && lhs.rank == rhs.rank } - + public func hash(into hasher: inout Hasher) { hasher.combine(self.$galaxy.id) hasher.combine(self.$jurisdiction.id) hasher.combine(self.rank) } } - + @CompositeID() public var id: IDValue? - + public init() {} - + public init(id: IDValue) { self.id = id } @@ -50,7 +50,7 @@ public final class GalacticJurisdiction: Model, @unchecked Sendable { public struct GalacticJurisdictionMigration: Migration { public init() {} - + public func prepare(on database: any Database) -> EventLoopFuture { database.schema(GalacticJurisdiction.schema) .field("galaxy_id", .uuid, .required, .references(Galaxy.schema, .id, onDelete: .cascade, onUpdate: .cascade)) @@ -59,7 +59,7 @@ public struct GalacticJurisdictionMigration: Migration { .compositeIdentifier(over: "galaxy_id", "jurisdiction_id", "rank") .create() } - + public func revert(on database: any Database) -> EventLoopFuture { database.schema(GalacticJurisdiction.schema) .delete() @@ -68,11 +68,11 @@ public struct GalacticJurisdictionMigration: Migration { public struct GalacticJurisdictionSeed: Migration { public init() {} - + public func prepare(on database: any Database) -> EventLoopFuture { database.eventLoop.flatSubmit { Galaxy.query(on: database).all().and( - Jurisdiction.query(on: database).all()) + Jurisdiction.query(on: database).all()) }.flatMap { galaxies, jurisdictions in [ ("Milky Way", "Old", 0), @@ -86,16 +86,18 @@ public struct GalacticJurisdictionSeed: Migration { ] .reduce(database.eventLoop.makeSucceededVoidFuture()) { future, data in future.flatMap { - GalacticJurisdiction.init(id: try! .init( - galaxy: galaxies.first(where: { $0.name == data.0 })!, - jurisdiction: jurisdictions.first(where: { $0.title == data.1 })!, - rank: data.2 - )).create(on: database) + GalacticJurisdiction.init( + id: try! .init( + galaxy: galaxies.first(where: { $0.name == data.0 })!, + jurisdiction: jurisdictions.first(where: { $0.title == data.1 })!, + rank: data.2 + ) + ).create(on: database) } } } } - + public func revert(on database: any Database) -> EventLoopFuture { GalacticJurisdiction.query(on: database).delete() } diff --git a/Sources/FluentBenchmark/SolarSystem/Galaxy.swift b/Sources/FluentBenchmark/SolarSystem/Galaxy.swift index 3bba210a..1c497360 100644 --- a/Sources/FluentBenchmark/SolarSystem/Galaxy.swift +++ b/Sources/FluentBenchmark/SolarSystem/Galaxy.swift @@ -5,7 +5,7 @@ import XCTest public final class Galaxy: Model, @unchecked Sendable { public static let schema = "galaxies" - + @ID public var id: UUID? @@ -14,7 +14,7 @@ public final class Galaxy: Model, @unchecked Sendable { @Children(for: \.$galaxy) public var stars: [Star] - + @Siblings(through: GalacticJurisdiction.self, from: \.$id.$galaxy, to: \.$id.$jurisdiction) public var jurisdictions: [Jurisdiction] @@ -49,7 +49,7 @@ public struct GalaxySeed: AsyncMigration { "Andromeda", "Milky Way", "Pinwheel Galaxy", - "Messier 82" + "Messier 82", ] .map { Galaxy(name: $0) } .create(on: database) diff --git a/Sources/FluentBenchmark/SolarSystem/Governor.swift b/Sources/FluentBenchmark/SolarSystem/Governor.swift index fbb28f8f..63d42586 100644 --- a/Sources/FluentBenchmark/SolarSystem/Governor.swift +++ b/Sources/FluentBenchmark/SolarSystem/Governor.swift @@ -15,7 +15,7 @@ public final class Governor: Model, @unchecked Sendable { @Parent(key: "planet_id") public var planet: Planet - public init() { } + public init() {} public init(id: IDValue? = nil, name: String) { self.id = id @@ -45,22 +45,23 @@ public struct GovernorMigration: Migration { } public struct GovernorSeed: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { Planet.query(on: database).all().flatMap { planets in - .andAllSucceed(planets.map { planet in - let governor: Governor? - switch planet.name { - case "Mars": - governor = .init(name: "John Doe") - case "Earth": - governor = .init(name: "Jane Doe") - default: - return database.eventLoop.makeSucceededVoidFuture() - } - return planet.$governor.create(governor!, on: database) - }, on: database.eventLoop) + .andAllSucceed( + planets.map { planet in + let governor: Governor? + switch planet.name { + case "Mars": + governor = .init(name: "John Doe") + case "Earth": + governor = .init(name: "Jane Doe") + default: + return database.eventLoop.makeSucceededVoidFuture() + } + return planet.$governor.create(governor!, on: database) + }, on: database.eventLoop) } } diff --git a/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift b/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift index a48c0f4a..75375589 100644 --- a/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift +++ b/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift @@ -5,18 +5,18 @@ import XCTest public final class Jurisdiction: Model, @unchecked Sendable { public static let schema = "jurisdictions" - + @ID(key: .id) public var id: UUID? - + @Field(key: "title") public var title: String - + @Siblings(through: GalacticJurisdiction.self, from: \.$id.$jurisdiction, to: \.$id.$galaxy) public var galaxies: [Galaxy] - + public init() {} - + public init(id: IDValue? = nil, title: String) { self.id = id self.title = title @@ -25,7 +25,7 @@ public final class Jurisdiction: Model, @unchecked Sendable { public struct JurisdictionMigration: Migration { public init() {} - + public func prepare(on database: any Database) -> EventLoopFuture { database.schema(Jurisdiction.schema) .field(.id, .uuid, .identifier(auto: false), .required) @@ -33,7 +33,7 @@ public struct JurisdictionMigration: Migration { .unique(on: "title") .create() } - + public func revert(on database: any Database) -> EventLoopFuture { database.schema(Jurisdiction.schema) .delete() @@ -42,7 +42,7 @@ public struct JurisdictionMigration: Migration { public struct JurisdictionSeed: Migration { public init() {} - + public func prepare(on database: any Database) -> EventLoopFuture { [ "Old", @@ -54,7 +54,7 @@ public struct JurisdictionSeed: Migration { .map { Jurisdiction(title: $0) } .create(on: database) } - + public func revert(on database: any Database) -> EventLoopFuture { Jurisdiction.query(on: database) .delete() diff --git a/Sources/FluentBenchmark/SolarSystem/Moon.swift b/Sources/FluentBenchmark/SolarSystem/Moon.swift index 3900518b..17cdcf8f 100644 --- a/Sources/FluentBenchmark/SolarSystem/Moon.swift +++ b/Sources/FluentBenchmark/SolarSystem/Moon.swift @@ -20,7 +20,7 @@ public final class Moon: Model, @unchecked Sendable { @Parent(key: "planet_id") public var planet: Planet - public init() { } + public init() {} public init( id: IDValue? = nil, @@ -36,7 +36,7 @@ public final class Moon: Model, @unchecked Sendable { } public struct MoonMigration: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { database.schema("moons") @@ -54,41 +54,42 @@ public struct MoonMigration: Migration { } public final class MoonSeed: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { Planet.query(on: database).all().flatMap { planets in - .andAllSucceed(planets.map { planet in - let moons: [Moon] - switch planet.name { - case "Earth": - moons = [ - .init(name: "Moon", craters: 10, comets: 10) - ] - case "Mars": - moons = [ - .init(name: "Deimos", craters: 1, comets: 5), - .init(name: "Phobos", craters: 20, comets: 3) - ] - case "Jupiter": - moons = [ - .init(name: "Io", craters: 10, comets: 10), - .init(name: "Europa", craters: 10, comets: 10), - .init(name: "Ganymede", craters: 10, comets: 10), - .init(name: "callisto", craters: 10, comets: 10), - ] - case "Saturn": - moons = [ - .init(name: "Titan", craters: 10, comets: 10), - .init(name: "Prometheus", craters: 10, comets: 10), - .init(name: "Atlas", craters: 9, comets: 8), - .init(name: "Janus", craters: 15, comets: 9) - ] - default: - moons = [] - } - return planet.$moons.create(moons, on: database) - }, on: database.eventLoop) + .andAllSucceed( + planets.map { planet in + let moons: [Moon] + switch planet.name { + case "Earth": + moons = [ + .init(name: "Moon", craters: 10, comets: 10) + ] + case "Mars": + moons = [ + .init(name: "Deimos", craters: 1, comets: 5), + .init(name: "Phobos", craters: 20, comets: 3), + ] + case "Jupiter": + moons = [ + .init(name: "Io", craters: 10, comets: 10), + .init(name: "Europa", craters: 10, comets: 10), + .init(name: "Ganymede", craters: 10, comets: 10), + .init(name: "callisto", craters: 10, comets: 10), + ] + case "Saturn": + moons = [ + .init(name: "Titan", craters: 10, comets: 10), + .init(name: "Prometheus", craters: 10, comets: 10), + .init(name: "Atlas", craters: 9, comets: 8), + .init(name: "Janus", craters: 15, comets: 9), + ] + default: + moons = [] + } + return planet.$moons.create(moons, on: database) + }, on: database.eventLoop) } } diff --git a/Sources/FluentBenchmark/SolarSystem/Planet.swift b/Sources/FluentBenchmark/SolarSystem/Planet.swift index f9474d7e..eab7e8e5 100644 --- a/Sources/FluentBenchmark/SolarSystem/Planet.swift +++ b/Sources/FluentBenchmark/SolarSystem/Planet.swift @@ -20,17 +20,17 @@ public final class Planet: Model, @unchecked Sendable { @Children(for: \.$planet) public var moons: [Moon] - + @OptionalChild(for: \.$planet) public var governor: Governor? @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag) public var tags: [Tag] - + @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? - public init() { } + public init() {} public init(id: IDValue? = nil, name: String) { self.id = id @@ -61,33 +61,34 @@ public struct PlanetMigration: Migration { } public struct PlanetSeed: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { Star.query(on: database).all().flatMap { stars in - .andAllSucceed(stars.map { star in - let planets: [Planet] - switch star.name { - case "Sol": - planets = [ - .init(name: "Mercury"), - .init(name: "Venus"), - .init(name: "Earth"), - .init(name: "Mars"), - .init(name: "Jupiter"), - .init(name: "Saturn"), - .init(name: "Uranus"), - .init(name: "Nepture"), - ] - case "Alpha Centauri": - planets = [ - .init(name: "Proxima Centauri b") - ] - default: - planets = [] - } - return star.$planets.create(planets, on: database) - }, on: database.eventLoop) + .andAllSucceed( + stars.map { star in + let planets: [Planet] + switch star.name { + case "Sol": + planets = [ + .init(name: "Mercury"), + .init(name: "Venus"), + .init(name: "Earth"), + .init(name: "Mars"), + .init(name: "Jupiter"), + .init(name: "Saturn"), + .init(name: "Uranus"), + .init(name: "Nepture"), + ] + case "Alpha Centauri": + planets = [ + .init(name: "Proxima Centauri b") + ] + default: + planets = [] + } + return star.$planets.create(planets, on: database) + }, on: database.eventLoop) } } diff --git a/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift b/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift index 8dc2e74d..f0008a03 100644 --- a/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift +++ b/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift @@ -5,7 +5,7 @@ import XCTest public final class PlanetTag: Model, @unchecked Sendable { public static let schema = "planet+tag" - + @ID(key: .id) public var id: UUID? @@ -14,11 +14,11 @@ public final class PlanetTag: Model, @unchecked Sendable { @Parent(key: "tag_id") public var tag: Tag - + @OptionalField(key: "comments") public var comments: String? - public init() { } + public init() {} public init(id: IDValue? = nil, planetID: Planet.IDValue, tagID: Tag.IDValue, comments: String? = nil) { self.id = id @@ -29,7 +29,7 @@ public final class PlanetTag: Model, @unchecked Sendable { } public struct PlanetTagMigration: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { database.schema(PlanetTag.schema) @@ -48,8 +48,8 @@ public struct PlanetTagMigration: Migration { } public struct PlanetTagSeed: Migration { - public init() { } - + public init() {} + public func prepare(on database: any Database) -> EventLoopFuture { let planets = Planet.query(on: database).all() let tags = Tag.query(on: database).all() @@ -58,20 +58,21 @@ public struct PlanetTagSeed: Migration { let gasGiant = tags.filter { $0.name == "Gas Giant" }.first! let smallRocky = tags.filter { $0.name == "Small Rocky" }.first! - return .andAllSucceed(planets.map { planet in - let tags: [Tag] - switch planet.name { - case "Mercury", "Venus", "Mars", "Proxima Centauri b": - tags = [smallRocky] - case "Earth": - tags = [inhabited, smallRocky] - case "Jupiter", "Saturn", "Uranus", "Neptune": - tags = [gasGiant] - default: - tags = [] - } - return planet.$tags.attach(tags, on: database) - }, on: database.eventLoop) + return .andAllSucceed( + planets.map { planet in + let tags: [Tag] + switch planet.name { + case "Mercury", "Venus", "Mars", "Proxima Centauri b": + tags = [smallRocky] + case "Earth": + tags = [inhabited, smallRocky] + case "Jupiter", "Saturn", "Uranus", "Neptune": + tags = [gasGiant] + default: + tags = [] + } + return planet.$tags.attach(tags, on: database) + }, on: database.eventLoop) } } diff --git a/Sources/FluentBenchmark/SolarSystem/Star.swift b/Sources/FluentBenchmark/SolarSystem/Star.swift index c687fc4c..5de097f5 100644 --- a/Sources/FluentBenchmark/SolarSystem/Star.swift +++ b/Sources/FluentBenchmark/SolarSystem/Star.swift @@ -17,11 +17,11 @@ public final class Star: Model, @unchecked Sendable { @Children(for: \.$star) public var planets: [Planet] - + @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? - public init() { } + public init() {} public init(id: IDValue? = nil, name: String, galaxyId: Galaxy.IDValue? = nil) { self.id = id @@ -52,13 +52,13 @@ public final class StarSeed: AsyncMigration { public func prepare(on database: any Database) async throws { var stars: [Star] = [] - + for galaxy in try await Galaxy.query(on: database).all() { switch galaxy.name { case "Milky Way": stars.append(contentsOf: [ .init(name: "Sol", galaxyId: galaxy.id!), - .init(name: "Alpha Centauri", galaxyId: galaxy.id!) + .init(name: "Alpha Centauri", galaxyId: galaxy.id!), ]) case "Andromeda": stars.append(.init(name: "Alpheratz", galaxyId: galaxy.id!)) diff --git a/Sources/FluentBenchmark/SolarSystem/Tag.swift b/Sources/FluentBenchmark/SolarSystem/Tag.swift index e99342a0..9b64135b 100644 --- a/Sources/FluentBenchmark/SolarSystem/Tag.swift +++ b/Sources/FluentBenchmark/SolarSystem/Tag.swift @@ -15,7 +15,7 @@ public final class Tag: Model, @unchecked Sendable { @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet) public var planets: [Planet] - public init() { } + public init() {} public init(id: UUID? = nil, name: String) { self.id = id @@ -24,7 +24,7 @@ public final class Tag: Model, @unchecked Sendable { } public struct TagMigration: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { database.schema("tags") @@ -39,15 +39,16 @@ public struct TagMigration: Migration { } public final class TagSeed: Migration { - public init() { } + public init() {} public func prepare(on database: any Database) -> EventLoopFuture { - .andAllSucceed([ - "Small Rocky", "Gas Giant", "Inhabited" - ].map { - Tag(name: $0) - .create(on: database) - }, on: database.eventLoop) + .andAllSucceed( + [ + "Small Rocky", "Gas Giant", "Inhabited", + ].map { + Tag(name: $0) + .create(on: database) + }, on: database.eventLoop) } public func revert(on database: any Database) -> EventLoopFuture { diff --git a/Sources/FluentBenchmark/Tests/AggregateTests.swift b/Sources/FluentBenchmark/Tests/AggregateTests.swift index 4103635c..16d8cebd 100644 --- a/Sources/FluentBenchmark/Tests/AggregateTests.swift +++ b/Sources/FluentBenchmark/Tests/AggregateTests.swift @@ -8,9 +8,12 @@ extension FluentBenchmarker { } private func testAggregate_all(max: Bool) throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { // whole table let count = try Planet.query(on: self.database) .count().wait() @@ -52,9 +55,12 @@ extension FluentBenchmarker { } private func testAggregate_emptyDatabase(max: Bool) throws { - try self.runTest(#function, [ - SolarSystem(seed: false) - ]) { + try self.runTest( + #function, + [ + SolarSystem(seed: false) + ] + ) { // whole table let count = try Planet.query(on: self.database) .count().wait() @@ -62,10 +68,10 @@ extension FluentBenchmarker { // max if max { - let maxName = try Planet.query(on: self.database) - .max(\.$name).wait() - // expect error? - XCTAssertNil(maxName) + let maxName = try Planet.query(on: self.database) + .max(\.$name).wait() + // expect error? + XCTAssertNil(maxName) } } } diff --git a/Sources/FluentBenchmark/Tests/ArrayTests.swift b/Sources/FluentBenchmark/Tests/ArrayTests.swift index 8ca7b7d4..0cd36621 100644 --- a/Sources/FluentBenchmark/Tests/ArrayTests.swift +++ b/Sources/FluentBenchmark/Tests/ArrayTests.swift @@ -11,9 +11,12 @@ extension FluentBenchmarker { } private func testArray_basic() throws { - try runTest(#function, [ - FooMigration(), - ]) { + try runTest( + #function, + [ + FooMigration() + ] + ) { let new = Foo( bar: [1, 2, 3], baz: ["4", "5", "6"], @@ -32,9 +35,12 @@ extension FluentBenchmarker { } private func testArray_set() throws { - try self.runTest(#function, [ - FooSetMigration(), - ]) { + try self.runTest( + #function, + [ + FooSetMigration() + ] + ) { let foo = FooSet(bar: ["a", "b", "c"]) try foo.create(on: self.database).wait() let fetched = try FooSet.find(foo.id, on: self.database).wait() @@ -43,9 +49,12 @@ extension FluentBenchmarker { } private func testArray_stringEnum() throws { - try self.runTest(#function, [ - UserMigration(), - ]) { + try self.runTest( + #function, + [ + UserMigration() + ] + ) { // test array w/ 2 values do { let user = User(roles: [.admin, .employee]) @@ -83,7 +92,7 @@ private final class Foo: Model, @unchecked Sendable { @Field(key: "qux") var qux: [Qux] - init() { } + init() {} init(id: UUID? = nil, bar: [Int], baz: [String]?, qux: [Qux]) { self.id = id @@ -123,7 +132,7 @@ private final class User: Model, @unchecked Sendable { @Field(key: "roles") var roles: [Role] - init() { } + init() {} init(id: UUID? = nil, roles: [Role]) { self.id = id @@ -153,7 +162,7 @@ private final class FooSet: Model, @unchecked Sendable { @Field(key: "bar") var bar: Set - init() { } + init() {} init(id: IDValue? = nil, bar: Set) { self.id = id diff --git a/Sources/FluentBenchmark/Tests/BatchTests.swift b/Sources/FluentBenchmark/Tests/BatchTests.swift index 3e8abc77..ac44f8d9 100644 --- a/Sources/FluentBenchmark/Tests/BatchTests.swift +++ b/Sources/FluentBenchmark/Tests/BatchTests.swift @@ -9,9 +9,12 @@ extension FluentBenchmarker { } private func testBatch_create() throws { - try runTest(#function, [ - GalaxyMigration() - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let galaxies = Array("abcdefghijklmnopqrstuvwxyz").map { letter in return Galaxy(name: .init(letter)) } @@ -23,10 +26,13 @@ extension FluentBenchmarker { } private func testBatch_update() throws { - try runTest(#function, [ - GalaxyMigration(), - GalaxySeed() - ]) { + try runTest( + #function, + [ + GalaxyMigration(), + GalaxySeed(), + ] + ) { try Galaxy.query(on: self.database).set(\.$name, to: "Foo") .update().wait() @@ -38,15 +44,20 @@ extension FluentBenchmarker { } private func testBatch_delete() throws { - try runTest(#function, [ - GalaxyMigration(), - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let galaxies = Array("abcdefghijklmnopqrstuvwxyz").map { letter in return Galaxy(name: .init(letter)) } - try EventLoopFuture.andAllSucceed(galaxies.map { - $0.create(on: self.database) - }, on: self.database.eventLoop).wait() + try EventLoopFuture.andAllSucceed( + galaxies.map { + $0.create(on: self.database) + }, on: self.database.eventLoop + ).wait() let count = try Galaxy.query(on: self.database).count().wait() XCTAssertEqual(count, 26) diff --git a/Sources/FluentBenchmark/Tests/CRUDTests.swift b/Sources/FluentBenchmark/Tests/CRUDTests.swift index ccc9258a..f2494f1d 100644 --- a/Sources/FluentBenchmark/Tests/CRUDTests.swift +++ b/Sources/FluentBenchmark/Tests/CRUDTests.swift @@ -11,18 +11,22 @@ extension FluentBenchmarker { } private func testCRUD_create() throws { - try self.runTest(#function, [ - GalaxyMigration() - ]) { + try self.runTest( + #function, + [ + GalaxyMigration() + ] + ) { let galaxy = Galaxy(name: "Messier") galaxy.name += " 82" try! galaxy.save(on: self.database).wait() XCTAssertNotNil(galaxy.id) - guard let fetched = try Galaxy.query(on: self.database) - .filter(\.$name == "Messier 82") - .first() - .wait() + guard + let fetched = try Galaxy.query(on: self.database) + .filter(\.$name == "Messier 82") + .first() + .wait() else { XCTFail("unexpected empty result set") return @@ -38,13 +42,17 @@ extension FluentBenchmarker { } private func testCRUD_read() throws { - try runTest(#function, [ - GalaxyMigration(), - GalaxySeed() - ]) { - guard let milkyWay = try Galaxy.query(on: self.database) - .filter(\.$name == "Milky Way") - .first().wait() + try runTest( + #function, + [ + GalaxyMigration(), + GalaxySeed(), + ] + ) { + guard + let milkyWay = try Galaxy.query(on: self.database) + .filter(\.$name == "Milky Way") + .first().wait() else { XCTFail("unpexected missing galaxy") return @@ -57,9 +65,12 @@ extension FluentBenchmarker { } private func testCRUD_update() throws { - try runTest(#function, [ - GalaxyMigration() - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let galaxy = Galaxy(name: "Milkey Way") try galaxy.save(on: self.database).wait() galaxy.name = "Milky Way" @@ -81,9 +92,12 @@ extension FluentBenchmarker { } private func testCRUD_delete() throws { - try runTest(#function, [ - GalaxyMigration(), - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let galaxy = Galaxy(name: "Milky Way") try galaxy.save(on: self.database).wait() try galaxy.delete(on: self.database).wait() @@ -98,9 +112,12 @@ extension FluentBenchmarker { } public func testAsyncCreate() throws { - try runTest(#function, [ - GalaxyMigration() - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let a = Galaxy(name: "a") let b = Galaxy(name: "b") _ = try a.save(on: self.database).and(b.save(on: self.database)).wait() diff --git a/Sources/FluentBenchmark/Tests/ChildTests.swift b/Sources/FluentBenchmark/Tests/ChildTests.swift index cfda2c1e..ff953664 100644 --- a/Sources/FluentBenchmark/Tests/ChildTests.swift +++ b/Sources/FluentBenchmark/Tests/ChildTests.swift @@ -1,34 +1,37 @@ import FluentKit +import FluentSQL import Foundation import NIOCore -import FluentSQL import SQLKit import XCTest extension FluentBenchmarker { public func testChild() throws { try self.testChild_with() - + guard let sql = self.database as? any SQLDatabase else { return } try self.testChild_sqlIdInt(sql) - + } private func testChild_with() throws { - try self.runTest(#function, [ - FooMigration(), - BarMigration(), - BazMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration(), + BarMigration(), + BazMigration(), + ] + ) { let foo = Foo(name: "a") try foo.save(on: self.database).wait() let bar = Bar(bar: 42, fooID: foo.id!) try bar.save(on: self.database).wait() let baz = Baz(baz: 3.14) try baz.save(on: self.database).wait() - + // Test relationship @Parent - @OptionalChild // query(on: Parent) let foos = try Foo.query(on: self.database) @@ -42,71 +45,72 @@ extension FluentBenchmarker { // Child `baz` isn't eager loaded XCTAssertNil(foo.baz?.baz) } - + // Test relationship @Parent - @OptionalChild // query(on: Child) let bars = try Bar.query(on: self.database) .with(\.$foo) .all().wait() - + for bar in bars { XCTAssertEqual(bar.foo.name, "a") } - + // Test relationship @OptionalParent - @OptionalChild // query(on: Child) let bazs = try Baz.query(on: self.database) .with(\.$foo) .all().wait() - + for baz in bazs { // test with missing parent XCTAssertNil(baz.foo?.name) } - + baz.$foo.id = foo.id try baz.save(on: self.database).wait() - + let updatedBazs = try Baz.query(on: self.database) .with(\.$foo) .all().wait() - + for updatedBaz in updatedBazs { // test with valid parent XCTAssertEqual(updatedBaz.foo?.name, "a") } } } - + private func testChild_sqlIdInt(_ sql: any SQLDatabase) throws { - try self.runTest(#function, [ - GameMigration(), - PlayerMigration() - ]) { + try self.runTest( + #function, + [ + GameMigration(), + PlayerMigration(), + ] + ) { let game = Game(title: "Solitare") try game.save(on: self.database).wait() - + let frantisek = Player(name: "Frantisek", gameID: game.id!) try frantisek.save(on: self.database).wait() - - + let player = try Player.query(on: self.database) .with(\.$game) .first().wait() - + XCTAssertNotNil(player) if let player = player { XCTAssertEqual(player.id, frantisek.id) XCTAssertEqual(player.name, frantisek.name) XCTAssertEqual(player.$game.id, frantisek.$game.id) XCTAssertEqual(player.$game.id, game.id) - + } } } } - private final class Foo: Model, @unchecked Sendable { static let schema = "foos" @@ -122,7 +126,7 @@ private final class Foo: Model, @unchecked Sendable { @OptionalChild(for: \.$foo) var baz: Baz? - init() { } + init() {} init(id: IDValue? = nil, name: String) { self.id = id @@ -155,7 +159,7 @@ private final class Bar: Model, @unchecked Sendable { @Parent(key: "foo_id") var foo: Foo - init() { } + init() {} init(id: IDValue? = nil, bar: Int, fooID: Foo.IDValue) { self.id = id @@ -191,7 +195,7 @@ private final class Baz: Model, @unchecked Sendable { @OptionalParent(key: "foo_id") var foo: Foo? - init() { } + init() {} init(id: IDValue? = nil, baz: Double, fooID: Foo.IDValue? = nil) { self.id = id @@ -226,8 +230,8 @@ private final class Game: Model, @unchecked Sendable { // It's a solitare game :P @OptionalChild(for: \.$game) var player: Player? - - init() { } + + init() {} init( id: Int? = nil, @@ -263,7 +267,7 @@ private final class Player: Model, @unchecked Sendable { @Parent(key: "game_id") var game: Game - init() { } + init() {} init( id: Int? = nil, diff --git a/Sources/FluentBenchmark/Tests/ChildrenTests.swift b/Sources/FluentBenchmark/Tests/ChildrenTests.swift index 62b3d0f0..474ab97b 100644 --- a/Sources/FluentBenchmark/Tests/ChildrenTests.swift +++ b/Sources/FluentBenchmark/Tests/ChildrenTests.swift @@ -9,11 +9,14 @@ extension FluentBenchmarker { } private func testChildren_with() throws { - try self.runTest(#function, [ - FooMigration(), - BarMigration(), - BazMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration(), + BarMigration(), + BazMigration(), + ] + ) { let foo = Foo(name: "a") try foo.save(on: self.database).wait() let bar = Bar(bar: 42, fooID: foo.id!) @@ -34,7 +37,6 @@ extension FluentBenchmarker { } } - private final class Foo: Model, @unchecked Sendable { static let schema = "foos" @@ -50,7 +52,7 @@ private final class Foo: Model, @unchecked Sendable { @Children(for: \.$foo) var bazs: [Baz] - init() { } + init() {} init(id: IDValue? = nil, name: String) { self.id = id @@ -83,7 +85,7 @@ private final class Bar: Model, @unchecked Sendable { @Parent(key: "foo_id") var foo: Foo - init() { } + init() {} init(id: IDValue? = nil, bar: Int, fooID: Foo.IDValue) { self.id = id @@ -118,7 +120,7 @@ private final class Baz: Model, @unchecked Sendable { @Parent(key: "foo_id") var foo: Foo - init() { } + init() {} init(id: IDValue? = nil, baz: Double, fooID: Foo.IDValue) { self.id = id diff --git a/Sources/FluentBenchmark/Tests/ChunkTests.swift b/Sources/FluentBenchmark/Tests/ChunkTests.swift index cd7f136a..dae57616 100644 --- a/Sources/FluentBenchmark/Tests/ChunkTests.swift +++ b/Sources/FluentBenchmark/Tests/ChunkTests.swift @@ -1,5 +1,5 @@ -import NIOCore import NIOConcurrencyHelpers +import NIOCore import XCTest extension FluentBenchmarker { @@ -8,9 +8,12 @@ extension FluentBenchmarker { } private func testChunk_fetch() throws { - try runTest(#function, [ - GalaxyMigration(), - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let saves = (1...512).map { i -> EventLoopFuture in return Galaxy(name: "Milky Way \(i)") diff --git a/Sources/FluentBenchmark/Tests/CodableTests.swift b/Sources/FluentBenchmark/Tests/CodableTests.swift index 90b0d71c..3110aa81 100644 --- a/Sources/FluentBenchmark/Tests/CodableTests.swift +++ b/Sources/FluentBenchmark/Tests/CodableTests.swift @@ -10,13 +10,13 @@ extension FluentBenchmarker { private func testCodable_decodeError() throws { let json = """ - { - "title": "Test Question", - "body": "Test Question Body", - "answer": "Test Answer", - "project": "TestProject" - } - """ + { + "title": "Test Question", + "body": "Test Question Body", + "answer": "Test Answer", + "project": "TestProject" + } + """ do { _ = try JSONDecoder().decode(Question.self, from: .init(json.utf8)) @@ -46,7 +46,7 @@ final class Question: Model, @unchecked Sendable { @Parent(key: "project_id") var project: Project - init() { } + init() {} init(id: Int? = nil, title: String, body: String = "", answer: String = "", projectId: Project.IDValue) { self.id = id @@ -72,7 +72,7 @@ final class Project: Model, @unchecked Sendable { @Children(for: \.$project) var questions: [Question] - init() { } + init() {} init(id: String, title: String, links: [String] = []) { self.id = id diff --git a/Sources/FluentBenchmark/Tests/CompositeIDTests.swift b/Sources/FluentBenchmark/Tests/CompositeIDTests.swift index 9f53949b..b2ec5479 100644 --- a/Sources/FluentBenchmark/Tests/CompositeIDTests.swift +++ b/Sources/FluentBenchmark/Tests/CompositeIDTests.swift @@ -1,8 +1,8 @@ import FluentKit import Foundation import NIOCore -import XCTest import SQLKit +import XCTest extension FluentBenchmarker { public func testCompositeID() throws { @@ -13,29 +13,32 @@ extension FluentBenchmarker { try self.testCompositeID_eagerLoaders() try self.testCompositeID_arrayCreateAndDelete() try self.testCompositeID_count() - + // Embed this here instead of having to update all the Fluent drivers if self.database is any SQLDatabase { try self.testCompositeRelations() } } - + private func testCompositeID_create() throws { - try self.runTest(#function, [ - CompositeIDModelMigration(), - ]) { + try self.runTest( + #function, + [ + CompositeIDModelMigration() + ] + ) { let newModel = CompositeIDModel(name: "A", dimensions: 1, additionalInfo: nil) try newModel.create(on: self.database).wait() - + let count = try CompositeIDModel.query(on: self.database).count(\.$id.$name).wait() XCTAssertEqual(count, 1) - + let anotherNewModel = CompositeIDModel(name: "A", dimensions: 1, additionalInfo: nil) XCTAssertThrowsError(try anotherNewModel.create(on: self.database).wait()) - + let differentNewModel = CompositeIDModel(name: "B", dimensions: 1, additionalInfo: nil) try differentNewModel.create(on: self.database).wait() - + let anotherDifferentNewModel = CompositeIDModel(name: "A", dimensions: 2, additionalInfo: nil) try anotherDifferentNewModel.create(on: self.database).wait() @@ -43,133 +46,170 @@ extension FluentBenchmarker { XCTAssertEqual(countAgain, 3) } } - + private func testCompositeID_lookup() throws { - try self.runTest(#function, [ - CompositeIDModelMigration(), - CompositeIDModelSeed(), - ]) { + try self.runTest( + #function, + [ + CompositeIDModelMigration(), + CompositeIDModelSeed(), + ] + ) { let found = try CompositeIDModel.find(.init(name: "A", dimensions: 1), on: self.database).wait() XCTAssertNotNil(found) - + let foundByPartial = try CompositeIDModel.query(on: self.database).filter(\.$id.$name == "B").all().wait() XCTAssertEqual(foundByPartial.count, 1) XCTAssertEqual(foundByPartial.first?.id?.dimensions, 1) - + let foundByOtherPartial = try CompositeIDModel.query(on: self.database).filter(\.$id.$dimensions == 2).all().wait() XCTAssertEqual(foundByOtherPartial.count, 1) XCTAssertEqual(foundByOtherPartial.first?.id?.name, "A") } } - + private func testCompositeID_update() throws { - try self.runTest(#function, [ - CompositeIDModelMigration(), - CompositeIDModelSeed(), - ]) { + try self.runTest( + #function, + [ + CompositeIDModelMigration(), + CompositeIDModelSeed(), + ] + ) { let existing = try XCTUnwrap(CompositeIDModel.find(.init(name: "A", dimensions: 1), on: self.database).wait()) - + existing.additionalInfo = "additional" try existing.update(on: self.database).wait() - - XCTAssertEqual(try CompositeIDModel.query(on: self.database).filter(\.$additionalInfo == "additional").count(\.$id.$name).wait(), 1) - - try CompositeIDModel.query(on: self.database).filter(\.$id.$name == "A").filter(\.$id.$dimensions == 1).set(\.$id.$dimensions, to: 3).update().wait() + + XCTAssertEqual( + try CompositeIDModel.query(on: self.database).filter(\.$additionalInfo == "additional").count(\.$id.$name).wait(), 1) + + try CompositeIDModel.query(on: self.database).filter(\.$id.$name == "A").filter(\.$id.$dimensions == 1).set( + \.$id.$dimensions, to: 3 + ).update().wait() XCTAssertNotNil(try CompositeIDModel.find(.init(name: "A", dimensions: 3), on: self.database).wait()) } } - + private func testCompositeID_asPivot() throws { - try self.runTest(#function, [ - GalaxyMigration(), - JurisdictionMigration(), - GalacticJurisdictionMigration(), - GalaxySeed(), - JurisdictionSeed(), - ]) { + try self.runTest( + #function, + [ + GalaxyMigration(), + JurisdictionMigration(), + GalacticJurisdictionMigration(), + GalaxySeed(), + JurisdictionSeed(), + ] + ) { let milkyWayGalaxy = try XCTUnwrap(Galaxy.query(on: self.database).filter(\.$name == "Milky Way").first().wait()) let andromedaGalaxy = try XCTUnwrap(Galaxy.query(on: self.database).filter(\.$name == "Andromeda").first().wait()) let oldJurisdiction = try XCTUnwrap(Jurisdiction.query(on: self.database).filter(\.$title == "Old").first().wait()) let noneJurisdiction = try XCTUnwrap(Jurisdiction.query(on: self.database).filter(\.$title == "None").first().wait()) - + try milkyWayGalaxy.$jurisdictions.attach(oldJurisdiction, method: .always, on: self.database, { $0.$id.$rank.value = 1 }).wait() try noneJurisdiction.$galaxies.attach(andromedaGalaxy, method: .always, on: self.database, { $0.$id.$rank.value = 0 }).wait() try noneJurisdiction.$galaxies.attach(milkyWayGalaxy, method: .always, on: self.database, { $0.$id.$rank.value = 2 }).wait() - + let pivots = try GalacticJurisdiction.query(on: self.database).all().wait() - + XCTAssertEqual(pivots.count, 3) - XCTAssertTrue(pivots.contains(where: { $0.id!.$galaxy.id == milkyWayGalaxy.id! && $0.id!.$jurisdiction.id == oldJurisdiction.id! && $0.id!.rank == 1 })) - XCTAssertTrue(pivots.contains(where: { $0.id!.$galaxy.id == milkyWayGalaxy.id! && $0.id!.$jurisdiction.id == noneJurisdiction.id! && $0.id!.rank == 2 })) - XCTAssertTrue(pivots.contains(where: { $0.id!.$galaxy.id == andromedaGalaxy.id! && $0.id!.$jurisdiction.id == noneJurisdiction.id! && $0.id!.rank == 0 })) + XCTAssertTrue( + pivots.contains(where: { + $0.id!.$galaxy.id == milkyWayGalaxy.id! && $0.id!.$jurisdiction.id == oldJurisdiction.id! && $0.id!.rank == 1 + })) + XCTAssertTrue( + pivots.contains(where: { + $0.id!.$galaxy.id == milkyWayGalaxy.id! && $0.id!.$jurisdiction.id == noneJurisdiction.id! && $0.id!.rank == 2 + })) + XCTAssertTrue( + pivots.contains(where: { + $0.id!.$galaxy.id == andromedaGalaxy.id! && $0.id!.$jurisdiction.id == noneJurisdiction.id! && $0.id!.rank == 0 + })) } } - + private func testCompositeID_eagerLoaders() throws { - try self.runTest(#function, [ - GalaxyMigration(), - StarMigration(), - JurisdictionMigration(), - GalacticJurisdictionMigration(), - GalaxySeed(), - StarSeed(), - JurisdictionSeed(), - GalacticJurisdictionSeed(), - ]) { - let milkyWayGalaxy = try XCTUnwrap(Galaxy.query(on: self.database).filter(\.$name == "Milky Way").with(\.$jurisdictions).first().wait()) + try self.runTest( + #function, + [ + GalaxyMigration(), + StarMigration(), + JurisdictionMigration(), + GalacticJurisdictionMigration(), + GalaxySeed(), + StarSeed(), + JurisdictionSeed(), + GalacticJurisdictionSeed(), + ] + ) { + let milkyWayGalaxy = try XCTUnwrap( + Galaxy.query(on: self.database).filter(\.$name == "Milky Way").with(\.$jurisdictions).first().wait()) XCTAssertEqual(milkyWayGalaxy.jurisdictions.count, 2) - - let militaryJurisdiction = try XCTUnwrap(Jurisdiction.query(on: self.database).filter(\.$title == "Military").with(\.$galaxies).with(\.$galaxies.$pivots).first().wait()) + + let militaryJurisdiction = try XCTUnwrap( + Jurisdiction.query(on: self.database).filter(\.$title == "Military").with(\.$galaxies).with(\.$galaxies.$pivots).first() + .wait()) XCTAssertEqual(militaryJurisdiction.galaxies.count, 2) XCTAssertEqual(militaryJurisdiction.$galaxies.pivots.count, 2) - - let corporateMilkyWayPivot = try XCTUnwrap(GalacticJurisdiction.query(on: self.database) - .join(parent: \.$id.$galaxy).filter(Galaxy.self, \.$name == "Milky Way") - .join(parent: \.$id.$jurisdiction).filter(Jurisdiction.self, \.$title == "Corporate") - .with(\.$id.$galaxy) { $0.with(\.$stars) }.with(\.$id.$jurisdiction) - .first().wait()) + + let corporateMilkyWayPivot = try XCTUnwrap( + GalacticJurisdiction.query(on: self.database) + .join(parent: \.$id.$galaxy).filter(Galaxy.self, \.$name == "Milky Way") + .join(parent: \.$id.$jurisdiction).filter(Jurisdiction.self, \.$title == "Corporate") + .with(\.$id.$galaxy) { $0.with(\.$stars) }.with(\.$id.$jurisdiction) + .first().wait()) XCTAssertNotNil(corporateMilkyWayPivot.$id.$galaxy.value) XCTAssertNotNil(corporateMilkyWayPivot.$id.$jurisdiction.value) XCTAssertEqual(corporateMilkyWayPivot.id!.galaxy.stars.count, 2) } } - + private func testCompositeID_arrayCreateAndDelete() throws { - try self.runTest(#function, [ - GalaxyMigration(), - JurisdictionMigration(), - GalacticJurisdictionMigration(), - GalaxySeed(), - JurisdictionSeed(), - ]) { + try self.runTest( + #function, + [ + GalaxyMigration(), + JurisdictionMigration(), + GalacticJurisdictionMigration(), + GalaxySeed(), + JurisdictionSeed(), + ] + ) { let milkyWayGalaxy = try XCTUnwrap(Galaxy.query(on: self.database).filter(\.$name == "Milky Way").first().wait()) let allJurisdictions = try Jurisdiction.query(on: self.database).all().wait() - + assert(!allJurisdictions.isEmpty, "Test expects there to be at least one jurisdiction defined") - - try milkyWayGalaxy.$jurisdictions.attach(allJurisdictions, on: self.database) { $0.id!.rank = 1 }.wait() // `Siblings.attach(_:on:)` uses array create. - - let milkyWayGalaxyReloaded = try XCTUnwrap(Galaxy.query(on: self.database).filter(\.$name == "Milky Way").with(\.$jurisdictions).with(\.$jurisdictions.$pivots).first().wait()) + + try milkyWayGalaxy.$jurisdictions.attach(allJurisdictions, on: self.database) { $0.id!.rank = 1 }.wait() // `Siblings.attach(_:on:)` uses array create. + + let milkyWayGalaxyReloaded = try XCTUnwrap( + Galaxy.query(on: self.database).filter(\.$name == "Milky Way").with(\.$jurisdictions).with(\.$jurisdictions.$pivots).first() + .wait()) XCTAssertEqual(milkyWayGalaxyReloaded.jurisdictions.count, allJurisdictions.count) - - try milkyWayGalaxyReloaded.$jurisdictions.pivots.delete(on: self.database).wait() // `Silbings.detach(_:on:)` does *not* use array delete, though, so do it ourselves. - let milkyWayGalaxyRevolutions = try XCTUnwrap(Galaxy.query(on: self.database).filter(\.$name == "Milky Way").with(\.$jurisdictions).first().wait()) + try milkyWayGalaxyReloaded.$jurisdictions.pivots.delete(on: self.database).wait() // `Silbings.detach(_:on:)` does *not* use array delete, though, so do it ourselves. + + let milkyWayGalaxyRevolutions = try XCTUnwrap( + Galaxy.query(on: self.database).filter(\.$name == "Milky Way").with(\.$jurisdictions).first().wait()) XCTAssertEqual(milkyWayGalaxyRevolutions.jurisdictions.count, 0) } } - + private func testCompositeID_count() throws { - try self.runTest(#function, [ - GalaxyMigration(), - JurisdictionMigration(), - GalacticJurisdictionMigration(), - GalaxySeed(), - JurisdictionSeed(), - GalacticJurisdictionSeed(), - ]) { + try self.runTest( + #function, + [ + GalaxyMigration(), + JurisdictionMigration(), + GalacticJurisdictionMigration(), + GalaxySeed(), + JurisdictionSeed(), + GalacticJurisdictionSeed(), + ] + ) { let pivotCount = try GalacticJurisdiction.query(on: self.database).count().wait() - + XCTAssertGreaterThan(pivotCount, 0) } } @@ -177,44 +217,44 @@ extension FluentBenchmarker { public final class CompositeIDModel: Model, @unchecked Sendable { public static let schema = "composite_id_models" - + public final class IDValue: Fields, Hashable, @unchecked Sendable { @Field(key: "name") public var name: String - + @Field(key: "dimensions") public var dimensions: Int - + public init() {} - + public init(name: String, dimensions: Int) { self.name = name self.dimensions = dimensions } - + public static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.name == rhs.name && lhs.dimensions == rhs.dimensions } - + public func hash(into hasher: inout Hasher) { hasher.combine(self.name) hasher.combine(self.dimensions) } } - + @CompositeID() public var id: IDValue? - + @OptionalField(key: "additional_info") public var additionalInfo: String? - + public init() {} - + public init(id: IDValue, additionalInfo: String? = nil) { self.id = id self.additionalInfo = additionalInfo } - + public convenience init(name: String, dimensions: Int, additionalInfo: String? = nil) { self.init(id: .init(name: name, dimensions: dimensions), additionalInfo: additionalInfo) } @@ -222,7 +262,7 @@ public final class CompositeIDModel: Model, @unchecked Sendable { public struct CompositeIDModelMigration: Migration { public init() {} - + public func prepare(on database: any Database) -> EventLoopFuture { database.schema(CompositeIDModel.schema) .field("name", .string, .required) @@ -231,7 +271,7 @@ public struct CompositeIDModelMigration: Migration { .compositeIdentifier(over: "name", "dimensions") .create() } - + public func revert(on database: any Database) -> EventLoopFuture { database.schema(CompositeIDModel.schema) .delete() @@ -240,17 +280,17 @@ public struct CompositeIDModelMigration: Migration { public struct CompositeIDModelSeed: Migration { public init() {} - + public func prepare(on database: any Database) -> EventLoopFuture { - EventLoopFuture.andAllSucceed([ - CompositeIDModel(name: "A", dimensions: 1, additionalInfo: nil), - CompositeIDModel(name: "A", dimensions: 2, additionalInfo: nil), - CompositeIDModel(name: "B", dimensions: 1, additionalInfo: nil), - ].map { $0.create(on: database) }, on: database.eventLoop) + EventLoopFuture.andAllSucceed( + [ + CompositeIDModel(name: "A", dimensions: 1, additionalInfo: nil), + CompositeIDModel(name: "A", dimensions: 2, additionalInfo: nil), + CompositeIDModel(name: "B", dimensions: 1, additionalInfo: nil), + ].map { $0.create(on: database) }, on: database.eventLoop) } - + public func revert(on database: any Database) -> EventLoopFuture { CompositeIDModel.query(on: database).delete() } } - diff --git a/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift b/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift index fb2202d4..1f9d4b03 100644 --- a/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift +++ b/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift @@ -11,15 +11,18 @@ extension FluentBenchmarker { } private func testCompositeParent_loading() throws { - try self.runTest(#function, [ - CompositeIDParentModel.ModelMigration(), - CompositeIDParentModel.ModelSeed(), - CompositeIDChildModel.ModelMigration(), - CompositeIDChildModel.ModelSeed(), - ]) { + try self.runTest( + #function, + [ + CompositeIDParentModel.ModelMigration(), + CompositeIDParentModel.ModelSeed(), + CompositeIDChildModel.ModelMigration(), + CompositeIDChildModel.ModelSeed(), + ] + ) { let child1 = try XCTUnwrap(CompositeIDChildModel.find(1, on: self.database).wait()) let child2 = try XCTUnwrap(CompositeIDChildModel.find(2, on: self.database).wait()) - + XCTAssertEqual(child1.$compositeIdParentModel.id, .init(name: "A")) XCTAssertNil(child1.$additionalCompositeIdParentModel.id) XCTAssertEqual(child1.$linkedCompositeIdParentModel.id, .init(name: "A")) @@ -29,20 +32,31 @@ extension FluentBenchmarker { XCTAssertEqual(child2.$additionalCompositeIdParentModel.id, .init(name: "B")) XCTAssertEqual(child2.$linkedCompositeIdParentModel.id, .init(name: "B")) XCTAssertEqual(child2.$additionalLinkedCompositeIdParentModel.id, .init(name: "A")) - + XCTAssertEqual(try child1.$compositeIdParentModel.get(on: self.database).wait().id, child1.$compositeIdParentModel.id) XCTAssertNil(try child1.$additionalCompositeIdParentModel.get(on: self.database).wait()) - XCTAssertEqual(try child1.$linkedCompositeIdParentModel.get(on: self.database).wait().id, child1.$linkedCompositeIdParentModel.id) + XCTAssertEqual( + try child1.$linkedCompositeIdParentModel.get(on: self.database).wait().id, child1.$linkedCompositeIdParentModel.id) XCTAssertNil(try child1.$additionalLinkedCompositeIdParentModel.get(on: self.database).wait()) XCTAssertEqual(try child2.$compositeIdParentModel.get(on: self.database).wait().id, child2.$compositeIdParentModel.id) - XCTAssertEqual(try child2.$additionalCompositeIdParentModel.get(on: self.database).wait()?.id, child2.$additionalCompositeIdParentModel.id) - XCTAssertEqual(try child2.$linkedCompositeIdParentModel.get(on: self.database).wait().id, child2.$linkedCompositeIdParentModel.id) - XCTAssertEqual(try child2.$additionalLinkedCompositeIdParentModel.get(on: self.database).wait()?.id, child2.$additionalLinkedCompositeIdParentModel.id) - - let child3 = try XCTUnwrap(CompositeIDChildModel.query(on: self.database).filter(\.$id == 1).with(\.$compositeIdParentModel).with(\.$additionalCompositeIdParentModel).with(\.$linkedCompositeIdParentModel).with(\.$additionalLinkedCompositeIdParentModel).first().wait()) - let child4 = try XCTUnwrap(CompositeIDChildModel.query(on: self.database).filter(\.$id == 2).with(\.$compositeIdParentModel).with(\.$additionalCompositeIdParentModel).with(\.$linkedCompositeIdParentModel).with(\.$additionalLinkedCompositeIdParentModel).first().wait()) - + XCTAssertEqual( + try child2.$additionalCompositeIdParentModel.get(on: self.database).wait()?.id, child2.$additionalCompositeIdParentModel.id) + XCTAssertEqual( + try child2.$linkedCompositeIdParentModel.get(on: self.database).wait().id, child2.$linkedCompositeIdParentModel.id) + XCTAssertEqual( + try child2.$additionalLinkedCompositeIdParentModel.get(on: self.database).wait()?.id, + child2.$additionalLinkedCompositeIdParentModel.id) + + let child3 = try XCTUnwrap( + CompositeIDChildModel.query(on: self.database).filter(\.$id == 1).with(\.$compositeIdParentModel).with( + \.$additionalCompositeIdParentModel + ).with(\.$linkedCompositeIdParentModel).with(\.$additionalLinkedCompositeIdParentModel).first().wait()) + let child4 = try XCTUnwrap( + CompositeIDChildModel.query(on: self.database).filter(\.$id == 2).with(\.$compositeIdParentModel).with( + \.$additionalCompositeIdParentModel + ).with(\.$linkedCompositeIdParentModel).with(\.$additionalLinkedCompositeIdParentModel).first().wait()) + XCTAssertEqual(child3.$compositeIdParentModel.value?.id, child3.$compositeIdParentModel.id) XCTAssertNil(child3.$additionalCompositeIdParentModel.value??.id) XCTAssertEqual(child3.$linkedCompositeIdParentModel.value?.id, child3.$linkedCompositeIdParentModel.id) @@ -56,21 +70,24 @@ extension FluentBenchmarker { } private func testCompositeChildren_loading() throws { - try self.runTest(#function, [ - CompositeIDParentModel.ModelMigration(), - CompositeIDParentModel.ModelSeed(), - CompositeIDChildModel.ModelMigration(), - CompositeIDChildModel.ModelSeed(), - ]) { + try self.runTest( + #function, + [ + CompositeIDParentModel.ModelMigration(), + CompositeIDParentModel.ModelSeed(), + CompositeIDChildModel.ModelMigration(), + CompositeIDChildModel.ModelSeed(), + ] + ) { let parent1 = try XCTUnwrap(CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "A").first().wait()) let parent2 = try XCTUnwrap(CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "B").first().wait()) let parent3 = try XCTUnwrap(CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "C").first().wait()) - + let children1_1 = try parent1.$compositeIdChildModels.get(on: self.database).wait() let children1_2 = try parent1.$additionalCompositeIdChildModels.get(on: self.database).wait() let children1_3 = try parent1.$linkedCompositeIdChildModel.get(on: self.database).wait() let children1_4 = try parent1.$additionalLinkedCompositeIdChildModel.get(on: self.database).wait() - + let children2_1 = try parent2.$compositeIdChildModels.get(on: self.database).wait() let children2_2 = try parent2.$additionalCompositeIdChildModels.get(on: self.database).wait() let children2_3 = try parent2.$linkedCompositeIdChildModel.get(on: self.database).wait() @@ -80,67 +97,81 @@ extension FluentBenchmarker { let children3_2 = try parent3.$additionalCompositeIdChildModels.get(on: self.database).wait() let children3_3 = try parent3.$linkedCompositeIdChildModel.get(on: self.database).wait() let children3_4 = try parent3.$additionalLinkedCompositeIdChildModel.get(on: self.database).wait() - + XCTAssertEqual(children1_1.compactMap(\.id).sorted(), [1, 2, 3]) XCTAssertTrue(children1_2.isEmpty) XCTAssertEqual(children1_3?.id, 1) XCTAssertEqual(children1_4?.id, 2) - + XCTAssertTrue(children2_1.isEmpty) XCTAssertEqual(children2_2.compactMap(\.id).sorted(), [2, 3]) XCTAssertEqual(children2_3?.id, 2) XCTAssertEqual(children2_4?.id, 3) - + XCTAssertTrue(children3_1.isEmpty) XCTAssertTrue(children3_2.isEmpty) XCTAssertEqual(children3_3?.id, 3) XCTAssertNil(children3_4) - let parent4 = try XCTUnwrap(CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "A").with(\.$compositeIdChildModels).with(\.$additionalCompositeIdChildModels).with(\.$linkedCompositeIdChildModel).with(\.$additionalLinkedCompositeIdChildModel).first().wait()) - let parent5 = try XCTUnwrap(CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "B").with(\.$compositeIdChildModels).with(\.$additionalCompositeIdChildModels).with(\.$linkedCompositeIdChildModel).with(\.$additionalLinkedCompositeIdChildModel).first().wait()) - let parent6 = try XCTUnwrap(CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "C").with(\.$compositeIdChildModels).with(\.$additionalCompositeIdChildModels).with(\.$linkedCompositeIdChildModel).with(\.$additionalLinkedCompositeIdChildModel).first().wait()) - + let parent4 = try XCTUnwrap( + CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "A").with(\.$compositeIdChildModels).with( + \.$additionalCompositeIdChildModels + ).with(\.$linkedCompositeIdChildModel).with(\.$additionalLinkedCompositeIdChildModel).first().wait()) + let parent5 = try XCTUnwrap( + CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "B").with(\.$compositeIdChildModels).with( + \.$additionalCompositeIdChildModels + ).with(\.$linkedCompositeIdChildModel).with(\.$additionalLinkedCompositeIdChildModel).first().wait()) + let parent6 = try XCTUnwrap( + CompositeIDParentModel.query(on: self.database).filter(\.$id.$name == "C").with(\.$compositeIdChildModels).with( + \.$additionalCompositeIdChildModels + ).with(\.$linkedCompositeIdChildModel).with(\.$additionalLinkedCompositeIdChildModel).first().wait()) + XCTAssertEqual(parent4.$compositeIdChildModels.value?.compactMap(\.id).sorted(), [1, 2, 3]) XCTAssertTrue(parent4.$additionalCompositeIdChildModels.value?.isEmpty ?? false) XCTAssertEqual(parent4.$linkedCompositeIdChildModel.value??.id, 1) XCTAssertEqual(parent4.$additionalLinkedCompositeIdChildModel.value??.id, 2) - + XCTAssertTrue(parent5.$compositeIdChildModels.value?.isEmpty ?? false) XCTAssertEqual(parent5.$additionalCompositeIdChildModels.value?.compactMap(\.id).sorted(), [2, 3]) XCTAssertEqual(parent5.$linkedCompositeIdChildModel.value??.id, 2) XCTAssertEqual(parent5.$additionalLinkedCompositeIdChildModel.value??.id, 3) - + XCTAssertTrue(parent6.$compositeIdChildModels.value?.isEmpty ?? false) XCTAssertTrue(parent6.$additionalCompositeIdChildModels.value?.isEmpty ?? false) XCTAssertEqual(parent6.$linkedCompositeIdChildModel.value??.id, 3) XCTAssertNil(parent6.$additionalLinkedCompositeIdChildModel.value??.id) } } - + private func testCompositeParent_nestedInCompositeID() throws { - try self.runTest(#function, [ - GalaxyMigration(), - GalaxySeed(), - CompositeParentTheFirst.ModelMigration(), - CompositeParentTheSecond.ModelMigration(), - ]) { + try self.runTest( + #function, + [ + GalaxyMigration(), + GalaxySeed(), + CompositeParentTheFirst.ModelMigration(), + CompositeParentTheSecond.ModelMigration(), + ] + ) { let anyGalaxy = try XCTUnwrap(Galaxy.query(on: self.database).first().wait()) - + let parentFirst = CompositeParentTheFirst(parentId: try anyGalaxy.requireID()) try parentFirst.create(on: self.database).wait() - + let parentSecond = CompositeParentTheSecond(parentId: try parentFirst.requireID()) try parentSecond.create(on: self.database).wait() - + XCTAssertEqual(try CompositeParentTheFirst.query(on: self.database).filter(\.$id == parentFirst.requireID()).count().wait(), 1) - - let parentFirstAgain = try XCTUnwrap(CompositeParentTheFirst.query(on: self.database).filter(\.$id.$parent.$id == anyGalaxy.requireID()).with(\.$id.$parent).with(\.$children).first().wait()) - + + let parentFirstAgain = try XCTUnwrap( + CompositeParentTheFirst.query(on: self.database).filter(\.$id.$parent.$id == anyGalaxy.requireID()).with(\.$id.$parent) + .with(\.$children).first().wait()) + XCTAssertEqual(parentFirstAgain.id?.$parent.value?.id, anyGalaxy.id) XCTAssertEqual(parentFirstAgain.$children.value?.first?.id?.$parent.id, parentFirstAgain.id) - + try Galaxy.query(on: self.database).filter(\.$id == anyGalaxy.requireID()).delete(force: true).wait() - + XCTAssertEqual(try CompositeParentTheFirst.query(on: self.database).count().wait(), 0) XCTAssertEqual(try CompositeParentTheSecond.query(on: self.database).count().wait(), 0) } @@ -149,52 +180,52 @@ extension FluentBenchmarker { final class CompositeIDParentModel: Model, @unchecked Sendable { static let schema = "composite_id_parent_models" - + final class IDValue: Fields, Hashable, @unchecked Sendable { @Field(key: "name") var name: String - + @Field(key: "dimensions") var dimensions: Int - + init() {} - + init(name: String, dimensions: Int = 1) { self.name = name self.dimensions = dimensions } - + static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.name == rhs.name && lhs.dimensions == rhs.dimensions } - + func hash(into hasher: inout Hasher) { hasher.combine(self.name) hasher.combine(self.dimensions) } } - + @CompositeID var id: IDValue? - - @CompositeChildren(for: \.$compositeIdParentModel) // Children referencing required composite parent + + @CompositeChildren(for: \.$compositeIdParentModel) // Children referencing required composite parent var compositeIdChildModels: [CompositeIDChildModel] - - @CompositeChildren(for: \.$additionalCompositeIdParentModel) // Children referencing optional composite parent + + @CompositeChildren(for: \.$additionalCompositeIdParentModel) // Children referencing optional composite parent var additionalCompositeIdChildModels: [CompositeIDChildModel] - - @CompositeOptionalChild(for: \.$linkedCompositeIdParentModel) // Optional child referencing required composite parent + + @CompositeOptionalChild(for: \.$linkedCompositeIdParentModel) // Optional child referencing required composite parent var linkedCompositeIdChildModel: CompositeIDChildModel? - - @CompositeOptionalChild(for: \.$additionalLinkedCompositeIdParentModel) // Optional child referencing optional composite parent + + @CompositeOptionalChild(for: \.$additionalLinkedCompositeIdParentModel) // Optional child referencing optional composite parent var additionalLinkedCompositeIdChildModel: CompositeIDChildModel? - + init() {} - + init(id: IDValue) { self.id = id } - + convenience init(name: String, dimensions: Int) { self.init(id: .init(name: name, dimensions: dimensions)) } @@ -207,7 +238,7 @@ final class CompositeIDParentModel: Model, @unchecked Sendable { .compositeIdentifier(over: "name", "dimensions") .create() } - + func revert(on database: any Database) -> EventLoopFuture { database.schema(CompositeIDParentModel.schema) .delete() @@ -216,13 +247,14 @@ final class CompositeIDParentModel: Model, @unchecked Sendable { struct ModelSeed: Migration { func prepare(on database: any Database) -> EventLoopFuture { - EventLoopFuture.andAllSucceed([ - CompositeIDParentModel(name: "A", dimensions: 1), - CompositeIDParentModel(name: "B", dimensions: 1), - CompositeIDParentModel(name: "C", dimensions: 1), - ].map { $0.create(on: database) }, on: database.eventLoop) + EventLoopFuture.andAllSucceed( + [ + CompositeIDParentModel(name: "A", dimensions: 1), + CompositeIDParentModel(name: "B", dimensions: 1), + CompositeIDParentModel(name: "C", dimensions: 1), + ].map { $0.create(on: database) }, on: database.eventLoop) } - + func revert(on database: any Database) -> EventLoopFuture { CompositeIDParentModel.query(on: database).delete() } @@ -231,24 +263,24 @@ final class CompositeIDParentModel: Model, @unchecked Sendable { final class CompositeIDChildModel: Model, @unchecked Sendable { static let schema = "composite_id_child_models" - + @ID(custom: .id) var id: Int? - - @CompositeParent(prefix: "comp_parent_model", strategy: .snakeCase) // required composite parent referencing multiple children + + @CompositeParent(prefix: "comp_parent_model", strategy: .snakeCase) // required composite parent referencing multiple children var compositeIdParentModel: CompositeIDParentModel - - @CompositeOptionalParent(prefix: "addl_comp_parent_model", strategy: .snakeCase) // optional composite parent referencing multiple children + + @CompositeOptionalParent(prefix: "addl_comp_parent_model", strategy: .snakeCase) // optional composite parent referencing multiple children var additionalCompositeIdParentModel: CompositeIDParentModel? - - @CompositeParent(prefix: "comp_linked_model", strategy: .snakeCase) // required composite parent referencing one optional child + + @CompositeParent(prefix: "comp_linked_model", strategy: .snakeCase) // required composite parent referencing one optional child var linkedCompositeIdParentModel: CompositeIDParentModel - - @CompositeOptionalParent(prefix: "addl_comp_linked_model", strategy: .snakeCase) // optional composite parent referencing one optional child + + @CompositeOptionalParent(prefix: "addl_comp_linked_model", strategy: .snakeCase) // optional composite parent referencing one optional child var additionalLinkedCompositeIdParentModel: CompositeIDParentModel? - + init() {} - + init( id: Int? = nil, parentId: CompositeIDParentModel.IDValue, @@ -262,7 +294,7 @@ final class CompositeIDChildModel: Model, @unchecked Sendable { self.$linkedCompositeIdParentModel.id = linkedId self.$additionalLinkedCompositeIdParentModel.id = additionalLinkedId } - + struct ModelMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema(CompositeIDChildModel.schema) @@ -270,48 +302,57 @@ final class CompositeIDChildModel: Model, @unchecked Sendable { .field("comp_parent_model_name", .string, .required) .field("comp_parent_model_dimensions", .int, .required) - .foreignKey(["comp_parent_model_name", "comp_parent_model_dimensions"], + .foreignKey( + ["comp_parent_model_name", "comp_parent_model_dimensions"], references: CompositeIDParentModel.schema, ["name", "dimensions"] ) .field("addl_comp_parent_model_name", .string) .field("addl_comp_parent_model_dimensions", .int) .constraint(.optionalCompositeReferenceCheck("addl_comp_parent_model_name", "addl_comp_parent_model_dimensions")) - .foreignKey(["addl_comp_parent_model_name", "addl_comp_parent_model_dimensions"], + .foreignKey( + ["addl_comp_parent_model_name", "addl_comp_parent_model_dimensions"], references: CompositeIDParentModel.schema, ["name", "dimensions"] ) .field("comp_linked_model_name", .string, .required) .field("comp_linked_model_dimensions", .int, .required) .unique(on: "comp_linked_model_name", "comp_linked_model_dimensions") - .foreignKey(["comp_linked_model_name", "comp_linked_model_dimensions"], + .foreignKey( + ["comp_linked_model_name", "comp_linked_model_dimensions"], references: CompositeIDParentModel.schema, ["name", "dimensions"] ) - + .field("addl_comp_linked_model_name", .string) .field("addl_comp_linked_model_dimensions", .int) .unique(on: "addl_comp_linked_model_name", "addl_comp_linked_model_dimensions") .constraint(.optionalCompositeReferenceCheck("addl_comp_linked_model_name", "addl_comp_linked_model_dimensions")) - .foreignKey(["addl_comp_linked_model_name", "addl_comp_linked_model_dimensions"], + .foreignKey( + ["addl_comp_linked_model_name", "addl_comp_linked_model_dimensions"], references: CompositeIDParentModel.schema, ["name", "dimensions"] ) .create() } - + func revert(on database: any Database) -> EventLoopFuture { database.schema(CompositeIDChildModel.schema).delete() } } - + struct ModelSeed: Migration { func prepare(on database: any Database) -> EventLoopFuture { [ - CompositeIDChildModel(id: 1, parentId: .init(name: "A"), additionalParentId: nil, linkedId: .init(name: "A"), additionalLinkedId: nil), - CompositeIDChildModel(id: 2, parentId: .init(name: "A"), additionalParentId: .init(name: "B"), linkedId: .init(name: "B"), additionalLinkedId: .init(name: "A")), - CompositeIDChildModel(id: 3, parentId: .init(name: "A"), additionalParentId: .init(name: "B"), linkedId: .init(name: "C"), additionalLinkedId: .init(name: "B")), + CompositeIDChildModel( + id: 1, parentId: .init(name: "A"), additionalParentId: nil, linkedId: .init(name: "A"), additionalLinkedId: nil), + CompositeIDChildModel( + id: 2, parentId: .init(name: "A"), additionalParentId: .init(name: "B"), linkedId: .init(name: "B"), + additionalLinkedId: .init(name: "A")), + CompositeIDChildModel( + id: 3, parentId: .init(name: "A"), additionalParentId: .init(name: "B"), linkedId: .init(name: "C"), + additionalLinkedId: .init(name: "B")), ].create(on: database) } - + func revert(on database: any Database) -> EventLoopFuture { CompositeIDChildModel.query(on: database).delete() } @@ -320,7 +361,9 @@ final class CompositeIDChildModel: Model, @unchecked Sendable { extension DatabaseSchema.Constraint { /// Convenience overload of `optionalCompositeReferenceCheck(_:)`. - static func optionalCompositeReferenceCheck(_ field1: FieldKey, _ field2: FieldKey, _ moreFields: FieldKey...) -> DatabaseSchema.Constraint { + static func optionalCompositeReferenceCheck(_ field1: FieldKey, _ field2: FieldKey, _ moreFields: FieldKey...) + -> DatabaseSchema.Constraint + { return self.optionalCompositeReferenceCheck([field1, field2] + moreFields) } @@ -331,28 +374,33 @@ extension DatabaseSchema.Constraint { static func optionalCompositeReferenceCheck(_ fields: C) -> DatabaseSchema.Constraint where C: Collection, C.Element == FieldKey { guard fields.count > 1 else { fatalError("A composite reference check must cover at least two fields.") } let fields = fields.map { SQLIdentifier($0.description) } - - return .sql(.check(SQLGroupExpression(SQLBinaryExpression( - SQLGroupExpression(SQLBinaryExpression(fields.first!, .is, SQLLiteral.null)), - .equal, - SQLGroupExpression(SQLBinaryExpression(SQLFunction.coalesce([SQLLiteral.null] + fields.dropFirst().map{$0}), .is, SQLLiteral.null)) - )))) + + return .sql( + .check( + SQLGroupExpression( + SQLBinaryExpression( + SQLGroupExpression(SQLBinaryExpression(fields.first!, .is, SQLLiteral.null)), + .equal, + SQLGroupExpression( + SQLBinaryExpression( + SQLFunction.coalesce([SQLLiteral.null] + fields.dropFirst().map { $0 }), .is, SQLLiteral.null)) + )))) } } final class CompositeParentTheFirst: Model, @unchecked Sendable { static let schema = "composite_parent_the_first" - + final class IDValue: Fields, Hashable, @unchecked Sendable { @Parent(key: "parent_id") var parent: Galaxy - + init() {} - + init(parentId: Galaxy.IDValue) { self.$parent.id = parentId } - + static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.$parent.id == rhs.$parent.id } @@ -361,19 +409,19 @@ final class CompositeParentTheFirst: Model, @unchecked Sendable { hasher.combine(self.$parent.id) } } - + @CompositeID var id: IDValue? - + @CompositeChildren(for: \.$id.$parent) var children: [CompositeParentTheSecond] - + init() {} - + init(parentId: Galaxy.IDValue) { self.id = .init(parentId: parentId) } - + struct ModelMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema(CompositeParentTheFirst.schema) @@ -382,7 +430,7 @@ final class CompositeParentTheFirst: Model, @unchecked Sendable { .compositeIdentifier(over: "parent_id") .create() } - + func revert(on database: any Database) -> EventLoopFuture { database.schema(CompositeParentTheFirst.schema) .delete() @@ -392,7 +440,7 @@ final class CompositeParentTheFirst: Model, @unchecked Sendable { final class CompositeParentTheSecond: Model, @unchecked Sendable { static let schema = "composite_parent_the_second" - + final class IDValue: Fields, Hashable, @unchecked Sendable { @CompositeParent(prefix: "ref", strategy: .snakeCase) var parent: CompositeParentTheFirst @@ -411,25 +459,27 @@ final class CompositeParentTheSecond: Model, @unchecked Sendable { hasher.combine(self.$parent.id) } } - + @CompositeID var id: IDValue? - + init() {} - + init(parentId: CompositeParentTheFirst.IDValue) { self.id = .init(parentId: parentId) } - + struct ModelMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema(CompositeParentTheSecond.schema) .field("ref_parent_id", .uuid, .required) - .foreignKey("ref_parent_id", references: CompositeParentTheFirst.schema, "parent_id", onDelete: .cascade, onUpdate: .cascade) + .foreignKey( + "ref_parent_id", references: CompositeParentTheFirst.schema, "parent_id", onDelete: .cascade, onUpdate: .cascade + ) .compositeIdentifier(over: "ref_parent_id") .create() } - + func revert(on database: any Database) -> EventLoopFuture { database.schema(CompositeParentTheSecond.schema) .delete() diff --git a/Sources/FluentBenchmark/Tests/EagerLoadTests.swift b/Sources/FluentBenchmark/Tests/EagerLoadTests.swift index b245313c..30f17762 100644 --- a/Sources/FluentBenchmark/Tests/EagerLoadTests.swift +++ b/Sources/FluentBenchmark/Tests/EagerLoadTests.swift @@ -1,8 +1,8 @@ import FluentKit import Foundation +import Logging import NIOCore import XCTest -import Logging extension FluentBenchmarker { public func testEagerLoad() throws { @@ -21,9 +21,12 @@ extension FluentBenchmarker { } private func testEagerLoad_nesting() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let galaxies = try Galaxy.query(on: self.database) .with(\.$stars) { $0.with(\.$planets) { @@ -37,9 +40,12 @@ extension FluentBenchmarker { } private func testEagerLoad_children() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let galaxies = try Galaxy.query(on: self.database) .with(\.$stars) .all().wait() @@ -52,7 +58,7 @@ extension FluentBenchmarker { true ) XCTAssertEqual( - galaxy.stars.contains { $0.name == "Alpheratz"}, + galaxy.stars.contains { $0.name == "Alpheratz" }, false ) default: break @@ -60,25 +66,30 @@ extension FluentBenchmarker { } } } - + private func testEagerLoad_childrenDeleted() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { try Planet.query(on: self.database).filter(\.$name == "Jupiter").delete().wait() - - let sun1 = try XCTUnwrap(Star.query(on: self.database) - .filter(\.$name == "Sol") - .with(\.$planets, withDeleted: true) - .first().wait() + + let sun1 = try XCTUnwrap( + Star.query(on: self.database) + .filter(\.$name == "Sol") + .with(\.$planets, withDeleted: true) + .first().wait() ) XCTAssertTrue(sun1.planets.contains { $0.name == "Earth" }) XCTAssertTrue(sun1.planets.contains { $0.name == "Jupiter" }) - - let sun2 = try XCTUnwrap(Star.query(on: self.database) - .filter(\.$name == "Sol") - .with(\.$planets) - .first().wait() + + let sun2 = try XCTUnwrap( + Star.query(on: self.database) + .filter(\.$name == "Sol") + .with(\.$planets) + .first().wait() ) XCTAssertTrue(sun2.planets.contains { $0.name == "Earth" }) XCTAssertFalse(sun2.planets.contains { $0.name == "Jupiter" }) @@ -86,9 +97,12 @@ extension FluentBenchmarker { } private func testEagerLoad_parent() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .with(\.$star) .all().wait() @@ -104,26 +118,30 @@ extension FluentBenchmarker { } } } - + private func testEagerLoad_parentDeleted() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { try Star.query(on: self.database).filter(\.$name == "Sol").delete().wait() - - let planet = try XCTUnwrap(Planet.query(on: self.database) - .filter(\.$name == "Earth") - .with(\.$star, withDeleted: true) - .first().wait() + + let planet = try XCTUnwrap( + Planet.query(on: self.database) + .filter(\.$name == "Earth") + .with(\.$star, withDeleted: true) + .first().wait() ) XCTAssertEqual(planet.star.name, "Sol") - + XCTAssertThrowsError( try Planet.query(on: self.database) .with(\.$star) .all().wait() ) { error in - guard case let .missingParent(from, to, key, _) = error as? FluentError else { + guard case .missingParent(let from, let to, let key, _) = error as? FluentError else { return XCTFail("Unexpected error \(error) thrown") } XCTAssertEqual(from, "Planet") @@ -134,9 +152,12 @@ extension FluentBenchmarker { } private func testEagerLoad_siblings() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .with(\.$star) .with(\.$tags) @@ -158,33 +179,41 @@ extension FluentBenchmarker { } } } - + private func testEagerLoad_siblingsDeleted() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { try Planet.query(on: self.database).filter(\.$name == "Earth").delete().wait() - - let tag1 = try XCTUnwrap(Tag.query(on: self.database) - .filter(\.$name == "Inhabited") - .with(\.$planets, withDeleted: true) - .first().wait() + + let tag1 = try XCTUnwrap( + Tag.query(on: self.database) + .filter(\.$name == "Inhabited") + .with(\.$planets, withDeleted: true) + .first().wait() ) XCTAssertEqual(Set(tag1.planets.map(\.name)), ["Earth"]) - - let tag2 = try XCTUnwrap(Tag.query(on: self.database) - .filter(\.$name == "Inhabited") - .with(\.$planets) - .first().wait() + + let tag2 = try XCTUnwrap( + Tag.query(on: self.database) + .filter(\.$name == "Inhabited") + .with(\.$planets) + .first().wait() ) XCTAssertEqual(Set(tag2.planets.map(\.name)), []) } } private func testEagerLoad_parentJSON() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .with(\.$star) .all().wait() @@ -193,9 +222,12 @@ extension FluentBenchmarker { } private func testEagerLoad_childrenJSON() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let galaxies = try Galaxy.query(on: self.database) .with(\.$stars) .all().wait() @@ -205,9 +237,12 @@ extension FluentBenchmarker { // https://github.com/vapor/fluent-kit/issues/117 private func testEagerLoad_emptyChildren() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let galaxies = try Galaxy.query(on: self.database) .filter(\.$name == "foo") .with(\.$stars) @@ -218,9 +253,12 @@ extension FluentBenchmarker { } private func testEagerLoad_throughNilOptionalParent() throws { - try self.runTest(#function, [ - ABCMigration() - ]) { + try self.runTest( + #function, + [ + ABCMigration() + ] + ) { do { let c = C() try c.create(on: self.database).wait() @@ -254,9 +292,12 @@ extension FluentBenchmarker { } private func testEagerLoad_throughAllNilOptionalParent() throws { - try self.runTest(#function, [ - ABCMigration() - ]) { + try self.runTest( + #function, + [ + ABCMigration() + ] + ) { do { let c = C() try c.create(on: self.database).wait() @@ -287,7 +328,7 @@ private final class A: Model, @unchecked Sendable { @OptionalParent(key: "b_id") var b: B? - init() { } + init() {} } private final class B: Model, @unchecked Sendable { @@ -299,7 +340,7 @@ private final class B: Model, @unchecked Sendable { @Parent(key: "c_id") var c: C - init() { } + init() {} } private final class C: Model, @unchecked Sendable { @@ -308,30 +349,31 @@ private final class C: Model, @unchecked Sendable { @ID var id: UUID? - init() { } + init() {} } private struct ABCMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { - .andAllSucceed([ - database.schema("a").id().field("b_id", .uuid).create(), - database.schema("b").id().field("c_id", .uuid, .required).create(), - database.schema("c").id().create(), - ], on: database.eventLoop) + .andAllSucceed( + [ + database.schema("a").id().field("b_id", .uuid).create(), + database.schema("b").id().field("c_id", .uuid, .required).create(), + database.schema("c").id().create(), + ], on: database.eventLoop) } func revert(on database: any Database) -> EventLoopFuture { - .andAllSucceed([ - database.schema("a").delete(), - database.schema("b").delete(), - database.schema("c").delete(), - ], on: database.eventLoop) + .andAllSucceed( + [ + database.schema("a").delete(), + database.schema("b").delete(), + database.schema("c").delete(), + ], on: database.eventLoop) } } func prettyJSON(_ value: T) -> Logger.Message - where T: Encodable -{ +where T: Encodable { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted return try! .init(stringLiteral: String(decoding: encoder.encode(value), as: UTF8.self)) diff --git a/Sources/FluentBenchmark/Tests/EnumTests.swift b/Sources/FluentBenchmark/Tests/EnumTests.swift index aa648f4f..5520fb75 100644 --- a/Sources/FluentBenchmark/Tests/EnumTests.swift +++ b/Sources/FluentBenchmark/Tests/EnumTests.swift @@ -11,16 +11,19 @@ extension FluentBenchmarker { try self.testEnum_queryFound() try self.testEnum_queryMissing() try self.testEnum_decode() - + // Note: These should really be in their own top-level test case, but then I'd have to open // PRs against all the drivers again. try self.testBooleanProperties() } private func testEnum_basic() throws { - try self.runTest(#function, [ - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration() + ] + ) { let foo = Foo(bar: .baz, baz: .qux) XCTAssertTrue(foo.hasChanges) try foo.save(on: self.database).wait() @@ -33,10 +36,13 @@ extension FluentBenchmarker { } private func testEnum_addCases() throws { - try self.runTest(#function, [ - FooMigration(), - BarAddQuzAndQuzzMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration(), + BarAddQuzAndQuzzMigration(), + ] + ) { let foo = Foo(bar: .quz, baz: .quzz) try foo.save(on: self.database).wait() @@ -47,9 +53,12 @@ extension FluentBenchmarker { } public func testEnum_raw() throws { - try runTest(#function, [ - PetMigration() - ]) { + try runTest( + #function, + [ + PetMigration() + ] + ) { let pet = Pet(type: .cat) try pet.save(on: self.database).wait() @@ -60,13 +69,17 @@ extension FluentBenchmarker { public func testEnum_queryFound() throws { // equal - try self.runTest(#function, [ - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration() + ] + ) { let foo = Foo(bar: .baz, baz: .qux) try foo.save(on: self.database).wait() - let fetched = try Foo + let fetched = + try Foo .query(on: self.database) .filter(\.$bar == .baz) .first() @@ -78,7 +91,8 @@ extension FluentBenchmarker { let foo2 = Foo(bar: .baz, baz: .qux) try foo2.save(on: self.database).wait() - let fetched2 = try Foo + let fetched2 = + try Foo .query(on: self.database) .filter(\.$bar != .qux) .first() @@ -90,7 +104,8 @@ extension FluentBenchmarker { let foo3 = Foo(bar: .baz, baz: .qux) try foo3.save(on: self.database).wait() - let fetched3 = try Foo + let fetched3 = + try Foo .query(on: self.database) .filter(\.$bar ~~ [.baz, .qux]) .first() @@ -98,7 +113,8 @@ extension FluentBenchmarker { XCTAssertEqual(fetched3?.bar, .baz) XCTAssertEqual(fetched3?.baz, .qux) - let fetched3Opt = try Foo + let fetched3Opt = + try Foo .query(on: self.database) .filter(\.$baz ~~ [.baz, .qux]) .first() @@ -110,39 +126,43 @@ extension FluentBenchmarker { let foo4 = Foo(bar: .baz, baz: .qux) try foo4.save(on: self.database).wait() - let fetched4 = try Foo + let fetched4 = + try Foo .query(on: self.database) .filter(\.$bar !~ [.qux]) .first() .wait() XCTAssertEqual(fetched4?.bar, .baz) XCTAssertEqual(fetched4?.baz, .qux) - - let fetched4Opt = try Foo + + let fetched4Opt = + try Foo .query(on: self.database) .filter(\.$baz !~ [.baz]) .first() .wait() XCTAssertEqual(fetched4Opt?.bar, .baz) XCTAssertEqual(fetched4Opt?.baz, .qux) - + // is null let foo5 = Foo(bar: .baz, baz: nil) try foo5.save(on: self.database).wait() - - let fetched5 = try Foo + + let fetched5 = + try Foo .query(on: self.database) .filter(\.$baz == .null) .first() .wait() XCTAssertEqual(fetched5?.bar, .baz) XCTAssertNil(fetched5?.baz) - + // is not null let foo6 = Foo(bar: .baz, baz: .qux) try foo6.save(on: self.database).wait() - - let fetched6 = try Foo + + let fetched6 = + try Foo .query(on: self.database) .filter(\.$baz != .null) .first() @@ -154,13 +174,17 @@ extension FluentBenchmarker { public func testEnum_queryMissing() throws { // equal - try self.runTest(#function, [ - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration() + ] + ) { let foo = Foo(bar: .baz, baz: .qux) try foo.save(on: self.database).wait() - let fetched = try Foo + let fetched = + try Foo .query(on: self.database) .filter(\.$bar == .qux) .first() @@ -171,7 +195,8 @@ extension FluentBenchmarker { let foo2 = Foo(bar: .baz, baz: .qux) try foo2.save(on: self.database).wait() - let fetched2 = try Foo + let fetched2 = + try Foo .query(on: self.database) .filter(\.$bar != .baz) .first() @@ -182,7 +207,8 @@ extension FluentBenchmarker { let foo3 = Foo(bar: .baz, baz: .qux) try foo3.save(on: self.database).wait() - let fetched3 = try Foo + let fetched3 = + try Foo .query(on: self.database) .filter(\.$bar ~~ [.qux]) .first() @@ -193,29 +219,32 @@ extension FluentBenchmarker { let foo4 = Foo(bar: .baz, baz: .qux) try foo4.save(on: self.database).wait() - let fetched4 = try Foo + let fetched4 = + try Foo .query(on: self.database) .filter(\.$bar !~ [.baz, .qux]) .first() .wait() XCTAssertNil(fetched4) - + // is null let foo5 = Foo(bar: .baz, baz: .qux) try foo5.save(on: self.database).wait() - let fetched5 = try Foo + let fetched5 = + try Foo .query(on: self.database) .filter(\.$baz == .null) .first() .wait() XCTAssertNil(fetched5) - + // is not null let foo6 = Foo(bar: .qux, baz: nil) try foo6.save(on: self.database).wait() - let fetched6 = try Foo + let fetched6 = + try Foo .query(on: self.database) .filter(\.$bar == .qux) .filter(\.$baz != .null) @@ -226,12 +255,15 @@ extension FluentBenchmarker { } public func testEnum_decode() throws { - try runTest(#function, [ - FooMigration() - ]) { + try runTest( + #function, + [ + FooMigration() + ] + ) { let data = """ - { "bar": "baz", "baz": "qux" } - """ + { "bar": "baz", "baz": "qux" } + """ let foo = try JSONDecoder().decode(Foo.self, from: .init(data.utf8)) try foo.create(on: self.database).wait() @@ -240,23 +272,26 @@ extension FluentBenchmarker { XCTAssertEqual(fetched?.baz, .qux) } } - + public func testBooleanProperties() throws { - try runTest(#function, [ - FlagsMigration() - ]) { - let flags1 = Flags(inquired: true, required: true, desired: true, expired: true, inspired: true, retired: true), - flags2 = Flags(inquired: false, required: false, desired: false, expired: false, inspired: false, retired: false), - flags3 = Flags(inquired: true, required: true, desired: true, expired: nil, inspired: nil, retired: nil) - + try runTest( + #function, + [ + FlagsMigration() + ] + ) { + let flags1 = Flags(inquired: true, required: true, desired: true, expired: true, inspired: true, retired: true) + let flags2 = Flags(inquired: false, required: false, desired: false, expired: false, inspired: false, retired: false) + let flags3 = Flags(inquired: true, required: true, desired: true, expired: nil, inspired: nil, retired: nil) + try flags1.create(on: self.database).wait() try flags2.create(on: self.database).wait() try flags3.create(on: self.database).wait() - - let rawFlags1 = try XCTUnwrap(RawFlags.find(flags1.id!, on: self.database).wait()), - rawFlags2 = try XCTUnwrap(RawFlags.find(flags2.id!, on: self.database).wait()), - rawFlags3 = try XCTUnwrap(RawFlags.find(flags3.id!, on: self.database).wait()) - + + let rawFlags1 = try XCTUnwrap(RawFlags.find(flags1.id!, on: self.database).wait()) + let rawFlags2 = try XCTUnwrap(RawFlags.find(flags2.id!, on: self.database).wait()) + let rawFlags3 = try XCTUnwrap(RawFlags.find(flags3.id!, on: self.database).wait()) + XCTAssertEqual(rawFlags1.inquired, true) XCTAssertEqual(rawFlags1.required, 1) XCTAssertEqual(rawFlags1.desired, "true") @@ -278,10 +313,10 @@ extension FluentBenchmarker { XCTAssertNil(rawFlags3.inspired) XCTAssertNil(rawFlags3.retired) - let savedFlags1 = try XCTUnwrap(Flags.find(flags1.id!, on: self.database).wait()), - savedFlags2 = try XCTUnwrap(Flags.find(flags2.id!, on: self.database).wait()), - savedFlags3 = try XCTUnwrap(Flags.find(flags3.id!, on: self.database).wait()) - + let savedFlags1 = try XCTUnwrap(Flags.find(flags1.id!, on: self.database).wait()) + let savedFlags2 = try XCTUnwrap(Flags.find(flags2.id!, on: self.database).wait()) + let savedFlags3 = try XCTUnwrap(Flags.find(flags3.id!, on: self.database).wait()) + XCTAssertEqual(savedFlags1.inquired, flags1.inquired) XCTAssertEqual(savedFlags1.required, flags1.required) XCTAssertEqual(savedFlags1.desired, flags1.desired) @@ -318,11 +353,11 @@ private final class Foo: Model, @unchecked Sendable { @Enum(key: "bar") var bar: Bar - + @OptionalEnum(key: "baz") var baz: Bar? - init() { } + init() {} init(id: IDValue? = nil, bar: Bar, baz: Bar?) { self.id = id @@ -331,21 +366,19 @@ private final class Foo: Model, @unchecked Sendable { } } - private struct FooMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.enum("bar") .case("baz") .case("qux") .create() - .flatMap - { bar in - database.schema("foos") - .field("id", .uuid, .identifier(auto: false)) - .field("bar", bar, .required) - .field("baz", bar) - .create() - } + .flatMap { bar in + database.schema("foos") + .field("id", .uuid, .identifier(auto: false)) + .field("bar", bar, .required) + .field("baz", bar) + .create() + } } func revert(on database: any Database) -> EventLoopFuture { @@ -361,30 +394,27 @@ private struct BarAddQuzAndQuzzMigration: Migration { .case("quz") .case("quzz") .update() - .flatMap - { bar in - database.schema("foos") - .updateField("bar", bar) - .updateField("baz", bar) - .update() - } + .flatMap { bar in + database.schema("foos") + .updateField("bar", bar) + .updateField("baz", bar) + .update() + } } func revert(on database: any Database) -> EventLoopFuture { database.enum("bar") .deleteCase("quuz") .update() - .flatMap - { bar in - database.schema("foos") - .updateField("bar", bar) - .updateField("baz", bar) - .update() - } + .flatMap { bar in + database.schema("foos") + .updateField("bar", bar) + .updateField("baz", bar) + .update() + } } } - private enum Animal: UInt8, Codable { case dog, cat } @@ -398,7 +428,7 @@ private final class Pet: Model, @unchecked Sendable { @Field(key: "type") var type: Animal - init() { } + init() {} init(id: IDValue? = nil, type: Animal) { self.id = id @@ -406,7 +436,6 @@ private final class Pet: Model, @unchecked Sendable { } } - private struct PetMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema("pets") @@ -422,19 +451,19 @@ private struct PetMigration: Migration { private final class Flags: Model, @unchecked Sendable { static let schema = "flags" - + @ID(key: .id) var id: UUID? - + @Boolean(key: "inquired") var inquired: Bool - + @Boolean(key: "required", format: .integer) var required: Bool @Boolean(key: "desired", format: .trueFalse) var desired: Bool - + @OptionalBoolean(key: "expired") var expired: Bool? @@ -443,10 +472,12 @@ private final class Flags: Model, @unchecked Sendable { @OptionalBoolean(key: "retired", format: .trueFalse) var retired: Bool? - + init() {} - - init(id: IDValue? = nil, inquired: Bool, required: Bool, desired: Bool, expired: Bool? = nil, inspired: Bool? = nil, retired: Bool? = nil) { + + init( + id: IDValue? = nil, inquired: Bool, required: Bool, desired: Bool, expired: Bool? = nil, inspired: Bool? = nil, retired: Bool? = nil + ) { self.id = id self.inquired = inquired self.required = required @@ -459,7 +490,7 @@ private final class Flags: Model, @unchecked Sendable { private final class RawFlags: Model, @unchecked Sendable { static let schema = "flags" - + @ID(key: .id) var id: UUID? @Field(key: "inquired") var inquired: Bool @Field(key: "required") var required: Int @@ -467,7 +498,7 @@ private final class RawFlags: Model, @unchecked Sendable { @OptionalField(key: "expired") var expired: Bool? @OptionalField(key: "inspired") var inspired: Int? @OptionalField(key: "retired") var retired: String? - + init() {} } @@ -483,7 +514,7 @@ private struct FlagsMigration: Migration { .field("retired", .string) .create() } - + func revert(on database: any Database) -> EventLoopFuture { database.schema(Flags.schema) .delete() diff --git a/Sources/FluentBenchmark/Tests/FilterTests.swift b/Sources/FluentBenchmark/Tests/FilterTests.swift index 58dbd8c5..ed252ce8 100644 --- a/Sources/FluentBenchmark/Tests/FilterTests.swift +++ b/Sources/FluentBenchmark/Tests/FilterTests.swift @@ -1,8 +1,8 @@ import FluentKit +import FluentSQL import Foundation import NIOCore import XCTest -import FluentSQL extension FluentBenchmarker { public func testFilter(sql: Bool = true) throws { @@ -23,9 +23,12 @@ extension FluentBenchmarker { } private func testFilter_field() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let equalNumbers = try Moon.query(on: self.database) .filter(\.$craters == \.$comets).all().wait() XCTAssertEqual(equalNumbers.count, 7) @@ -41,9 +44,12 @@ extension FluentBenchmarker { } private func testFilter_sqlValue() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let moon = try Moon.query(on: self.database) .filter(\.$name == .sql(unsafeRaw: "'Moon'")) .first() @@ -55,9 +61,12 @@ extension FluentBenchmarker { } private func testFilter_sqlEmbedValue() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let moon = try Moon.query(on: self.database) .filter(\.$name == .sql(embed: "\(literal: "Moon")")) .first() @@ -69,9 +78,12 @@ extension FluentBenchmarker { } private func testFilter_sqlEmbedField() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let moon = try Moon.query(on: self.database) .filter(.sql(embed: "\(ident: "name")"), .equal, .bind("Moon")) .first() @@ -83,9 +95,12 @@ extension FluentBenchmarker { } private func testFilter_sqlEmbedFilter() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let moon = try Moon.query(on: self.database) .filter(.sql(embed: "\(ident: "name")=\(literal: "Moon")")) .first() @@ -97,9 +112,12 @@ extension FluentBenchmarker { } private func testFilter_group() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .group(.or) { $0.filter(\.$name == "Earth") @@ -119,9 +137,12 @@ extension FluentBenchmarker { } private func testFilter_emptyGroup() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .group(.or) { _ in } .all().wait() @@ -131,9 +152,12 @@ extension FluentBenchmarker { // https://github.com/vapor/fluent-kit/issues/257 private func testFilter_emptyRightHandSide() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { guard let correctUUID = try Planet.query(on: self.database).first().wait()?.id else { XCTFail("Cannot get UUID to test against") return @@ -148,11 +172,14 @@ extension FluentBenchmarker { } private func testFilter_optionalStringContains() throws { - try self.runTest(#function, [ - FooOwnerMigration(), - FooEnumMigration(), - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooOwnerMigration(), + FooEnumMigration(), + FooMigration(), + ] + ) { try Foo(bar: "foo").create(on: self.database).wait() try Foo(bar: "bar").create(on: self.database).wait() try Foo(bar: "baz").create(on: self.database).wait() @@ -165,11 +192,14 @@ extension FluentBenchmarker { } private func testFilter_enum() throws { - try self.runTest(#function, [ - FooOwnerMigration(), - FooEnumMigration(), - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooOwnerMigration(), + FooEnumMigration(), + FooMigration(), + ] + ) { try Foo(bar: "foo1", type: .foo).create(on: self.database).wait() try Foo(bar: "foo2", type: .foo).create(on: self.database).wait() try Foo(bar: "baz", type: .baz).create(on: self.database).wait() @@ -187,11 +217,14 @@ extension FluentBenchmarker { } private func testFilter_joinedEnum() throws { - try self.runTest(#function, [ - FooOwnerMigration(), - FooEnumMigration(), - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooOwnerMigration(), + FooEnumMigration(), + FooMigration(), + ] + ) { let fooOwner = FooOwner(name: "foo_owner") try fooOwner.create(on: self.database).wait() @@ -217,11 +250,14 @@ extension FluentBenchmarker { } private func testFilter_joinedAliasedEnum() throws { - try self.runTest(#function, [ - FooOwnerMigration(), - FooEnumMigration(), - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooOwnerMigration(), + FooEnumMigration(), + FooMigration(), + ] + ) { let fooOwner = FooOwner(name: "foo_owner") try fooOwner.create(on: self.database).wait() diff --git a/Sources/FluentBenchmark/Tests/GroupTests.swift b/Sources/FluentBenchmark/Tests/GroupTests.swift index 678c512d..09d0a310 100644 --- a/Sources/FluentBenchmark/Tests/GroupTests.swift +++ b/Sources/FluentBenchmark/Tests/GroupTests.swift @@ -6,13 +6,16 @@ import XCTest extension FluentBenchmarker { public func testGroup() throws { try self.testGroup_flat() -// try self.testGroup_nested() + // try self.testGroup_nested() } func testGroup_flat() throws { - try runTest(#function, [ - FlatMoonMigration(), - FlatMoonSeed() - ]) { + try runTest( + #function, + [ + FlatMoonMigration(), + FlatMoonSeed(), + ] + ) { // Test filtering moons let moons = try FlatMoon.query(on: self.database) .filter(\.$planet.$type == .smallRocky) @@ -48,44 +51,44 @@ extension FluentBenchmarker { } } -// func testGroup_nested() throws { -// try runTest(#function, [ -// NestedMoonMigration(), -// NestedMoonSeed() -// ]) { -// // Test filtering moons -// let moons = try NestedMoon.query(on: self.database) -// .filter(\.$planet.$type == .smallRocky) -// .all().wait() -// -// XCTAssertEqual(moons.count, 1) -// guard let moon = moons.first else { -// return -// } -// -// XCTAssertEqual(moon.name, "Moon") -// XCTAssertEqual(moon.planet.name, "Earth") -// XCTAssertEqual(moon.planet.type, .smallRocky) -// XCTAssertEqual(moon.planet.star.name, "Sol") -// XCTAssertEqual(moon.planet.star.galaxy.name, "Milky Way") -// -// // Test JSON -// let json = try prettyJSON(moon) -// let decoded = try JSONDecoder().decode(NestedMoon.self, from: Data(json.utf8)) -// XCTAssertEqual(decoded.name, "Moon") -// XCTAssertEqual(decoded.planet.name, "Earth") -// XCTAssertEqual(decoded.planet.type, .smallRocky) -// XCTAssertEqual(decoded.planet.star.name, "Sol") -// XCTAssertEqual(decoded.planet.star.galaxy.name, "Milky Way") -// -// // Test deeper filter -// let all = try FlatMoon.query(on: self.database) -// .filter(\.$planet.$star.$galaxy.$name == "Milky Way") -// .all() -// .wait() -// XCTAssertEqual(all.count, 2) -// } -// } + // func testGroup_nested() throws { + // try runTest(#function, [ + // NestedMoonMigration(), + // NestedMoonSeed() + // ]) { + // // Test filtering moons + // let moons = try NestedMoon.query(on: self.database) + // .filter(\.$planet.$type == .smallRocky) + // .all().wait() + // + // XCTAssertEqual(moons.count, 1) + // guard let moon = moons.first else { + // return + // } + // + // XCTAssertEqual(moon.name, "Moon") + // XCTAssertEqual(moon.planet.name, "Earth") + // XCTAssertEqual(moon.planet.type, .smallRocky) + // XCTAssertEqual(moon.planet.star.name, "Sol") + // XCTAssertEqual(moon.planet.star.galaxy.name, "Milky Way") + // + // // Test JSON + // let json = try prettyJSON(moon) + // let decoded = try JSONDecoder().decode(NestedMoon.self, from: Data(json.utf8)) + // XCTAssertEqual(decoded.name, "Moon") + // XCTAssertEqual(decoded.planet.name, "Earth") + // XCTAssertEqual(decoded.planet.type, .smallRocky) + // XCTAssertEqual(decoded.planet.star.name, "Sol") + // XCTAssertEqual(decoded.planet.star.galaxy.name, "Milky Way") + // + // // Test deeper filter + // let all = try FlatMoon.query(on: self.database) + // .filter(\.$planet.$star.$galaxy.$name == "Milky Way") + // .all() + // .wait() + // XCTAssertEqual(all.count, 2) + // } + // } } // MARK: Flat @@ -118,7 +121,7 @@ private final class FlatMoon: Model, @unchecked Sendable { @Field(key: "name") var name: String - init() { } + init() {} init(name: String) { self.name = name @@ -128,7 +131,7 @@ private final class FlatMoon: Model, @unchecked Sendable { @Group(key: "galaxy") var galaxy: Galaxy - init() { } + init() {} init(name: String, galaxy: Galaxy) { self.name = name @@ -139,7 +142,7 @@ private final class FlatMoon: Model, @unchecked Sendable { @Group(key: "star") var star: Star - init() { } + init() {} init(name: String, type: PlanetType, star: Star) { self.name = name @@ -151,7 +154,7 @@ private final class FlatMoon: Model, @unchecked Sendable { @Group(key: "planet") var planet: Planet - init() { } + init() {} init(id: IDValue? = nil, name: String, planet: Planet) { self.id = id @@ -160,7 +163,6 @@ private final class FlatMoon: Model, @unchecked Sendable { } } - private struct FlatMoonMigration: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema("moons") @@ -178,9 +180,8 @@ private struct FlatMoonMigration: Migration { } } - private struct FlatMoonSeed: Migration { - init() { } + init() {} func prepare(on database: any Database) -> EventLoopFuture { let moon = FlatMoon( diff --git a/Sources/FluentBenchmark/Tests/IDTests.swift b/Sources/FluentBenchmark/Tests/IDTests.swift index 9407507a..9a87f487 100644 --- a/Sources/FluentBenchmark/Tests/IDTests.swift +++ b/Sources/FluentBenchmark/Tests/IDTests.swift @@ -21,9 +21,12 @@ extension FluentBenchmarker { } private func testID_default() throws { - try self.runTest(#function, [ - FooMigration() - ]) { + try self.runTest( + #function, + [ + FooMigration() + ] + ) { let foo1 = Foo() try foo1.save(on: self.database).wait() XCTAssertNotNil(foo1.id) @@ -35,9 +38,12 @@ extension FluentBenchmarker { } private func testID_string() throws { - try self.runTest(#function, [ - StringFooMigration() - ]) { + try self.runTest( + #function, + [ + StringFooMigration() + ] + ) { let foo1 = StringFoo(id: "a") try foo1.save(on: self.database).wait() XCTAssertEqual(foo1.id, "a") @@ -48,9 +54,12 @@ extension FluentBenchmarker { } private func testID_autoincrementing() throws { - try self.runTest(#function, [ - AutoincrementingFooMigration() - ]) { + try self.runTest( + #function, + [ + AutoincrementingFooMigration() + ] + ) { let foo1 = AutoincrementingFoo() try foo1.save(on: self.database).wait() XCTAssertEqual(foo1.id, 1) @@ -60,11 +69,13 @@ extension FluentBenchmarker { } } - private func testID_customAutoincrementing() throws { - try self.runTest(#function, [ - CustomAutoincrementingFooMigration() - ]) { + try self.runTest( + #function, + [ + CustomAutoincrementingFooMigration() + ] + ) { let foo1 = CustomAutoincrementingFoo() try foo1.save(on: self.database).wait() XCTAssertEqual(foo1.id, 1) @@ -82,7 +93,7 @@ private final class Foo: Model, @unchecked Sendable { @ID var id: UUID? - init() { } + init() {} init(id: UUID? = nil) { self.id = id @@ -107,7 +118,7 @@ private final class StringFoo: Model, @unchecked Sendable { @ID(custom: .id, generatedBy: .user) var id: String? - init() { } + init() {} init(id: String) { self.id = id @@ -132,7 +143,7 @@ private final class AutoincrementingFoo: Model, @unchecked Sendable { @ID(custom: .id, generatedBy: .database) var id: Int? - init() { } + init() {} init(id: Int? = nil) { self.id = id @@ -157,7 +168,7 @@ private final class CustomAutoincrementingFoo: Model, @unchecked Sendable { @ID(custom: "bar", generatedBy: .database) var id: Int? - init() { } + init() {} init(id: Int? = nil) { self.id = id diff --git a/Sources/FluentBenchmark/Tests/JoinTests.swift b/Sources/FluentBenchmark/Tests/JoinTests.swift index d2b6fcc5..0f13a792 100644 --- a/Sources/FluentBenchmark/Tests/JoinTests.swift +++ b/Sources/FluentBenchmark/Tests/JoinTests.swift @@ -1,7 +1,7 @@ -import SQLKit import FluentKit import Foundation import NIOCore +import SQLKit import XCTest extension FluentBenchmarker { @@ -16,9 +16,12 @@ extension FluentBenchmarker { } private func testJoin_basic() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .join(Star.self, on: \Planet.$star.$id == \Star.$id) .all().wait() @@ -53,11 +56,14 @@ extension FluentBenchmarker { } private func testJoin_sameTable() throws { - try self.runTest(#function, [ - TeamMigration(), - MatchMigration(), - TeamMatchSeed() - ]) { + try self.runTest( + #function, + [ + TeamMigration(), + MatchMigration(), + TeamMatchSeed(), + ] + ) { // test fetching teams do { let teams = try Team.query(on: self.database) @@ -118,12 +124,15 @@ extension FluentBenchmarker { private func testJoin_fieldFilter() throws { // seeded db - try runTest(#function, [ - CityMigration(), - CitySeed(), - SchoolMigration(), - SchoolSeed() - ]) { + try runTest( + #function, + [ + CityMigration(), + CitySeed(), + SchoolMigration(), + SchoolSeed(), + ] + ) { let smallSchools = try School.query(on: self.database) .join(City.self, on: \School.$city.$id == \City.$id) .filter(\School.$pupils < \City.$averagePupils) @@ -147,7 +156,6 @@ extension FluentBenchmarker { } } - private func testJoin_fieldOrdering() throws { _ = School.query(on: self.database) .join(City.self, on: \School.$city.$id == \City.$id) @@ -187,9 +195,12 @@ extension FluentBenchmarker { } private func testJoin_partialSelect() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .field(\.$name) .join(Star.self, on: \Planet.$star.$id == \Star.$id) @@ -212,23 +223,26 @@ extension FluentBenchmarker { } } } - + private func testJoin_complexCondition() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { guard self.database is any SQLDatabase else { return } - + let planets = try Planet.query(on: self.database) .join(Star.self, on: \Planet.$star.$id == \Star.$id && \Star.$name != \Planet.$name) .all().wait() - + XCTAssertFalse(planets.isEmpty) - + let morePlanets = try Planet.query(on: self.database) .join(Star.self, on: \Planet.$star.$id == \Star.$id && \Star.$name != "Sol") .all().wait() - + XCTAssertEqual(morePlanets.count, 1) } } @@ -249,7 +263,7 @@ private final class Team: Model, @unchecked Sendable { @Children(for: \.$awayTeam) var awayMatches: [Match] - init() { } + init() {} init(id: IDValue? = nil, name: String) { self.id = id @@ -285,7 +299,7 @@ private final class Match: Model, @unchecked Sendable { @Parent(key: "away_team_id") var awayTeam: Team - init() { } + init() {} init(id: IDValue? = nil, name: String, homeTeam: Team, awayTeam: Team) { self.id = id @@ -316,14 +330,15 @@ private struct TeamMatchSeed: Migration { let b = Team(name: "b") let c = Team(name: "c") return a.create(on: database).and(b.create(on: database)).and(c.create(on: database)).flatMap { _ -> EventLoopFuture in - .andAllSucceed([ - Match(name: "a vs. b", homeTeam: a, awayTeam: b).save(on: database), - Match(name: "a vs. c", homeTeam: a, awayTeam: c).save(on: database), - Match(name: "b vs. c", homeTeam: b, awayTeam: c).save(on: database), - Match(name: "b vs. a", homeTeam: b, awayTeam: a).save(on: database), - Match(name: "c vs. b", homeTeam: c, awayTeam: b).save(on: database), - Match(name: "c vs. a", homeTeam: c, awayTeam: a).save(on: database), - ], on: database.eventLoop) + .andAllSucceed( + [ + Match(name: "a vs. b", homeTeam: a, awayTeam: b).save(on: database), + Match(name: "a vs. c", homeTeam: a, awayTeam: c).save(on: database), + Match(name: "b vs. c", homeTeam: b, awayTeam: c).save(on: database), + Match(name: "b vs. a", homeTeam: b, awayTeam: a).save(on: database), + Match(name: "c vs. b", homeTeam: c, awayTeam: b).save(on: database), + Match(name: "c vs. a", homeTeam: c, awayTeam: a).save(on: database), + ], on: database.eventLoop) } } @@ -336,7 +351,6 @@ private struct TeamMatchSeed: Migration { } } - private final class School: Model, @unchecked Sendable { static let schema = "schools" @@ -352,7 +366,7 @@ private final class School: Model, @unchecked Sendable { @Parent(key: "city_id") var city: City - init() { } + init() {} init(id: IDValue? = nil, name: String, pupils: Int, cityID: City.IDValue) { self.id = id @@ -384,7 +398,7 @@ private struct SchoolSeed: Migration { (name: "schoolA1", pupils: 500), (name: "schoolA2", pupils: 250), (name: "schoolA3", pupils: 400), - (name: "schoolA4", pupils: 50) + (name: "schoolA4", pupils: 50), ], to: "Amsterdam", on: database @@ -394,7 +408,7 @@ private struct SchoolSeed: Migration { (name: "schoolB1", pupils: 500), (name: "schoolB2", pupils: 500), (name: "schoolB3", pupils: 400), - (name: "schoolB4", pupils: 200) + (name: "schoolB4", pupils: 200), ], to: "New York", on: database @@ -438,7 +452,7 @@ private final class City: Model, @unchecked Sendable { @Children(for: \.$city) var schools: [School] - init() { } + init() {} init(id: IDValue? = nil, name: String, averagePupils: Int) { self.id = id @@ -462,12 +476,12 @@ private struct CityMigration: Migration { } private struct CitySeed: Migration { - init() { } + init() {} func prepare(on database: any Database) -> EventLoopFuture { let saves = [ City(name: "Amsterdam", averagePupils: 300), - City(name: "New York", averagePupils: 400) + City(name: "New York", averagePupils: 400), ].map { city -> EventLoopFuture in return city.save(on: database) } diff --git a/Sources/FluentBenchmark/Tests/MiddlewareTests.swift b/Sources/FluentBenchmark/Tests/MiddlewareTests.swift index f2efae66..0044629d 100644 --- a/Sources/FluentBenchmark/Tests/MiddlewareTests.swift +++ b/Sources/FluentBenchmark/Tests/MiddlewareTests.swift @@ -10,14 +10,17 @@ extension FluentBenchmarker { try self.testMiddleware_batchCreationFail() try self.testAsyncMiddleware_methods() } - + public func testMiddleware_methods() throws { self.databases.middleware.use(UserMiddleware()) defer { self.databases.middleware.clear() } - try self.runTest(#function, [ - UserMigration(), - ]) { + try self.runTest( + #function, + [ + UserMigration() + ] + ) { let user = User(name: "A") // create do { @@ -66,9 +69,12 @@ extension FluentBenchmarker { self.databases.middleware.use(AsyncUserMiddleware()) defer { self.databases.middleware.clear() } - try self.runTest(#function, [ - UserMigration(), - ]) { + try self.runTest( + #function, + [ + UserMigration() + ] + ) { let user = User(name: "A") // create do { @@ -112,24 +118,27 @@ extension FluentBenchmarker { XCTAssertEqual(user.name, "G") } } - + public func testMiddleware_batchCreationFail() throws { self.databases.middleware.clear() self.databases.middleware.use(UserBatchMiddleware()) defer { self.databases.middleware.clear() } - try self.runTest(#function, [ - UserMigration(), - ]) { + try self.runTest( + #function, + [ + UserMigration() + ] + ) { let user = User(name: "A") let user2 = User(name: "B") let user3 = User(name: "C") - + XCTAssertThrowsError(try [user, user2, user3].create(on: self.database).wait()) { error in let testError = (error as? TestError) XCTAssertEqual(testError?.string, "cancelCreation") } - + let userCount = try User.query(on: self.database).count().wait() XCTAssertEqual(userCount, 0) } @@ -152,7 +161,7 @@ private final class User: Model, @unchecked Sendable { @Timestamp(key: "deletedAt", on: .delete) var deletedAt: Date? - init() { } + init() {} init(id: IDValue? = nil, name: String) { self.id = id @@ -199,7 +208,7 @@ private struct AsyncUserMiddleware: AsyncModelMiddleware { func restore(model: User, on db: any Database, next: any AnyAsyncModelResponder) async throws { model.name = "F" - try await next.restore(model , on: db) + try await next.restore(model, on: db) throw TestError(string: "didRestore") } @@ -214,7 +223,7 @@ private struct AsyncUserMiddleware: AsyncModelMiddleware { private struct UserMiddleware: ModelMiddleware { func create(model: User, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { model.name = "B" - + return next.create(model, on: db).flatMap { db.eventLoop.makeFailedFuture(TestError(string: "didCreate")) } @@ -239,7 +248,7 @@ private struct UserMiddleware: ModelMiddleware { func restore(model: User, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { model.name = "F" - return next.restore(model , on: db).flatMap { + return next.restore(model, on: db).flatMap { db.eventLoop.makeFailedFuture(TestError(string: "didRestore")) } } diff --git a/Sources/FluentBenchmark/Tests/MigratorTests.swift b/Sources/FluentBenchmark/Tests/MigratorTests.swift index f8dfdfa7..8f57a02d 100644 --- a/Sources/FluentBenchmark/Tests/MigratorTests.swift +++ b/Sources/FluentBenchmark/Tests/MigratorTests.swift @@ -1,8 +1,8 @@ import FluentKit import Foundation +import Logging import NIOCore import XCTest -import Logging extension FluentBenchmarker { public func testMigrator() throws { @@ -17,7 +17,7 @@ extension FluentBenchmarker { let migrations = Migrations() migrations.add(GalaxyMigration()) migrations.add(StarMigration()) - + let database = self.database let migrator = Migrator( databaseFactory: { _ in database }, @@ -46,7 +46,7 @@ extension FluentBenchmarker { migrations.add(GalaxyMigration()) migrations.add(ErrorMigration()) migrations.add(StarMigration()) - + let database = self.database let migrator = Migrator( databaseFactory: { _ in database }, @@ -84,7 +84,6 @@ extension FluentBenchmarker { let migrations = Migrations() - // Migration #1 migrations.add(GalaxyMigration(), to: databaseID.0) @@ -115,7 +114,6 @@ extension FluentBenchmarker { // will have not been created yet. } - // Migration #2 migrations.add(GalaxyMigration(), to: databaseID.1) @@ -129,7 +127,6 @@ extension FluentBenchmarker { try XCTAssertEqual(MigrationLog.query(on: database1).count().wait(), 1) - // Teardown try migrator.revertAllBatches().wait() } @@ -141,7 +138,7 @@ extension FluentBenchmarker { let databaseIds = Array(self.databases.ids()).prefix(2) let databases = databaseIds.map { self.databases.database($0, logger: logger, on: self.databases.eventLoopGroup.next())! } let migrations = Migrations() - + migrations.add([GalaxyMigration(), StarMigration(), GalaxySeed()], to: databaseIds[0]) migrations.add(GalaxyMigration(), StarMigration(), PlanetMigration(), to: databaseIds[1]) @@ -173,9 +170,9 @@ extension FluentBenchmarker { } internal struct ErrorMigration: Migration { - init() { } + init() {} - struct Error: Swift.Error { } + struct Error: Swift.Error {} func prepare(on database: any Database) -> EventLoopFuture { return database.eventLoop.makeFailedFuture(Error()) diff --git a/Sources/FluentBenchmark/Tests/ModelTests.swift b/Sources/FluentBenchmark/Tests/ModelTests.swift index 88a9258b..942ecc7f 100644 --- a/Sources/FluentBenchmark/Tests/ModelTests.swift +++ b/Sources/FluentBenchmark/Tests/ModelTests.swift @@ -22,9 +22,12 @@ extension FluentBenchmarker { } private func testModel_uuid() throws { - try self.runTest(#function, [ - UserMigration(), - ]) { + try self.runTest( + #function, + [ + UserMigration() + ] + ) { try User(name: "Vapor") .save(on: self.database).wait() let count = try User.query(on: self.database).count().wait() @@ -33,12 +36,15 @@ extension FluentBenchmarker { } private func testModel_decode() throws { - try self.runTest(#function, [ - TodoMigration(), - ]) { + try self.runTest( + #function, + [ + TodoMigration() + ] + ) { let todo = """ - {"title": "Finish Vapor 4"} - """ + {"title": "Finish Vapor 4"} + """ try JSONDecoder().decode(Todo.self, from: todo.data(using: .utf8)!) .save(on: self.database).wait() guard try Todo.query(on: self.database).count().wait() == 1 else { @@ -49,9 +55,12 @@ extension FluentBenchmarker { } private func testModel_nullField() throws { - try runTest(#function, [ - FooMigration(), - ]) { + try runTest( + #function, + [ + FooMigration() + ] + ) { let foo = Foo(bar: "test") try foo.save(on: self.database).wait() guard foo.bar != nil else { @@ -77,9 +86,10 @@ extension FluentBenchmarker { .all().wait() XCTAssertEqual(all.count, 1) - guard let fetched = try Foo.query(on: self.database) - .filter(\.$id == foo.id!) - .first().wait() + guard + let fetched = try Foo.query(on: self.database) + .filter(\.$id == foo.id!) + .first().wait() else { XCTFail("no model returned") return @@ -92,9 +102,12 @@ extension FluentBenchmarker { } private func testModel_nullField_batchCreate() throws { - try runTest(#function, [ - FooMigration(), - ]) { + try runTest( + #function, + [ + FooMigration() + ] + ) { let a = Foo(bar: "test") let b = Foo(bar: nil) try [a, b].create(on: self.database).wait() @@ -102,9 +115,12 @@ extension FluentBenchmarker { } private func testModel_idGeneration() throws { - try runTest(#function, [ - GalaxyMigration(), - ]) { + try runTest( + #function, + [ + GalaxyMigration() + ] + ) { let galaxy = Galaxy(name: "Milky Way") guard galaxy.id == nil else { XCTFail("id should not be set") @@ -126,9 +142,12 @@ extension FluentBenchmarker { } private func testModel_jsonColumn() throws { - try runTest(#function, [ - BarMigration(), - ]) { + try runTest( + #function, + [ + BarMigration() + ] + ) { let bar = Bar(baz: .init(quux: "test")) try bar.save(on: self.database).wait() @@ -146,9 +165,12 @@ extension FluentBenchmarker { } private func testModel_hasChanges() throws { - try runTest(#function, [ - FooMigration(), - ]) { + try runTest( + #function, + [ + FooMigration() + ] + ) { // Test create let foo = Foo(bar: "test") XCTAssertTrue(foo.hasChanges) @@ -156,9 +178,10 @@ extension FluentBenchmarker { XCTAssertFalse(foo.hasChanges) // Test update - guard let fetched = try Foo.query(on: self.database) - .filter(\.$id == foo.id!) - .first().wait() + guard + let fetched = try Foo.query(on: self.database) + .filter(\.$id == foo.id!) + .first().wait() else { XCTFail("no model returned") return @@ -181,7 +204,7 @@ extension FluentBenchmarker { } } } - + private func testModel_useOfFieldsWithoutGroup() throws { try runTest(#function, []) { final class Contained: Fields, @unchecked Sendable { @@ -195,7 +218,7 @@ extension FluentBenchmarker { @Field(key: "primary") var primary: Contained @Field(key: "additional") var additional: [Contained] init() {} - + struct Migration: FluentKit.Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema(Enclosure.schema) @@ -207,10 +230,10 @@ extension FluentBenchmarker { func revert(on database: any Database) -> EventLoopFuture { database.schema(Enclosure.schema).delete() } } } - + try (self.database as? any SQLDatabase)?.drop(table: Enclosure.schema).ifExists().run().wait() try Enclosure.Migration().prepare(on: self.database).wait() - + do { let enclosure = Enclosure() enclosure.primary = .init() @@ -245,13 +268,14 @@ struct BadFooOutput: DatabaseOutput { } func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T : Decodable - { - throw DecodingError.typeMismatch(T.self, .init( - codingPath: [], - debugDescription: "Failed to decode", - underlyingError: nil - )) + where T: Decodable { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: [], + debugDescription: "Failed to decode", + underlyingError: nil + )) } var description: String { @@ -268,7 +292,7 @@ private final class Foo: Model, @unchecked Sendable { @OptionalField(key: "bar") var bar: String? - init() { } + init() {} init(id: IDValue? = nil, bar: String?) { self.id = id @@ -298,7 +322,7 @@ private final class User: Model, @unchecked Sendable { @Field(key: "name") var name: String - init() { } + init() {} init(id: UUID? = nil, name: String) { self.id = id self.name = name @@ -327,7 +351,7 @@ private final class Todo: Model, @unchecked Sendable { @Field(key: "title") var title: String - init() { } + init() {} init(id: UUID? = nil, title: String) { self.id = id self.title = title @@ -360,7 +384,7 @@ private final class Bar: Model, @unchecked Sendable { @Field(key: "baz") var baz: Baz - init() { } + init() {} init(id: IDValue? = nil, baz: Baz) { self.id = id diff --git a/Sources/FluentBenchmark/Tests/OptionalParentTests.swift b/Sources/FluentBenchmark/Tests/OptionalParentTests.swift index 1f844653..7d201b98 100644 --- a/Sources/FluentBenchmark/Tests/OptionalParentTests.swift +++ b/Sources/FluentBenchmark/Tests/OptionalParentTests.swift @@ -5,9 +5,12 @@ import XCTest extension FluentBenchmarker { public func testOptionalParent() throws { - try runTest(#function, [ - UserMigration() - ]) { + try runTest( + #function, + [ + UserMigration() + ] + ) { // seed do { let swift = User( @@ -65,20 +68,21 @@ extension FluentBenchmarker { .all().wait() XCTAssertEqual(users2.count, 1) XCTAssert(users2.first?.bestFriend == nil) - + // Test deleted OptionalParent try User.query(on: self.database).filter(\.$name == "Swift").delete().wait() - + let users3 = try User.query(on: self.database) .with(\.$bestFriend, withDeleted: true) .all().wait() XCTAssertEqual(users3.first?.bestFriend?.name, "Swift") - - XCTAssertThrowsError(try User.query(on: self.database) - .with(\.$bestFriend) - .all().wait() + + XCTAssertThrowsError( + try User.query(on: self.database) + .with(\.$bestFriend) + .all().wait() ) { error in - guard case let .missingParent(from, to, key, _) = error as? FluentError else { + guard case .missingParent(let from, let to, let key, _) = error as? FluentError else { return XCTFail("Unexpected error \(error) thrown") } XCTAssertEqual(from, "User") @@ -113,11 +117,11 @@ private final class User: Model, @unchecked Sendable { @Children(for: \.$bestFriend) var friends: [User] - + @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? - init() { } + init() {} init(id: IDValue? = nil, name: String, pet: Pet, bestFriend: User? = nil) { self.id = id @@ -143,9 +147,8 @@ private struct UserMigration: Migration { } } - private struct UserSeed: Migration { - init() { } + init() {} func prepare(on database: any Database) -> EventLoopFuture { let tanner = User(name: "Tanner", pet: .init(name: "Ziz", type: .cat)) diff --git a/Sources/FluentBenchmark/Tests/PaginationTests.swift b/Sources/FluentBenchmark/Tests/PaginationTests.swift index 3f7c08e6..b4c1f8e4 100644 --- a/Sources/FluentBenchmark/Tests/PaginationTests.swift +++ b/Sources/FluentBenchmark/Tests/PaginationTests.swift @@ -3,9 +3,12 @@ import XCTest extension FluentBenchmarker { public func testPagination() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { do { let planetsPage1 = try Planet.query(on: self.database) .sort(\.$name) @@ -53,9 +56,12 @@ extension FluentBenchmarker { } public func testPaginationDoesntCrashWithInvalidValues() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { do { _ = try Planet.query(on: self.database) .sort(\.$name) diff --git a/Sources/FluentBenchmark/Tests/ParentTests.swift b/Sources/FluentBenchmark/Tests/ParentTests.swift index 73d48b4c..f99e055a 100644 --- a/Sources/FluentBenchmark/Tests/ParentTests.swift +++ b/Sources/FluentBenchmark/Tests/ParentTests.swift @@ -11,9 +11,12 @@ extension FluentBenchmarker { } private func testParent_serialization() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let stars = try Star.query(on: self.database).all().wait() let encoded = try JSONEncoder().encode(stars) @@ -24,11 +27,14 @@ extension FluentBenchmarker { XCTAssertEqual(stars.map { $0.$galaxy.id }, decoded.map { $0.galaxy.id }) } } - + private func testParent_get() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .all().wait() @@ -46,12 +52,16 @@ extension FluentBenchmarker { } private func testParent_value() throws { - try runTest(#function, [ - SolarSystem() - ]) { - guard let earth = try Planet.query(on: self.database) - .filter(\.$name == "Earth") - .first().wait() + try runTest( + #function, + [ + SolarSystem() + ] + ) { + guard + let earth = try Planet.query(on: self.database) + .filter(\.$name == "Earth") + .first().wait() else { XCTFail("Could not load Planet earth") return diff --git a/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift b/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift index 14ebb41b..1eaa280b 100644 --- a/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift +++ b/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift @@ -1,31 +1,34 @@ -import XCTest import Dispatch import FluentKit import Foundation import NIOCore import SQLKit +import XCTest extension FluentBenchmarker { internal func testPerformance_siblings() throws { // we know database will outlive this test // so doing this is fine. // otherwise threading is a PITA - let conn = try self.database.withConnection { + let conn = try self.database.withConnection { $0.eventLoop.makeSucceededFuture($0) }.wait() // this test makes a ton of queries so doing it // on a single connection helps combat pool timeouts - try self.runTest("testPerformance_siblings", [ - PersonMigration(), - ExpeditionMigration(), - ExpeditionOfficerMigration(), - ExpeditionScientistMigration(), - ExpeditionDoctorMigration(), - PersonSeed(), - ExpeditionSeed(), - ExpeditionPeopleSeed(), - ], on: conn) { conn in + try self.runTest( + "testPerformance_siblings", + [ + PersonMigration(), + ExpeditionMigration(), + ExpeditionOfficerMigration(), + ExpeditionScientistMigration(), + ExpeditionDoctorMigration(), + PersonSeed(), + ExpeditionSeed(), + ExpeditionPeopleSeed(), + ], on: conn + ) { conn in let start = Date() let expeditions = try Expedition.query(on: conn) .with(\.$officers) @@ -42,13 +45,24 @@ extension FluentBenchmarker { conn.logger.info("Run took \(time) seconds.") XCTAssertEqual(expeditions.count, 300) if let sqlConn = conn as? any SQLDatabase { - struct DTO1: Codable { let id: UUID; let name: String, area: String, objective: String } + struct DTO1: Codable { + let id: UUID + let name: String, area: String, objective: String + } struct DTO2: Codable { let id: UUID, expedition_id: UUID, person_id: UUID } let start = Date() - let expeditions = try sqlConn.select().columns("id", "name", "area", "objective").from(Expedition.schema).all(decoding: DTO1.self).wait() - let officers = try sqlConn.select().columns("id", "expedition_id", "person_id").from(ExpeditionOfficer.schema).where(SQLIdentifier("expedition_id"), .in, expeditions.map(\.id)).all(decoding: DTO2.self).wait() - let scientists = try sqlConn.select().columns("id", "expedition_id", "person_id").from(ExpeditionScientist.schema).where(SQLIdentifier("expedition_id"), .in, expeditions.map(\.id)).all(decoding: DTO2.self).wait() - let doctors = try sqlConn.select().columns("id", "expedition_id", "person_id").from(ExpeditionDoctor.schema).where(SQLIdentifier("expedition_id"), .in, expeditions.map(\.id)).all(decoding: DTO2.self).wait() + let expeditions = try sqlConn.select().columns("id", "name", "area", "objective").from(Expedition.schema).all( + decoding: DTO1.self + ).wait() + let officers = try sqlConn.select().columns("id", "expedition_id", "person_id").from(ExpeditionOfficer.schema).where( + SQLIdentifier("expedition_id"), .in, expeditions.map(\.id) + ).all(decoding: DTO2.self).wait() + let scientists = try sqlConn.select().columns("id", "expedition_id", "person_id").from(ExpeditionScientist.schema).where( + SQLIdentifier("expedition_id"), .in, expeditions.map(\.id) + ).all(decoding: DTO2.self).wait() + let doctors = try sqlConn.select().columns("id", "expedition_id", "person_id").from(ExpeditionDoctor.schema).where( + SQLIdentifier("expedition_id"), .in, expeditions.map(\.id) + ).all(decoding: DTO2.self).wait() let time = Date().timeIntervalSince(start) // Run (SQLKit mode) took 0.6164050102233887 seconds. // Run (SQLKit mode) took 0.050302982330322266 seconds. @@ -65,15 +79,19 @@ extension FluentBenchmarker { private struct PersonSeed: Migration { func prepare(on database: any Database) -> EventLoopFuture { if let sqlDatabase = database as? any SQLDatabase { - struct DTO: Codable { let id: UUID; let first_name: String, last_name: String } + struct DTO: Codable { + let id: UUID + let first_name: String, last_name: String + } return try! sqlDatabase.insert(into: Person.schema) .models((1...600).map { DTO(id: UUID(), first_name: "Foo #\($0)", last_name: "Bar") }) .run() } else { - return .andAllSucceed((1...600).map { i in - Person(firstName: "Foo #\(i)", lastName: "Bar") - .create(on: database) - }, on: database.eventLoop) + return .andAllSucceed( + (1...600).map { i in + Person(firstName: "Foo #\(i)", lastName: "Bar") + .create(on: database) + }, on: database.eventLoop) } } @@ -85,15 +103,19 @@ private struct PersonSeed: Migration { private struct ExpeditionSeed: Migration { func prepare(on database: any Database) -> EventLoopFuture { if let sqlDatabase = database as? any SQLDatabase { - struct DTO: Codable { let id: UUID; let name: String, area: String, objective: String } + struct DTO: Codable { + let id: UUID + let name: String, area: String, objective: String + } return try! sqlDatabase.insert(into: Expedition.schema) .models((1...300).map { DTO(id: UUID(), name: "Baz #\($0)", area: "Qux", objective: "Quuz") }) .run() } else { - return .andAllSucceed((1...300).map { i in - Expedition(name: "Baz #\(i)", area: "Qux", objective: "Quuz") - .create(on: database) - }, on: database.eventLoop) + return .andAllSucceed( + (1...300).map { i in + Expedition(name: "Baz #\(i)", area: "Qux", objective: "Quuz") + .create(on: database) + }, on: database.eventLoop) } } @@ -106,44 +128,56 @@ private struct ExpeditionPeopleSeed: Migration { func prepare(on database: any Database) -> EventLoopFuture { if let sqlDatabase = database as? any SQLDatabase { return - sqlDatabase.select().column("id").from(Expedition.schema).all().flatMapThrowing { try $0.map { try $0.decode(column: "id", as: UUID.self) } } - .and(sqlDatabase.select().column("id").from(Person.schema).all().flatMapThrowing { try $0.map { try $0.decode(column: "id", as: UUID.self) } }) + sqlDatabase.select().column("id").from(Expedition.schema).all().flatMapThrowing { + try $0.map { try $0.decode(column: "id", as: UUID.self) } + } + .and( + sqlDatabase.select().column("id").from(Person.schema).all().flatMapThrowing { + try $0.map { try $0.decode(column: "id", as: UUID.self) } + } + ) .flatMap { expeditions, people in struct DTO: Codable { let id: UUID, expedition_id: UUID, person_id: UUID } - var officers: [DTO] = [], scientists: [DTO] = [], doctors: [DTO] = [] - + var officers: [DTO] = [] + var scientists: [DTO] = [] + var doctors: [DTO] = [] + for expedition in expeditions { - officers.append(contentsOf: people.pickRandomly(2).map { DTO(id: UUID(), expedition_id: expedition, person_id: $0) }) - scientists.append(contentsOf: people.pickRandomly(5).map { DTO(id: UUID(), expedition_id: expedition, person_id: $0) }) + officers.append( + contentsOf: people.pickRandomly(2).map { DTO(id: UUID(), expedition_id: expedition, person_id: $0) }) + scientists.append( + contentsOf: people.pickRandomly(5).map { DTO(id: UUID(), expedition_id: expedition, person_id: $0) }) doctors.append(contentsOf: people.pickRandomly(3).map { DTO(id: UUID(), expedition_id: expedition, person_id: $0) }) } - return EventLoopFuture.andAllSucceed([ - try! sqlDatabase.insert(into: ExpeditionOfficer.schema).models(officers).run(), - try! sqlDatabase.insert(into: ExpeditionScientist.schema).models(scientists).run(), - try! sqlDatabase.insert(into: ExpeditionDoctor.schema).models(doctors).run(), - ], on: sqlDatabase.eventLoop) + return EventLoopFuture.andAllSucceed( + [ + try! sqlDatabase.insert(into: ExpeditionOfficer.schema).models(officers).run(), + try! sqlDatabase.insert(into: ExpeditionScientist.schema).models(scientists).run(), + try! sqlDatabase.insert(into: ExpeditionDoctor.schema).models(doctors).run(), + ], on: sqlDatabase.eventLoop) } } else { return Expedition.query(on: database).all() .and(Person.query(on: database).all()) - .flatMap - { (expeditions, people) in - .andAllSucceed(expeditions.map { expedition in - expedition.$officers.attach(people.pickRandomly(2), on: database) - .and(expedition.$scientists.attach(people.pickRandomly(5), on: database)) - .and(expedition.$doctors.attach(people.pickRandomly(3), on: database)) - .map { _ in } - }, on: database.eventLoop) - } + .flatMap { (expeditions, people) in + .andAllSucceed( + expeditions.map { expedition in + expedition.$officers.attach(people.pickRandomly(2), on: database) + .and(expedition.$scientists.attach(people.pickRandomly(5), on: database)) + .and(expedition.$doctors.attach(people.pickRandomly(3), on: database)) + .map { _ in } + }, on: database.eventLoop) + } } } func revert(on database: any Database) -> EventLoopFuture { - .andAllSucceed([ - ExpeditionOfficer.query(on: database).delete(), - ExpeditionScientist.query(on: database).delete(), - ExpeditionDoctor.query(on: database).delete(), - ], on: database.eventLoop) + .andAllSucceed( + [ + ExpeditionOfficer.query(on: database).delete(), + ExpeditionScientist.query(on: database).delete(), + ExpeditionDoctor.query(on: database).delete(), + ], on: database.eventLoop) } } @@ -168,7 +202,7 @@ private final class Person: Model, @unchecked Sendable { @Siblings(through: ExpeditionDoctor.self, from: \.$person, to: \.$expedition) var expeditionsAsDoctor: [Expedition] - init() { } + init() {} init(id: UUID? = nil, firstName: String, lastName: String) { self.id = id @@ -215,7 +249,7 @@ private final class Expedition: Model, @unchecked Sendable { @Siblings(through: ExpeditionDoctor.self, from: \.$expedition, to: \.$person) var doctors: [Person] - init() { } + init() {} init( id: UUID? = nil, @@ -257,7 +291,7 @@ private final class ExpeditionOfficer: Model, @unchecked Sendable { @Parent(key: "person_id") var person: Person - init() { } + init() {} } private struct ExpeditionOfficerMigration: Migration { @@ -286,7 +320,7 @@ private final class ExpeditionScientist: Model, @unchecked Sendable { @Parent(key: "person_id") var person: Person - init() { } + init() {} } private struct ExpeditionScientistMigration: Migration { @@ -303,7 +337,6 @@ private struct ExpeditionScientistMigration: Migration { } } - private final class ExpeditionDoctor: Model, @unchecked Sendable { static let schema = "expedition+doctor" @@ -316,7 +349,7 @@ private final class ExpeditionDoctor: Model, @unchecked Sendable { @Parent(key: "person_id") var person: Person - init() { } + init() {} init(expeditionID: UUID, personID: UUID) { self.$expedition.id = expeditionID diff --git a/Sources/FluentBenchmark/Tests/PerformanceTests.swift b/Sources/FluentBenchmark/Tests/PerformanceTests.swift index d0b818a3..215c3175 100644 --- a/Sources/FluentBenchmark/Tests/PerformanceTests.swift +++ b/Sources/FluentBenchmark/Tests/PerformanceTests.swift @@ -1,11 +1,12 @@ import FluentKit +import NIOCore +import XCTest + #if !canImport(Darwin) -@preconcurrency import Foundation + @preconcurrency import Foundation #else -import Foundation + import Foundation #endif -import NIOCore -import XCTest extension FluentBenchmarker { public func testPerformance(decimalType: DatabaseSchema.DataType = .string) throws { @@ -14,9 +15,12 @@ extension FluentBenchmarker { } private func testPerformance_largeModel(decimalType: DatabaseSchema.DataType) throws { - try runTest(#function, [ - FooMigration(decimalType: decimalType) - ]) { + try runTest( + #function, + [ + FooMigration(decimalType: decimalType) + ] + ) { for _ in 0..<100 { let foo = Foo( bar: 42, @@ -44,59 +48,59 @@ extension FluentBenchmarker { } private final class Foo: Model, @unchecked Sendable { - static let schema = "foos" + static let schema = "foos" - struct Thud: Codable { - var foo: Int - var bar: Double - var baz: String - } + struct Thud: Codable { + var foo: Int + var bar: Double + var baz: String + } - @ID(key: .id) var id: UUID? - @Field(key: "bar") var bar: Int - @Field(key: "baz") var baz: Double - @Field(key: "qux") var qux: String - @Field(key: "quux") var quux: Date - @Field(key: "quuz") var quuz: Float - @Field(key: "corge") var corge: [Int] - @Field(key: "grault") var grault: [Double] - @Field(key: "garply") var garply: [String] - @Field(key: "fred") var fred: Decimal - @Field(key: "plugh") var plugh: Int? - @Field(key: "xyzzy") var xyzzy: Double? - @Field(key: "thud") var thud: Thud + @ID(key: .id) var id: UUID? + @Field(key: "bar") var bar: Int + @Field(key: "baz") var baz: Double + @Field(key: "qux") var qux: String + @Field(key: "quux") var quux: Date + @Field(key: "quuz") var quuz: Float + @Field(key: "corge") var corge: [Int] + @Field(key: "grault") var grault: [Double] + @Field(key: "garply") var garply: [String] + @Field(key: "fred") var fred: Decimal + @Field(key: "plugh") var plugh: Int? + @Field(key: "xyzzy") var xyzzy: Double? + @Field(key: "thud") var thud: Thud - init() { } + init() {} - init( - id: UUID? = nil, - bar: Int, - baz: Double, - qux: String, - quux: Date, - quuz: Float, - corge: [Int], - grault: [Double], - garply: [String], - fred: Decimal, - plugh: Int?, - xyzzy: Double?, - thud: Thud - ) { - self.id = id - self.bar = bar - self.baz = baz - self.qux = qux - self.quux = quux - self.quuz = quuz - self.corge = corge - self.grault = grault - self.garply = garply - self.fred = fred - self.plugh = plugh - self.xyzzy = xyzzy - self.thud = thud - } + init( + id: UUID? = nil, + bar: Int, + baz: Double, + qux: String, + quux: Date, + quuz: Float, + corge: [Int], + grault: [Double], + garply: [String], + fred: Decimal, + plugh: Int?, + xyzzy: Double?, + thud: Thud + ) { + self.id = id + self.bar = bar + self.baz = baz + self.qux = qux + self.quux = quux + self.quuz = quuz + self.corge = corge + self.grault = grault + self.garply = garply + self.fred = fred + self.plugh = plugh + self.xyzzy = xyzzy + self.thud = thud + } } private struct FooMigration: Migration { diff --git a/Sources/FluentBenchmark/Tests/RangeTests.swift b/Sources/FluentBenchmark/Tests/RangeTests.swift index 809a51e4..96bd2a69 100644 --- a/Sources/FluentBenchmark/Tests/RangeTests.swift +++ b/Sources/FluentBenchmark/Tests/RangeTests.swift @@ -5,11 +5,14 @@ extension FluentBenchmarker { public func testRange() throws { try self.testRange_basic() } - + private func testRange_basic() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { do { let planets = try Planet.query(on: self.database) .range(2..<5) diff --git a/Sources/FluentBenchmark/Tests/SQLTests.swift b/Sources/FluentBenchmark/Tests/SQLTests.swift index 24cfc5b7..3dbe00e4 100644 --- a/Sources/FluentBenchmark/Tests/SQLTests.swift +++ b/Sources/FluentBenchmark/Tests/SQLTests.swift @@ -19,9 +19,12 @@ extension FluentBenchmarker { } private func testSQL_rawDecode(_ sql: any SQLDatabase) throws { - try self.runTest(#function, [ - UserMigration() - ]) { + try self.runTest( + #function, + [ + UserMigration() + ] + ) { let tanner = User(firstName: "Tanner", lastName: "Nelson", parentID: UUID()) try tanner.create(on: self.database).wait() @@ -66,7 +69,6 @@ extension FluentBenchmarker { } } - private final class User: Model, @unchecked Sendable { static let schema = "users" @@ -82,10 +84,10 @@ private final class User: Model, @unchecked Sendable { @OptionalParent(key: "parent_id") var parent: User? - init() { } + init() {} init( - id: UUID? = nil, + id: UUID? = nil, firstName: String, lastName: String, parentID: UUID? = nil @@ -111,4 +113,3 @@ private struct UserMigration: Migration { database.schema("users").delete() } } - diff --git a/Sources/FluentBenchmark/Tests/SchemaTests.swift b/Sources/FluentBenchmark/Tests/SchemaTests.swift index 7dc8ad57..b87d3fa5 100644 --- a/Sources/FluentBenchmark/Tests/SchemaTests.swift +++ b/Sources/FluentBenchmark/Tests/SchemaTests.swift @@ -1,9 +1,9 @@ import FluentKit +import FluentSQL import Foundation import NIOCore -import XCTest import SQLKit -import FluentSQL +import XCTest extension FluentBenchmarker { public func testSchema(foreignKeys: Bool = true) throws { @@ -20,9 +20,12 @@ extension FluentBenchmarker { } private func testSchema_addConstraint() throws { - try self.runTest(#function, [ - CreateCategories() - ]) { + try self.runTest( + #function, + [ + CreateCategories() + ] + ) { guard let sql = self.database as? any SQLDatabase, sql.dialect.alterTableSyntax.allowsBatch else { self.database.logger.warning("Skipping \(#function)") return @@ -46,9 +49,12 @@ extension FluentBenchmarker { } private func testSchema_addNamedConstraint() throws { - try self.runTest(#function, [ - CreateCategories() - ]) { + try self.runTest( + #function, + [ + CreateCategories() + ] + ) { guard let sql = self.database as? any SQLDatabase, sql.dialect.alterTableSyntax.allowsBatch else { self.database.logger.warning("Skipping \(#function)") return @@ -70,11 +76,14 @@ extension FluentBenchmarker { try Category(name: "a").create(on: self.database).wait() } } - + private func testSchema_fieldReference() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { XCTAssertThrowsError( try Star.query(on: self.database) .filter(\.$name == "Sol") @@ -84,92 +93,105 @@ extension FluentBenchmarker { } private func testSchema_customSqlConstraints() throws { - try self.runTest(#function, [ - DeleteTableMigration(name: "custom_constraints") - ]) { + try self.runTest( + #function, + [ + DeleteTableMigration(name: "custom_constraints") + ] + ) { let normalized1 = (self.database as! any SQLDatabase).dialect.normalizeSQLConstraint(identifier: SQLIdentifier("id_unq_1")) - + try self.database.schema("custom_constraints") .id() - + // Test query string SQL for entire table constraints: .constraint(.sql(embed: "CONSTRAINT \(normalized1) UNIQUE (\(ident: "id"))")) - + // Test raw SQL for table constraint definitions (but not names): .constraint(.constraint(.sql(unsafeRaw: "UNIQUE (id)"), name: "id_unq_2")) .constraint(.constraint(.sql(embed: "UNIQUE (\(ident: "id"))"), name: "id_unq_3")) - + .create().wait() - + 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(unsafeRaw: "")))")) + .deleteConstraint( + .sql(embed: "\(SQLDropTypedConstraint(name: SQLIdentifier("id_unq_1"), algorithm: .sql(unsafeRaw: "")))") + ) .update().wait() } } } private func testSchema_customSqlFields() throws { - try self.runTest(#function, [ - DeleteTableMigration(name: "custom_fields") - ]) { + try self.runTest( + #function, + [ + DeleteTableMigration(name: "custom_fields") + ] + ) { try self.database.schema("custom_fields") .id() - + // Test query string SQL for field data types: .field("morenotid", .sql(embed: "\(unsafeRaw: "TEXT")")) - + // Test raw SQL for field names: .field(.definition(name: .sql(embed: "\(ident: "stillnotid")"), dataType: .int, constraints: [.required])) - + // Test raw SQL for field constraints: .field("neverbeid", .string, .sql(embed: "NOT NULL")) - + // Test raw SQL for entire field definitions: .field(.sql(unsafeRaw: "idnah INTEGER NOT NULL")) .field(.sql(embed: "\(ident: "notid") INTEGER")) - + .create().wait() - + if (self.database as! any SQLDatabase).dialect.alterTableSyntax.allowsBatch { try self.database.schema("custom_fields") - + // Test raw SQL for field updates: .updateField(.sql(embed: "\(SQLAlterColumnDefinitionType(column: .init("notid"), dataType: .text))")) - + .update().wait() } } } - + private func testSchema_deleteConstraints() throws { - try self.runTest(#function, [ - CreateCategories(), - DeleteTableMigration(name: "normal_constraints") - ]) { + try self.runTest( + #function, + [ + CreateCategories(), + DeleteTableMigration(name: "normal_constraints"), + ] + ) { try self.database.schema("normal_constraints") .id() - + .field("catid", .uuid) .foreignKey(["catid"], references: Category.schema, [.id], onDelete: .noAction, onUpdate: .noAction) .foreignKey(["catid"], references: Category.schema, [.id], onDelete: .noAction, onUpdate: .noAction, name: "second_fkey") .unique(on: "catid") .unique(on: "id", name: "second_ukey") - + .create().wait() - + if (self.database as! any SQLDatabase).dialect.alterTableSyntax.allowsBatch { try self.database.schema("normal_constraints") // Test `DROP FOREIGN KEY` (MySQL) or `DROP CONSTRAINT` (Postgres) - .deleteConstraint(.constraint(.foreignKey([.key("catid")], Category.schema, [.key(.id)], onDelete: .noAction, onUpdate: .noAction))) + .deleteConstraint( + .constraint(.foreignKey([.key("catid")], Category.schema, [.key(.id)], onDelete: .noAction, onUpdate: .noAction)) + ) // Test name-based `DROP FOREIGN KEY` (MySQL) .deleteForeignKey(name: "second_fkey") // Test `DROP KEY` (MySQL) or `DROP CONSTRAINT` (Postgres) .deleteUnique(on: "catid") // Test name-based `DROP KEY` (MySQL) or `DROP CONSTRAINT` (Postgres) .deleteConstraint(name: "second_ukey") - + .update().wait() } } @@ -180,7 +202,7 @@ final class Category: Model, @unchecked Sendable { static let schema = "categories" @ID var id: UUID? @Field(key: "name") var name: String - init() { } + init() {} init(id: UUID? = nil, name: String) { self.id = id self.name = name @@ -213,7 +235,6 @@ struct AddUniqueConstraintToCategories: Migration { } } - struct AddNamedUniqueConstraintToCategories: Migration { func prepare(on database: any Database) -> EventLoopFuture { database.schema("categories") @@ -230,11 +251,11 @@ struct AddNamedUniqueConstraintToCategories: Migration { /// Specialized utility used by the custom SQL tests, used to ensure they clean up after themselves. struct DeleteTableMigration: Migration { let name: String - + func prepare(on database: any Database) -> EventLoopFuture { database.eventLoop.makeSucceededVoidFuture() } - + func revert(on database: any Database) -> EventLoopFuture { database.schema(self.name).delete() } diff --git a/Sources/FluentBenchmark/Tests/SetTests.swift b/Sources/FluentBenchmark/Tests/SetTests.swift index e543fdff..4df09339 100644 --- a/Sources/FluentBenchmark/Tests/SetTests.swift +++ b/Sources/FluentBenchmark/Tests/SetTests.swift @@ -9,11 +9,14 @@ extension FluentBenchmarker { try self.testSet_optional() try self.testSet_enum() } - + private func testSet_multiple() throws { - try runTest(#function, [ - TestMigration(), - ]) { + try runTest( + #function, + [ + TestMigration() + ] + ) { // Int value set first try Test.query(on: self.database) .set(\.$intValue, to: 1) @@ -29,9 +32,12 @@ extension FluentBenchmarker { } private func testSet_optional() throws { - try runTest(#function, [ - TestMigration(), - ]) { + try runTest( + #function, + [ + TestMigration() + ] + ) { try Test.query(on: self.database) .set(\.$intValue, to: nil) .set(\.$stringValue, to: nil) @@ -40,9 +46,12 @@ extension FluentBenchmarker { } private func testSet_enum() throws { - try runTest(#function, [ - Test2Migration(), - ]) { + try runTest( + #function, + [ + Test2Migration() + ] + ) { try Test2.query(on: self.database) .set(\.$foo, to: .bar) .update().wait() @@ -107,4 +116,3 @@ private struct Test2Migration: FluentKit.Migration { } } } - diff --git a/Sources/FluentBenchmark/Tests/SiblingsTests.swift b/Sources/FluentBenchmark/Tests/SiblingsTests.swift index ea41f3cb..f192ad04 100644 --- a/Sources/FluentBenchmark/Tests/SiblingsTests.swift +++ b/Sources/FluentBenchmark/Tests/SiblingsTests.swift @@ -1,5 +1,5 @@ -import XCTest import FluentKit +import XCTest extension FluentBenchmarker { public func testSiblings() throws { @@ -10,20 +10,26 @@ extension FluentBenchmarker { } private func testSiblings_attach() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { - let inhabited = try XCTUnwrap(try Tag.query(on: self.database) - .filter(\.$name == "Inhabited") - .first().wait() + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { + let inhabited = try XCTUnwrap( + try Tag.query(on: self.database) + .filter(\.$name == "Inhabited") + .first().wait() ) - let smallRocky = try XCTUnwrap(try Tag.query(on: self.database) - .filter(\.$name == "Small Rocky") - .first().wait() + let smallRocky = try XCTUnwrap( + try Tag.query(on: self.database) + .filter(\.$name == "Small Rocky") + .first().wait() ) - let earth = try XCTUnwrap(try Planet.query(on: self.database) - .filter(\.$name == "Earth") - .first().wait() + let earth = try XCTUnwrap( + try Planet.query(on: self.database) + .filter(\.$name == "Earth") + .first().wait() ) // check tag has expected planet @@ -65,9 +71,9 @@ extension FluentBenchmarker { XCTAssertEqual(tags.count, 2) XCTAssertEqual(tags.map(\.name).sorted(), ["Inhabited", "Small Rocky"]) } - + try earth.$tags.detachAll(on: self.database).wait() - + // check pivot provided to the edit closure has the "to" model when attaching try earth.$tags.attach([inhabited, smallRocky], on: self.database) { pivot in guard pivot.$tag.value != nil else { @@ -75,10 +81,10 @@ extension FluentBenchmarker { } pivot.comments = "Tagged with name \(pivot.tag.name)" }.wait() - + do { let pivots = try earth.$tags.$pivots.get(reload: true, on: self.database).wait() - + XCTAssertEqual(pivots.count, 2) XCTAssertEqual(pivots.compactMap(\.comments).sorted(), ["Tagged with name Inhabited", "Tagged with name Small Rocky"]) } @@ -86,9 +92,12 @@ extension FluentBenchmarker { } private func testSiblings_detachArray() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let inhabited = try Tag.query(on: self.database) .filter(\.$name == "Inhabited") .first().wait()! @@ -112,33 +121,39 @@ extension FluentBenchmarker { } } } - + private func testSiblings_pivotLoading() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let earth = try Planet.query(on: self.database) .filter(\.$name == "Earth").with(\.$tags).with(\.$tags.$pivots) .first().wait()! - + // verify tag count XCTAssertEqual(earth.$tags.pivots.count, 2) } } - + private func testSiblings_detachAll() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let earth = try Planet.query(on: self.database) .filter(\.$name == "Earth") .first().wait()! - + // verify tag count try XCTAssertEqual(earth.$tags.query(on: self.database).count().wait(), 2) - + try earth.$tags.detachAll(on: self.database).wait() - + // check earth has tags removed do { let tags = try earth.$tags.query(on: self.database) diff --git a/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift b/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift index 42fa5a61..a1be8e52 100644 --- a/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift +++ b/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift @@ -12,7 +12,7 @@ extension FluentBenchmarker { try self.testSoftDelete_forceOnQuery() try self.testSoftDelete_parent() } - + private func testCounts( allCount: Int, realCount: Int, @@ -25,9 +25,12 @@ extension FluentBenchmarker { } private func testSoftDelete_model() throws { - try self.runTest(#function, [ - TrashMigration(), - ]) { + try self.runTest( + #function, + [ + TrashMigration() + ] + ) { // save two users try Trash(contents: "A").save(on: self.database).wait() try Trash(contents: "B").save(on: self.database).wait() @@ -49,9 +52,12 @@ extension FluentBenchmarker { } private func testSoftDelete_query() throws { - try self.runTest(#function, [ - TrashMigration() - ]) { + try self.runTest( + #function, + [ + TrashMigration() + ] + ) { // a is scheduled for soft-deletion let a = Trash(contents: "a") a.deletedAt = Date(timeIntervalSinceNow: 50) @@ -73,9 +79,12 @@ extension FluentBenchmarker { } private func testSoftDelete_timestampUpdate() throws { - try self.runTest(#function, [ - TrashMigration() - ]) { + try self.runTest( + #function, + [ + TrashMigration() + ] + ) { // Create soft-deletable model. let a = Trash(contents: "A") try a.create(on: self.database).wait() @@ -104,9 +113,12 @@ extension FluentBenchmarker { } private func testSoftDelete_onBulkDelete() throws { - try self.runTest(#function, [ - TrashMigration(), - ]) { + try self.runTest( + #function, + [ + TrashMigration() + ] + ) { // save two users try Trash(contents: "A").save(on: self.database).wait() try Trash(contents: "B").save(on: self.database).wait() @@ -116,11 +128,14 @@ extension FluentBenchmarker { try testCounts(allCount: 0, realCount: 2) } } - + private func testSoftDelete_forceOnQuery() throws { - try self.runTest(#function, [ - TrashMigration() - ]) { + try self.runTest( + #function, + [ + TrashMigration() + ] + ) { // save two users try Trash(contents: "A").save(on: self.database).wait() try Trash(contents: "B").save(on: self.database).wait() @@ -142,7 +157,7 @@ extension FluentBenchmarker { @Parent(key: "bar") var bar: Bar - init() { } + init() {} } struct FooMigration: Migration { @@ -167,7 +182,7 @@ extension FluentBenchmarker { @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? - init() { } + init() {} } struct BarMigration: Migration { @@ -183,10 +198,13 @@ extension FluentBenchmarker { } } - try self.runTest(#function, [ - FooMigration(), - BarMigration(), - ]) { + try self.runTest( + #function, + [ + FooMigration(), + BarMigration(), + ] + ) { let bar1 = Bar() try bar1.create(on: self.database).wait() let bar2 = Bar() @@ -213,7 +231,7 @@ extension FluentBenchmarker { // this should throw an error now because one of the // parents is missing and the results cannot be loaded XCTAssertThrowsError(try Foo.query(on: self.database).with(\.$bar).all().wait()) { error in - guard case let .missingParent(from, to, key, id) = error as? FluentError else { + guard case .missingParent(let from, let to, let key, let id) = error as? FluentError else { return XCTFail("Expected FluentError.missingParent, but got \(error)") } XCTAssertEqual(from, "\(Foo.self)") @@ -221,7 +239,7 @@ extension FluentBenchmarker { XCTAssertEqual(key, "bar") XCTAssertEqual(id, "\(bar1.id!)") } - + XCTAssertNoThrow(try Foo.query(on: self.database).with(\.$bar, withDeleted: true).all().wait()) } } @@ -239,7 +257,7 @@ private final class Trash: Model, @unchecked Sendable { @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? - init() { } + init() {} init(id: UUID? = nil, contents: String, deletedAt: Date? = nil) { if let id = id { diff --git a/Sources/FluentBenchmark/Tests/SortTests.swift b/Sources/FluentBenchmark/Tests/SortTests.swift index c9690942..60f19eeb 100644 --- a/Sources/FluentBenchmark/Tests/SortTests.swift +++ b/Sources/FluentBenchmark/Tests/SortTests.swift @@ -1,6 +1,6 @@ import FluentSQL -import XCTest import SQLKit +import XCTest extension FluentBenchmarker { public func testSort(sql: Bool = true) throws { @@ -12,9 +12,12 @@ extension FluentBenchmarker { } private func testSort_basic() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let ascending = try Galaxy.query(on: self.database) .sort(\.$name, .ascending) .all().wait() @@ -29,9 +32,12 @@ extension FluentBenchmarker { } private func testSort_sql() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .sort(.sql("name", .notEqual, "Earth")) .all().wait() @@ -40,9 +46,12 @@ extension FluentBenchmarker { } private func testSort_embedSql() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let planets = try Planet.query(on: self.database) .sort(.sql(embed: "\(ident: "name")\(SQLBinaryOperator.notEqual)\(literal: "Earth")")) .all().wait() diff --git a/Sources/FluentBenchmark/Tests/TimestampTests.swift b/Sources/FluentBenchmark/Tests/TimestampTests.swift index 50d50c66..dd2baa07 100644 --- a/Sources/FluentBenchmark/Tests/TimestampTests.swift +++ b/Sources/FluentBenchmark/Tests/TimestampTests.swift @@ -15,9 +15,12 @@ extension FluentBenchmarker { } private func testTimestamp_touch() throws { - try runTest(#function, [ - UserMigration(), - ]) { + try runTest( + #function, + [ + UserMigration() + ] + ) { let user = User(name: "A") XCTAssertNil(user.createdAt) XCTAssertNil(user.updatedAt) @@ -37,14 +40,18 @@ extension FluentBenchmarker { user.addressedAt = addressedTime try user.save(on: self.database).wait() XCTAssertNotNil(user.addressedAt) - XCTAssertEqual(user.addressedAt?.timeIntervalSinceReferenceDate ?? 0.0, addressedTime.timeIntervalSinceReferenceDate, accuracy: 0.10) + XCTAssertEqual( + user.addressedAt?.timeIntervalSinceReferenceDate ?? 0.0, addressedTime.timeIntervalSinceReferenceDate, accuracy: 0.10) } } private func testTimestamp_ISO8601() throws { - try runTest(#function, [ - EventMigration(), - ]) { + try runTest( + #function, + [ + EventMigration() + ] + ) { let event = Event(name: "ServerSide.swift") try event.create(on: self.database).wait() @@ -76,18 +83,21 @@ extension FluentBenchmarker { try XCTAssertEqual(Event.query(on: self.database).all().wait().count, 0) } } - + private func testTimestamp_createOnUpdate() throws { - try runTest(#function, [ - EventMigration() - ]) { + try runTest( + #function, + [ + EventMigration() + ] + ) { let event = Event(name: "C") try event.create(on: self.database).wait() XCTAssertNotNil(event.createdAt) XCTAssertNotNil(event.updatedAt) XCTAssertEqual(event.createdAt, event.updatedAt) - - Thread.sleep(forTimeInterval: 0.001) // ensure update timestamp with millisecond precision increments + + Thread.sleep(forTimeInterval: 0.001) // ensure update timestamp with millisecond precision increments let storedEvent = try Event.find(event.id, on: self.database).wait() XCTAssertNotNil(storedEvent) @@ -96,11 +106,14 @@ extension FluentBenchmarker { XCTAssertEqual(storedEvent?.createdAt, event.createdAt) } } - + private func testTimestamp_createOnBulkCreate() throws { - try runTest(#function, [ - UserMigration(), - ]) { + try runTest( + #function, + [ + UserMigration() + ] + ) { let userOne = User(name: "A") let userTwo = User(name: "B") XCTAssertEqual(userOne.createdAt, nil) @@ -116,11 +129,14 @@ extension FluentBenchmarker { XCTAssertEqual(userTwo.updatedAt, userTwo.createdAt) } } - + private func testTimestamp_createOnBulkUpdate() throws { - try runTest(#function, [ - UserMigration(), - ]) { + try runTest( + #function, + [ + UserMigration() + ] + ) { let userOne = User(name: "A") let userTwo = User(name: "B") XCTAssertEqual(userOne.createdAt, nil) @@ -128,29 +144,34 @@ extension FluentBenchmarker { XCTAssertEqual(userTwo.createdAt, nil) XCTAssertEqual(userTwo.updatedAt, nil) try [userOne, userTwo].create(on: self.database).wait() - + let originalOne = userOne.updatedAt let originalTwo = userTwo.updatedAt - + Thread.sleep(forTimeInterval: 1) - + try User.query(on: self.database).set(\.$name, to: "C").update().wait() - - XCTAssertNotEqual(try User.find(userOne.id, on: self.database).wait()!.updatedAt!.timeIntervalSinceNow, originalOne!.timeIntervalSinceNow) - XCTAssertNotEqual(try User.find(userTwo.id, on: self.database).wait()!.updatedAt!.timeIntervalSinceNow, originalTwo!.timeIntervalSinceNow) + + XCTAssertNotEqual( + try User.find(userOne.id, on: self.database).wait()!.updatedAt!.timeIntervalSinceNow, originalOne!.timeIntervalSinceNow) + XCTAssertNotEqual( + try User.find(userTwo.id, on: self.database).wait()!.updatedAt!.timeIntervalSinceNow, originalTwo!.timeIntervalSinceNow) } } private func testTimestamp_updateNoChanges() throws { - try runTest(#function, [ - EventMigration() - ]) { + try runTest( + #function, + [ + EventMigration() + ] + ) { let event = Event(name: "C") try event.create(on: self.database).wait() let updatedAtPreSave = event.updatedAt XCTAssertFalse(event.hasChanges) - Thread.sleep(forTimeInterval: 0.001) // ensure update timestamp with millisecond precision increments + Thread.sleep(forTimeInterval: 0.001) // ensure update timestamp with millisecond precision increments try event.save(on: self.database).wait() let storedEvent = try Event.find(event.id, on: self.database).wait() @@ -158,11 +179,10 @@ extension FluentBenchmarker { } } - private func testTimestamp_decode() throws { let json = """ - { "name": "Vapor", "createdAt": null } - """.data(using: .utf8)! + { "name": "Vapor", "createdAt": null } + """.data(using: .utf8)! let user = try! JSONDecoder().decode(User.self, from: json) XCTAssertNil(user.createdAt) XCTAssertNil(user.updatedAt) @@ -187,8 +207,8 @@ private final class User: Model, @unchecked Sendable { @Timestamp(key: "addressed_at", on: .none) var addressedAt: Date? - init() { } - + init() {} + init(id: IDValue? = nil, name: String) { self.id = id self.name = name @@ -212,7 +232,6 @@ private struct UserMigration: Migration { } } - private final class Event: Model, @unchecked Sendable { static let schema = "events" @@ -234,7 +253,7 @@ private final class Event: Model, @unchecked Sendable { @Timestamp(key: "nudged_at", on: .delete, format: .iso8601(withMilliseconds: true)) var nudgedAt: Date? - init() { } + init() {} init(id: IDValue? = nil, name: String) { self.id = id diff --git a/Sources/FluentBenchmark/Tests/TransactionTests.swift b/Sources/FluentBenchmark/Tests/TransactionTests.swift index 0fe21d14..adcd9c03 100644 --- a/Sources/FluentBenchmark/Tests/TransactionTests.swift +++ b/Sources/FluentBenchmark/Tests/TransactionTests.swift @@ -1,6 +1,6 @@ +import FluentKit import NIOCore import XCTest -import FluentKit extension FluentBenchmarker { public func testTransaction() throws { @@ -9,30 +9,32 @@ extension FluentBenchmarker { } private func testTransaction_basic() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { let result = self.database.transaction { transaction in Star.query(on: transaction) .filter(\.$name == "Sol") .first() - .flatMapWithEventLoop - { sun, eventLoop -> EventLoopFuture in - guard let sun else { return eventLoop.makeFailedFuture(FluentError.missingField(name: "Sol")) } - let pluto = Planet(name: "Pluto") - return sun.$planets.create(pluto, on: transaction).map { - pluto + .flatMapWithEventLoop { sun, eventLoop -> EventLoopFuture in + guard let sun else { return eventLoop.makeFailedFuture(FluentError.missingField(name: "Sol")) } + let pluto = Planet(name: "Pluto") + return sun.$planets.create(pluto, on: transaction).map { + pluto + } + }.flatMap { pluto -> EventLoopFuture<(Planet, Tag)> in + let tag = Tag(name: "Dwarf") + return tag.create(on: transaction).map { + (pluto, tag) + } + }.flatMap { (pluto, tag) in + tag.$planets.attach(pluto, on: transaction) + }.flatMapThrowing { + throw Test() } - }.flatMap { pluto -> EventLoopFuture<(Planet, Tag)> in - let tag = Tag(name: "Dwarf") - return tag.create(on: transaction).map { - (pluto, tag) - } - }.flatMap { (pluto, tag) in - tag.$planets.attach(pluto, on: transaction) - }.flatMapThrowing { - throw Test() - } } do { try result.wait() @@ -51,9 +53,12 @@ extension FluentBenchmarker { } private func testTransaction_in() throws { - try self.runTest(#function, [ - SolarSystem() - ]) { + try self.runTest( + #function, + [ + SolarSystem() + ] + ) { try self.database.transaction { transaction in XCTAssertEqual(transaction.inTransaction, true) return transaction.transaction { nested in @@ -65,4 +70,4 @@ extension FluentBenchmarker { } } -private struct Test: Error { } +private struct Test: Error {} diff --git a/Sources/FluentBenchmark/Tests/UniqueTests.swift b/Sources/FluentBenchmark/Tests/UniqueTests.swift index 5b76510c..a655e46c 100644 --- a/Sources/FluentBenchmark/Tests/UniqueTests.swift +++ b/Sources/FluentBenchmark/Tests/UniqueTests.swift @@ -10,9 +10,12 @@ extension FluentBenchmarker { } private func testUnique_fields() throws { - try self.runTest(#function, [ - FooMigration(), - ]) { + try self.runTest( + #function, + [ + FooMigration() + ] + ) { try Foo(bar: "a", baz: 1).save(on: self.database).wait() try Foo(bar: "a", baz: 2).save(on: self.database).wait() do { @@ -26,10 +29,13 @@ extension FluentBenchmarker { // https://github.com/vapor/fluent-kit/issues/112 public func testUnique_duplicateKey() throws { - try runTest(#function, [ - BarMigration(), - BazMigration() - ]) { + try runTest( + #function, + [ + BarMigration(), + BazMigration(), + ] + ) { // } } @@ -47,7 +53,7 @@ private final class Foo: Model, @unchecked Sendable { @Field(key: "baz") var baz: Int - init() { } + init() {} init(id: IDValue? = nil, bar: String, baz: Int) { self.id = id diff --git a/Sources/FluentKit/Concurrency/AsyncMigration.swift b/Sources/FluentKit/Concurrency/AsyncMigration.swift index fbeebfde..9711b768 100644 --- a/Sources/FluentKit/Concurrency/AsyncMigration.swift +++ b/Sources/FluentKit/Concurrency/AsyncMigration.swift @@ -5,14 +5,14 @@ public protocol AsyncMigration: Migration { func revert(on database: any Database) async throws } -public extension AsyncMigration { - func prepare(on database: any Database) -> EventLoopFuture { +extension AsyncMigration { + public func prepare(on database: any Database) -> EventLoopFuture { database.eventLoop.makeFutureWithTask { try await self.prepare(on: database) } } - - func revert(on database: any Database) -> EventLoopFuture { + + public func revert(on database: any Database) -> EventLoopFuture { database.eventLoop.makeFutureWithTask { try await self.revert(on: database) } diff --git a/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift b/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift index e9838610..dd4cdf99 100644 --- a/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift +++ b/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift @@ -2,7 +2,7 @@ import NIOCore public protocol AsyncModelMiddleware: AnyModelMiddleware { associatedtype Model: FluentKit.Model - + func create(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws func update(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws func delete(model: Model, force: Bool, on db: any Database, next: any AnyAsyncModelResponder) async throws @@ -40,23 +40,23 @@ extension AsyncModelMiddleware { } } } - + public func create(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws { try await next.create(model, on: db) } - + public func update(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws { try await next.update(model, on: db) } - + public func delete(model: Model, force: Bool, on db: any Database, next: any AnyAsyncModelResponder) async throws { try await next.delete(model, force: force, on: db) } - + public func softDelete(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws { try await next.softDelete(model, on: db) } - + public func restore(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws { try await next.restore(model, on: db) } diff --git a/Sources/FluentKit/Concurrency/Children+Concurrency.swift b/Sources/FluentKit/Concurrency/Children+Concurrency.swift index 47e97636..c13d9c20 100644 --- a/Sources/FluentKit/Concurrency/Children+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Children+Concurrency.swift @@ -1,21 +1,21 @@ import NIOCore -public extension ChildrenProperty { - func load(on database: any Database) async throws { +extension ChildrenProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } - - func create(_ to: To, on database: any Database) async throws { + + public func create(_ to: To, on database: any Database) async throws { try await self.create(to, on: database).get() } - - func create(_ to: [To], on database: any Database) async throws { + + public func create(_ to: [To], on database: any Database) async throws { try await self.create(to, on: database).get() } } -public extension CompositeChildrenProperty { - func load(on database: any Database) async throws { +extension CompositeChildrenProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } } diff --git a/Sources/FluentKit/Concurrency/Database+Concurrency.swift b/Sources/FluentKit/Concurrency/Database+Concurrency.swift index 787f4834..465067b7 100644 --- a/Sources/FluentKit/Concurrency/Database+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Database+Concurrency.swift @@ -1,26 +1,26 @@ import NIOCore -public extension Database { - func execute( +extension Database { + public func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) async throws { try await self.execute(query: query, onOutput: onOutput).get() } - func execute( + public func execute( schema: DatabaseSchema ) async throws { try await self.execute(schema: schema).get() } - func execute( + public 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 { + public func transaction(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { try await self.transaction { db in self.eventLoop.makeFutureWithTask { try await closure(db) @@ -28,7 +28,7 @@ public extension Database { }.get() } - func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { + public func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { try await self.withConnection { db in self.eventLoop.makeFutureWithTask { try await closure(db) diff --git a/Sources/FluentKit/Concurrency/EnumBuilder+Concurrency.swift b/Sources/FluentKit/Concurrency/EnumBuilder+Concurrency.swift index d2b6f040..45d275e7 100644 --- a/Sources/FluentKit/Concurrency/EnumBuilder+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/EnumBuilder+Concurrency.swift @@ -1,19 +1,19 @@ import NIOCore -public extension EnumBuilder { - func create() async throws -> DatabaseSchema.DataType { +extension EnumBuilder { + public func create() async throws -> DatabaseSchema.DataType { try await self.create().get() } - - func read() async throws -> DatabaseSchema.DataType { + + public func read() async throws -> DatabaseSchema.DataType { try await self.read().get() } - - func update() async throws -> DatabaseSchema.DataType { + + public func update() async throws -> DatabaseSchema.DataType { try await self.update().get() } - - func delete() async throws { + + public func delete() async throws { try await self.delete().get() } } diff --git a/Sources/FluentKit/Concurrency/Model+Concurrency.swift b/Sources/FluentKit/Concurrency/Model+Concurrency.swift index 4bf3ae34..bdf93f8e 100644 --- a/Sources/FluentKit/Concurrency/Model+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Model+Concurrency.swift @@ -1,41 +1,41 @@ import NIOCore -public extension Model { - static func find( +extension Model { + public static func find( _ id: Self.IDValue?, on database: any Database ) async throws -> Self? { try await self.find(id, on: database).get() } - + // MARK: - CRUD - func save(on database: any Database) async throws { + public func save(on database: any Database) async throws { try await self.save(on: database).get() } - - func create(on database: any Database) async throws { + + public func create(on database: any Database) async throws { try await self.create(on: database).get() } - - func update(on database: any Database) async throws { + + public func update(on database: any Database) async throws { try await self.update(on: database).get() } - - func delete(force: Bool = false, on database: any Database) async throws { + + public func delete(force: Bool = false, on database: any Database) async throws { try await self.delete(force: force, on: database).get() } - - func restore(on database: any Database) async throws { + + public func restore(on database: any Database) async throws { try await self.restore(on: database).get() } } -public extension Collection where Element: FluentKit.Model, Self: Sendable { - func delete(force: Bool = false, on database: any Database) async throws { +extension Collection where Element: FluentKit.Model, Self: Sendable { + public func delete(force: Bool = false, on database: any Database) async throws { try await self.delete(force: force, on: database).get() } - - func create(on database: any Database) async throws { + + public func create(on database: any Database) async throws { try await self.create(on: database).get() } } diff --git a/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift b/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift index dff70113..f8369d4e 100644 --- a/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift @@ -20,19 +20,19 @@ extension AnyAsyncModelResponder { public func create(_ model: any AnyModel, on db: any Database) async throws { try await handle(.create, model, on: db) } - + public func update(_ model: any AnyModel, on db: any Database) async throws { try await handle(.update, model, on: db) } - + public func restore(_ model: any AnyModel, on db: any Database) async throws { try await handle(.restore, model, on: db) } - + public func softDelete(_ model: any AnyModel, on db: any Database) async throws { try await handle(.softDelete, model, on: db) } - + public func delete(_ model: any AnyModel, force: Bool, on db: any Database) async throws { try await handle(.delete(force), model, on: db) } diff --git a/Sources/FluentKit/Concurrency/OptionalChild+Concurrency.swift b/Sources/FluentKit/Concurrency/OptionalChild+Concurrency.swift index 6e35237d..3887f45d 100644 --- a/Sources/FluentKit/Concurrency/OptionalChild+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/OptionalChild+Concurrency.swift @@ -1,17 +1,17 @@ import NIOCore -public extension OptionalChildProperty { - func load(on database: any Database) async throws { +extension OptionalChildProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } - - func create(_ to: To, on database: any Database) async throws { + + public func create(_ to: To, on database: any Database) async throws { try await self.create(to, on: database).get() } } -public extension CompositeOptionalChildProperty { - func load(on database: any Database) async throws { +extension CompositeOptionalChildProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } } diff --git a/Sources/FluentKit/Concurrency/OptionalParent+Concurrency.swift b/Sources/FluentKit/Concurrency/OptionalParent+Concurrency.swift index 9388746d..d5b00941 100644 --- a/Sources/FluentKit/Concurrency/OptionalParent+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/OptionalParent+Concurrency.swift @@ -1,13 +1,13 @@ import NIOCore -public extension OptionalParentProperty { - func load(on database: any Database) async throws { +extension OptionalParentProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } } -public extension CompositeOptionalParentProperty { - func load(on database: any Database) async throws { +extension CompositeOptionalParentProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } } diff --git a/Sources/FluentKit/Concurrency/Parent+Concurrency.swift b/Sources/FluentKit/Concurrency/Parent+Concurrency.swift index e52cfec3..c12afab6 100644 --- a/Sources/FluentKit/Concurrency/Parent+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Parent+Concurrency.swift @@ -1,13 +1,13 @@ import NIOCore -public extension ParentProperty { - func load(on database: any Database) async throws { +extension ParentProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } } -public extension CompositeParentProperty { - func load(on database: any Database) async throws { +extension CompositeParentProperty { + public func load(on database: any Database) async throws { try await self.load(on: database).get() } } diff --git a/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift b/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift index aa4469dd..f0212ee7 100644 --- a/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift @@ -1,221 +1,197 @@ import NIOCore -public extension QueryBuilder { +extension QueryBuilder { // MARK: - Actions - func create() async throws { + public func create() async throws { try await self.create().get() } - - func update() async throws { + + public func update() async throws { try await self.update().get() } - - func delete(force: Bool = false) async throws { + + public func delete(force: Bool = false) async throws { try await self.delete(force: force).get() } - + // MARK: - Fetch - - func chunk(max: Int, closure: @escaping @Sendable ([Result]) -> ()) async throws { + + public func chunk(max: Int, closure: @escaping @Sendable ([Result]) -> Void) async throws { try await self.chunk(max: max, closure: closure).get() } - - func first() async throws -> Model? { + + public func first() async throws -> Model? { try await self.first().get() } - - func all(_ key: KeyPath) async throws -> [Field.Value] - where Field: QueryableProperty, Field.Model == Model - { + + public func all(_ key: KeyPath) async throws -> [Field.Value] + where Field: QueryableProperty, Field.Model == Model { try await self.all(key).get() } - - func all( + + public func all( _ joined: Joined.Type, _ field: KeyPath ) async throws -> [Field.Value] - where Joined: Schema, Field: QueryableProperty, Field.Model == Joined - { + where Joined: Schema, Field: QueryableProperty, Field.Model == Joined { try await self.all(joined, field).get() } - func all() async throws -> [Model] { + public func all() async throws -> [Model] { try await self.all().get() } - - func run() async throws { + + public func run() async throws { try await self.run().get() } - - func all(_ onOutput: @escaping @Sendable (Result) -> ()) async throws { + + public func all(_ onOutput: @escaping @Sendable (Result) -> Void) async throws { try await self.all(onOutput).get() } - - func run(_ onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) async throws { + + public func run(_ onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) async throws { try await self.run(onOutput).get() } - + // MARK: - Aggregate - func count() async throws -> Int { + public func count() async throws -> Int { try await self.count().get() } - - func count(_ key: KeyPath) async throws -> Int - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + + public func count(_ key: KeyPath) async throws -> Int + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { try await self.count(key).get() } - func count(_ key: KeyPath) async throws -> Int - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + public func count(_ key: KeyPath) async throws -> Int + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { try await self.count(key).get() } - func sum(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + public func sum(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { try await self.sum(key).get() } - - func sum(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + + public func sum(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { try await self.sum(key).get() } - - func sum(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + + public func sum(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { try await self.sum(key).get() } - - func sum(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + + public func sum(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { try await self.sum(key).get() } - - func average(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + + public func average(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { try await self.average(key).get() } - - func average(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + + public func average(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { try await self.average(key).get() } - - func average(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + + public func average(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { try await self.average(key).get() } - - func average(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + + public func average(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { try await self.average(key).get() } - - func min(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + + public func min(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { try await self.min(key).get() } - - func min(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + + public func min(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { try await self.min(key).get() } - - func min(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + + public func min(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { try await self.min(key).get() } - - func min(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + + public func min(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { try await self.min(key).get() } - - func max(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + + public func max(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { try await self.max(key).get() } - - func max(_ key: KeyPath) async throws -> Field.Value? - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + + public func max(_ key: KeyPath) async throws -> Field.Value? + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { try await self.max(key).get() } - - func max(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + + public func max(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { try await self.max(key).get() } - - func max(_ key: KeyPath) async throws -> Field.Value - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + + public func max(_ key: KeyPath) async throws -> Field.Value + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { try await self.max(key).get() } - func aggregate( + public func aggregate( _ method: DatabaseQuery.Aggregate.Method, _ field: KeyPath, as type: Result.Type = Result.self ) async throws -> Result - where Field: QueryableProperty, Field.Model == Model, Result: Codable & Sendable - { + where Field: QueryableProperty, Field.Model == Model, Result: Codable & Sendable { try await self.aggregate(method, field, as: type).get() } - - func aggregate( + + public func aggregate( _ method: DatabaseQuery.Aggregate.Method, _ field: KeyPath, as type: Result.Type = Result.self ) async throws -> Result - where Field: QueryableProperty, Field.Model == Model.IDValue, Result: Codable & Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Result: Codable & Sendable { try await self.aggregate(method, field, as: type).get() } - - func aggregate( + + public func aggregate( _ method: DatabaseQuery.Aggregate.Method, _ field: FieldKey, as type: Result.Type = Result.self ) async throws -> Result - where Result: Codable & Sendable - { + where Result: Codable & Sendable { try await self.aggregate(method, field, as: type).get() } - - func aggregate( + + public func aggregate( _ method: DatabaseQuery.Aggregate.Method, _ path: [FieldKey], as type: Result.Type = Result.self ) async throws -> Result - where Result: Codable & Sendable - { + where Result: Codable & Sendable { try await self.aggregate(method, path, as: type).get() } - + // MARK: - Paginate - func paginate( + public func paginate( _ request: PageRequest ) async throws -> Page { try await self.paginate(request).get() } - - func page( + + public func page( withIndex page: Int, size per: Int ) async throws -> Page { diff --git a/Sources/FluentKit/Concurrency/Relation+Concurrency.swift b/Sources/FluentKit/Concurrency/Relation+Concurrency.swift index cbde9bd9..629f879e 100644 --- a/Sources/FluentKit/Concurrency/Relation+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Relation+Concurrency.swift @@ -1,7 +1,7 @@ import NIOCore -public extension Relation { - func get(reload: Bool = false, on database: any Database) async throws -> RelatedValue { +extension Relation { + public func get(reload: Bool = false, on database: any Database) async throws -> RelatedValue { try await self.get(reload: reload, on: database).get() } } diff --git a/Sources/FluentKit/Concurrency/SchemaBuilder+Concurrency.swift b/Sources/FluentKit/Concurrency/SchemaBuilder+Concurrency.swift index 0ab0e7b0..c555b575 100644 --- a/Sources/FluentKit/Concurrency/SchemaBuilder+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/SchemaBuilder+Concurrency.swift @@ -1,15 +1,15 @@ import NIOCore -public extension SchemaBuilder { - func create() async throws { +extension SchemaBuilder { + public func create() async throws { try await self.create().get() } - - func update() async throws { + + public func update() async throws { try await self.update().get() } - - func delete() async throws { + + public func delete() async throws { try await self.delete().get() } } diff --git a/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift b/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift index 88695964..aca20491 100644 --- a/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift +++ b/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift @@ -1,59 +1,59 @@ import NIOCore -public extension SiblingsProperty { - - func load(on database: any Database) async throws { +extension SiblingsProperty { + + public func load(on database: any Database) async throws { try await self.load(on: database).get() } - + // MARK: Checking state - - func isAttached(to: To, on database: any Database) async throws -> Bool { + + public func isAttached(to: To, on database: any Database) async throws -> Bool { try await self.isAttached(to: to, on: database).get() } - - func isAttached(toID: To.IDValue, on database: any Database) async throws -> Bool { + + public func isAttached(toID: To.IDValue, on database: any Database) async throws -> Bool { try await self.isAttached(toID: toID, on: database).get() } - + // MARK: Operations - + /// Attach multiple models with plain edit closure. - func attach(_ tos: [To], on database: any Database, _ edit: @escaping @Sendable (Through) -> () = { _ in }) async throws { + public func attach(_ tos: [To], on database: any Database, _ edit: @escaping @Sendable (Through) -> Void = { _ in }) async throws { try await self.attach(tos, on: database, edit).get() } /// Attach single model with plain edit closure. - func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) -> () = { _ in }) async throws { + public func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) -> Void = { _ in }) async throws { try await self.attach(to, method: .always, on: database, edit) } - + /// Attach single model by specific method with plain edit closure. - func attach( + public func attach( _ to: To, method: AttachMethod, on database: any Database, - _ edit: @escaping @Sendable (Through) -> () = { _ in } + _ edit: @escaping @Sendable (Through) -> Void = { _ in } ) async throws { try await self.attach(to, method: method, on: database, edit).get() } - + /// A version of ``attach(_:on:_:)-791gu`` whose edit closure is async and can throw. /// /// This method provides "all or none" semantics- if the edit closure throws an error, any already- /// processed pivots are discarded. Only if all pivots are successfully edited are any of them saved. /// /// These semantics require us to reimplement, rather than calling through to, the ELF version. - func attach( + public func attach( _ tos: [To], on database: any Database, - _ edit: @escaping @Sendable (Through) async throws -> () + _ edit: @escaping @Sendable (Through) async throws -> Void ) async throws { guard let fromID = self.idValue else { throw SiblingsPropertyError.owningModelIdRequired(property: self.name) } - + var pivots: [Through] = [] pivots.reserveCapacity(tos.count) - + for to in tos { guard let toID = to.id else { throw SiblingsPropertyError.operandModelIdRequired(property: self.name) @@ -71,16 +71,16 @@ public extension SiblingsProperty { /// A version of ``attach(_:on:_:)-791gu`` whose edit closure is async and can throw. /// /// These semantics require us to reimplement, rather than calling through to, the ELF version. - func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) async throws -> ()) async throws { + public func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) async throws -> Void) async throws { try await self.attach(to, method: .always, on: database, edit) } - + /// A version of ``attach(_:method:on:_:)-20vs`` whose edit closure is async and can throw. /// /// These semantics require us to reimplement, rather than calling through to, the ELF version. - func attach( + public func attach( _ to: To, method: AttachMethod, on database: any Database, - _ edit: @escaping @Sendable (Through) async throws -> () + _ edit: @escaping @Sendable (Through) async throws -> Void ) async throws { switch method { case .ifNotExists: @@ -90,16 +90,16 @@ public extension SiblingsProperty { try await self.attach([to], on: database, edit) } } - - func detach(_ tos: [To], on database: any Database) async throws { + + public func detach(_ tos: [To], on database: any Database) async throws { try await self.detach(tos, on: database).get() } - - func detach(_ to: To, on database: any Database) async throws { + + public func detach(_ to: To, on database: any Database) async throws { try await self.detach(to, on: database).get() } - - func detachAll(on database: any Database) async throws { + + public func detachAll(on database: any Database) async throws { try await self.detachAll(on: database).get() } } diff --git a/Sources/FluentKit/Database/Database+Logging.swift b/Sources/FluentKit/Database/Database+Logging.swift index 2840a62a..5d729370 100644 --- a/Sources/FluentKit/Database/Database+Logging.swift +++ b/Sources/FluentKit/Database/Database+Logging.swift @@ -1,5 +1,5 @@ -import NIOCore import Logging +import NIOCore import SQLKit extension Database { @@ -22,10 +22,10 @@ extension LoggingOverrideDatabase: Database { history: self.database.context.history ) } - + func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) -> EventLoopFuture { self.database.execute(query: query, onOutput: onOutput) } @@ -49,17 +49,17 @@ extension LoggingOverrideDatabase: Database { func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { self.database.transaction(closure) } - + func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { self.database.withConnection(closure) } } extension LoggingOverrideDatabase: SQLDatabase where D: SQLDatabase { - 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 { diff --git a/Sources/FluentKit/Database/Database.swift b/Sources/FluentKit/Database/Database.swift index 1c124b1c..0d0e76b1 100644 --- a/Sources/FluentKit/Database/Database.swift +++ b/Sources/FluentKit/Database/Database.swift @@ -1,12 +1,12 @@ -import NIOCore import Logging +import NIOCore public protocol Database: Sendable { var context: DatabaseContext { get } - + func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) -> EventLoopFuture func execute( @@ -20,14 +20,13 @@ public protocol Database: Sendable { var inTransaction: Bool { get } func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture - + func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture } extension Database { public func query(_ model: Model.Type) -> QueryBuilder - where Model: FluentKit.Model - { + where Model: FluentKit.Model { .init(database: self) } } @@ -36,11 +35,11 @@ extension Database { public var configuration: any DatabaseConfiguration { self.context.configuration } - + public var logger: Logger { self.context.logger } - + public var eventLoop: any EventLoop { self.context.eventLoop } @@ -60,8 +59,8 @@ public protocol DatabaseDriver: Sendable { func shutdownAsync() async } -public extension DatabaseDriver { - func shutdownAsync() async { +extension DatabaseDriver { + public func shutdownAsync() async { shutdown() } } @@ -77,7 +76,7 @@ public struct DatabaseContext: Sendable { public let eventLoop: any EventLoop public let history: QueryHistory? public let pageSizeLimit: Int? - + public init( configuration: any DatabaseConfiguration, logger: Logger, diff --git a/Sources/FluentKit/Database/DatabaseID.swift b/Sources/FluentKit/Database/DatabaseID.swift index b13be1d1..b26ae7b8 100644 --- a/Sources/FluentKit/Database/DatabaseID.swift +++ b/Sources/FluentKit/Database/DatabaseID.swift @@ -1,6 +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/DatabaseInput.swift b/Sources/FluentKit/Database/DatabaseInput.swift index 19ad36e3..73aa6dc0 100644 --- a/Sources/FluentKit/Database/DatabaseInput.swift +++ b/Sources/FluentKit/Database/DatabaseInput.swift @@ -35,7 +35,7 @@ public protocol DatabaseInput { /// behavior unless the semantics of the conforming type explicitly require otherwise and /// the alternate behavior is clearly documented. func set(_ value: DatabaseQuery.Value, at key: FieldKey) - + /// Indicates whether this ``DatabaseInput`` instance is requesting key/value pairs for _all_ defined /// database fields regardless of status, or only those pairs where the current value /// is known to be out of date (also referred to variously as "dirty", "modified", or "has changes"). @@ -75,7 +75,7 @@ extension DatabaseInput { public func prefixed(by prefix: FieldKey) -> any DatabaseInput { PrefixedDatabaseInput(prefix: prefix, strategy: .none, base: self) } - + /// Return a ``DatabaseInput`` wrapping `self` so as to apply a given prefix, according to a given /// ``KeyPrefixingStrategy``, to each field key before processing. public func prefixed(by prefix: FieldKey, using stratgey: KeyPrefixingStrategy) -> any DatabaseInput { @@ -89,7 +89,7 @@ private struct PrefixedDatabaseInput: DatabaseInput { let prefix: FieldKey let strategy: KeyPrefixingStrategy let base: Base - + var wantsUnmodifiedKeys: Bool { self.base.wantsUnmodifiedKeys } func set(_ value: DatabaseQuery.Value, at key: FieldKey) { @@ -121,13 +121,13 @@ private struct PrefixedDatabaseInput: DatabaseInput { internal struct QueryFilterInput: DatabaseInput { let builder: QueryBuilder let inverted: Bool - + var wantsUnmodifiedKeys: Bool { true } - + init(builder: QueryBuilder, inverted: Bool = false) where BuilderModel == InputModel { self.init(BuilderModel.self, builder: builder, inverted: inverted) } - + init(_: InputModel.Type, builder: QueryBuilder, inverted: Bool = false) { self.builder = builder self.inverted = inverted @@ -151,7 +151,7 @@ internal struct QueryFilterInput: DatabaseInput { let base: Base var wantsUnmodifiedKeys: Bool { true } - + func set(_: DatabaseQuery.Value, at key: FieldKey) { self.base.set(.null, at: key) } diff --git a/Sources/FluentKit/Database/DatabaseOutput.swift b/Sources/FluentKit/Database/DatabaseOutput.swift index 3927e517..a11670eb 100644 --- a/Sources/FluentKit/Database/DatabaseOutput.swift +++ b/Sources/FluentKit/Database/DatabaseOutput.swift @@ -3,16 +3,15 @@ public protocol DatabaseOutput: CustomStringConvertible, Sendable { func contains(_ key: FieldKey) -> Bool func decodeNil(_ key: FieldKey) throws -> Bool func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T: Decodable + where T: Decodable } extension DatabaseOutput { public func decode(_ key: FieldKey) throws -> T - where T: Decodable - { + where T: Decodable { try self.decode(key, as: T.self) } - + public func qualifiedSchema(space: String?, _ schema: String) -> any DatabaseOutput { self.schema([space, schema].compactMap({ $0 }).joined(separator: "_")) } @@ -22,7 +21,7 @@ extension DatabaseOutput { public func prefixed(by prefix: FieldKey) -> any DatabaseOutput { PrefixedDatabaseOutput(prefix: prefix, strategy: .none, base: self) } - + public func prefixed(by prefix: FieldKey, using stratgey: KeyPrefixingStrategy) -> any DatabaseOutput { PrefixedDatabaseOutput(prefix: prefix, strategy: stratgey, base: self) } @@ -59,11 +58,11 @@ private struct CombinedOutput: DatabaseOutput { private struct PrefixedDatabaseOutput: DatabaseOutput { let prefix: FieldKey, strategy: KeyPrefixingStrategy let base: any DatabaseOutput - + func schema(_ schema: String) -> any DatabaseOutput { PrefixedDatabaseOutput(prefix: self.prefix, strategy: self.strategy, base: self.base.schema(schema)) } - + func contains(_ key: FieldKey) -> Bool { self.base.contains(self.strategy.apply(prefix: self.prefix, to: key)) } @@ -72,7 +71,7 @@ private struct PrefixedDatabaseOutput: DatabaseOutput { try self.base.decodeNil(self.strategy.apply(prefix: self.prefix, to: key)) } - func decode(_ key: FieldKey, as type: T.Type) throws -> T where T : Decodable { + func decode(_ key: FieldKey, as type: T.Type) throws -> T where T: Decodable { try self.base.decode(self.strategy.apply(prefix: self.prefix, to: key), as: T.self) } @@ -80,4 +79,3 @@ private struct PrefixedDatabaseOutput: DatabaseOutput { "Prefix(\(self.prefix) by \(self.strategy), of: \(self.base.description))" } } - diff --git a/Sources/FluentKit/Database/Databases.swift b/Sources/FluentKit/Database/Databases.swift index 3ab006c4..41bfd2f4 100644 --- a/Sources/FluentKit/Database/Databases.swift +++ b/Sources/FluentKit/Database/Databases.swift @@ -1,8 +1,9 @@ import Foundation -import struct NIOConcurrencyHelpers.NIOLock +import Logging import NIOCore import NIOPosix -import Logging + +import struct NIOConcurrencyHelpers.NIOLock public struct DatabaseConfigurationFactory: Sendable { public let make: @Sendable () -> any DatabaseConfiguration @@ -12,7 +13,7 @@ public struct DatabaseConfigurationFactory: Sendable { } } -public final class Databases: @unchecked Sendable { // @unchecked is safe here; mutable data is protected by lock +public final class Databases: @unchecked Sendable { // @unchecked is safe here; mutable data is protected by lock public let eventLoopGroup: any EventLoopGroup public let threadPool: NIOThreadPool @@ -25,7 +26,7 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; // Synchronize access across threads. private var lock: NIOLock - + public struct Middleware { let databases: Databases @@ -40,7 +41,7 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; self.databases.configurations[id] = configuration } } - + public func clear(on id: DatabaseID? = nil) { self.databases.lock.withLockVoid { let id = id ?? self.databases._requireDefaultID() @@ -54,7 +55,7 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; public var middleware: Middleware { .init(databases: self) } - + public init(threadPool: NIOThreadPool, on eventLoopGroup: any EventLoopGroup) { self.eventLoopGroup = eventLoopGroup self.threadPool = threadPool @@ -62,7 +63,7 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; self.drivers = [:] self.lock = .init() } - + public func use( _ configuration: DatabaseConfigurationFactory, as id: DatabaseID, @@ -70,7 +71,7 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; ) { self.use(configuration.make(), as: id, isDefault: isDefault) } - + public func use( _ driver: any DatabaseConfiguration, as id: DatabaseID, @@ -89,13 +90,13 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; self.defaultID = id } } - + public func configuration(for id: DatabaseID? = nil) -> (any DatabaseConfiguration)? { self.lock.withLock { self.configurations[id ?? self._requireDefaultID()] } } - + public func database( _ id: DatabaseID? = nil, logger: Logger, @@ -150,10 +151,10 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; self.drivers = [:] } } - + public func shutdownAsync() async { var driversToShutdown: [any DatabaseDriver] = [] - + self.lock.withLockVoid { for driver in self.drivers.values { driversToShutdown.append(driver) @@ -171,7 +172,7 @@ public final class Databases: @unchecked Sendable { // @unchecked is safe here; } return configuration } - + private func _requireDefaultID() -> DatabaseID { guard let id = self.defaultID else { fatalError("No default database configured.") diff --git a/Sources/FluentKit/Database/KeyPrefixingStrategy.swift b/Sources/FluentKit/Database/KeyPrefixingStrategy.swift index fa72f30b..91383f88 100644 --- a/Sources/FluentKit/Database/KeyPrefixingStrategy.swift +++ b/Sources/FluentKit/Database/KeyPrefixingStrategy.swift @@ -2,19 +2,19 @@ public enum KeyPrefixingStrategy: CustomStringConvertible, Sendable { /// The "do nothing" strategy - the prefix is applied to each key by simple concatenation. case none - + /// Each key has its first character capitalized and the prefix is applied to the result. case camelCase - + /// An underscore is placed between the prefix and each key. case snakeCase - + /// A custom strategy - for each key, the closure is called with that key and the prefix with which the /// wrapper was initialized, and must return the field key to actually use. The closure must be "pure" /// (i.e. for any given pair of inputs it must always return the same result, in the same way that hash /// values must be consistent within a single execution context). case custom(@Sendable (_ prefix: FieldKey, _ idFieldKey: FieldKey) -> FieldKey) - + // See `CustomStringConvertible.description`. public var description: String { switch self { @@ -28,13 +28,13 @@ public enum KeyPrefixingStrategy: CustomStringConvertible, Sendable { ".custom(...)" } } - + /// Apply this prefixing strategy and the given prefix to the given key, and return the result. public func apply(prefix: FieldKey, to key: FieldKey) -> FieldKey { switch self { case .none: .prefix(prefix, key) - + // This strategy converts `.id` and `.aggregate` keys (but not prefixes) into generic `.string()`s. case .camelCase: switch key { @@ -54,12 +54,12 @@ public enum KeyPrefixingStrategy: CustomStringConvertible, Sendable { } } -fileprivate extension String { - func withUppercasedFirstCharacter() -> String { +extension String { + fileprivate func withUppercasedFirstCharacter() -> String { guard !self.isEmpty else { return self } - + var result = self - result.replaceSubrange(result.startIndex ... result.startIndex, with: result[result.startIndex].uppercased()) + result.replaceSubrange(result.startIndex...result.startIndex, with: result[result.startIndex].uppercased()) return result } } diff --git a/Sources/FluentKit/Enum/EnumBuilder.swift b/Sources/FluentKit/Enum/EnumBuilder.swift index 4a186462..60d567cb 100644 --- a/Sources/FluentKit/Enum/EnumBuilder.swift +++ b/Sources/FluentKit/Enum/EnumBuilder.swift @@ -1,5 +1,5 @@ -import NIOCore import NIOConcurrencyHelpers +import NIOCore import SQLKit extension Database { @@ -67,10 +67,11 @@ public final class EnumBuilder: Sendable { EnumMetadata.query(on: self.database).filter(\.$name == self.enum.name).all() }.map { cases in // Convert latest cases to usable DataType. - .enum(.init( - name: self.enum.name, - cases: cases.map { $0.case } - )) + .enum( + .init( + name: self.enum.name, + cases: cases.map { $0.case } + )) } } @@ -92,16 +93,15 @@ public final class EnumBuilder: Sendable { EnumMetadata.query(on: self.database) .filter(\.$name == self.enum.name) .delete() - .flatMap - { _ in - EnumMetadata.query(on: self.database).count() - }.flatMap { count in - // If no enums are left, remove table. - if count == 0 { - return EnumMetadata.migration.revert(on: self.database) - } else { - return self.database.eventLoop.makeSucceededFuture(()) + .flatMap { _ in + EnumMetadata.query(on: self.database).count() + }.flatMap { count in + // If no enums are left, remove table. + if count == 0 { + return EnumMetadata.migration.revert(on: self.database) + } else { + return self.database.eventLoop.makeSucceededFuture(()) + } } - } } } diff --git a/Sources/FluentKit/Enum/EnumMetadata.swift b/Sources/FluentKit/Enum/EnumMetadata.swift index 78c397e4..c724fc1a 100644 --- a/Sources/FluentKit/Enum/EnumMetadata.swift +++ b/Sources/FluentKit/Enum/EnumMetadata.swift @@ -1,5 +1,5 @@ -import NIOCore import Foundation +import NIOCore final class EnumMetadata: Model, @unchecked Sendable { static let schema = "_fluent_enums" diff --git a/Sources/FluentKit/Enum/EnumProperty.swift b/Sources/FluentKit/Enum/EnumProperty.swift index 81a1845c..52c50495 100644 --- a/Sources/FluentKit/Enum/EnumProperty.swift +++ b/Sources/FluentKit/Enum/EnumProperty.swift @@ -1,18 +1,20 @@ extension Fields { public typealias Enum = EnumProperty - where Value: Codable & Sendable, - Value: RawRepresentable, - Value.RawValue == String + where + Value: Codable & Sendable, + Value: RawRepresentable, + Value.RawValue == String } // MARK: Type @propertyWrapper public final class EnumProperty - where Model: FluentKit.Fields, - Value: Codable & Sendable, - Value: RawRepresentable, - Value.RawValue == String +where + Model: FluentKit.Fields, + Value: Codable & Sendable, + Value: RawRepresentable, + Value.RawValue == String { public let field: FieldProperty @@ -39,7 +41,7 @@ public final class EnumProperty // MARK: Property -extension EnumProperty: AnyProperty { } +extension EnumProperty: AnyProperty {} extension EnumProperty: Property { public var value: Value? { diff --git a/Sources/FluentKit/Enum/OptionalEnumProperty.swift b/Sources/FluentKit/Enum/OptionalEnumProperty.swift index 4a601977..2dad9bb9 100644 --- a/Sources/FluentKit/Enum/OptionalEnumProperty.swift +++ b/Sources/FluentKit/Enum/OptionalEnumProperty.swift @@ -1,18 +1,20 @@ extension Fields { public typealias OptionalEnum = OptionalEnumProperty - where Value: Codable & Sendable, - Value: RawRepresentable, - Value.RawValue == String + where + Value: Codable & Sendable, + Value: RawRepresentable, + Value.RawValue == String } // MARK: Type @propertyWrapper public final class OptionalEnumProperty - where Model: FluentKit.Fields, - WrappedValue: Codable & Sendable, - WrappedValue: RawRepresentable, - WrappedValue.RawValue == String +where + Model: FluentKit.Fields, + WrappedValue: Codable & Sendable, + WrappedValue: RawRepresentable, + WrappedValue.RawValue == String { public let field: OptionalFieldProperty @@ -36,7 +38,7 @@ public final class OptionalEnumProperty // MARK: Property -extension OptionalEnumProperty: AnyProperty { } +extension OptionalEnumProperty: AnyProperty {} extension OptionalEnumProperty: Property { public var value: WrappedValue?? { @@ -101,7 +103,6 @@ extension OptionalEnumProperty: AnyDatabaseProperty { value = self.field.inputValue ?? .default } - switch value { case .bind(let bind as String): input.set(.enumCase(bind), at: self.field.key) diff --git a/Sources/FluentKit/Exports.swift b/Sources/FluentKit/Exports.swift index a4b838d4..c6ab6e92 100644 --- a/Sources/FluentKit/Exports.swift +++ b/Sources/FluentKit/Exports.swift @@ -1,10 +1,9 @@ -@_documentation(visibility: internal) @_exported import struct Foundation.Date -@_documentation(visibility: internal) @_exported import struct Foundation.UUID - @_documentation(visibility: internal) @_exported import Logging +@_documentation(visibility: internal) @_exported import struct Foundation.Date +@_documentation(visibility: internal) @_exported import struct Foundation.UUID @_documentation(visibility: internal) @_exported import protocol NIO.EventLoop @_documentation(visibility: internal) @_exported import class NIO.EventLoopFuture -@_documentation(visibility: internal) @_exported import struct NIO.EventLoopPromise @_documentation(visibility: internal) @_exported import protocol NIO.EventLoopGroup +@_documentation(visibility: internal) @_exported import struct NIO.EventLoopPromise @_documentation(visibility: internal) @_exported import class NIO.NIOThreadPool diff --git a/Sources/FluentKit/FluentError.swift b/Sources/FluentKit/FluentError.swift index 7a55c83d..5ee44941 100644 --- a/Sources/FluentKit/FluentError.swift +++ b/Sources/FluentKit/FluentError.swift @@ -82,7 +82,8 @@ extension FluentError { } internal static func missingParentError( - _: Child.Type = Child.self, _: Parent.Type = Parent.self, keyPath: KeyPath>, id: Parent.IDValue + _: Child.Type = Child.self, _: Parent.Type = Parent.self, keyPath: KeyPath>, + id: Parent.IDValue ) -> Self where Parent.IDValue: Fields { .missingParent( from: "\(Child.self)", @@ -104,7 +105,7 @@ public enum SiblingsPropertyError: Error, LocalizedError, CustomStringConvertibl /// /// Includes the relation name of the siblings property. case owningModelIdRequired(property: String) - + /// An attempt was made to attach, detach, or check attachment to a siblings property of a model whose /// ID is not currently known (usually because that model has not yet been saved to the database). /// @@ -114,20 +115,20 @@ public enum SiblingsPropertyError: Error, LocalizedError, CustomStringConvertibl /// /// Includes the relation name of the siblings property. case operandModelIdRequired(property: String) - + // `CustomStringConvertible` conformance. public var description: String { switch self { - case .owningModelIdRequired(property: let property): + case .owningModelIdRequired(let property): "siblings relation \(property) is missing owning model's ID (owner likely unsaved)" - case .operandModelIdRequired(property: let property): + case .operandModelIdRequired(let property): "operant model for siblings relation \(property) has no ID (attach/detach/etc. model likely unsaved)" } } - + // `CustomDebugStringConvertible` conformance. public var debugDescription: String { self.description } - + // `LocalizedError` conformance. public var errorDescription: String? { self.description } diff --git a/Sources/FluentKit/Middleware/ModelMiddleware.swift b/Sources/FluentKit/Middleware/ModelMiddleware.swift index 1a75d709..7cfd832b 100644 --- a/Sources/FluentKit/Middleware/ModelMiddleware.swift +++ b/Sources/FluentKit/Middleware/ModelMiddleware.swift @@ -11,7 +11,7 @@ public protocol AnyModelMiddleware: Sendable { public protocol ModelMiddleware: AnyModelMiddleware { associatedtype Model: FluentKit.Model - + func create(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture func update(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture func delete(model: Model, force: Bool, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture @@ -20,11 +20,13 @@ public protocol ModelMiddleware: AnyModelMiddleware { } extension ModelMiddleware { - public func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database, chainingTo next: any AnyModelResponder) -> EventLoopFuture { + public func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database, chainingTo next: any AnyModelResponder) + -> EventLoopFuture + { guard let modelType = model as? Model else { return next.handle(event, model, on: db) } - + switch event { case .create: return create(model: modelType, on: db, next: next) @@ -38,23 +40,23 @@ extension ModelMiddleware { return restore(model: modelType, on: db, next: next) } } - + public func create(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { next.create(model, on: db) } - + public func update(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { next.update(model, on: db) } - + public func delete(model: Model, force: Bool, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { next.delete(model, force: force, on: db) } - + public func softDelete(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { next.softDelete(model, on: db) } - + public func restore(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture { next.restore(model, on: db) } @@ -82,7 +84,7 @@ extension Array where Element == any AnyModelMiddleware { private struct ModelMiddlewareResponder: AnyModelResponder { var middleware: any AnyModelMiddleware var responder: any AnyModelResponder - + func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture { self.middleware.handle(event, model, on: db, chainingTo: responder) } diff --git a/Sources/FluentKit/Middleware/ModelResponder.swift b/Sources/FluentKit/Middleware/ModelResponder.swift index e612d4e7..5d1f5ab7 100644 --- a/Sources/FluentKit/Middleware/ModelResponder.swift +++ b/Sources/FluentKit/Middleware/ModelResponder.swift @@ -12,19 +12,19 @@ extension AnyModelResponder { public func create(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { self.handle(.create, model, on: db) } - + public func update(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { self.handle(.update, model, on: db) } - + public func restore(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { self.handle(.restore, model, on: db) } - + public func softDelete(_ model: any AnyModel, on db: any Database) -> EventLoopFuture { self.handle(.softDelete, model, on: db) } - + public func delete(_ model: any AnyModel, force: Bool, on db: any Database) -> EventLoopFuture { self.handle(.delete(force), model, on: db) } @@ -32,21 +32,20 @@ extension AnyModelResponder { internal struct BasicModelResponder: AnyModelResponder where Model: FluentKit.Model { private let _handle: @Sendable (ModelEvent, Model, any Database) throws -> EventLoopFuture - + internal func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture { guard let modelType = model as? Model else { fatalError("Could not convert type AnyModel to \(Model.self)") } - + do { return try self._handle(event, modelType, db) } catch { return db.eventLoop.makeFailedFuture(error) } } - + init(handle: @escaping @Sendable (ModelEvent, Model, any Database) throws -> EventLoopFuture) { self._handle = handle } } - diff --git a/Sources/FluentKit/Migration/Migration.swift b/Sources/FluentKit/Migration/Migration.swift index 5c6e7c23..75b24737 100644 --- a/Sources/FluentKit/Migration/Migration.swift +++ b/Sources/FluentKit/Migration/Migration.swift @@ -4,17 +4,17 @@ import NIOCore /// adding new table, changing existing tables or adding /// seed data. These actions are executed only once. public protocol Migration: Sendable { - + /// The name of the migration which Fluent uses to track the state of. var name: String { get } - + /// Called when a migration is executed. /// - Parameters: /// - database: `Database` to run the migration on, /// - returns: An asynchronous `Void`. - + func prepare(on database: any Database) -> EventLoopFuture - + /// Called when the changes from a migration are reverted. /// - Parameters: /// - database: `Database` to revert the migration on. @@ -29,19 +29,19 @@ extension Migration { internal var defaultName: String { #if compiler(<6.2) - /// `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 - /// creating it slows this accessor down by at least 30% at best, and - /// as much as 50% in some cases. Given that it's already incorrect to - /// be depending on the stability of `String(reflecting:)`'s output - /// anyway, it seems to make more sense to just call the underlying - /// 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. - Swift._typeName(Self.self, qualified: true) + /// `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 + /// creating it slows this accessor down by at least 30% at best, and + /// as much as 50% in some cases. Given that it's already incorrect to + /// be depending on the stability of `String(reflecting:)`'s output + /// anyway, it seems to make more sense to just call the underlying + /// 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. + Swift._typeName(Self.self, qualified: true) #else - String(reflecting: Self.self) + String(reflecting: Self.self) #endif } } diff --git a/Sources/FluentKit/Migration/MigrationLog.swift b/Sources/FluentKit/Migration/MigrationLog.swift index 9529123a..bef51ea7 100644 --- a/Sources/FluentKit/Migration/MigrationLog.swift +++ b/Sources/FluentKit/Migration/MigrationLog.swift @@ -1,5 +1,5 @@ -import NIOCore import Foundation +import NIOCore /// Stores information about `Migration`s that have been run. public final class MigrationLog: Model, @unchecked Sendable { diff --git a/Sources/FluentKit/Migration/Migrations.swift b/Sources/FluentKit/Migration/Migrations.swift index 44dd36f3..4ad05c56 100644 --- a/Sources/FluentKit/Migration/Migrations.swift +++ b/Sources/FluentKit/Migration/Migrations.swift @@ -2,15 +2,15 @@ import NIOConcurrencyHelpers public final class Migrations: Sendable { let storage: NIOLockedValueBox<[DatabaseID?: [any Migration]]> - + public init() { self.storage = .init([:]) } - + public func add(_ migration: any Migration, to id: DatabaseID? = nil) { self.storage.withLockedValue { $0[id, default: []].append(migration) } } - + @inlinable public func add(_ migrations: any Migration..., to id: DatabaseID? = nil) { self.add(migrations, to: id) diff --git a/Sources/FluentKit/Migration/Migrator.swift b/Sources/FluentKit/Migration/Migrator.swift index ae77acd6..abdee85a 100644 --- a/Sources/FluentKit/Migration/Migrator.swift +++ b/Sources/FluentKit/Migration/Migrator.swift @@ -37,62 +37,62 @@ public struct Migrator: Sendable { self.eventLoop = eventLoop self.migrationLogLevel = migrationLogLevel } - + // MARK: Setup - + public func setupIfNeeded() -> EventLoopFuture { - self.migrators() { $0.setupIfNeeded() }.map { _ in } + self.migrators { $0.setupIfNeeded() }.map { _ in } } - + // MARK: Prepare - + public func prepareBatch() -> EventLoopFuture { - self.migrators() { $0.prepareBatch() }.map { _ in } + self.migrators { $0.prepareBatch() }.map { _ in } } - + // MARK: Revert - + public func revertLastBatch() -> EventLoopFuture { - self.migrators() { $0.revertLastBatch() }.map { _ in } + self.migrators { $0.revertLastBatch() }.map { _ in } } - + public func revertBatch(number: Int) -> EventLoopFuture { - self.migrators() { $0.revertBatch(number: number) }.map { _ in } + self.migrators { $0.revertBatch(number: number) }.map { _ in } } - + public func revertAllBatches() -> EventLoopFuture { - self.migrators() { $0.revertAllBatches() }.map { _ in } + self.migrators { $0.revertAllBatches() }.map { _ in } } - + // MARK: Preview - + public func previewPrepareBatch() -> EventLoopFuture<[(any Migration, DatabaseID?)]> { - self.migrators() { migrator in + 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?)]> { - self.migrators() { migrator in + 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?)]> { - self.migrators() { migrator in + self.migrators { migrator in // This is not correct, but can't be fixed as it would require changing this API's parameters. migrator.previewPrepareBatch().and(value: migrator.id) }.map { $0.flatMap { migrations, id in migrations.map { ($0, id) } } } } - + public func previewRevertAllBatches() -> EventLoopFuture<[(any Migration, DatabaseID?)]> { - self.migrators() { migrator in + self.migrators { migrator in migrator.previewRevertAllBatches().and(value: migrator.id) }.map { $0.flatMap { migrations, id in migrations.map { ($0, id) } } @@ -102,9 +102,10 @@ public struct Migrator: Sendable { private func migrators( _ handler: (DatabaseMigrator) -> EventLoopFuture ) -> EventLoopFuture<[Result]> { - EventLoopFuture.whenAllSucceed(self.migrations.storage.withLockedValue { $0 }.map { - handler(.init(id: $0, database: self.databaseFactory($0), migrations: $1, migrationLogLevel: self.migrationLogLevel)) - }, on: self.eventLoop) + EventLoopFuture.whenAllSucceed( + self.migrations.storage.withLockedValue { $0 }.map { + handler(.init(id: $0, database: self.databaseFactory($0), migrations: $1, migrationLogLevel: self.migrationLogLevel)) + }, on: self.eventLoop) } } @@ -135,13 +136,16 @@ private final class DatabaseMigrator: Sendable { /// will include an identifier that can change from one execution to the next. private func preventUnstableNames() { for migration in self.migrations - where migration.name == migration.defaultName && migration.name.contains("$") - { + where migration.name == migration.defaultName && migration.name.contains("$") { if migration.name.contains("unknown context at") { - self.database.logger.critical("The migration at \(migration.name) is in a private context. Either explicitly give it a name by adding the `var name: String` property or make the migration `internal` or `public` instead of `private`.") + self.database.logger.critical( + "The migration at \(migration.name) is in a private context. Either explicitly give it a name by adding the `var name: String` property or make the migration `internal` or `public` instead of `private`." + ) fatalError("Private migrations not allowed") } - self.database.logger.error("The migration has an unexpected default name. Consider giving it an explicit name by adding a `var name: String` property before applying these migrations.", metadata: ["migration": .string(migration.name)]) + self.database.logger.error( + "The migration has an unexpected default name. Consider giving it an explicit name by adding a `var name: String` property before applying these migrations.", + metadata: ["migration": .string(migration.name)]) } } @@ -164,7 +168,9 @@ private final class DatabaseMigrator: Sendable { } func revertBatch(number: Int) -> EventLoopFuture { - self.preparedMigrations(batch: number).flatMapWithEventLoop { $0.reduce($1.makeSucceededVoidFuture()) { f, m in f.flatMap { self.revert(m) } } } + self.preparedMigrations(batch: number).flatMapWithEventLoop { + $0.reduce($1.makeSucceededVoidFuture()) { f, m in f.flatMap { self.revert(m) } } + } } func revertAllBatches() -> EventLoopFuture { @@ -194,29 +200,35 @@ private final class DatabaseMigrator: Sendable { // MARK: Private private func prepare(_ migration: any Migration, batch: Int) -> EventLoopFuture { - self.database.logger.log(level: self.migrationLogLevel, "[Migrator] Starting prepare", metadata: ["migration": .string(migration.name)]) - + 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)]) - + 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", metadata: ["migration": .string(migration.name), "error": .string(String(reflecting: $0))]) - + self.database.logger.error( + "[Migrator] Failed prepare", metadata: ["migration": .string(migration.name), "error": .string(String(reflecting: $0))]) + throw $0 } } private func revert(_ migration: any Migration) -> EventLoopFuture { - self.database.logger.log(level: self.migrationLogLevel, "[Migrator] Starting revert", metadata: ["migration": .string(migration.name)]) - + 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)]) - + 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", metadata: ["migration": .string(migration.name), "error": .string(String(reflecting: $0))]) - + self.database.logger.error( + "[Migrator] Failed revert", metadata: ["migration": .string(migration.name), "error": .string(String(reflecting: $0))]) + throw $0 } } diff --git a/Sources/FluentKit/Model/AnyModel.swift b/Sources/FluentKit/Model/AnyModel.swift index ed14a6f1..65bfb3cf 100644 --- a/Sources/FluentKit/Model/AnyModel.swift +++ b/Sources/FluentKit/Model/AnyModel.swift @@ -1,4 +1,4 @@ -public protocol AnyModel: Schema, CustomStringConvertible { } +public protocol AnyModel: Schema, CustomStringConvertible {} extension AnyModel { public static var alias: String? { nil } @@ -9,17 +9,16 @@ extension AnyModel { let input = self.collectInput() let info = [ "input": !input.isEmpty ? input.description : nil, - "output": self.anyID.cachedOutput?.description + "output": self.anyID.cachedOutput?.description, ].compactMapValues({ $0 }) - + return "\(Self.self)(\(info.isEmpty ? ":" : info.map { "\($0): \($1)" }.joined(separator: ", ")))" } // MARK: Joined public func joined(_ model: Joined.Type) throws -> Joined - where Joined: Schema - { + where Joined: Schema { guard let output = self.anyID.cachedOutput else { fatalError("Can only access joined models using models fetched from database (from \(Self.self) to \(Joined.self)).") } @@ -31,11 +30,11 @@ extension AnyModel { var anyID: any AnyID { for (nameC, child) in _FastChildSequence(subject: self) { /// Match a property named `_id` which conforms to `AnyID`. `as?` is expensive, so check that last. - if nameC?[0] == 0x5f/* '_' */, - nameC?[1] == 0x69/* 'i' */, - nameC?[2] == 0x64/* 'd' */, - nameC?[3] == 0x00/* '\0' */, - let idChild = child as? any AnyID + if nameC?[0] == 0x5f /* '_' */, + nameC?[1] == 0x69 /* 'i' */, + nameC?[2] == 0x64 /* 'd' */, + nameC?[3] == 0x00 /* '\0' */, + let idChild = child as? any AnyID { return idChild } diff --git a/Sources/FluentKit/Model/EagerLoad.swift b/Sources/FluentKit/Model/EagerLoad.swift index 804e11e1..6afc2cdc 100644 --- a/Sources/FluentKit/Model/EagerLoad.swift +++ b/Sources/FluentKit/Model/EagerLoad.swift @@ -23,7 +23,7 @@ public protocol EagerLoadable { _ relationKey: KeyPath, to builder: Builder ) where Builder: EagerLoadBuilder, Builder.Model == From - + static func eagerLoad( _ relationKey: KeyPath, withDeleted: Bool, @@ -34,7 +34,9 @@ public protocol EagerLoadable { _ loader: Loader, through: KeyPath, to builder: Builder - ) where Loader: EagerLoader, + ) + where + Loader: EagerLoader, Builder: EagerLoadBuilder, Loader.Model == To, Builder.Model == From diff --git a/Sources/FluentKit/Model/Fields+Codable.swift b/Sources/FluentKit/Model/Fields+Codable.swift index 0a137014..33a9e335 100644 --- a/Sources/FluentKit/Model/Fields+Codable.swift +++ b/Sources/FluentKit/Model/Fields+Codable.swift @@ -3,35 +3,39 @@ import struct SQLKit.SomeCodingKey extension Fields { public init(from decoder: any Decoder) throws { self.init() - + let container = try decoder.container(keyedBy: SomeCodingKey.self) - + for (key, property) in self.codableProperties { let propDecoder = try container.superDecoder(forKey: key) do { try property.decode(from: propDecoder) } catch { - throw DecodingError.typeMismatch(type(of: property).anyValueType, .init( - codingPath: container.codingPath + [key], - debugDescription: "Could not decode property", - underlyingError: error - )) + throw DecodingError.typeMismatch( + type(of: property).anyValueType, + .init( + codingPath: container.codingPath + [key], + debugDescription: "Could not decode property", + underlyingError: error + )) } } } public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: SomeCodingKey.self) - + for (key, property) in self.codableProperties where !property.skipPropertyEncoding { do { try property.encode(to: container.superEncoder(forKey: key)) - } catch let error where error is EncodingError { // trapping all errors breaks value handling logic in database driver layers - throw EncodingError.invalidValue(property.anyValue ?? "null", .init( - codingPath: container.codingPath + [key], - debugDescription: "Could not encode property", - underlyingError: error - )) + } catch let error where error is EncodingError { // trapping all errors breaks value handling logic in database driver layers + throw EncodingError.invalidValue( + property.anyValue ?? "null", + .init( + codingPath: container.codingPath + [key], + debugDescription: "Could not encode property", + underlyingError: error + )) } } } diff --git a/Sources/FluentKit/Model/Fields.swift b/Sources/FluentKit/Model/Fields.swift index cc8f06b9..483ab4fb 100644 --- a/Sources/FluentKit/Model/Fields.swift +++ b/Sources/FluentKit/Model/Fields.swift @@ -25,9 +25,9 @@ public protocol Fields: AnyObject, Codable, Sendable { /// > carries the same cost; it is not possible to meaningfully cache the results. See /// > `MirrorBypass.swift` for a considerable amount of very low-level detail. var properties: [any AnyProperty] { get } - + init() - + func input(to input: any DatabaseInput) func output(from output: any DatabaseOutput) throws } @@ -46,9 +46,8 @@ extension Fields { /// version of FluentKit will always yield field paths with exactly one component. Unfortunately, the /// API can not be changed to eliminate the array wrapper without major source breakage. public static func path(for field: KeyPath) -> [FieldKey] - where Property: AnyQueryableProperty - { - Self.init()[keyPath: field].path + where Property: AnyQueryableProperty { + Self.init()[keyPath: field].path } } @@ -95,7 +94,7 @@ extension Fields { public var properties: [any AnyProperty] { return _FastChildSequence(subject: self).compactMap { $1 as? any AnyProperty } } - + /// A wrapper around ``properties`` which returns only the properties which have database keys and can be /// input to and output from a database (corresponding to the ``AnyDatabaseProperty`` protocol). internal var databaseProperties: [any AnyDatabaseProperty] { @@ -117,15 +116,16 @@ extension Fields { /// > Warning: Like ``properties``, this method uses reflection, and incurs all of the accompanying /// > performance penalties. internal var codableProperties: [SomeCodingKey: any AnyCodableProperty] { - return .init(uniqueKeysWithValues: _FastChildSequence(subject: self).compactMap { - guard let value = $1 as? any AnyCodableProperty, - let nameC = $0, nameC[0] != 0, nameC[1] != 0, - let name = String(utf8String: nameC + 1) - else { - return nil - } - return (.init(stringValue: name), value) - }) + return .init( + uniqueKeysWithValues: _FastChildSequence(subject: self).compactMap { + guard let value = $1 as? any AnyCodableProperty, + let nameC = $0, nameC[0] != 0, nameC[1] != 0, + let name = String(utf8String: nameC + 1) + else { + return nil + } + return (.init(stringValue: name), value) + }) } } @@ -171,7 +171,7 @@ extension Fields { private final class DictionaryInput: DatabaseInput { var storage: [FieldKey: DatabaseQuery.Value] = [:] let wantsUnmodifiedKeys: Bool - + init(wantsUnmodifiedKeys: Bool = false) { self.wantsUnmodifiedKeys = wantsUnmodifiedKeys } diff --git a/Sources/FluentKit/Model/MirrorBypass.swift b/Sources/FluentKit/Model/MirrorBypass.swift index 39099b0a..d77bc7ea 100644 --- a/Sources/FluentKit/Model/MirrorBypass.swift +++ b/Sources/FluentKit/Model/MirrorBypass.swift @@ -1,16 +1,16 @@ #if compiler(<6.2) -@_silgen_name("swift_reflectionMirror_normalizedType") -internal func _getNormalizedType(_: T, type: Any.Type) -> Any.Type + @_silgen_name("swift_reflectionMirror_normalizedType") + internal func _getNormalizedType(_: T, type: Any.Type) -> Any.Type -@_silgen_name("swift_reflectionMirror_count") -internal func _getChildCount(_: T, type: Any.Type) -> Int + @_silgen_name("swift_reflectionMirror_count") + internal func _getChildCount(_: T, type: Any.Type) -> Int -@_silgen_name("swift_reflectionMirror_subscript") -internal func _getChild( - of: T, type: Any.Type, index: Int, - outName: UnsafeMutablePointer?>, - outFreeFunc: UnsafeMutablePointer<(@convention(c) (UnsafePointer?) -> Void)?> -) -> Any + @_silgen_name("swift_reflectionMirror_subscript") + internal func _getChild( + of: T, type: Any.Type, index: Int, + outName: UnsafeMutablePointer?>, + outFreeFunc: UnsafeMutablePointer<(@convention(c) (UnsafePointer?) -> Void)?> + ) -> Any #endif internal struct _FastChildIterator: IteratorProtocol { @@ -23,30 +23,30 @@ internal struct _FastChildIterator: IteratorProtocol { } deinit { self.freeFunc(self.ptr) } } - + #if compiler(<6.2) - private let subject: AnyObject - private let type: Any.Type - private let childCount: Int - private var index: Int + private let subject: AnyObject + private let type: Any.Type + private let childCount: Int + private var index: Int #else - private var iterator: Mirror.Children.Iterator + private var iterator: Mirror.Children.Iterator #endif private var lastNameBox: _CStringBox? - + #if compiler(<6.2) - fileprivate init(subject: AnyObject, type: Any.Type, childCount: Int) { - self.subject = subject - self.type = type - self.childCount = childCount - self.index = 0 - } + fileprivate init(subject: AnyObject, type: Any.Type, childCount: Int) { + self.subject = subject + self.type = type + self.childCount = childCount + self.index = 0 + } #else - fileprivate init(iterator: Mirror.Children.Iterator) { - self.iterator = iterator - } + fileprivate init(iterator: Mirror.Children.Iterator) { + self.iterator = iterator + } #endif - + /// The `name` pointer returned by this iterator has a rather unusual lifetime guarantee - it shall remain valid /// until either the proceeding call to `next()` or the end of the iterator's scope. This admittedly bizarre /// semantic is a concession to the fact that this entire API is intended to bypass the massive speed penalties of @@ -57,64 +57,64 @@ internal struct _FastChildIterator: IteratorProtocol { /// > an _additional_ performance penalty. mutating func next() -> (name: UnsafePointer?, child: Any)? { #if compiler(<6.2) - guard self.index < self.childCount else { - self.lastNameBox = nil // ensure any lingering name gets freed - return nil - } + guard self.index < self.childCount else { + self.lastNameBox = nil // ensure any lingering name gets freed + return nil + } - var nameC: UnsafePointer? = nil - var freeFunc: (@convention(c) (UnsafePointer?) -> Void)? = nil - let child = _getChild(of: self.subject, type: self.type, index: self.index, outName: &nameC, outFreeFunc: &freeFunc) - - self.index += 1 - self.lastNameBox = nameC.flatMap { nameC in freeFunc.map { _CStringBox(ptr: nameC, freeFunc: $0) } } // don't make a box if there's no name or no free function to call - return (name: nameC, child: child) + var nameC: UnsafePointer? = nil + var freeFunc: (@convention(c) (UnsafePointer?) -> Void)? = nil + let child = _getChild(of: self.subject, type: self.type, index: self.index, outName: &nameC, outFreeFunc: &freeFunc) + + self.index += 1 + self.lastNameBox = nameC.flatMap { nameC in freeFunc.map { _CStringBox(ptr: nameC, freeFunc: $0) } } // don't make a box if there's no name or no free function to call + return (name: nameC, child: child) #else - guard let child = self.iterator.next() else { - self.lastNameBox = nil - return nil - } - if var label = child.label { - let nameC = label.withUTF8 { - let buf = UnsafeMutableBufferPointer.allocate(capacity: $0.count + 1) - buf.initialize(repeating: 0) - _ = $0.withMemoryRebound(to: CChar.self) { buf.update(fromContentsOf: $0) } - return buf.baseAddress! + guard let child = self.iterator.next() else { + self.lastNameBox = nil + return nil + } + if var label = child.label { + let nameC = label.withUTF8 { + let buf = UnsafeMutableBufferPointer.allocate(capacity: $0.count + 1) + buf.initialize(repeating: 0) + _ = $0.withMemoryRebound(to: CChar.self) { buf.update(fromContentsOf: $0) } + return buf.baseAddress! + } + self.lastNameBox = _CStringBox(ptr: UnsafePointer(nameC), freeFunc: { $0?.deallocate() }) + return (name: UnsafePointer(nameC), child: child.value) + } else { + self.lastNameBox = nil + return (name: nil, child: child.value) } - self.lastNameBox = _CStringBox(ptr: UnsafePointer(nameC), freeFunc: { $0?.deallocate() }) - return (name: UnsafePointer(nameC), child: child.value) - } else { - self.lastNameBox = nil - return (name: nil, child: child.value) - } #endif } } internal struct _FastChildSequence: Sequence { #if compiler(<6.2) - private let subject: AnyObject - private let type: Any.Type - private let childCount: Int + private let subject: AnyObject + private let type: Any.Type + private let childCount: Int #else - private let children: Mirror.Children + private let children: Mirror.Children #endif init(subject: AnyObject) { #if compiler(<6.2) - self.subject = subject - self.type = _getNormalizedType(subject, type: Swift.type(of: subject)) - self.childCount = _getChildCount(subject, type: self.type) + self.subject = subject + self.type = _getNormalizedType(subject, type: Swift.type(of: subject)) + self.childCount = _getChildCount(subject, type: self.type) #else - self.children = Mirror(reflecting: subject).children + self.children = Mirror(reflecting: subject).children #endif } - + func makeIterator() -> _FastChildIterator { #if compiler(<6.2) - _FastChildIterator(subject: self.subject, type: self.type, childCount: self.childCount) + _FastChildIterator(subject: self.subject, type: self.type, childCount: self.childCount) #else - _FastChildIterator(iterator: self.children.makeIterator()) + _FastChildIterator(iterator: self.children.makeIterator()) #endif } } diff --git a/Sources/FluentKit/Model/Model+CRUD.swift b/Sources/FluentKit/Model/Model+CRUD.swift index 3d4a9fd2..7eb305a5 100644 --- a/Sources/FluentKit/Model/Model+CRUD.swift +++ b/Sources/FluentKit/Model/Model+CRUD.swift @@ -1,4 +1,5 @@ import NIOCore + import protocol SQLKit.SQLDatabase extension Model { @@ -64,10 +65,9 @@ extension Model { .filter(id: id) .set(input) .update() - .flatMapThrowing - { - try self.output(from: SavedInput(input)) - } + .flatMapThrowing { + try self.output(from: SavedInput(input)) + } } public func delete(force: Bool = false, on database: any Database) -> EventLoopFuture { @@ -88,12 +88,11 @@ extension Model { return Self.query(on: database) .filter(id: id) .delete(force: force) - .map - { - if force || self.deletedTimestamp == nil { - self._$idExists = false + .map { + if force || self.deletedTimestamp == nil { + self._$idExists = false + } } - } } public func restore(on database: any Database) -> EventLoopFuture { @@ -115,11 +114,10 @@ extension Model { .set(self.collectInput()) .action(.update) .run() - .flatMapThrowing - { - try self.output(from: SavedInput(self.collectInput())) - self._$idExists = true - } + .flatMapThrowing { + try self.output(from: SavedInput(self.collectInput())) + self._$idExists = true + } } private func handle(_ event: ModelEvent, on db: any Database) throws -> EventLoopFuture { @@ -143,20 +141,22 @@ extension Collection where Element: FluentKit.Model, Self: Sendable { guard !self.isEmpty else { return database.eventLoop.makeSucceededFuture(()) } - + precondition(self.allSatisfy { $0._$idExists }) - return EventLoopFuture.andAllSucceed(self.map { model in - database.configuration.middleware.chainingTo(Element.self) { event, model, db in - db.eventLoop.makeSucceededFuture(()) - }.delete(model, force: force, on: database) - }, on: database.eventLoop).flatMap { + return EventLoopFuture.andAllSucceed( + self.map { model in + database.configuration.middleware.chainingTo(Element.self) { event, model, db in + db.eventLoop.makeSucceededFuture(()) + }.delete(model, force: force, on: database) + }, on: database.eventLoop + ).flatMap { Element.query(on: database) .filter(ids: self.map { $0.id! }) .delete(force: force) }.map { guard force else { return } - + for model in self where model.deletedTimestamp == nil { model._$idExists = false } @@ -167,18 +167,20 @@ extension Collection where Element: FluentKit.Model, Self: Sendable { guard !self.isEmpty else { return database.eventLoop.makeSucceededFuture(()) } - + precondition(self.allSatisfy { !$0._$idExists }) - - return EventLoopFuture.andAllSucceed(self.enumerated().map { idx, model in - database.configuration.middleware.chainingTo(Element.self) { event, model, db in - if model.anyID is any AnyQueryableProperty { - model._$id.generate() - } - model.touchTimestamps(.create, .update) - return db.eventLoop.makeSucceededFuture(()) - }.create(model, on: database) - }, on: database.eventLoop).flatMap { + + return EventLoopFuture.andAllSucceed( + self.enumerated().map { idx, model in + database.configuration.middleware.chainingTo(Element.self) { event, model, db in + if model.anyID is any AnyQueryableProperty { + model._$id.generate() + } + model.touchTimestamps(.create, .update) + return db.eventLoop.makeSucceededFuture(()) + }.create(model, on: database) + }, on: database.eventLoop + ).flatMap { Element.query(on: database) .set(self.map { $0.collectInput(withDefaultedValues: database is any SQLDatabase) }) .create() @@ -201,7 +203,7 @@ public enum MiddlewareFailureHandler { private struct SavedInput: DatabaseOutput { var input: [FieldKey: DatabaseQuery.Value] - + init(_ input: [FieldKey: DatabaseQuery.Value]) { self.input = input } @@ -209,7 +211,7 @@ private struct SavedInput: DatabaseOutput { func schema(_ schema: String) -> any DatabaseOutput { self } - + func contains(_ key: FieldKey) -> Bool { self.input[key] != nil } @@ -235,10 +237,9 @@ private struct SavedInput: DatabaseOutput { return false } } - + func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T : Decodable - { + where T: Decodable { guard let value = self.input[key] else { throw FluentError.missingField(name: key.description) } diff --git a/Sources/FluentKit/Model/Model.swift b/Sources/FluentKit/Model/Model.swift index 175821bb..574d7632 100644 --- a/Sources/FluentKit/Model/Model.swift +++ b/Sources/FluentKit/Model/Model.swift @@ -28,7 +28,7 @@ extension Model { } return id } - + /// Replaces the existing common usage of `model._$id.exists`, which indicates whether any /// particular generic model has a non-`nil` ID that was loaded from a database query (or /// was overridden to allow Fluent to assume as such without having to check first). This diff --git a/Sources/FluentKit/Model/ModelAlias.swift b/Sources/FluentKit/Model/ModelAlias.swift index 8efcedc0..4b66c243 100644 --- a/Sources/FluentKit/Model/ModelAlias.swift +++ b/Sources/FluentKit/Model/ModelAlias.swift @@ -46,10 +46,10 @@ public protocol ModelAlias: Schema { /// The model type to be aliased. associatedtype Model: FluentKit.Model - + /// The actual alias name to be used in place of `Model.schema`. static var name: String { get } - + /// An instance of the orignal model type. Holds returned data from lookups, and /// is used as a data source for CRUD operations. /// @@ -64,7 +64,7 @@ public protocol ModelAlias: Schema { /// automatically and should not be overriden by conforming types. See /// ``ModelAlias/subscript(dynamicMember:)-8hc9u`` for details. subscript(dynamicMember keyPath: KeyPath) -> AliasedField - where Field.Model == Self.Model + where Field.Model == Self.Model { get } /// `@dynamicMemberLookup` support. The implementation of this subscript is provided @@ -76,7 +76,7 @@ public protocol ModelAlias: Schema { extension ModelAlias { /// An alias's ``space`` is always that of the original Model. public static var space: String? { Model.space } - + /// An alias's ``schema`` is always that of the original Model. This is the full, unaliased /// schema name , and must remain so even for the aliased model type in order to correctly /// specify for the database driver what identifier the alias applies to. @@ -107,8 +107,7 @@ extension ModelAlias { /// print(alias.$id.exists) // false /// ``` public subscript(dynamicMember keyPath: KeyPath) -> AliasedField - where Field.Model == Self.Model - { + where Field.Model == Self.Model { .init(field: self.model[keyPath: keyPath]) } @@ -140,8 +139,7 @@ extension ModelAlias { /// levels of nested projected properties values. @dynamicMemberLookup public final class AliasedField - where Alias: ModelAlias, Field: Property, Alias.Model == Field.Model -{ +where Alias: ModelAlias, Field: Property, Alias.Model == Field.Model { public let field: Field fileprivate init(field: Field) { self.field = field } diff --git a/Sources/FluentKit/Model/Schema.swift b/Sources/FluentKit/Model/Schema.swift index e409cbd2..e4218d6f 100644 --- a/Sources/FluentKit/Model/Schema.swift +++ b/Sources/FluentKit/Model/Schema.swift @@ -6,11 +6,11 @@ public protocol Schema: Fields { extension Schema { public static var space: String? { nil } - + public static var schemaOrAlias: String { self.alias ?? self.schema } - + public static var spaceIfNotAliased: String? { self.alias == nil ? self.space : nil } diff --git a/Sources/FluentKit/Operators/FieldOperators.swift b/Sources/FluentKit/Operators/FieldOperators.swift index e797c2f3..b5de83d6 100644 --- a/Sources/FluentKit/Operators/FieldOperators.swift +++ b/Sources/FluentKit/Operators/FieldOperators.swift @@ -10,8 +10,7 @@ extension QueryBuilder { @discardableResult public func filter(_ filter: ModelFieldFilter) -> Self - where Left: Schema, Right: Schema - { + where Left: Schema, Right: Schema { self.filter( .extendedPath(filter.lhsPath, schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), filter.method, @@ -24,10 +23,11 @@ public func == ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where Left: Schema, - LeftField: QueryableProperty, - Right: Schema, - RightField: QueryableProperty +where + Left: Schema, + LeftField: QueryableProperty, + Right: Schema, + RightField: QueryableProperty { .init(lhs, .equal, rhs) } @@ -36,10 +36,11 @@ public func != ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where Left: Schema, - LeftField: QueryableProperty, - Right: Schema, - RightField: QueryableProperty +where + Left: Schema, + LeftField: QueryableProperty, + Right: Schema, + RightField: QueryableProperty { .init(lhs, .notEqual, rhs) } @@ -48,10 +49,11 @@ public func >= ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where Left: Schema, - LeftField: QueryableProperty, - Right: Schema, - RightField: QueryableProperty +where + Left: Schema, + LeftField: QueryableProperty, + Right: Schema, + RightField: QueryableProperty { .init(lhs, .greaterThanOrEqual, rhs) } @@ -60,10 +62,11 @@ public func > ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where Left: Schema, - LeftField: QueryableProperty, - Right: Schema, - RightField: QueryableProperty +where + Left: Schema, + LeftField: QueryableProperty, + Right: Schema, + RightField: QueryableProperty { .init(lhs, .greaterThan, rhs) } @@ -72,10 +75,11 @@ public func < ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where Left: Schema, - LeftField: QueryableProperty, - Right: Schema, - RightField: QueryableProperty +where + Left: Schema, + LeftField: QueryableProperty, + Right: Schema, + RightField: QueryableProperty { .init(lhs, .lessThan, rhs) } @@ -84,10 +88,11 @@ public func <= ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - RightField.Model == Right, - RightField: QueryableProperty +where + LeftField.Model == Left, + LeftField: QueryableProperty, + RightField.Model == Right, + RightField: QueryableProperty { .init(lhs, .lessThanOrEqual, rhs) } @@ -96,12 +101,13 @@ public func ~= ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - LeftField.Value: CustomStringConvertible, - RightField.Model == Right, - RightField: QueryableProperty, - RightField.Value: CustomStringConvertible +where + LeftField.Model == Left, + LeftField: QueryableProperty, + LeftField.Value: CustomStringConvertible, + RightField.Model == Right, + RightField: QueryableProperty, + RightField.Value: CustomStringConvertible { .init(lhs, .contains(inverse: false, .suffix), rhs) } @@ -110,12 +116,13 @@ public func ~~ ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - LeftField.Value: CustomStringConvertible, - RightField.Model == Right, - RightField: QueryableProperty, - RightField.Value: CustomStringConvertible +where + LeftField.Model == Left, + LeftField: QueryableProperty, + LeftField.Value: CustomStringConvertible, + RightField.Model == Right, + RightField: QueryableProperty, + RightField.Value: CustomStringConvertible { .init(lhs, .contains(inverse: false, .anywhere), rhs) } @@ -124,12 +131,13 @@ public func =~ ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - LeftField.Value: CustomStringConvertible, - RightField.Model == Right, - RightField: QueryableProperty, - RightField.Value: CustomStringConvertible +where + LeftField.Model == Left, + LeftField: QueryableProperty, + LeftField.Value: CustomStringConvertible, + RightField.Model == Right, + RightField: QueryableProperty, + RightField.Value: CustomStringConvertible { .init(lhs, .contains(inverse: false, .prefix), rhs) } @@ -138,12 +146,13 @@ public func !~= ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - LeftField.Value: CustomStringConvertible, - RightField.Model == Right, - RightField: QueryableProperty, - RightField.Value: CustomStringConvertible +where + LeftField.Model == Left, + LeftField: QueryableProperty, + LeftField.Value: CustomStringConvertible, + RightField.Model == Right, + RightField: QueryableProperty, + RightField.Value: CustomStringConvertible { .init(lhs, .contains(inverse: true, .suffix), rhs) } @@ -152,12 +161,13 @@ public func !~ ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - LeftField.Value: CustomStringConvertible, - RightField.Model == Right, - RightField: QueryableProperty, - RightField.Value: CustomStringConvertible +where + LeftField.Model == Left, + LeftField: QueryableProperty, + LeftField.Value: CustomStringConvertible, + RightField.Model == Right, + RightField: QueryableProperty, + RightField.Value: CustomStringConvertible { .init(lhs, .contains(inverse: true, .anywhere), rhs) } @@ -166,26 +176,25 @@ public func !=~ ( lhs: KeyPath, rhs: KeyPath ) -> ModelFieldFilter - where LeftField.Model == Left, - LeftField: QueryableProperty, - LeftField.Value: CustomStringConvertible, - RightField.Model == Right, - RightField: QueryableProperty, - RightField.Value: CustomStringConvertible +where + LeftField.Model == Left, + LeftField: QueryableProperty, + LeftField.Value: CustomStringConvertible, + RightField.Model == Right, + RightField: QueryableProperty, + RightField.Value: CustomStringConvertible { .init(lhs, .contains(inverse: true, .prefix), rhs) } public struct ModelFieldFilter: Sendable - where Left: FluentKit.Schema, Right: FluentKit.Schema -{ +where Left: FluentKit.Schema, Right: FluentKit.Schema { public init( _ lhs: KeyPath, _ method: DatabaseQuery.Filter.Method, _ rhs: KeyPath ) - where LeftField: QueryableProperty, RightField: QueryableProperty - { + where LeftField: QueryableProperty, RightField: QueryableProperty { self.lhsPath = Left.init()[keyPath: lhs].path self.method = method self.rhsPath = Right.init()[keyPath: rhs].path diff --git a/Sources/FluentKit/Operators/ValueOperators+Array.swift b/Sources/FluentKit/Operators/ValueOperators+Array.swift index 09f26807..edbb7a6a 100644 --- a/Sources/FluentKit/Operators/ValueOperators+Array.swift +++ b/Sources/FluentKit/Operators/ValueOperators+Array.swift @@ -1,41 +1,45 @@ // MARK: Field.Value public func ~~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter - where Model: FluentKit.Schema, - Field: QueryableProperty, - Values: Collection, - Values.Element == Field.Value +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Values: Collection, + Values.Element == Field.Value { lhs ~~ .array(rhs.map { Field.queryValue($0) }) } public func ~~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter - where Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: Codable, - Values: Collection, - Values.Element == Field.Value.Wrapped +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: Codable, + Values: Collection, + Values.Element == Field.Value.Wrapped { lhs ~~ .array(rhs.map { Field.queryValue(.init($0)) }) } public func !~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter - where Model: FluentKit.Schema, - Field: QueryableProperty, - Values: Collection, - Values.Element == Field.Value +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Values: Collection, + Values.Element == Field.Value { lhs !~ .array(rhs.map { Field.queryValue($0) }) } public func !~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter - where Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: Codable, - Values: Collection, - Values.Element == Field.Value.Wrapped +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: Codable, + Values: Collection, + Values.Element == Field.Value.Wrapped { lhs !~ .array(rhs.map { Field.queryValue(.init($0)) }) } @@ -43,13 +47,11 @@ public func !~ (lhs: KeyPath, rhs: Values) - // MARK: DatabaseQuery.Value public func ~~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: FluentKit.Schema, Field: QueryableProperty -{ +where Model: FluentKit.Schema, Field: QueryableProperty { .init(lhs, .subset(inverse: false), rhs) } public func !~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: FluentKit.Schema, Field: QueryableProperty -{ +where Model: FluentKit.Schema, Field: QueryableProperty { .init(lhs, .subset(inverse: true), rhs) } diff --git a/Sources/FluentKit/Operators/ValueOperators+String.swift b/Sources/FluentKit/Operators/ValueOperators+String.swift index 01864534..244e7566 100644 --- a/Sources/FluentKit/Operators/ValueOperators+String.swift +++ b/Sources/FluentKit/Operators/ValueOperators+String.swift @@ -1,115 +1,115 @@ // MARK: Field.Value public func ~= (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value == String { lhs ~= Field.queryValue(rhs) } public func ~= (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped == String { lhs ~= Field.queryValue(.init(rhs)) } public func ~~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value == String { lhs ~~ Field.queryValue(rhs) } public func ~~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped == String { lhs ~~ Field.queryValue(.init(rhs)) } public func =~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value == String { lhs =~ Field.queryValue(rhs) } public func =~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped == String { lhs =~ Field.queryValue(.init(rhs)) } public func !~= (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value == String { lhs !~= Field.queryValue(rhs) } public func !~= (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped == String { lhs !~= Field.queryValue(.init(rhs)) } public func !~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value == String { lhs !~ Field.queryValue(rhs) } public func !~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped == String { lhs !~ Field.queryValue(.init(rhs)) } public func !=~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value == String { lhs !=~ Field.queryValue(rhs) } public func !=~ (lhs: KeyPath, rhs: String) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped == String +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped == String { lhs !=~ Field.queryValue(.init(rhs)) } @@ -117,115 +117,115 @@ public func !=~ (lhs: KeyPath, rhs: String) -> Model // MARK: DatabaseQuery.Value public func ~= (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: CustomStringConvertible { .init(lhs, .contains(inverse: false, .suffix), rhs) } public func ~= (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: CustomStringConvertible { .init(lhs, .contains(inverse: false, .suffix), rhs) } public func ~~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: CustomStringConvertible { .init(lhs, .contains(inverse: false, .anywhere), rhs) } public func ~~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: CustomStringConvertible { .init(lhs, .contains(inverse: false, .anywhere), rhs) } public func =~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: CustomStringConvertible { .init(lhs, .contains(inverse: false, .prefix), rhs) } public func =~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: CustomStringConvertible { .init(lhs, .contains(inverse: false, .prefix), rhs) } public func !~= (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: CustomStringConvertible { .init(lhs, .contains(inverse: true, .suffix), rhs) } public func !~= (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: CustomStringConvertible { .init(lhs, .contains(inverse: true, .suffix), rhs) } public func !~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: CustomStringConvertible { .init(lhs, .contains(inverse: true, .anywhere), rhs) } public func !~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: CustomStringConvertible { .init(lhs, .contains(inverse: true, .anywhere), rhs) } public func !=~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: CustomStringConvertible { .init(lhs, .contains(inverse: true, .prefix), rhs) } public func !=~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where - Model: FluentKit.Schema, - Field: QueryableProperty, - Field.Value: OptionalType, - Field.Value.Wrapped: CustomStringConvertible +where + Model: FluentKit.Schema, + Field: QueryableProperty, + Field.Value: OptionalType, + Field.Value.Wrapped: CustomStringConvertible { .init(lhs, .contains(inverse: true, .prefix), rhs) } diff --git a/Sources/FluentKit/Operators/ValueOperators.swift b/Sources/FluentKit/Operators/ValueOperators.swift index ed356969..5be9e39f 100644 --- a/Sources/FluentKit/Operators/ValueOperators.swift +++ b/Sources/FluentKit/Operators/ValueOperators.swift @@ -9,37 +9,34 @@ extension QueryBuilder { _ schema: Joined.Type, _ filter: ModelValueFilter ) -> Self - where Joined: Schema - { + where Joined: Schema { self.filter( .extendedPath(filter.path, schema: Joined.schemaOrAlias, space: Joined.spaceIfNotAliased), filter.method, filter.value ) } - + @discardableResult public func filter(_ filter: ModelCompositeIDFilter) -> Self - where Model.IDValue: Fields - { + where Model.IDValue: Fields { self.filter(Model.self, filter) } - + @discardableResult public func filter( _ schema: Joined.Type, _ filter: ModelCompositeIDFilter ) -> Self - where Joined: Schema, Joined.IDValue: Fields - { + where Joined: Schema, Joined.IDValue: Fields { let relation: DatabaseQuery.Filter.Relation let inverted: Bool switch filter.method { case .equality(false): (relation, inverted) = (.and, false) - case .equality(true): (relation, inverted) = (.or, true) + case .equality(true): (relation, inverted) = (.or, true) default: fatalError("unreachable") } - + return self.group(relation) { filter.value.input(to: QueryFilterInput(builder: $0, inverted: inverted)) } } } @@ -47,86 +44,78 @@ extension QueryBuilder { // MARK: Field.Value public func == (lhs: KeyPath, rhs: Field.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { lhs == Field.queryValue(rhs) } public func != (lhs: KeyPath, rhs: Field.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { lhs != Field.queryValue(rhs) } public func >= (lhs: KeyPath, rhs: Field.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { lhs >= Field.queryValue(rhs) } public func > (lhs: KeyPath, rhs: Field.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { lhs > Field.queryValue(rhs) } public func < (lhs: KeyPath, rhs: Field.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { lhs < Field.queryValue(rhs) } public func <= (lhs: KeyPath, rhs: Field.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { lhs <= Field.queryValue(rhs) } // MARK: CompositeID.Value -public func == (lhs: KeyPath>, rhs: Model.IDValue) -> ModelCompositeIDFilter { +public func == (lhs: KeyPath>, rhs: Model.IDValue) -> ModelCompositeIDFilter< + Model +> { .init(.equal, rhs) } -public func != (lhs: KeyPath>, rhs: Model.IDValue) -> ModelCompositeIDFilter { +public func != (lhs: KeyPath>, rhs: Model.IDValue) -> ModelCompositeIDFilter< + Model +> { .init(.notEqual, rhs) } // MARK: DatabaseQuery.Value public func == (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { .init(lhs, .equal, rhs) } public func != (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { .init(lhs, .notEqual, rhs) } public func >= (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { .init(lhs, .greaterThanOrEqual, rhs) } public func > (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { .init(lhs, .greaterThan, rhs) } public func < (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { .init(lhs, .lessThan, rhs) } public func <= (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter - where Model: Fields, Field: QueryableProperty -{ +where Model: Fields, Field: QueryableProperty { .init(lhs, .lessThanOrEqual, rhs) } @@ -136,8 +125,7 @@ public struct ModelValueFilter: Sendable where Model: Fields { _ method: DatabaseQuery.Filter.Method, _ rhs: DatabaseQuery.Value ) - where Field: QueryableProperty - { + where Field: QueryableProperty { self.path = Model.path(for: lhs) self.method = method self.value = rhs @@ -154,11 +142,11 @@ public struct ModelCompositeIDFilter: Sendable where Model: FluentKit.Mod _ rhs: Model.IDValue ) { guard case .equality(_) = method else { preconditionFailure("Composite IDs may only be compared for equality or inequality.") } - + self.method = method self.value = rhs } - + let method: DatabaseQuery.Filter.Method let value: Model.IDValue } diff --git a/Sources/FluentKit/Properties/Boolean.swift b/Sources/FluentKit/Properties/Boolean.swift index 50e5570a..056ebc45 100644 --- a/Sources/FluentKit/Properties/Boolean.swift +++ b/Sources/FluentKit/Properties/Boolean.swift @@ -1,6 +1,6 @@ extension Fields { public typealias Boolean = BooleanProperty - where Format: BooleanPropertyFormat + where Format: BooleanPropertyFormat } /// A Fluent model property which represents a boolean (true/false) value. @@ -44,8 +44,7 @@ extension Fields { /// > Note: See also ``OptionalBooleanProperty`` and ``BooleanPropertyFormat``. @propertyWrapper public final class BooleanProperty - where Model: FluentKit.Fields, Format: BooleanPropertyFormat -{ +where Model: FluentKit.Fields, Format: BooleanPropertyFormat { @FieldProperty public var field: Format.Value public let format: Format diff --git a/Sources/FluentKit/Properties/BooleanPropertyFormat.swift b/Sources/FluentKit/Properties/BooleanPropertyFormat.swift index 873b2cd3..1742af32 100644 --- a/Sources/FluentKit/Properties/BooleanPropertyFormat.swift +++ b/Sources/FluentKit/Properties/BooleanPropertyFormat.swift @@ -11,11 +11,11 @@ public protocol BooleanPropertyFormat: Sendable { /// Represent a `Bool` natively, using the database's underlying support (if any). This is the default. public struct DefaultBooleanPropertyFormat: BooleanPropertyFormat { public init() {} - + public func parse(_ value: Bool) -> Bool? { value } - + public func serialize(_ bool: Bool) -> Bool { bool } @@ -32,7 +32,7 @@ extension BooleanPropertyFormat where Self == DefaultBooleanPropertyFormat { /// > model with a `BIGINT` field instead of the default `TINYINT`. public struct IntegerBooleanPropertyFormat: BooleanPropertyFormat { public init() {} - + public func parse(_ value: T) -> Bool? { switch value { case .zero: false @@ -40,7 +40,7 @@ public struct IntegerBooleanPropertyFormat T { .zero.advanced(by: bool ? 1 : 0) } @@ -53,7 +53,7 @@ extension BooleanPropertyFormat where Self == IntegerBooleanPropertyFormat /// Represent a `Bool` as the strings "0" and "1". Any other value is considered invalid. public struct OneZeroBooleanPropertyFormat: BooleanPropertyFormat { public init() {} - + public func parse(_ value: String) -> Bool? { switch value { case "0": false @@ -61,7 +61,7 @@ public struct OneZeroBooleanPropertyFormat: BooleanPropertyFormat { default: nil } } - + public func serialize(_ bool: Bool) -> String { bool ? "1" : "0" } @@ -74,7 +74,7 @@ extension BooleanPropertyFormat where Self == OneZeroBooleanPropertyFormat { /// Represent a `Bool` as the strings "N" and "Y". Parsing is case-insensitive. Serialization always stores uppercase. public struct YNBooleanPropertyFormat: BooleanPropertyFormat { public init() {} - + public func parse(_ value: String) -> Bool? { switch value.lowercased() { case "n": false @@ -82,7 +82,7 @@ public struct YNBooleanPropertyFormat: BooleanPropertyFormat { default: nil } } - + public func serialize(_ bool: Bool) -> String { bool ? "Y" : "N" } @@ -92,11 +92,10 @@ extension BooleanPropertyFormat where Self == YNBooleanPropertyFormat { public static var yn: Self { .init() } } - /// Represent a `Bool` as the strings "NO" and "YES". Parsing is case-insensitive. Serialization always stores uppercase. public struct YesNoBooleanPropertyFormat: BooleanPropertyFormat { public init() {} - + public func parse(_ value: String) -> Bool? { switch value.lowercased() { case "no": false @@ -104,7 +103,7 @@ public struct YesNoBooleanPropertyFormat: BooleanPropertyFormat { default: nil } } - + public func serialize(_ bool: Bool) -> String { bool ? "YES" : "NO" } @@ -125,7 +124,7 @@ public struct OnOffBooleanPropertyFormat: BooleanPropertyFormat { default: nil } } - + public func serialize(_ bool: Bool) -> String { bool ? "ON" : "OFF" } @@ -138,7 +137,7 @@ extension BooleanPropertyFormat where Self == OnOffBooleanPropertyFormat { /// Represent a `Bool` as the strings "false" and "true". Parsing is case-insensitive. Serialization always stores lowercase. public struct TrueFalseBooleanPropertyFormat: BooleanPropertyFormat { public init() {} - + public func parse(_ value: String) -> Bool? { switch value.lowercased() { case "false": false @@ -146,7 +145,7 @@ public struct TrueFalseBooleanPropertyFormat: BooleanPropertyFormat { default: nil } } - + public func serialize(_ bool: Bool) -> String { bool ? "true" : "false" } @@ -166,7 +165,7 @@ extension BooleanPropertyFormatFactory { public static var integer: BooleanPropertyFormatFactory> { .init(format: .init()) } - + public static var oneZero: BooleanPropertyFormatFactory { .init(format: .init()) } diff --git a/Sources/FluentKit/Properties/Children.swift b/Sources/FluentKit/Properties/Children.swift index f8dacb62..7e81980d 100644 --- a/Sources/FluentKit/Properties/Children.swift +++ b/Sources/FluentKit/Properties/Children.swift @@ -2,20 +2,19 @@ import NIOCore extension Model { public typealias Children = ChildrenProperty - where To: FluentKit.Model + where To: FluentKit.Model } // MARK: Type @propertyWrapper public final class ChildrenProperty: @unchecked Sendable - where From: Model, To: Model -{ +where From: Model, To: Model { public typealias Key = RelationParentKey public let parentKey: Key var idValue: From.IDValue? - + public var value: [To]? public convenience init(for parent: KeyPath>) { @@ -25,7 +24,7 @@ public final class ChildrenProperty: @unchecked Sendable public convenience init(for optionalParent: KeyPath>) { self.init(for: .optional(optionalParent)) } - + private init(for parentKey: Key) { self.parentKey = parentKey } @@ -45,7 +44,7 @@ public final class ChildrenProperty: @unchecked Sendable public var projectedValue: ChildrenProperty { self } - + public var fromId: From.IDValue? { get { self.idValue } set { self.idValue = newValue } @@ -141,9 +140,9 @@ extension ChildrenProperty: AnyCodableProperty { public func decode(from decoder: any Decoder) throws { // don't decode } - + public var skipPropertyEncoding: Bool { - self.value == nil // Avoids leaving an empty JSON object lying around in some cases. + self.value == nil // Avoids leaving an empty JSON object lying around in some cases. } } @@ -168,28 +167,26 @@ extension ChildrenProperty: EagerLoadable { _ relationKey: KeyPath>, to builder: Builder ) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad( _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder ) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = ChildrenEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } - public static func eagerLoad( _ loader: Loader, through: KeyPath>, to builder: Builder - ) where + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, @@ -201,11 +198,10 @@ extension ChildrenProperty: EagerLoadable { } private struct ChildrenEagerLoader: EagerLoader - where From: Model, To: Model -{ +where From: Model, To: Model { let relationKey: KeyPath> let withDeleted: Bool - + func run(models: [From], on database: any Database) -> EventLoopFuture { let ids = models.map { $0.id! } @@ -217,7 +213,7 @@ private struct ChildrenEagerLoader: EagerLoader case .required(let required): builder.filter(required.appending(path: \.$id) ~~ Set(ids)) } - if (self.withDeleted) { + if self.withDeleted { builder.withDeleted() } return builder.all().map { @@ -237,8 +233,7 @@ private struct ChildrenEagerLoader: EagerLoader } private struct ThroughChildrenEagerLoader: EagerLoader - where From: Model, Loader: EagerLoader, Loader.Model == Through -{ +where From: Model, Loader: EagerLoader, Loader.Model == Through { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/CompositeChildren.swift b/Sources/FluentKit/Properties/CompositeChildren.swift index d9dd329c..26f2ff46 100644 --- a/Sources/FluentKit/Properties/CompositeChildren.swift +++ b/Sources/FluentKit/Properties/CompositeChildren.swift @@ -4,7 +4,7 @@ extension Model { /// A convenience alias for ``CompositeChildrenProperty``. It is strongly recommended that callers use this /// alias rather than referencing ``CompositeChildrenProperty`` directly whenever possible. public typealias CompositeChildren = CompositeChildrenProperty - where To: FluentKit.Model, Self.IDValue: Fields + where To: FluentKit.Model, Self.IDValue: Fields } /// Declares a many-to-one relation between the referenced ("child") model and the referencing ("parent") model, @@ -74,10 +74,9 @@ extension Model { /// ``` @propertyWrapper public final class CompositeChildrenProperty: @unchecked Sendable - where From: Model, To: Model, From.IDValue: Fields -{ +where From: Model, To: Model, From.IDValue: Fields { public typealias Key = CompositeRelationParentKey - + public let parentKey: Key var idValue: From.IDValue? @@ -86,7 +85,7 @@ public final class CompositeChildrenProperty: @unchecked Sendable public init(for parentKey: KeyPath>) { self.parentKey = .required(parentKey) } - + public init(for parentKey: KeyPath>) { self.parentKey = .optional(parentKey) } @@ -104,7 +103,7 @@ public final class CompositeChildrenProperty: @unchecked Sendable } public var projectedValue: CompositeChildrenProperty { self } - + public var fromId: From.IDValue? { get { self.idValue } set { self.idValue = newValue } @@ -128,7 +127,7 @@ extension CompositeChildrenProperty: CustomStringConvertible { public var description: String { self.name } } -extension CompositeChildrenProperty: AnyProperty { } +extension CompositeChildrenProperty: AnyProperty {} extension CompositeChildrenProperty: Property { public typealias Model = From @@ -139,7 +138,7 @@ extension CompositeChildrenProperty: AnyDatabaseProperty { public var keys: [FieldKey] { [] } public func input(to input: any DatabaseInput) {} public func output(from output: any DatabaseOutput) throws { - if From.IDValue.keys.reduce(true, { $0 && output.contains($1) }) { // don't output unless all keys are present + if From.IDValue.keys.reduce(true, { $0 && output.contains($1) }) { // don't output unless all keys are present self.idValue = From.IDValue() try self.idValue!.output(from: output) } @@ -164,30 +163,25 @@ extension CompositeChildrenProperty: Relation { extension CompositeChildrenProperty: EagerLoadable { public static func eagerLoad(_ relationKey: KeyPath>, to builder: Builder) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad(_ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = CompositeChildrenEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } - public static func eagerLoad(_ loader: Loader, through: KeyPath>, to builder: Builder) - where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From - { + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From { let loader = ThroughCompositeChildrenEagerLoader(relationKey: through, loader: loader) builder.add(loader: loader) } } private struct CompositeChildrenEagerLoader: EagerLoader - where From: Model, To: Model, From.IDValue: Fields -{ +where From: Model, To: Model, From.IDValue: Fields { let relationKey: KeyPath> let withDeleted: Bool @@ -195,17 +189,17 @@ private struct CompositeChildrenEagerLoader: EagerLoader let ids = Set(models.map(\.id!)) let parentKey = From()[keyPath: self.relationKey].parentKey let builder = To.query(on: database) - + builder.group(.or) { query in _ = parentKey.queryFilterIds(ids, in: query) } if self.withDeleted { builder.withDeleted() } - + return builder.all().map { let indexedResults = Dictionary(grouping: $0, by: { parentKey.referencedId(in: $0)! }) - + for model in models { model[keyPath: self.relationKey].value = indexedResults[model[keyPath: self.relationKey].idValue!] ?? [] } @@ -214,8 +208,7 @@ private struct CompositeChildrenEagerLoader: EagerLoader } private struct ThroughCompositeChildrenEagerLoader: EagerLoader - where From: Model, From.IDValue: Fields, Loader: EagerLoader, Loader.Model == Through -{ +where From: Model, From.IDValue: Fields, Loader: EagerLoader, Loader.Model == Through { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/CompositeID.swift b/Sources/FluentKit/Properties/CompositeID.swift index bcee307d..8bdf3bd1 100644 --- a/Sources/FluentKit/Properties/CompositeID.swift +++ b/Sources/FluentKit/Properties/CompositeID.swift @@ -2,21 +2,20 @@ import NIOConcurrencyHelpers extension Model { public typealias CompositeID = CompositeIDProperty - where Value: Fields + where Value: Fields } // MARK: Type @propertyWrapper @dynamicMemberLookup public final class CompositeIDProperty: @unchecked Sendable - where Model: FluentKit.Model, Value: FluentKit.Fields -{ +where Model: FluentKit.Model, Value: FluentKit.Fields { public var value: Value? = .init(.init()) public var exists: Bool = false var cachedOutput: (any DatabaseOutput)? public var projectedValue: CompositeIDProperty { self } - + public var wrappedValue: Value? { get { self.value } set { self.value = newValue } @@ -25,10 +24,9 @@ public final class CompositeIDProperty: @unchecked Sendable public init() {} public subscript( - dynamicMember keyPath: KeyPath + dynamicMember keyPath: KeyPath ) -> Nested - where Nested: Property - { + where Nested: Property { self.value![keyPath: keyPath] } } diff --git a/Sources/FluentKit/Properties/CompositeOptionalChild.swift b/Sources/FluentKit/Properties/CompositeOptionalChild.swift index 7ff6a62f..f2bb6b28 100644 --- a/Sources/FluentKit/Properties/CompositeOptionalChild.swift +++ b/Sources/FluentKit/Properties/CompositeOptionalChild.swift @@ -4,7 +4,7 @@ extension Model { /// A convenience alias for ``CompositeOptionalChildProperty``. It is strongly recommended that callers use this /// alias rather than referencing ``CompositeOptionalChildProperty`` directly whenever possible. public typealias CompositeOptionalChild = CompositeOptionalChildProperty - where To: FluentKit.Model, Self.IDValue: Fields + where To: FluentKit.Model, Self.IDValue: Fields } /// Declares an optional one-to-one relation between the referenced ("child") model and the referencing @@ -59,10 +59,9 @@ extension Model { /// ``` @propertyWrapper public final class CompositeOptionalChildProperty: @unchecked Sendable - where From: Model, To: Model, From.IDValue: Fields -{ +where From: Model, To: Model, From.IDValue: Fields { public typealias Key = CompositeRelationParentKey - + public let parentKey: Key var idValue: From.IDValue? @@ -71,7 +70,7 @@ public final class CompositeOptionalChildProperty: @unchecked Sendable public init(for parentKey: KeyPath>) { self.parentKey = .required(parentKey) } - + public init(for parentKey: KeyPath>) { self.parentKey = .optional(parentKey) } @@ -89,13 +88,13 @@ public final class CompositeOptionalChildProperty: @unchecked Sendable } public var projectedValue: CompositeOptionalChildProperty { self } - + public var fromId: From.IDValue? { get { self.idValue } set { self.idValue = newValue } } - public func query(on database:any Database) -> QueryBuilder { + public func query(on database: any Database) -> QueryBuilder { guard let id = self.idValue else { fatalError("Cannot query child relation \(self.name) from unsaved model.") } @@ -113,7 +112,7 @@ extension CompositeOptionalChildProperty: CustomStringConvertible { public var description: String { self.name } } -extension CompositeOptionalChildProperty: AnyProperty { } +extension CompositeOptionalChildProperty: AnyProperty {} extension CompositeOptionalChildProperty: Property { public typealias Model = From @@ -124,7 +123,7 @@ extension CompositeOptionalChildProperty: AnyDatabaseProperty { public var keys: [FieldKey] { [] } public func input(to input: any DatabaseInput) {} public func output(from output: any DatabaseOutput) throws { - if From.IDValue.keys.reduce(true, { $0 && output.contains($1) }) { // don't output unless all keys are present + if From.IDValue.keys.reduce(true, { $0 && output.contains($1) }) { // don't output unless all keys are present self.idValue = From.IDValue() try self.idValue!.output(from: output) } @@ -149,30 +148,29 @@ extension CompositeOptionalChildProperty: Relation { extension CompositeOptionalChildProperty: EagerLoadable { public static func eagerLoad(_ relationKey: KeyPath>, to builder: Builder) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - - public static func eagerLoad(_ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder) - where Builder: EagerLoadBuilder, Builder.Model == From - { + + public static func eagerLoad( + _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder + ) + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = CompositeOptionalChildEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } - - public static func eagerLoad(_ loader: Loader, through: KeyPath>, to builder: Builder) - where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From - { + public static func eagerLoad( + _ loader: Loader, through: KeyPath>, to builder: Builder + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From { let loader = ThroughCompositeOptionalChildEagerLoader(relationKey: through, loader: loader) builder.add(loader: loader) } } private struct CompositeOptionalChildEagerLoader: EagerLoader - where From: Model, To: Model, From.IDValue: Fields -{ +where From: Model, To: Model, From.IDValue: Fields { let relationKey: KeyPath> let withDeleted: Bool @@ -180,7 +178,7 @@ private struct CompositeOptionalChildEagerLoader: EagerLoader let ids = Set(models.map(\.id!)) let parentKey = From()[keyPath: self.relationKey].parentKey let builder = To.query(on: database) - + builder.group(.or) { query in _ = parentKey.queryFilterIds(ids, in: query) } @@ -189,7 +187,7 @@ private struct CompositeOptionalChildEagerLoader: EagerLoader } return builder.all().map { let indexedResults = Dictionary(grouping: $0, by: { parentKey.referencedId(in: $0)! }) - + for model in models { model[keyPath: self.relationKey].value = indexedResults[model[keyPath: self.relationKey].idValue!]?.first } @@ -198,8 +196,7 @@ private struct CompositeOptionalChildEagerLoader: EagerLoader } private struct ThroughCompositeOptionalChildEagerLoader: EagerLoader - where From: Model, From.IDValue: Fields, Loader: EagerLoader, Loader.Model == Through -{ +where From: Model, From.IDValue: Fields, Loader: EagerLoader, Loader.Model == Through { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/CompositeOptionalParent.swift b/Sources/FluentKit/Properties/CompositeOptionalParent.swift index 5623371c..ce072416 100644 --- a/Sources/FluentKit/Properties/CompositeOptionalParent.swift +++ b/Sources/FluentKit/Properties/CompositeOptionalParent.swift @@ -1,11 +1,12 @@ import NIOCore + import struct SQLKit.SomeCodingKey extension Model { /// A convenience alias for ``CompositeOptionalParentProperty``. It is strongly recommended that callers /// use this alias rather than referencing ``CompositeOptionalParentProperty`` directly whenever possible. public typealias CompositeOptionalParent = CompositeOptionalParentProperty - where To: Model, To.IDValue: Fields + where To: Model, To.IDValue: Fields } /// Declares an _optional_ one-to-many relation between the referenced ("parent") model and the referencing @@ -71,8 +72,7 @@ extension Model { /// ``` @propertyWrapper @dynamicMemberLookup public final class CompositeOptionalParentProperty: @unchecked Sendable - where From: Model, To: Model, To.IDValue: Fields -{ +where From: Model, To: Model, To.IDValue: Fields { public let prefix: FieldKey public let prefixingStrategy: KeyPrefixingStrategy public var value: To?? @@ -90,7 +90,7 @@ public final class CompositeOptionalParentProperty: @unchecked Sendabl } public var projectedValue: CompositeOptionalParentProperty { self } - + /// Configure a ``CompositeOptionalParentProperty`` with a key prefix and prefix strategy. /// /// - Parameters: @@ -109,8 +109,7 @@ public final class CompositeOptionalParentProperty: @unchecked Sendabl } public subscript(dynamicMember keyPath: KeyPath) -> Nested? - where Nested: Property - { + where Nested: Property { (self.id ?? To.IDValue())[keyPath: keyPath] } } @@ -125,7 +124,7 @@ extension CompositeOptionalParentProperty: Relation { public var name: String { "CompositeOptionalParent<\(From.self), \(To.self)>(prefix: \(self.prefix), strategy: \(self.prefixingStrategy))" } - + public func load(on database: any Database) -> EventLoopFuture { self.query(on: database) .first() @@ -148,18 +147,16 @@ extension CompositeOptionalParentProperty: AnyDatabaseProperty { self.prefixingStrategy.apply(prefix: self.prefix, to: $0) } } - + public func input(to input: any DatabaseInput) { let prefixedInput = input.prefixed(by: self.prefix, using: self.prefixingStrategy) let id: To.IDValue? - - if input.wantsUnmodifiedKeys { id = self.id } - else if let inId = self.inputId { id = inId } - else { return } - + + if input.wantsUnmodifiedKeys { id = self.id } else if let inId = self.inputId { id = inId } else { return } + id?.input(to: prefixedInput) ?? To.IDValue().input(to: prefixedInput.nullValueOveridden()) } - + public func output(from output: any DatabaseOutput) throws { if self.keys.reduce(true, { $0 && output.contains($1) }) { self.inputId = nil @@ -177,7 +174,7 @@ extension CompositeOptionalParentProperty: AnyDatabaseProperty { extension CompositeOptionalParentProperty: AnyCodableProperty { public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() - + if case .some(.some(let value)) = self.value { try container.encode(value) } else { @@ -193,30 +190,30 @@ extension CompositeOptionalParentProperty: AnyCodableProperty { extension CompositeOptionalParentProperty: EagerLoadable { public static func eagerLoad(_ relationKey: KeyPath>, to builder: Builder) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - - public static func eagerLoad(_ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder) - where Builder: EagerLoadBuilder, Builder.Model == From - { + + public static func eagerLoad( + _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder + ) + where Builder: EagerLoadBuilder, Builder.Model == From { builder.add(loader: CompositeOptionalParentEagerLoader(relationKey: relationKey, withDeleted: withDeleted)) } - public static func eagerLoad(_ loader: Loader, through: KeyPath>, to builder: Builder) - where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From - { + public static func eagerLoad( + _ loader: Loader, through: KeyPath>, to builder: Builder + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From { builder.add(loader: ThroughCompositeOptionalParentEagerLoader(relationKey: through, loader: loader)) } } private struct CompositeOptionalParentEagerLoader: EagerLoader - where From: Model, To: Model, To.IDValue: Fields -{ +where From: Model, To: Model, To.IDValue: Fields { let relationKey: KeyPath> let withDeleted: Bool - + func run(models: [From], on database: any Database) -> EventLoopFuture { var _sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id }) let nilParentModels = _sets.removeValue(forKey: nil) ?? [] @@ -227,31 +224,30 @@ private struct CompositeOptionalParentEagerLoader: EagerLoader if self.withDeleted { builder.withDeleted() } - + return builder.all().flatMapThrowing { - let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) }) - - for (parentId, models) in sets { - guard let parent = parents[parentId!] else { - database.logger.debug( - "Missing parent model in eager-load lookup results.", - metadata: ["parent": "\(To.self)", "id": "\(parentId!)"] - ) - throw FluentError.missingParentError(keyPath: self.relationKey, id: parentId!) - } - models.forEach { $0[keyPath: self.relationKey].value = .some(.some(parent)) } + let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) }) + + for (parentId, models) in sets { + guard let parent = parents[parentId!] else { + database.logger.debug( + "Missing parent model in eager-load lookup results.", + metadata: ["parent": "\(To.self)", "id": "\(parentId!)"] + ) + throw FluentError.missingParentError(keyPath: self.relationKey, id: parentId!) } - nilParentModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) } + models.forEach { $0[keyPath: self.relationKey].value = .some(.some(parent)) } } + nilParentModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) } + } } } private struct ThroughCompositeOptionalParentEagerLoader: EagerLoader - where From: Model, Loader: EagerLoader, Loader.Model == Through, Through.IDValue: Fields -{ +where From: Model, Loader: EagerLoader, Loader.Model == Through, Through.IDValue: Fields { let relationKey: KeyPath> let loader: Loader - + func run(models: [From], on database: any Database) -> EventLoopFuture { self.loader.run(models: models.compactMap { $0[keyPath: self.relationKey].value! }, on: database) } diff --git a/Sources/FluentKit/Properties/CompositeParent.swift b/Sources/FluentKit/Properties/CompositeParent.swift index 45c8bda9..51b7744d 100644 --- a/Sources/FluentKit/Properties/CompositeParent.swift +++ b/Sources/FluentKit/Properties/CompositeParent.swift @@ -1,11 +1,12 @@ import NIOCore + import struct SQLKit.SomeCodingKey extension Model { /// A convenience alias for ``CompositeParentProperty``. It is strongly recommended that callers use this /// alias rather than referencing ``CompositeParentProperty`` directly whenever possible. public typealias CompositeParent = CompositeParentProperty - where To: Model, To.IDValue: Fields + where To: Model, To.IDValue: Fields } /// Declares a one-to-many relation between the referenced ("parent") model and the referencing ("child") model, @@ -66,8 +67,7 @@ extension Model { /// ``` @propertyWrapper @dynamicMemberLookup public final class CompositeParentProperty: @unchecked Sendable - where From: Model, To: Model, To.IDValue: Fields -{ +where From: Model, To: Model, To.IDValue: Fields { public let prefix: FieldKey public let prefixingStrategy: KeyPrefixingStrategy public var id: To.IDValue = .init() @@ -84,7 +84,7 @@ public final class CompositeParentProperty: @unchecked Sendable } public var projectedValue: CompositeParentProperty { self } - + /// Configure a ``CompositeParentProperty`` with a key prefix and prefix strategy. /// /// - Parameters: @@ -101,8 +101,7 @@ public final class CompositeParentProperty: @unchecked Sendable } public subscript(dynamicMember keyPath: KeyPath) -> Nested - where Nested: Property - { + where Nested: Property { self.id[keyPath: keyPath] } } @@ -117,7 +116,7 @@ extension CompositeParentProperty: Relation { public var name: String { "CompositeParent<\(From.self), \(To.self)>(prefix: \(self.prefix), strategy: \(self.prefixingStrategy))" } - + public func load(on database: any Database) -> EventLoopFuture { self.query(on: database) .first() @@ -140,11 +139,11 @@ extension CompositeParentProperty: AnyDatabaseProperty { self.prefixingStrategy.apply(prefix: self.prefix, to: $0) } } - + public func input(to input: any DatabaseInput) { self.id.input(to: input.prefixed(by: self.prefix, using: self.prefixingStrategy)) } - + public func output(from output: any DatabaseOutput) throws { try self.id.output(from: output.prefixed(by: self.prefix, using: self.prefixingStrategy)) } @@ -153,7 +152,7 @@ extension CompositeParentProperty: AnyDatabaseProperty { extension CompositeParentProperty: AnyCodableProperty { public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() - + if let value = self.value { try container.encode(value) } else { @@ -169,30 +168,26 @@ extension CompositeParentProperty: AnyCodableProperty { extension CompositeParentProperty: EagerLoadable { public static func eagerLoad(_ relationKey: KeyPath>, to builder: Builder) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad(_ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { builder.add(loader: CompositeParentEagerLoader(relationKey: relationKey, withDeleted: withDeleted)) } public static func eagerLoad(_ loader: Loader, through: KeyPath>, to builder: Builder) - where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From - { + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, Builder.Model == From { builder.add(loader: ThroughCompositeParentEagerLoader(relationKey: through, loader: loader)) } } private struct CompositeParentEagerLoader: EagerLoader - where From: Model, To: Model, To.IDValue: Fields -{ +where From: Model, To: Model, To.IDValue: Fields { let relationKey: KeyPath> let withDeleted: Bool - + func run(models: [From], on database: any Database) -> EventLoopFuture { let sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id }) @@ -224,14 +219,14 @@ private struct CompositeParentEagerLoader: EagerLoader } private struct ThroughCompositeParentEagerLoader: EagerLoader - where From: Model, Loader: EagerLoader, Loader.Model == Through, Through.IDValue: Fields -{ +where From: Model, Loader: EagerLoader, Loader.Model == Through, Through.IDValue: Fields { let relationKey: KeyPath> let loader: Loader - + func run(models: [From], on database: any Database) -> EventLoopFuture { - self.loader.run(models: models.map { - $0[keyPath: self.relationKey].value! - }, on: database) + self.loader.run( + models: models.map { + $0[keyPath: self.relationKey].value! + }, on: database) } } diff --git a/Sources/FluentKit/Properties/Field.swift b/Sources/FluentKit/Properties/Field.swift index 96d45627..f25264c5 100644 --- a/Sources/FluentKit/Properties/Field.swift +++ b/Sources/FluentKit/Properties/Field.swift @@ -2,19 +2,18 @@ import NIOConcurrencyHelpers extension Fields { public typealias Field = FieldProperty - where Value: Codable & Sendable + where Value: Codable & Sendable } // MARK: Type @propertyWrapper public final class FieldProperty: @unchecked Sendable - where Model: FluentKit.Fields, Value: Codable & Sendable -{ +where Model: FluentKit.Fields, Value: Codable & Sendable { public let key: FieldKey var outputValue: Value? var inputValue: DatabaseQuery.Value? - + public var projectedValue: FieldProperty { self } @@ -44,7 +43,7 @@ extension FieldProperty: CustomStringConvertible { // MARK: Property -extension FieldProperty: AnyProperty { } +extension FieldProperty: AnyProperty {} extension FieldProperty: Property { public var value: Value? { @@ -80,7 +79,7 @@ extension FieldProperty: AnyQueryableProperty { } } -extension FieldProperty: QueryableProperty { } +extension FieldProperty: QueryableProperty {} // MARK: Query-addressable diff --git a/Sources/FluentKit/Properties/FieldKey.swift b/Sources/FluentKit/Properties/FieldKey.swift index a70c97e6..c11773a9 100644 --- a/Sources/FluentKit/Properties/FieldKey.swift +++ b/Sources/FluentKit/Properties/FieldKey.swift @@ -31,6 +31,6 @@ extension FieldKey: CustomStringConvertible { } } -extension FieldKey: Equatable { } +extension FieldKey: Equatable {} -extension FieldKey: Hashable { } +extension FieldKey: Hashable {} diff --git a/Sources/FluentKit/Properties/Group.swift b/Sources/FluentKit/Properties/Group.swift index 1b326880..d8205f5c 100644 --- a/Sources/FluentKit/Properties/Group.swift +++ b/Sources/FluentKit/Properties/Group.swift @@ -2,15 +2,14 @@ import NIOConcurrencyHelpers extension Fields { public typealias Group = GroupProperty - where Value: Fields + where Value: Fields } // MARK: Type @propertyWrapper @dynamicMemberLookup public final class GroupProperty: @unchecked Sendable - where Model: FluentKit.Fields, Value: FluentKit.Fields -{ +where Model: FluentKit.Fields, Value: FluentKit.Fields { public let key: FieldKey public var value: Value? @@ -36,10 +35,9 @@ public final class GroupProperty: @unchecked Sendable } public subscript( - dynamicMember keyPath: KeyPath + dynamicMember keyPath: KeyPath ) -> GroupPropertyPath - where Nested: Property - { + where Nested: Property { return .init(key: self.key, property: self.value![keyPath: keyPath]) } } @@ -52,9 +50,9 @@ extension GroupProperty: CustomStringConvertible { // MARK: + Property -extension GroupProperty: AnyProperty { } +extension GroupProperty: AnyProperty {} -extension GroupProperty: Property { } +extension GroupProperty: Property {} // MARK: + Database @@ -88,21 +86,19 @@ extension GroupProperty: AnyCodableProperty { public func decode(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() - + guard !container.decodeNil() else { return } self.value = .some(try container.decode(Value.self)) } - + public var skipPropertyEncoding: Bool { self.value == nil } } - // MARK: Path @dynamicMemberLookup public final class GroupPropertyPath - where Model: Fields -{ +where Model: Fields { let key: FieldKey let property: Property @@ -112,7 +108,7 @@ public final class GroupPropertyPath } public subscript( - dynamicMember keyPath: KeyPath + dynamicMember keyPath: KeyPath ) -> GroupPropertyPath { .init( key: self.key, @@ -124,8 +120,7 @@ public final class GroupPropertyPath // MARK: + Property extension GroupPropertyPath: AnyProperty - where Property: AnyProperty -{ +where Property: AnyProperty { public static var anyValueType: Any.Type { Property.anyValueType } @@ -136,8 +131,7 @@ extension GroupPropertyPath: AnyProperty } extension GroupPropertyPath: FluentKit.Property - where Property: FluentKit.Property -{ +where Property: FluentKit.Property { public typealias Model = Property.Model public typealias Value = Property.Value @@ -154,8 +148,7 @@ extension GroupPropertyPath: FluentKit.Property // MARK: + Queryable extension GroupPropertyPath: AnyQueryableProperty - where Property: QueryableProperty -{ +where Property: QueryableProperty { public var path: [FieldKey] { let subPath = self.property.path return [ @@ -165,8 +158,7 @@ extension GroupPropertyPath: AnyQueryableProperty } extension GroupPropertyPath: QueryableProperty - where Property: QueryableProperty -{ +where Property: QueryableProperty { public static func queryValue(_ value: Value) -> DatabaseQuery.Value { Property.queryValue(value) } @@ -175,12 +167,11 @@ extension GroupPropertyPath: QueryableProperty // MARK: + QueryAddressable extension GroupPropertyPath: AnyQueryAddressableProperty - where Property: AnyQueryAddressableProperty -{ +where Property: AnyQueryAddressableProperty { public var anyQueryableProperty: any AnyQueryableProperty { self.property.anyQueryableProperty } - + public var queryablePath: [FieldKey] { let subPath = self.property.queryablePath return [ @@ -190,10 +181,9 @@ extension GroupPropertyPath: AnyQueryAddressableProperty } extension GroupPropertyPath: QueryAddressableProperty - where Property: QueryAddressableProperty -{ +where Property: QueryAddressableProperty { public typealias QueryablePropertyType = Property.QueryablePropertyType - + public var queryableProperty: QueryablePropertyType { self.property.queryableProperty } diff --git a/Sources/FluentKit/Properties/ID.swift b/Sources/FluentKit/Properties/ID.swift index 97eb96dc..0f414bde 100644 --- a/Sources/FluentKit/Properties/ID.swift +++ b/Sources/FluentKit/Properties/ID.swift @@ -2,15 +2,14 @@ import Foundation extension Model { public typealias ID = IDProperty - where Value: Codable & Sendable + where Value: Codable & Sendable } // MARK: Type @propertyWrapper public final class IDProperty: @unchecked Sendable - where Model: FluentKit.Model, Value: Codable & Sendable -{ +where Model: FluentKit.Model, Value: Codable & Sendable { public enum Generator { case user case random @@ -44,7 +43,7 @@ public final class IDProperty: @unchecked Sendable public var projectedValue: IDProperty { self } - + public var wrappedValue: Value? { get { self.value } set { self.value = newValue } @@ -56,11 +55,11 @@ public final class IDProperty: @unchecked Sendable public convenience init() where Value == UUID { self.init(custom: .id, generatedBy: .random) } - + /// Helper type for compatibility initializer syntax. Do not use this type directly. public enum _DefaultIDFieldKey: ExpressibleByStringLiteral { case id - + @available(*, deprecated, message: "The `@ID(key: \"id\")` syntax is deprecated. Use `@ID` or `@ID()` instead.") public init(stringLiteral value: String) { guard value == "id" else { @@ -69,7 +68,7 @@ public final class IDProperty: @unchecked Sendable self = .id } } - + /// Compatibility syntax for initializing an `ID` property. /// /// This syntax is no longer recommended; use `@ID` instead. @@ -118,7 +117,7 @@ extension IDProperty: CustomStringConvertible { // MARK: Property -extension IDProperty: AnyProperty { } +extension IDProperty: AnyProperty {} extension IDProperty: Property { public var value: Value? { @@ -139,7 +138,7 @@ extension IDProperty: AnyQueryableProperty { } } -extension IDProperty: QueryableProperty { } +extension IDProperty: QueryableProperty {} // MARK: Query-addressable @@ -184,7 +183,7 @@ extension IDProperty: AnyCodableProperty { // MARK: ID -extension IDProperty: AnyID { } +extension IDProperty: AnyID {} protocol AnyID: AnyObject { func generate() diff --git a/Sources/FluentKit/Properties/OptionalBoolean.swift b/Sources/FluentKit/Properties/OptionalBoolean.swift index 7c77be61..78c3e206 100644 --- a/Sources/FluentKit/Properties/OptionalBoolean.swift +++ b/Sources/FluentKit/Properties/OptionalBoolean.swift @@ -1,6 +1,6 @@ extension Fields { public typealias OptionalBoolean = OptionalBooleanProperty - where Format: BooleanPropertyFormat + where Format: BooleanPropertyFormat } /// A Fluent model property which represents an optional boolean (true/false) value. @@ -44,8 +44,7 @@ extension Fields { /// > Note: See also ``BooleanProperty`` and ``BooleanPropertyFormat``. @propertyWrapper public final class OptionalBooleanProperty - where Model: FluentKit.Fields, Format: BooleanPropertyFormat -{ +where Model: FluentKit.Fields, Format: BooleanPropertyFormat { @OptionalFieldProperty public var field: Format.Value? public let format: Format diff --git a/Sources/FluentKit/Properties/OptionalChild.swift b/Sources/FluentKit/Properties/OptionalChild.swift index ea19915a..17746521 100644 --- a/Sources/FluentKit/Properties/OptionalChild.swift +++ b/Sources/FluentKit/Properties/OptionalChild.swift @@ -2,15 +2,14 @@ import NIOCore extension Model { public typealias OptionalChild = OptionalChildProperty - where To: FluentKit.Model + where To: FluentKit.Model } // MARK: Type @propertyWrapper public final class OptionalChildProperty: @unchecked Sendable - where From: Model, To: Model -{ +where From: Model, To: Model { public typealias Key = RelationParentKey public let parentKey: Key @@ -25,7 +24,7 @@ public final class OptionalChildProperty: @unchecked Sendable public convenience init(for optionalParent: KeyPath>) { self.init(for: .optional(optionalParent)) } - + private init(for parentKey: Key) { self.parentKey = parentKey } @@ -45,7 +44,7 @@ public final class OptionalChildProperty: @unchecked Sendable public var projectedValue: OptionalChildProperty { self } - + public var fromId: From.IDValue? { get { self.idValue } set { self.idValue = newValue } @@ -87,7 +86,7 @@ extension OptionalChildProperty: CustomStringConvertible { // MARK: Property -extension OptionalChildProperty: AnyProperty { } +extension OptionalChildProperty: AnyProperty {} extension OptionalChildProperty: Property { public typealias Model = From @@ -128,7 +127,7 @@ extension OptionalChildProperty: AnyCodableProperty { } public var skipPropertyEncoding: Bool { - self.value == nil // Avoids leaving an empty JSON object lying around in some cases. + self.value == nil // Avoids leaving an empty JSON object lying around in some cases. } } @@ -153,28 +152,26 @@ extension OptionalChildProperty: EagerLoadable { _ relationKey: KeyPath>, to builder: Builder ) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad( _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder ) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = OptionalChildEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } - public static func eagerLoad( _ loader: Loader, through: KeyPath>, to builder: Builder - ) where + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, @@ -186,8 +183,7 @@ extension OptionalChildProperty: EagerLoadable { } private struct OptionalChildEagerLoader: EagerLoader - where From: Model, To: Model -{ +where From: Model, To: Model { let relationKey: KeyPath> let withDeleted: Bool @@ -224,8 +220,7 @@ private struct OptionalChildEagerLoader: EagerLoader } private struct ThroughChildEagerLoader: EagerLoader - where From: Model, Loader: EagerLoader, Loader.Model == Through -{ +where From: Model, Loader: EagerLoader, Loader.Model == Through { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/OptionalField.swift b/Sources/FluentKit/Properties/OptionalField.swift index 926ac6fd..36cc27ec 100644 --- a/Sources/FluentKit/Properties/OptionalField.swift +++ b/Sources/FluentKit/Properties/OptionalField.swift @@ -2,15 +2,14 @@ import NIOConcurrencyHelpers extension Fields { public typealias OptionalField = OptionalFieldProperty - where Value: Codable & Sendable + where Value: Codable & Sendable } // MARK: Type @propertyWrapper public final class OptionalFieldProperty: @unchecked Sendable - where Model: FluentKit.Fields, WrappedValue: Codable & Sendable -{ +where Model: FluentKit.Fields, WrappedValue: Codable & Sendable { public let key: FieldKey var outputValue: WrappedValue?? var inputValue: DatabaseQuery.Value? @@ -31,7 +30,7 @@ public final class OptionalFieldProperty: @unchecked Sendab // MARK: Property -extension OptionalFieldProperty: AnyProperty { } +extension OptionalFieldProperty: AnyProperty {} extension OptionalFieldProperty: Property { public var value: WrappedValue?? { @@ -57,7 +56,8 @@ extension OptionalFieldProperty: Property { } set { if let value = newValue { - self.inputValue = value + self.inputValue = + value .flatMap { .bind($0) } ?? .null } else { @@ -75,7 +75,7 @@ extension OptionalFieldProperty: AnyQueryableProperty { } } -extension OptionalFieldProperty: QueryableProperty { } +extension OptionalFieldProperty: QueryableProperty {} // MARK: Query-addressable diff --git a/Sources/FluentKit/Properties/OptionalParent.swift b/Sources/FluentKit/Properties/OptionalParent.swift index df2232b2..535667a3 100644 --- a/Sources/FluentKit/Properties/OptionalParent.swift +++ b/Sources/FluentKit/Properties/OptionalParent.swift @@ -1,17 +1,17 @@ import NIOCore + import struct SQLKit.SomeCodingKey extension Model { public typealias OptionalParent = OptionalParentProperty - where To: Model + where To: Model } // MARK: Type @propertyWrapper public final class OptionalParentProperty: @unchecked Sendable - where From: Model, To: Model -{ +where From: Model, To: Model { @OptionalFieldProperty public var id: To.IDValue? @@ -71,7 +71,7 @@ extension OptionalParentProperty: Relation { // MARK: Property -extension OptionalParentProperty: AnyProperty { } +extension OptionalParentProperty: AnyProperty {} extension OptionalParentProperty: Property { public typealias Model = From @@ -95,7 +95,7 @@ extension OptionalParentProperty: AnyDatabaseProperty { public var keys: [FieldKey] { self.$id.keys } - + public func input(to input: any DatabaseInput) { self.$id.input(to: input) } @@ -110,7 +110,7 @@ extension OptionalParentProperty: AnyDatabaseProperty { extension OptionalParentProperty: AnyCodableProperty { public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() - if case .some(.some(let parent)) = self.value { // require truly non-nil so we don't mis-encode when value has been manually cleared + if case .some(.some(let parent)) = self.value { // require truly non-nil so we don't mis-encode when value has been manually cleared try container.encode(parent) } else { try container.encode([ @@ -132,18 +132,16 @@ extension OptionalParentProperty: EagerLoadable { _ relationKey: KeyPath>, to builder: Builder ) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad( _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder ) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = OptionalParentEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } @@ -152,7 +150,8 @@ extension OptionalParentProperty: EagerLoadable { _ loader: Loader, through: KeyPath>, to builder: Builder - ) where + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, @@ -164,8 +163,7 @@ extension OptionalParentProperty: EagerLoadable { } private struct OptionalParentEagerLoader: EagerLoader - where From: FluentKit.Model, To: FluentKit.Model -{ +where From: FluentKit.Model, To: FluentKit.Model { let relationKey: KeyPath> let withDeleted: Bool @@ -203,8 +201,7 @@ private struct OptionalParentEagerLoader: EagerLoader } private struct ThroughOptionalParentEagerLoader: EagerLoader - where From: Model, Loader: EagerLoader, Loader.Model == Through -{ +where From: Model, Loader: EagerLoader, Loader.Model == Through { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/Parent.swift b/Sources/FluentKit/Properties/Parent.swift index 310fe2eb..f0ed2b06 100644 --- a/Sources/FluentKit/Properties/Parent.swift +++ b/Sources/FluentKit/Properties/Parent.swift @@ -1,17 +1,17 @@ import NIOCore + import struct SQLKit.SomeCodingKey extension Model { public typealias Parent = ParentProperty - where To: FluentKit.Model + where To: FluentKit.Model } // MARK: Type @propertyWrapper public final class ParentProperty: @unchecked Sendable - where From: Model, To: Model -{ +where From: Model, To: Model { @FieldProperty public var id: To.IDValue @@ -35,7 +35,7 @@ public final class ParentProperty: @unchecked Sendable guard !(To.IDValue.self is any Fields.Type) else { fatalError("Can not use @Parent to target a model with composite ID; use @CompositeParent instead.") } - + self._id = .init(key: key) } @@ -67,7 +67,7 @@ extension ParentProperty: Relation { // MARK: Property -extension ParentProperty: AnyProperty { } +extension ParentProperty: AnyProperty {} extension ParentProperty: Property { public typealias Model = From @@ -91,7 +91,7 @@ extension ParentProperty: AnyDatabaseProperty { public var keys: [FieldKey] { self.$id.keys } - + public func input(to input: any DatabaseInput) { self.$id.input(to: input) } @@ -126,28 +126,26 @@ extension ParentProperty: EagerLoadable { _ relationKey: KeyPath>, to builder: Builder ) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad( _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder ) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = ParentEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } - public static func eagerLoad( _ loader: Loader, through: KeyPath>, to builder: Builder - ) where + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, @@ -159,8 +157,7 @@ extension ParentProperty: EagerLoadable { } private struct ParentEagerLoader: EagerLoader - where From: FluentKit.Model, To: FluentKit.Model -{ +where From: FluentKit.Model, To: FluentKit.Model { let relationKey: KeyPath> let withDeleted: Bool @@ -188,8 +185,7 @@ private struct ParentEagerLoader: EagerLoader } private struct ThroughParentEagerLoader: EagerLoader - where From: Model, Loader: EagerLoader, Loader.Model == Through -{ +where From: Model, Loader: EagerLoader, Loader.Model == Through { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/Property.swift b/Sources/FluentKit/Properties/Property.swift index 7e88790a..c1eb85ff 100644 --- a/Sources/FluentKit/Properties/Property.swift +++ b/Sources/FluentKit/Properties/Property.swift @@ -29,7 +29,7 @@ extension AnyProperty where Self: Property { public var anyValue: Any? { self.value } - + /// The type-erased type of a property's value is the type of the property's value. public static var anyValueType: Any.Type { Value.self @@ -68,10 +68,10 @@ public protocol AnyDatabaseProperty: AnyProperty { public protocol AnyCodableProperty: AnyProperty { /// Encode the property's data to an external representation. func encode(to encoder: any Encoder) throws - + /// Decode an external representation and replace the property's current data with the result. func decode(from decoder: any Decoder) throws - + /// Return `true` to skip encoding of this property. Defaults to `false` unless explicitly /// implemented. /// @@ -96,7 +96,7 @@ public protocol AnyQueryableProperty: AnyProperty { /// such as a field containing a complex JSON document, but at present this is not /// fully implemented by Fluent, making a multi-key path as invalid as an empty one. var path: [FieldKey] { get } - + /// If the property's current value has been set, return a description of the /// appropriate method for encoding that value into a database query. See /// ``DatabaseQuery/Value`` for more details. If the value is not set, the diff --git a/Sources/FluentKit/Properties/Relation.swift b/Sources/FluentKit/Properties/Relation.swift index 1c0e8759..7d8903c5 100644 --- a/Sources/FluentKit/Properties/Relation.swift +++ b/Sources/FluentKit/Properties/Relation.swift @@ -30,7 +30,7 @@ extension Relation { database.eventLoop.makeSucceededFuture(value) } else { self.load(on: database).flatMapThrowing { - guard let value = self.value else { // This should never actually happen, but just in case... + guard let value = self.value else { // This should never actually happen, but just in case... throw FluentError.relationNotLoaded(name: self.name) } return value @@ -38,15 +38,14 @@ extension Relation { } } } - + /// A helper type used by ``ChildrenProperty`` and ``OptionalChildProperty`` to generically track the keypath /// of the property of the child model that defines the parent-child relationship. /// /// This type was extracted from its original definitions as a subtype of the property types. A typealias is /// provided on the property types to maintain public API compatibility. public enum RelationParentKey: Sendable - where From: FluentKit.Model, To: FluentKit.Model -{ +where From: FluentKit.Model, To: FluentKit.Model { case required(KeyPath>) case optional(KeyPath>) } @@ -70,11 +69,10 @@ extension RelationParentKey: CustomStringConvertible { /// > Note: This type is public partly to allow FluentKit users to introspect model metadata, but mostly it's /// > to maintain parity with ``RelationParentKey``, which was public in its original definition. public enum CompositeRelationParentKey: Sendable - where From: FluentKit.Model, To: FluentKit.Model, From.IDValue: Fields -{ +where From: FluentKit.Model, To: FluentKit.Model, From.IDValue: Fields { case required(KeyPath>) case optional(KeyPath>) - + /// Use the stored key path to retrieve the appropriate parent ID from the given child model. internal func referencedId(in model: To) -> From.IDValue? { switch self { @@ -82,7 +80,7 @@ public enum CompositeRelationParentKey: Sendable case .optional(let keypath): model[keyPath: keypath].id } } - + /// Use the parent property specified by the key path to filter the given query builder by each of the /// given parent IDs in turn. An empty ID list will apply no filters. /// @@ -92,16 +90,25 @@ public enum CompositeRelationParentKey: Sendable /// /// See ``QueryFilterInput`` for additional implementation details. internal func queryFilterIds(_ ids: C, in builder: QueryBuilder) -> QueryBuilder - where C: Collection, C.Element == From.IDValue - { + where C: Collection, C.Element == From.IDValue { guard !ids.isEmpty else { return builder } switch self { case .required(let keypath): let prop = To()[keyPath: keypath] - return ids.reduce(builder) { b, id in b.group(.and) { prop.id = id; prop.input(to: QueryFilterInput(builder: $0)) } } + return ids.reduce(builder) { b, id in + b.group(.and) { + prop.id = id + prop.input(to: QueryFilterInput(builder: $0)) + } + } case .optional(let keypath): let prop = To()[keyPath: keypath] - return ids.reduce(builder) { b, id in b.group(.and) { prop.id = id; prop.input(to: QueryFilterInput(builder: $0)) } } + return ids.reduce(builder) { b, id in + b.group(.and) { + prop.id = id + prop.input(to: QueryFilterInput(builder: $0)) + } + } } } } diff --git a/Sources/FluentKit/Properties/Siblings.swift b/Sources/FluentKit/Properties/Siblings.swift index be3c160b..904b4a9d 100644 --- a/Sources/FluentKit/Properties/Siblings.swift +++ b/Sources/FluentKit/Properties/Siblings.swift @@ -2,15 +2,14 @@ import NIOCore extension Model { public typealias Siblings = SiblingsProperty - where To: Model, Through: Model + where To: Model, Through: Model } // MARK: Type @propertyWrapper public final class SiblingsProperty: @unchecked Sendable - where From: Model, To: Model, Through: Model -{ +where From: Model, To: Model, Through: Model { public enum AttachMethod { /// Always create the pivot model case always @@ -22,9 +21,9 @@ public final class SiblingsProperty: @unchecked Sendable public let from: KeyPath> public let to: KeyPath> var idValue: From.IDValue? - + public var value: [To]? - + /// Allows eager loading of pivot objects through the sibling relation. /// Example: /// @@ -114,15 +113,15 @@ public final class SiblingsProperty: @unchecked Sendable public func attach( _ tos: [To], on database: any Database, - _ edit: (Through) -> () = { _ in } + _ edit: (Through) -> Void = { _ in } ) -> EventLoopFuture { guard let fromID = self.idValue else { return database.eventLoop.makeFailedFuture(SiblingsPropertyError.owningModelIdRequired(property: self.name)) } - + var pivots: [Through] = [] pivots.reserveCapacity(tos.count) - + for to in tos { guard let toID = to.id else { return database.eventLoop.makeFailedFuture(SiblingsPropertyError.operandModelIdRequired(property: self.name)) @@ -148,7 +147,7 @@ public final class SiblingsProperty: @unchecked Sendable _ to: To, method: AttachMethod, on database: any Database, - _ edit: @escaping @Sendable (Through) -> () = { _ in } + _ edit: @escaping @Sendable (Through) -> Void = { _ in } ) -> EventLoopFuture { switch method { case .always: @@ -173,7 +172,7 @@ public final class SiblingsProperty: @unchecked Sendable public func attach( _ to: To, on database: any Database, - _ edit: @Sendable (Through) -> () = { _ in } + _ edit: @Sendable (Through) -> Void = { _ in } ) -> EventLoopFuture { guard let fromID = self.idValue else { return database.eventLoop.makeFailedFuture(SiblingsPropertyError.owningModelIdRequired(property: self.name)) @@ -199,10 +198,10 @@ public final class SiblingsProperty: @unchecked Sendable guard let fromID = self.idValue else { return database.eventLoop.makeFailedFuture(SiblingsPropertyError.owningModelIdRequired(property: self.name)) } - + var toIDs: [To.IDValue] = [] toIDs.reserveCapacity(tos.count) - + for to in tos { guard let toID = to.id else { return database.eventLoop.makeFailedFuture(SiblingsPropertyError.operandModelIdRequired(property: self.name)) @@ -234,13 +233,13 @@ public final class SiblingsProperty: @unchecked Sendable .filter(self.to.appending(path: \.$id) == toID) .delete() } - + /// Detach all models by deleting all pivots from this model. public func detachAll(on database: any Database) -> EventLoopFuture { guard let fromID = self.idValue else { return database.eventLoop.makeFailedFuture(SiblingsPropertyError.owningModelIdRequired(property: self.name)) } - + return Through.query(on: database) .filter(self.from.appending(path: \.$id) == fromID) .delete() @@ -269,7 +268,7 @@ extension SiblingsProperty: CustomStringConvertible { // MARK: Property -extension SiblingsProperty: AnyProperty { } +extension SiblingsProperty: AnyProperty {} extension SiblingsProperty: Property { public typealias Model = From @@ -282,7 +281,7 @@ extension SiblingsProperty: AnyDatabaseProperty { public var keys: [FieldKey] { [] } - + public func input(to input: any DatabaseInput) { // siblings never has input } @@ -311,7 +310,7 @@ extension SiblingsProperty: AnyCodableProperty { } public var skipPropertyEncoding: Bool { - self.value == nil // Avoids leaving an empty JSON object lying around in some cases. + self.value == nil // Avoids leaving an empty JSON object lying around in some cases. } } @@ -338,28 +337,26 @@ extension SiblingsProperty: EagerLoadable { _ relationKey: KeyPath>, to builder: Builder ) - where Builder : EagerLoadBuilder, From == Builder.Model - { + where Builder: EagerLoadBuilder, From == Builder.Model { self.eagerLoad(relationKey, withDeleted: false, to: builder) } - + public static func eagerLoad( _ relationKey: KeyPath>, withDeleted: Bool, to builder: Builder ) - where Builder: EagerLoadBuilder, Builder.Model == From - { + where Builder: EagerLoadBuilder, Builder.Model == From { let loader = SiblingsEagerLoader(relationKey: relationKey, withDeleted: withDeleted) builder.add(loader: loader) } - public static func eagerLoad( _ loader: Loader, through: KeyPath>, to builder: Builder - ) where + ) + where Loader: EagerLoader, Loader.Model == To, Builder: EagerLoadBuilder, @@ -370,10 +367,8 @@ extension SiblingsProperty: EagerLoadable { } } - private struct SiblingsEagerLoader: EagerLoader - where From: Model, Through: Model, To: Model -{ +where From: Model, Through: Model, To: Model { let relationKey: KeyPath> let withDeleted: Bool @@ -403,8 +398,7 @@ private struct SiblingsEagerLoader: EagerLoader } private struct ThroughSiblingsEagerLoader: EagerLoader - where From: Model, Through: Model, Loader: EagerLoader, Loader.Model == To -{ +where From: Model, Through: Model, Loader: EagerLoader, Loader.Model == To { let relationKey: KeyPath> let loader: Loader diff --git a/Sources/FluentKit/Properties/Timestamp.swift b/Sources/FluentKit/Properties/Timestamp.swift index b743b311..cfbfe113 100644 --- a/Sources/FluentKit/Properties/Timestamp.swift +++ b/Sources/FluentKit/Properties/Timestamp.swift @@ -2,7 +2,7 @@ import Foundation extension Model { public typealias Timestamp = TimestampProperty - where Format: TimestampFormat + where Format: TimestampFormat } // MARK: Trigger @@ -18,8 +18,7 @@ public enum TimestampTrigger { @propertyWrapper public final class TimestampProperty - where Model: FluentKit.Model, Format: TimestampFormat -{ +where Model: FluentKit.Model, Format: TimestampFormat { @OptionalFieldProperty public var timestamp: Format.Value? @@ -33,8 +32,8 @@ public final class TimestampProperty public var wrappedValue: Date? { get { switch self.value { - case .none, .some(.none): return nil - case .some(.some(let value)): return value + case .none, .some(.none): return nil + case .some(.some(let value)): return value } } set { @@ -75,28 +74,28 @@ extension TimestampProperty: CustomStringConvertible { // MARK: Property -extension TimestampProperty: AnyProperty { } +extension TimestampProperty: AnyProperty {} extension TimestampProperty: Property { public var value: Date?? { get { switch self.$timestamp.value { - case .some(.some(let timestamp)): - .some(self.format.parse(timestamp)) - case .some(.none): - .some(.none) - case .none: - .none + case .some(.some(let timestamp)): + .some(self.format.parse(timestamp)) + case .some(.none): + .some(.none) + case .none: + .none } } set { switch newValue { - case .some(.some(let newValue)): - self.$timestamp.value = .some(self.format.serialize(newValue)) - case .some(.none): - self.$timestamp.value = .some(.none) - case .none: - self.$timestamp.value = .none + case .some(.some(let newValue)): + self.$timestamp.value = .some(self.format.serialize(newValue)) + case .some(.none): + self.$timestamp.value = .some(.none) + case .none: + self.$timestamp.value = .none } } } @@ -110,7 +109,7 @@ extension TimestampProperty: AnyQueryableProperty { } } -extension TimestampProperty: QueryableProperty { } +extension TimestampProperty: QueryableProperty {} // MARK: Query-addressable @@ -129,7 +128,7 @@ extension TimestampProperty: AnyDatabaseProperty { public var keys: [FieldKey] { self.$timestamp.keys } - + public func input(to input: any DatabaseInput) { self.$timestamp.input(to: input) } @@ -188,7 +187,7 @@ extension Fields { $0 as? any AnyTimestamp } } - + func touchTimestamps(_ triggers: TimestampTrigger...) { self.touchTimestamps(triggers) } @@ -217,9 +216,11 @@ extension Schema { schema: self.schemaOrAlias, space: self.space ) - query.filters.append(.group([ - .value(deletedAtField, .equal, .null), - .value(deletedAtField, .greaterThan, timestamp.currentTimestampInput) - ], .or)) + query.filters.append( + .group( + [ + .value(deletedAtField, .equal, .null), + .value(deletedAtField, .greaterThan, timestamp.currentTimestampInput), + ], .or)) } } diff --git a/Sources/FluentKit/Properties/TimestampFormat.swift b/Sources/FluentKit/Properties/TimestampFormat.swift index deb642ff..4ba32623 100644 --- a/Sources/FluentKit/Properties/TimestampFormat.swift +++ b/Sources/FluentKit/Properties/TimestampFormat.swift @@ -1,6 +1,7 @@ +import Foundation import NIOConcurrencyHelpers + import class NIOPosix.ThreadSpecificVariable -import Foundation // MARK: Format @@ -13,7 +14,7 @@ public protocol TimestampFormat: Sendable { public struct TimestampFormatFactory { public let makeFormat: () -> Format - + public init(_ makeFormat: @escaping () -> Format) { self.makeFormat = makeFormat } @@ -41,7 +42,6 @@ public struct DefaultTimestampFormat: TimestampFormat { } } - // MARK: ISO8601 extension TimestampFormatFactory { @@ -53,10 +53,8 @@ extension TimestampFormatFactory { withMilliseconds: Bool ) -> TimestampFormatFactory { .init { - ISO8601TimestampFormat(formatter: (withMilliseconds ? - ISO8601DateFormatter.sharedWithMs : - ISO8601DateFormatter.sharedWithoutMs - ).value) + ISO8601TimestampFormat( + formatter: (withMilliseconds ? ISO8601DateFormatter.sharedWithMs : ISO8601DateFormatter.sharedWithoutMs).value) } } } @@ -66,7 +64,7 @@ extension ISO8601DateFormatter { // know that in reality, the formatter is safe to use simultaneously from multiple threads as long as the options // are not changed, and we never change the options after the formatter is first created. fileprivate struct FakeSendable: @unchecked Sendable { let value: T } - + fileprivate static let sharedWithoutMs: FakeSendable = .init(value: .init()) fileprivate static let sharedWithMs: FakeSendable = { let formatter = ISO8601DateFormatter() @@ -91,7 +89,6 @@ public struct ISO8601TimestampFormat: TimestampFormat, @unchecked Sendable { // MARK: Unix - extension TimestampFormatFactory { public static var unix: TimestampFormatFactory { .init { @@ -111,4 +108,3 @@ public struct UnixTimestampFormat: TimestampFormat { date.timeIntervalSince1970 } } - diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift index 2e700c22..29da531b 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift @@ -14,118 +14,100 @@ extension QueryBuilder { } public func count(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { self.aggregate(.count, key, as: Int.self) } public func count(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { self.aggregate(.count, key, as: Int.self) } // TODO: `Field.Value` is not always the correct result type for `SUM()`, try `.aggregate(.sum, key, as: ...)` for now public func sum(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { self.aggregate(.sum, key) } // TODO: `Field.Value` is not always the correct result type for `SUM()`, try `.aggregate(.sum, key, as: ...)` for now public func sum(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { self.aggregate(.sum, key) } // TODO: `Field.Value` is not always the correct result type for `SUM()`, try `.aggregate(.sum, key, as: ...)` for now public func sum(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { self.aggregate(.sum, key) } // TODO: `Field.Value` is not always the correct result type for `SUM()`, try `.aggregate(.sum, key, as: ...)` for now public func sum(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { self.aggregate(.sum, key) } // TODO: `Field.Value` is not always the correct result type for `AVG()`, try `.aggregate(.average, key, as: ...)` for now public func average(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { self.aggregate(.average, key) } // TODO: `Field.Value` is not always the correct result type for `AVG()`, try `.aggregate(.average, key, as: ...)` for now public func average(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { self.aggregate(.average, key) } // TODO: `Field.Value` is not always the correct result type for `AVG()`, try `.aggregate(.average, key, as: ...)` for now public func average(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { self.aggregate(.average, key) } // TODO: `Field.Value` is not always the correct result type for `AVG()`, try `.aggregate(.average, key, as: ...)` for now public func average(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { self.aggregate(.average, key) } public func min(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { self.aggregate(.minimum, key) } public func min(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { self.aggregate(.minimum, key) } public func min(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { self.aggregate(.minimum, key) } public func min(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { self.aggregate(.minimum, key) } public func max(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model, Field.Value: Sendable { self.aggregate(.maximum, key) } public func max(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Field.Value: Sendable { self.aggregate(.maximum, key) } public func max(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model { self.aggregate(.maximum, key) } public func max(_ key: KeyPath) -> EventLoopFuture - where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue - { + where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue { self.aggregate(.maximum, key) } @@ -134,8 +116,7 @@ extension QueryBuilder { _ field: KeyPath, as: Result.Type = Result.self ) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model, Result: Codable & Sendable - { + where Field: QueryableProperty, Field.Model == Model, Result: Codable & Sendable { self.aggregate(method, Model.path(for: field), as: Result.self) } @@ -144,19 +125,16 @@ extension QueryBuilder { _ field: KeyPath, as: Result.Type = Result.self ) -> EventLoopFuture - where Field: QueryableProperty, Field.Model == Model.IDValue, Result: Codable & Sendable - { + where Field: QueryableProperty, Field.Model == Model.IDValue, Result: Codable & Sendable { self.aggregate(method, Model.path(for: field), as: Result.self) } - public func aggregate( _ method: DatabaseQuery.Aggregate.Method, _ field: FieldKey, as: Result.Type = Result.self ) -> EventLoopFuture - where Result: Codable & Sendable - { + where Result: Codable & Sendable { self.aggregate(method, [field], as: Result.self) } @@ -165,8 +143,7 @@ extension QueryBuilder { _ path: [FieldKey], as: Result.Type = Result.self ) -> EventLoopFuture - where Result: Codable & Sendable - { + where Result: Codable & Sendable { self.aggregate( .field( .extendedPath(path, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased), @@ -175,13 +152,12 @@ extension QueryBuilder { as: Result.self ) } - + public func aggregate( _ aggregate: DatabaseQuery.Aggregate, as: Result.Type = Result.self ) -> EventLoopFuture - where Result: Codable & Sendable - { + where Result: Codable & Sendable { let copy = self.copy() // Remove all eager load requests otherwise we try to // read IDs from the aggreate reply when performing diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+EagerLoad.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+EagerLoad.swift index 2a39a71d..189142ff 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+EagerLoad.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+EagerLoad.swift @@ -1,7 +1,6 @@ extension QueryBuilder: EagerLoadBuilder { public func add(loader: Loader) - where Loader: EagerLoader, Loader.Model == Model - { + where Loader: EagerLoader, Loader.Model == Model { self.eagerLoaders.append(loader) } } @@ -9,17 +8,15 @@ extension QueryBuilder: EagerLoadBuilder { public protocol EagerLoadBuilder { associatedtype Model: FluentKit.Model func add(loader: Loader) - where Loader: EagerLoader, Loader.Model == Model + where Loader: EagerLoader, Loader.Model == Model } - extension EagerLoadBuilder { // MARK: Eager Load @discardableResult public func with(_ relationKey: KeyPath) -> Self - where Relation: EagerLoadable, Relation.From == Model - { + where Relation: EagerLoadable, Relation.From == Model { Relation.eagerLoad(relationKey, to: self) return self } @@ -27,23 +24,21 @@ extension EagerLoadBuilder { @discardableResult public func with( _ throughKey: KeyPath, - _ nested: (NestedEagerLoadBuilder) throws -> () + _ nested: (NestedEagerLoadBuilder) throws -> Void ) rethrows -> Self - where Relation: EagerLoadable, Relation.From == Model - { + where Relation: EagerLoadable, Relation.From == Model { Relation.eagerLoad(throughKey, to: self) let builder = NestedEagerLoadBuilder(builder: self, throughKey) try nested(builder) return self } - + @discardableResult public func with( _ relationKey: KeyPath, withDeleted: Bool ) -> Self - where Relation: EagerLoadable, Relation.From == Model - { + where Relation: EagerLoadable, Relation.From == Model { Relation.eagerLoad(relationKey, withDeleted: withDeleted, to: self) return self } @@ -52,10 +47,9 @@ extension EagerLoadBuilder { public func with( _ throughKey: KeyPath, withDeleted: Bool, - _ nested: (NestedEagerLoadBuilder) throws -> () + _ nested: (NestedEagerLoadBuilder) throws -> Void ) rethrows -> Self - where Relation: EagerLoadable, Relation.From == Model - { + where Relation: EagerLoadable, Relation.From == Model { Relation.eagerLoad(throughKey, withDeleted: withDeleted, to: self) let builder = NestedEagerLoadBuilder(builder: self, throughKey) try nested(builder) @@ -64,9 +58,10 @@ extension EagerLoadBuilder { } public struct NestedEagerLoadBuilder: EagerLoadBuilder - where Builder: EagerLoadBuilder, - Relation: EagerLoadable, - Builder.Model == Relation.From +where + Builder: EagerLoadBuilder, + Relation: EagerLoadable, + Builder.Model == Relation.From { public typealias Model = Relation.To let builder: Builder @@ -78,8 +73,7 @@ public struct NestedEagerLoadBuilder: EagerLoadBuilder } public func add(loader: Loader) - where Loader: EagerLoader, Loader.Model == Relation.To - { + where Loader: EagerLoader, Loader.Model == Relation.To { Relation.eagerLoad(loader, through: self.relationKey, to: self.builder) } } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Filter.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Filter.swift index 77bae849..1c1b86f6 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Filter.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Filter.swift @@ -1,6 +1,6 @@ extension QueryBuilder { // MARK: Filter - + @discardableResult func filter(id: Model.IDValue) -> Self { if let fields = id as? any Fields { @@ -9,7 +9,7 @@ extension QueryBuilder { self.filter(\Model._$id == id) } } - + @discardableResult func filter(ids: [Model.IDValue]) -> Self { guard let firstId = ids.first else { return self.limit(0) } @@ -26,13 +26,13 @@ extension QueryBuilder { _ method: DatabaseQuery.Filter.Method, _ value: Field.Value ) -> Self - where Field: QueryableProperty, Field.Model == Model - { - self.filter(.extendedPath( - Model.path(for: field), - schema: Model.schemaOrAlias, - space: Model.spaceIfNotAliased - ), method, Field.queryValue(value)) + where Field: QueryableProperty, Field.Model == Model { + self.filter( + .extendedPath( + Model.path(for: field), + schema: Model.schemaOrAlias, + space: Model.spaceIfNotAliased + ), method, Field.queryValue(value)) } @discardableResult @@ -42,13 +42,13 @@ extension QueryBuilder { _ method: DatabaseQuery.Filter.Method, _ value: Field.Value ) -> Self - where Joined: Schema, Field: QueryableProperty, Field.Model == Joined - { - self.filter(.extendedPath( - Joined.path(for: field), - schema: Joined.schemaOrAlias, - space: Joined.spaceIfNotAliased - ), method, Field.queryValue(value)) + where Joined: Schema, Field: QueryableProperty, Field.Model == Joined { + self.filter( + .extendedPath( + Joined.path(for: field), + schema: Joined.schemaOrAlias, + space: Joined.spaceIfNotAliased + ), method, Field.queryValue(value)) } @discardableResult @@ -57,10 +57,11 @@ extension QueryBuilder { _ method: DatabaseQuery.Filter.Method, _ rhsField: KeyPath ) -> Self - where Left: QueryableProperty, - Left.Model == Model, - Right: QueryableProperty, - Right.Model == Model + where + Left: QueryableProperty, + Left.Model == Model, + Right: QueryableProperty, + Right.Model == Model { self.filter(Model.path(for: lhsField), method, Model.path(for: rhsField)) } @@ -71,8 +72,7 @@ extension QueryBuilder { _ method: DatabaseQuery.Filter.Method, _ value: Value ) -> Self - where Value: Codable & Sendable - { + where Value: Codable & Sendable { self.filter([fieldName], method, value) } @@ -82,8 +82,7 @@ extension QueryBuilder { _ method: DatabaseQuery.Filter.Method, _ value: Value ) -> Self - where Value: Codable & Sendable - { + where Value: Codable & Sendable { self.filter( .extendedPath(fieldPath, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased), method, diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Group.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Group.swift index 987542dd..1b15968b 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Group.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Group.swift @@ -2,7 +2,7 @@ extension QueryBuilder { @discardableResult public func group( _ relation: DatabaseQuery.Filter.Relation = .and, - _ closure: (QueryBuilder) throws -> () + _ closure: (QueryBuilder) throws -> Void ) rethrows -> Self { let group = QueryBuilder(database: self.database) try closure(group) diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Join+DirectRelations.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Join+DirectRelations.swift index 16dedada..652e5ad3 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Join+DirectRelations.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Join+DirectRelations.swift @@ -1,6 +1,6 @@ extension QueryBuilder { // MARK: Parent, children, and siblings joins - + /// This will join a foreign table based on a `@Parent` relation /// /// This will not decode the joined data, but can be used in order to filter. @@ -84,7 +84,7 @@ extension QueryBuilder { ) -> Self { self.join(from: Model.self, parent: parent, method: method) } - + /// This will join a foreign table based on a `@OptionalChild` relation /// /// This will not decode the joined data, but can be used in order to filter. @@ -192,13 +192,13 @@ extension QueryBuilder { public func join( from model: From.Type, siblings: KeyPath> - ) -> Self - where From: FluentKit.Model, To: FluentKit.Model, Through: FluentKit.Model - { + ) -> Self where From: FluentKit.Model, To: FluentKit.Model, Through: FluentKit.Model { let siblings = From()[keyPath: siblings] - - return self.join(Through.self, on: siblings.from.appending(path: \.$id) == \From._$id) - .join(To.self, on: siblings.to.appending(path: \.$id) == \To._$id) + + return + self + .join(Through.self, on: siblings.from.appending(path: \.$id) == \From._$id) + .join(To.self, on: siblings.to.appending(path: \.$id) == \To._$id) } /// This will join the foreign table based on a `@Siblings`relation @@ -216,9 +216,7 @@ extension QueryBuilder { @discardableResult public func join( siblings: KeyPath> - ) -> Self - where To: FluentKit.Model, Through: FluentKit.Model - { + ) -> Self where To: FluentKit.Model, Through: FluentKit.Model { self.join(from: Model.self, siblings: siblings) } } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift index 8a3553c4..096bd717 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift @@ -17,14 +17,12 @@ extension QueryBuilder { _ foreign: Foreign.Type, on filter: ComplexJoinFilterGroup, method: DatabaseQuery.Join.Method = .inner - ) -> Self - where Foreign: Schema - { + ) -> Self where Foreign: Schema { self.join(Foreign.self, filter.filters.map(\.filter), method: method) } // MARK: - Fundamental join methods - + /// `.join(Foreign.self, filters, method: method)` /// /// Joins against `Foreign` with the specified method and using the given filter(s) as the join condition. @@ -33,12 +31,11 @@ extension QueryBuilder { _ foreign: Foreign.Type, _ filters: [DatabaseQuery.Filter], method: DatabaseQuery.Join.Method = .inner - ) -> Self - where Foreign: Schema - { - self.join(Foreign.self, on: .advancedJoin(schema: Foreign.schema, space: Foreign.space, alias: Foreign.alias, method, filters: filters)) + ) -> Self where Foreign: Schema { + self.join( + Foreign.self, on: .advancedJoin(schema: Foreign.schema, space: Foreign.space, alias: Foreign.alias, method, filters: filters)) } - + /// `.join(Foreign.self, on: databaseJoin)` /// /// Joins against `Foreign` using the given join description. @@ -59,17 +56,20 @@ extension QueryBuilder { _ foreign: Foreign.Type, on join: DatabaseQuery.Join ) -> Self - where Foreign: Schema - { + where Foreign: Schema { #if DEBUG - switch join { - case let .join(jschema, jalias, _, _, _): - assert(jschema == Foreign.schema && jalias == Foreign.alias, "Join specification does not match provided Model type \(Foreign.self)") - case let .extendedJoin(jschema, jspace, jalias, _, _, _), let .advancedJoin(jschema, jspace, jalias, _, _): - assert(jspace == Foreign.space && jschema == Foreign.schema && jalias == Foreign.alias, "Join specification does not match provided Model type \(Foreign.self)") - case.custom(_): - break // We can't validate custom joins - } + switch join { + case .join(let jschema, let jalias, _, _, _): + assert( + jschema == Foreign.schema && jalias == Foreign.alias, + "Join specification does not match provided Model type \(Foreign.self)") + case .extendedJoin(let jschema, let jspace, let jalias, _, _, _), .advancedJoin(let jschema, let jspace, let jalias, _, _): + assert( + jspace == Foreign.space && jschema == Foreign.schema && jalias == Foreign.alias, + "Join specification does not match provided Model type \(Foreign.self)") + case .custom(_): + break // We can't validate custom joins + } #endif self.models.append(Foreign.self) @@ -82,7 +82,8 @@ extension QueryBuilder { public func == ( lhs: KeyPath, rhs: KeyPath -) -> ComplexJoinFilter where +) -> ComplexJoinFilter +where Foreign: Schema, Local: Schema, ForeignField: QueryableProperty, LocalField: QueryableProperty, ForeignField.Value == LocalField.Value { @@ -91,7 +92,8 @@ public func == ( public func == ( lhs: KeyPath, rhs: KeyPath -) -> ComplexJoinFilter where +) -> ComplexJoinFilter +where Foreign: Schema, Local: Schema, ForeignField: QueryableProperty, LocalField: QueryableProperty, ForeignField.Value == LocalField.Value? { @@ -100,7 +102,8 @@ public func == ( public func == ( lhs: KeyPath, rhs: KeyPath -) -> ComplexJoinFilter where +) -> ComplexJoinFilter +where Foreign: Schema, Local: Schema, ForeignField: QueryableProperty, LocalField: QueryableProperty, ForeignField.Value? == LocalField.Value { @@ -111,7 +114,8 @@ public func == ( public func != ( lhs: KeyPath, rhs: KeyPath -) -> ComplexJoinFilter where +) -> ComplexJoinFilter +where Foreign: Schema, Local: Schema, ForeignField: QueryableProperty, LocalField: QueryableProperty, ForeignField.Value == LocalField.Value { @@ -120,7 +124,8 @@ public func != ( public func != ( lhs: KeyPath, rhs: KeyPath -) -> ComplexJoinFilter where +) -> ComplexJoinFilter +where Foreign: Schema, Local: Schema, ForeignField: QueryableProperty, LocalField: QueryableProperty, ForeignField.Value == LocalField.Value? { @@ -129,7 +134,8 @@ public func != ( public func != ( lhs: KeyPath, rhs: KeyPath -) -> ComplexJoinFilter where +) -> ComplexJoinFilter +where Foreign: Schema, Local: Schema, ForeignField: QueryableProperty, LocalField: QueryableProperty, ForeignField.Value? == LocalField.Value { @@ -144,12 +150,12 @@ public func && (lhs: ComplexJoinFilter, rhs: ComplexJoinFilter) -> ComplexJoinFi } /// `a ==/!= b && c >/< 1` -public func && (lhs: ComplexJoinFilter, rhs: ModelValueFilter) -> ComplexJoinFilterGroup { +public func && (lhs: ComplexJoinFilter, rhs: ModelValueFilter) -> ComplexJoinFilterGroup { .init(filters: [lhs, .init(rhs)]) } /// `c >/< 1 && a ==/!= b` -public func && (lhs: ModelValueFilter, rhs: ComplexJoinFilter) -> ComplexJoinFilterGroup { +public func && (lhs: ModelValueFilter, rhs: ComplexJoinFilter) -> ComplexJoinFilterGroup { .init(filters: [.init(lhs), rhs]) } @@ -179,47 +185,51 @@ public func && (lhs: ModelValueFilter, rhs: ComplexJoinFil /// compile times and avoiding "this expression is too complex..." errors. public struct ComplexJoinFilter { let filter: DatabaseQuery.Filter - + init(filter: DatabaseQuery.Filter) { self.filter = filter } - + init(_ filter: ModelValueFilter) { - self.init(filter: .value( - .extendedPath(filter.path, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased), - filter.method, - filter.value - )) + self.init( + filter: .value( + .extendedPath(filter.path, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased), + filter.method, + filter.value + )) } - + init( _ lhs: KeyPath, _ method: DatabaseQuery.Filter.Method, _ rhs: KeyPath ) where Left: Schema, Right: Schema, LField: QueryableProperty, RField: QueryableProperty, LField.Value == RField.Value { - self.init(filter: .field( - .extendedPath(Left.path(for: lhs), schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), - method, - .extendedPath(Right.path(for: rhs), schema: Right.schemaOrAlias, space: Right.spaceIfNotAliased) - )) + self.init( + filter: .field( + .extendedPath(Left.path(for: lhs), schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), + method, + .extendedPath(Right.path(for: rhs), schema: Right.schemaOrAlias, space: Right.spaceIfNotAliased) + )) } init( _ lhs: KeyPath, _ method: DatabaseQuery.Filter.Method, _ rhs: KeyPath ) where Left: Schema, Right: Schema, LField: QueryableProperty, RField: QueryableProperty, LField.Value? == RField.Value { - self.init(filter: .field( - .extendedPath(Left.path(for: lhs), schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), - method, - .extendedPath(Right.path(for: rhs), schema: Right.schemaOrAlias, space: Right.spaceIfNotAliased) - )) + self.init( + filter: .field( + .extendedPath(Left.path(for: lhs), schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), + method, + .extendedPath(Right.path(for: rhs), schema: Right.schemaOrAlias, space: Right.spaceIfNotAliased) + )) } init( _ lhs: KeyPath, _ method: DatabaseQuery.Filter.Method, _ rhs: KeyPath ) where Left: Schema, Right: Schema, LField: QueryableProperty, RField: QueryableProperty, LField.Value == RField.Value? { - self.init(filter: .field( - .extendedPath(Left.path(for: lhs), schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), - method, - .extendedPath(Right.path(for: rhs), schema: Right.schemaOrAlias, space: Right.spaceIfNotAliased) - )) + self.init( + filter: .field( + .extendedPath(Left.path(for: lhs), schema: Left.schemaOrAlias, space: Left.spaceIfNotAliased), + method, + .extendedPath(Right.path(for: rhs), schema: Right.schemaOrAlias, space: Right.spaceIfNotAliased) + )) } } @@ -238,30 +248,30 @@ extension QueryBuilder { _ foreign: Foreign.Type, on filter: DatabaseQuery.Join ) -> Self - where Local: Schema, Foreign: Schema - { + where Local: Schema, Foreign: Schema { self.join(Foreign.self, on: filter) } - + @discardableResult public func join( _ foreign: Foreign.Type, on filter: JoinFilter, method: DatabaseQuery.Join.Method = .inner ) -> Self - where Foreign: Schema, Local: Schema - { - self.join(Foreign.self, on: ComplexJoinFilter(filter: .field( - .extendedPath(filter.foreign, schema: Foreign.schemaOrAlias, space: Foreign.spaceIfNotAliased), - .equal, - .extendedPath(filter.local, schema: Local.schemaOrAlias, space: Local.spaceIfNotAliased) - )), method: method) + where Foreign: Schema, Local: Schema { + self.join( + Foreign.self, + on: ComplexJoinFilter( + filter: .field( + .extendedPath(filter.foreign, schema: Foreign.schemaOrAlias, space: Foreign.spaceIfNotAliased), + .equal, + .extendedPath(filter.local, schema: Local.schemaOrAlias, space: Local.spaceIfNotAliased) + )), method: method) } } public struct JoinFilter - where Foreign: Fields, Local: Fields, Value: Codable -{ +where Foreign: Fields, Local: Fields, Value: Codable { let foreign: [FieldKey] let local: [FieldKey] } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Paginate.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Paginate.swift index c8af9c20..2a772681 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Paginate.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Paginate.swift @@ -13,7 +13,7 @@ extension QueryBuilder { ) -> EventLoopFuture> { self.page(withIndex: request.page, size: request.per) } - + /// Returns a single `Page` out of the complete result set. /// /// This method will first `count()` the result set, then request a subset of the results using `range()` and `all()`. @@ -24,7 +24,8 @@ extension QueryBuilder { /// - Returns: A single `Page` of the result set containing the requested items and page metadata. public func page( withIndex page: Int, - size per: Int) -> EventLoopFuture> { + size per: Int + ) -> EventLoopFuture> { let trimmedRequest: PageRequest = { guard let pageSizeLimit = database.context.pageSizeLimit else { return .init(page: Swift.max(page, 1), per: Swift.max(per, 1)) @@ -85,13 +86,13 @@ public struct PageMetadata: Codable, Sendable { /// Total number of items available. public let total: Int - + /// Computed total number of pages with `1` being the minimum. public var pageCount: Int { - let count = Int((Double(self.total)/Double(self.per)).rounded(.up)) + let count = Int((Double(self.total) / Double(self.per)).rounded(.up)) return count < 1 ? 1 : count } - + /// Creates a new `PageMetadata` instance. /// /// - Parameters: diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Set.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Set.swift index af710439..2bcd6348 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Set.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Set.swift @@ -1,6 +1,6 @@ extension QueryBuilder { // MARK: Set - + @discardableResult public func set(_ data: [FieldKey: DatabaseQuery.Value]) -> Self { self.set([data]) @@ -20,9 +20,7 @@ extension QueryBuilder { public func set( _ field: KeyPath, to value: Field.Value - ) -> Self - where Field: QueryableProperty, Field.Model == Model.IDValue - { + ) -> Self where Field: QueryableProperty, Field.Model == Model.IDValue { if self.query.input.isEmpty { self.query.input = [.dictionary([:])] } @@ -44,9 +42,7 @@ extension QueryBuilder { public func set( _ field: KeyPath, to value: Field.Value - ) -> Self - where Field: QueryableProperty, Field.Model == Model - { + ) -> Self where Field: QueryableProperty, Field.Model == Model { if self.query.input.isEmpty { self.query.input = [.dictionary([:])] } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Sort.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Sort.swift index 2a972e24..2bab8a69 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Sort.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Sort.swift @@ -5,11 +5,7 @@ extension QueryBuilder { public func sort( _ field: KeyPath, _ direction: DatabaseQuery.Sort.Direction = .ascending - ) -> Self - where - Field: QueryableProperty, - Field.Model == Model - { + ) -> Self where Field: QueryableProperty, Field.Model == Model { self.sort(Model.path(for: field), direction) } @@ -17,9 +13,7 @@ extension QueryBuilder { public func sort( _ field: KeyPath>, _ direction: DatabaseQuery.Sort.Direction = .ascending - ) -> Self - where Field: QueryableProperty - { + ) -> Self where Field: QueryableProperty { self.sort(Model.path(for: field), direction) } @@ -45,24 +39,17 @@ extension QueryBuilder { _ field: KeyPath, _ direction: DatabaseQuery.Sort.Direction = .ascending, alias: String? = nil - ) -> Self - where - Field: QueryableProperty, - Field.Model == Joined, - Joined: Schema - { + ) -> Self where Field: QueryableProperty, Field.Model == Joined, Joined: Schema { self.sort(Joined.self, Joined.path(for: field), direction, alias: alias) } - + @discardableResult public func sort( _ model: Joined.Type, _ path: FieldKey, _ direction: DatabaseQuery.Sort.Direction = .ascending, alias: String? = nil - ) -> Self - where Joined: Schema - { + ) -> Self where Joined: Schema { self.sort(Joined.self, [path], direction) } @@ -72,9 +59,7 @@ extension QueryBuilder { _ path: [FieldKey], _ direction: DatabaseQuery.Sort.Direction = .ascending, alias: String? = nil - ) -> Self - where Joined: Schema - { + ) -> Self where Joined: Schema { self.sort(.extendedPath(path, schema: Joined.schemaOrAlias, space: Joined.spaceIfNotAliased), direction) } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder.swift b/Sources/FluentKit/Query/Builder/QueryBuilder.swift index 99831640..f19d86f4 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder.swift @@ -1,8 +1,7 @@ import NIOCore public final class QueryBuilder - where Model: FluentKit.Model -{ +where Model: FluentKit.Model { public var query: DatabaseQuery public let database: any Database @@ -56,11 +55,10 @@ public final class QueryBuilder } // MARK: Fields - + @discardableResult - public func fields(for model: Joined.Type) -> Self - where Joined: Schema & Fields - { + public func fields(for model: Joined.Type) -> Self + where Joined: Schema & Fields { self.addFields(for: Joined.self, to: &self.query) return self } @@ -73,19 +71,17 @@ public final class QueryBuilder @discardableResult public func field(_ field: KeyPath) -> Self - where Field: QueryableProperty, Field.Model == Model - { + where Field: QueryableProperty, Field.Model == Model { self.field(Model.self, field) } @discardableResult public func field(_ joined: Joined.Type, _ field: KeyPath) -> Self - where Joined: Schema, Field: QueryableProperty, Field.Model == Joined - { + where Joined: Schema, Field: QueryableProperty, Field.Model == Joined { self.query.fields.append(.extendedPath(Joined.path(for: field), schema: Joined.schemaOrAlias, space: Joined.spaceIfNotAliased)) return self } - + @discardableResult public func field(_ field: DatabaseQuery.Field) -> Self { self.query.fields.append(field) @@ -120,7 +116,7 @@ public final class QueryBuilder } // MARK: Limit - + @discardableResult public func limit(_ count: Int) -> Self { self.query.limits.append(.count(count)) @@ -145,10 +141,10 @@ public final class QueryBuilder // MARK: Fetch - public func chunk(max: Int, closure: @escaping @Sendable ([Result]) -> ()) -> EventLoopFuture { + public func chunk(max: Int, closure: @escaping @Sendable ([Result]) -> Void) -> EventLoopFuture { nonisolated(unsafe) var partial: [Result] = [] partial.reserveCapacity(max) - + return self.all { row in partial.append(row) if partial.count >= max { @@ -169,9 +165,9 @@ public final class QueryBuilder } public func all(_ key: KeyPath) -> EventLoopFuture<[Field.Value]> - where - Field: QueryableProperty, - Field.Model == Model + where + Field: QueryableProperty, + Field.Model == Model { let copy = self.copy() copy.query.fields = [.extendedPath(Model.path(for: key), schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased)] @@ -186,10 +182,10 @@ public final class QueryBuilder _ joined: Joined.Type, _ field: KeyPath ) -> EventLoopFuture<[Field.Value]> - where - Joined: Schema, - Field: QueryableProperty, - Field.Model == Joined + where + Joined: Schema, + Field: QueryableProperty, + Field.Model == Joined { let copy = self.copy() copy.query.fields = [.extendedPath(Joined.path(for: field), schema: Joined.schemaOrAlias, space: Joined.spaceIfNotAliased)] @@ -203,7 +199,8 @@ public final class QueryBuilder public func all() -> EventLoopFuture<[Model]> { nonisolated(unsafe) var models: [Result] = [] - return self + return + self .all { models.append($0) } .flatMapThrowing { try models.map { try $0.get() } } } @@ -212,23 +209,24 @@ public final class QueryBuilder self.run { _ in } } - public func all(_ onOutput: @escaping @Sendable (Result) -> ()) -> EventLoopFuture { + public func all(_ onOutput: @escaping @Sendable (Result) -> Void) -> EventLoopFuture { nonisolated(unsafe) var all: [Model] = [] let done = self.run { output in - onOutput(.init(catching: { - let model = Model() - try model.output(from: output.qualifiedSchema(space: Model.spaceIfNotAliased, Model.schemaOrAlias)) - all.append(model) - return model - })) + onOutput( + .init(catching: { + let model = Model() + try model.output(from: output.qualifiedSchema(space: Model.spaceIfNotAliased, Model.schemaOrAlias)) + all.append(model) + return model + })) } // if eager loads exist, run them, and update models if !self.eagerLoaders.isEmpty { let loaders = self.eagerLoaders let db = self.database - + return done.flatMapWithEventLoop { // don't run eager loads if result set was empty guard !all.isEmpty else { @@ -252,7 +250,7 @@ public final class QueryBuilder return self } - public func run(_ onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) -> EventLoopFuture { + public func run(_ onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) -> EventLoopFuture { // make a copy of this query before mutating it // so that run can be called multiple times var query = self.query @@ -274,7 +272,8 @@ public final class QueryBuilder } // TODO: combine this logic with model+crud timestamps - let forceDelete = Model.init().deletedTimestamp == nil + let forceDelete = + Model.init().deletedTimestamp == nil ? true : self.shouldForceDelete switch query.action { case .delete: @@ -297,7 +296,7 @@ public final class QueryBuilder self.database.history?.add(self.query) let loop = self.database.eventLoop - + let done = self.database.execute(query: query) { output in loop.assertInEventLoop() onOutput(output) diff --git a/Sources/FluentKit/Query/Database/DatabaseQuery+Filter.swift b/Sources/FluentKit/Query/Database/DatabaseQuery+Filter.swift index 34cbd126..0b233050 100644 --- a/Sources/FluentKit/Query/Database/DatabaseQuery+Filter.swift +++ b/Sources/FluentKit/Query/Database/DatabaseQuery+Filter.swift @@ -39,7 +39,7 @@ extension DatabaseQuery { case suffix case anywhere } - + /// RHS is [anchored] substring/isn't [anchored] substring of LHS case contains(inverse: Bool, Contains) diff --git a/Sources/FluentKit/Query/Database/DatabaseQuery+Join.swift b/Sources/FluentKit/Query/Database/DatabaseQuery+Join.swift index 033ffc69..4d29b4fa 100644 --- a/Sources/FluentKit/Query/Database/DatabaseQuery+Join.swift +++ b/Sources/FluentKit/Query/Database/DatabaseQuery+Join.swift @@ -13,7 +13,7 @@ extension DatabaseQuery { foreign: Field, local: Field ) - + case extendedJoin( schema: String, space: String?, @@ -22,7 +22,7 @@ extension DatabaseQuery { foreign: Field, local: Field ) - + case advancedJoin( schema: String, space: String?, @@ -30,7 +30,7 @@ extension DatabaseQuery { Method, filters: [Filter] ) - + case custom(any Sendable) } } @@ -55,20 +55,23 @@ extension DatabaseQuery.Join: CustomStringConvertible { var describedByLoggingMetadata: Logger.MetadataValue { switch self { case .join(let schema, let alias, let method, let foreign, let local): - .dictionary([ - "schema": "\(schema)", "alias": alias.map { "\($0)" }, "method": "\(method)", - "foreign": foreign.describedByLoggingMetadata, "local": local.describedByLoggingMetadata - ].compactMapValues { $0 }) + .dictionary( + [ + "schema": "\(schema)", "alias": alias.map { "\($0)" }, "method": "\(method)", + "foreign": foreign.describedByLoggingMetadata, "local": local.describedByLoggingMetadata, + ].compactMapValues { $0 }) case .extendedJoin(let schema, let space, let alias, let method, let foreign, let local): - .dictionary([ - "schema": "\(schema)", "space": space.map { "\($0)" }, "alias": alias.map { "\($0)" }, "method": "\(method)", - "foreign": foreign.describedByLoggingMetadata, "local": local.describedByLoggingMetadata - ].compactMapValues { $0 }) + .dictionary( + [ + "schema": "\(schema)", "space": space.map { "\($0)" }, "alias": alias.map { "\($0)" }, "method": "\(method)", + "foreign": foreign.describedByLoggingMetadata, "local": local.describedByLoggingMetadata, + ].compactMapValues { $0 }) case .advancedJoin(let schema, let space, let alias, let method, let filters): - .dictionary([ - "schema": "\(schema)", "space": space.map { "\($0)" }, "alias": alias.map { "\($0)" }, "method": "\(method)", - "filters": .array(filters.map(\.describedByLoggingMetadata)) - ].compactMapValues { $0 }) + .dictionary( + [ + "schema": "\(schema)", "space": space.map { "\($0)" }, "alias": alias.map { "\($0)" }, "method": "\(method)", + "filters": .array(filters.map(\.describedByLoggingMetadata)), + ].compactMapValues { $0 }) case .custom: .stringConvertible(self) } diff --git a/Sources/FluentKit/Query/Database/DatabaseQuery.swift b/Sources/FluentKit/Query/Database/DatabaseQuery.swift index df44e308..f73bac14 100644 --- a/Sources/FluentKit/Query/Database/DatabaseQuery.swift +++ b/Sources/FluentKit/Query/Database/DatabaseQuery.swift @@ -60,7 +60,7 @@ extension DatabaseQuery: CustomStringConvertible { } return parts.joined(separator: " ") } - + var describedByLoggingMetadata: Logger.Metadata { var result: Logger.Metadata = [ "action": "\(self.action)", diff --git a/Sources/FluentKit/Query/QueryHistory.swift b/Sources/FluentKit/Query/QueryHistory.swift index 8d03e591..a1f72462 100644 --- a/Sources/FluentKit/Query/QueryHistory.swift +++ b/Sources/FluentKit/Query/QueryHistory.swift @@ -4,7 +4,7 @@ import struct NIOConcurrencyHelpers.NIOLockedValueBox public final class QueryHistory: @unchecked Sendable { /// The underlying (locked) storage. private let _queries: NIOLockedValueBox<[DatabaseQuery]> = .init([]) - + /// The queries that have been executed. /// /// > Warning: This array can be modified aribtrarily by any code with access to the ``QueryHistory`` @@ -17,7 +17,7 @@ public final class QueryHistory: @unchecked Sendable { /// Create a new ``QueryHistory`` with no existing history. public init() {} - + /// Add a query to the history. func add(_ query: DatabaseQuery) { self._queries.withLockedValue { $0.append(query) } diff --git a/Sources/FluentKit/Schema/DatabaseSchema.swift b/Sources/FluentKit/Schema/DatabaseSchema.swift index 34666730..aab4212d 100644 --- a/Sources/FluentKit/Schema/DatabaseSchema.swift +++ b/Sources/FluentKit/Schema/DatabaseSchema.swift @@ -7,7 +7,7 @@ public struct DatabaseSchema: Sendable { case update case delete } - + public indirect enum DataType: Sendable { public static var int: DataType { .int64 @@ -16,7 +16,7 @@ public struct DatabaseSchema: Sendable { case int16 case int32 case int64 - + public static var uint: DataType { .uint64 } @@ -24,9 +24,9 @@ public struct DatabaseSchema: Sendable { case uint16 case uint32 case uint64 - + case bool - + public struct Enum: Sendable { public var name: String public var cases: [String] @@ -38,11 +38,11 @@ public struct DatabaseSchema: Sendable { } case `enum`(Enum) case string - + case time case date case datetime - + case float case double case data @@ -96,7 +96,7 @@ public struct DatabaseSchema: Sendable { case constraint(ConstraintAlgorithm, name: String?) case custom(any Sendable) } - + public enum ConstraintAlgorithm: Sendable { case unique(fields: [FieldName]) case foreignKey( @@ -118,7 +118,7 @@ public struct DatabaseSchema: Sendable { case setNull case setDefault } - + public enum FieldDefinition: Sendable { case definition( name: FieldName, @@ -132,7 +132,7 @@ public struct DatabaseSchema: Sendable { case dataType(name: FieldName, dataType: DataType) case custom(any Sendable) } - + public enum FieldName: Sendable { case key(FieldKey) case custom(any Sendable) @@ -142,9 +142,9 @@ public struct DatabaseSchema: Sendable { case constraint(ConstraintAlgorithm) case name(String) case custom(any Sendable) - + /// Deletion specifier for an explicitly-named constraint known to be a referential constraint. - /// + /// /// Certain old versions of certain databases (I'm looking at you, MySQL 5.7...) do not support dropping /// a `FOREIGN KEY` constraint by name without knowing ahead of time that it is a foreign key. When an /// unfortunate user runs into this, the options are: @@ -182,7 +182,7 @@ public struct DatabaseSchema: Sendable { public var createConstraints: [Constraint] public var deleteConstraints: [ConstraintDelete] public var exclusiveCreate: Bool - + public init(schema: String, space: String? = nil) { self.action = .create self.schema = schema diff --git a/Sources/FluentKit/Schema/SchemaBuilder.swift b/Sources/FluentKit/Schema/SchemaBuilder.swift index 07e04042..ea46a034 100644 --- a/Sources/FluentKit/Schema/SchemaBuilder.swift +++ b/Sources/FluentKit/Schema/SchemaBuilder.swift @@ -1,11 +1,11 @@ +import NIOCore + extension Database { public func schema(_ schema: String, space: String? = nil) -> SchemaBuilder { .init(database: self, schema: schema, space: space) } } -import NIOCore - public final class SchemaBuilder { let database: any Database public var schema: DatabaseSchema @@ -26,11 +26,12 @@ public final class SchemaBuilder { _ dataType: DatabaseSchema.DataType, _ constraints: DatabaseSchema.FieldConstraint... ) -> Self { - self.field(.definition( - name: .key(key), - dataType: dataType, - constraints: constraints - )) + self.field( + .definition( + name: .key(key), + dataType: dataType, + constraints: constraints + )) } @discardableResult @@ -41,12 +42,13 @@ public final class SchemaBuilder { @discardableResult public func unique(on fields: FieldKey..., name: String? = nil) -> Self { - self.constraint(.constraint( - .unique(fields: fields.map { .key($0) }), - name: name - )) + self.constraint( + .constraint( + .unique(fields: fields.map { .key($0) }), + name: name + )) } - + @discardableResult public func compositeIdentifier(over fields: FieldKey...) -> Self { self.constraint(.constraint(.compositeIdentifier(fields.map { .key($0) }), name: "")) @@ -94,17 +96,18 @@ public final class SchemaBuilder { onUpdate: DatabaseSchema.ForeignKeyAction = .noAction, name: String? = nil ) -> Self { - self.schema.createConstraints.append(.constraint( - .foreignKey( - [.key(field)], - foreignSchema, - space: foreignSpace, - [.key(foreignField)], - onDelete: onDelete, - onUpdate: onUpdate - ), - name: name - )) + self.schema.createConstraints.append( + .constraint( + .foreignKey( + [.key(field)], + foreignSchema, + space: foreignSpace, + [.key(foreignField)], + onDelete: onDelete, + onUpdate: onUpdate + ), + name: name + )) return self } @@ -118,17 +121,18 @@ public final class SchemaBuilder { onUpdate: DatabaseSchema.ForeignKeyAction = .noAction, name: String? = nil ) -> Self { - self.schema.createConstraints.append(.constraint( - .foreignKey( - fields.map { .key($0) }, - foreignSchema, - space: foreignSpace, - foreignFields.map { .key($0) }, - onDelete: onDelete, - onUpdate: onUpdate - ), - name: name - )) + self.schema.createConstraints.append( + .constraint( + .foreignKey( + fields.map { .key($0) }, + foreignSchema, + space: foreignSpace, + foreignFields.map { .key($0) }, + onDelete: onDelete, + onUpdate: onUpdate + ), + name: name + )) return self } @@ -137,10 +141,11 @@ public final class SchemaBuilder { _ key: FieldKey, _ dataType: DatabaseSchema.DataType ) -> Self { - self.updateField(.dataType( - name: .key(key), - dataType: dataType - )) + self.updateField( + .dataType( + name: .key(key), + dataType: dataType + )) } @discardableResult diff --git a/Sources/FluentKit/Utilities/UnsafeMutableTransferBox.swift b/Sources/FluentKit/Utilities/UnsafeMutableTransferBox.swift index 40352602..9f9873a4 100644 --- a/Sources/FluentKit/Utilities/UnsafeMutableTransferBox.swift +++ b/Sources/FluentKit/Utilities/UnsafeMutableTransferBox.swift @@ -2,7 +2,7 @@ final class UnsafeMutableTransferBox: @unchecked Sendable { @usableFromInline var wrappedValue: Wrapped - + @inlinable init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue diff --git a/Sources/FluentSQL/DatabaseQuery+SQL.swift b/Sources/FluentSQL/DatabaseQuery+SQL.swift index d60cc7f2..c07516ce 100644 --- a/Sources/FluentSQL/DatabaseQuery+SQL.swift +++ b/Sources/FluentSQL/DatabaseQuery+SQL.swift @@ -44,7 +44,9 @@ extension DatabaseQuery.Aggregate.Method { } extension DatabaseQuery.Field { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -67,7 +69,9 @@ extension DatabaseQuery.Field { } extension DatabaseQuery.Filter { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -138,7 +142,9 @@ extension DatabaseQuery.Filter.Relation { } extension DatabaseQuery.Join { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -174,7 +180,9 @@ extension DatabaseQuery.Join.Method { // of the `.custom()` cases of these types triggers a `fatalError()` in `SQLQueryConverter`. extension DatabaseQuery.Sort { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -231,7 +239,9 @@ extension DatabaseQuery.Sort.Direction { } extension DatabaseQuery.Value { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } diff --git a/Sources/FluentSQL/DatabaseSchema+SQL.swift b/Sources/FluentSQL/DatabaseSchema+SQL.swift index 3000e1a4..0cae4692 100644 --- a/Sources/FluentSQL/DatabaseSchema+SQL.swift +++ b/Sources/FluentSQL/DatabaseSchema+SQL.swift @@ -2,7 +2,9 @@ import FluentKit import SQLKit extension DatabaseSchema.DataType { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -25,7 +27,9 @@ extension DatabaseSchema.DataType { } extension DatabaseSchema.Constraint { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -48,7 +52,9 @@ extension DatabaseSchema.Constraint { } extension DatabaseSchema.ConstraintAlgorithm { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -62,12 +68,14 @@ extension DatabaseSchema.ConstraintAlgorithm { } public static func sql(_ expression: any SQLExpression) -> Self { - .custom(expression) + .custom(expression) } } extension DatabaseSchema.FieldConstraint { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -90,7 +98,9 @@ extension DatabaseSchema.FieldConstraint { } extension DatabaseSchema.FieldDefinition { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -109,7 +119,9 @@ extension DatabaseSchema.FieldDefinition { } extension DatabaseSchema.FieldUpdate { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -128,7 +140,9 @@ extension DatabaseSchema.FieldUpdate { } extension DatabaseSchema.FieldName { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } @@ -147,7 +161,9 @@ extension DatabaseSchema.FieldName { } extension DatabaseSchema.ConstraintDelete { - @available(*, deprecated, renamed: "sql(unsafeRaw:)", message: "Renamed to `.sql(unsafeRaw:)`. Please use caution when embedding raw SQL.") + @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(unsafeRaw: raw) } diff --git a/Sources/FluentSQL/SQLDatabase+Model+Concurrency.swift b/Sources/FluentSQL/SQLDatabase+Model+Concurrency.swift index 50dbfe96..5c656a33 100644 --- a/Sources/FluentSQL/SQLDatabase+Model+Concurrency.swift +++ b/Sources/FluentSQL/SQLDatabase+Model+Concurrency.swift @@ -1,12 +1,12 @@ +import FluentKit import NIOCore import SQLKit -import FluentKit extension SQLQueryFetcher { public func first(decodingFluent model: Model.Type) async throws -> Model? { try await self.first(decodingFluent: Model.self).get() } - + @available(*, deprecated, renamed: "first(decodingFluent:)", message: "Renamed to first(decodingFluent:)") public func first(decoding: Model.Type) async throws -> Model? { try await self.first(decodingFluent: Model.self) @@ -15,7 +15,7 @@ extension SQLQueryFetcher { public func all(decodingFluent: Model.Type) async throws -> [Model] { try await self.all(decodingFluent: Model.self).get() } - + @available(*, deprecated, renamed: "all(decodingFluent:)", message: "Renamed to all(decodingFluent:)") public func all(decoding: Model.Type) async throws -> [Model] { try await self.all(decodingFluent: Model.self) diff --git a/Sources/FluentSQL/SQLDatabase+Model.swift b/Sources/FluentSQL/SQLDatabase+Model.swift index a82488a7..4d3d7374 100644 --- a/Sources/FluentSQL/SQLDatabase+Model.swift +++ b/Sources/FluentSQL/SQLDatabase+Model.swift @@ -1,12 +1,12 @@ -import SQLKit import FluentKit +import SQLKit extension SQLQueryFetcher { @available(*, deprecated, renamed: "first(decodingFluent:)", message: "Renamed to first(decodingFluent:)") public func first(decoding: Model.Type) -> EventLoopFuture { self.first(decodingFluent: Model.self) } - + public func first(decodingFluent: Model.Type) -> EventLoopFuture { self.first().flatMapThrowing { row in try row?.decode(fluentModel: Model.self) } } @@ -15,7 +15,7 @@ extension SQLQueryFetcher { public func all(decoding: Model.Type) -> EventLoopFuture<[Model]> { self.all(decodingFluent: Model.self) } - + public func all(decodingFluent: Model.Type) -> EventLoopFuture<[Model]> { self.all().flatMapThrowing { rows in try rows.map { row in try row.decode(fluentModel: Model.self) } } } @@ -26,7 +26,7 @@ extension SQLRow { public func decode(model: Model.Type) throws -> Model { try self.decode(fluentModel: Model.self) } - + public func decode(fluentModel: Model.Type) throws -> Model { let model = Model() try model.output(from: SQLDatabaseOutput(sql: self)) @@ -53,9 +53,8 @@ struct SQLDatabaseOutput: DatabaseOutput { try self.sql.decodeNil(column: key.description) } - func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T: Decodable - { + func decode(_ key: FieldKey, as type: T.Type) throws -> T + where T: Decodable { try self.sql.decode(column: key.description, as: T.self) } } @@ -66,17 +65,17 @@ extension DatabaseQuery.Value { /// really, but why add more fatal errors than we have to?). fileprivate var asSQLExpression: any SQLExpression { switch self { - case .bind(let value): SQLBind(value) - case .null: SQLLiteral.null + case .bind(let value): SQLBind(value) + case .null: SQLLiteral.null case .array(let values): SQLGroupExpression(SQLKit.SQLList(values.map(\.asSQLExpression), separator: SQLRaw(","))) - case .default: SQLLiteral.default + case .default: SQLLiteral.default case .enumCase(let str): SQLLiteral.string(str) case .custom(let any as any SQLExpression): - any + any case .custom(let any as any CustomStringConvertible): - SQLRaw(any.description) - case .dictionary(_): fatalError("Dictionary database values are unimplemented for SQL") - case .custom(_): fatalError("Unsupported custom database value") + SQLRaw(any.description) + case .dictionary(_): fatalError("Dictionary database values are unimplemented for SQL") + case .custom(_): fatalError("Unsupported custom database value") } } } @@ -96,19 +95,23 @@ extension SQLInsertBuilder { @discardableResult public func fluentModels(_ models: [Model]) throws -> Self { var validColumns: [String] = [] - + for model in models { let pairs = model.encodeForSQL(withDefaultedValues: true) - + if validColumns.isEmpty { validColumns = pairs.map(\.0) self.columns(validColumns) } else { guard validColumns == pairs.map(\.0) else { - throw EncodingError.invalidValue(model, .init(codingPath: [], debugDescription: """ - One or more input Fluent models does not encode to the same set of columns. - """ - )) + throw EncodingError.invalidValue( + model, + .init( + codingPath: [], + debugDescription: """ + One or more input Fluent models does not encode to the same set of columns. + """ + )) } } self.values(pairs.map(\.1)) diff --git a/Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift b/Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift index 65ad2f36..3dfcf3d6 100644 --- a/Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift +++ b/Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift @@ -1,16 +1,16 @@ -import SQLKit import FluentKit +import SQLKit /// A thin deprecated wrapper around `SQLNestedSubpathExpression`. @available(*, deprecated, message: "Replaced by `SQLNestedSubpathExpression` in SQLKit") public struct SQLJSONColumnPath: SQLExpression { private var realExpression: SQLNestedSubpathExpression - + public var column: String { get { (self.realExpression.column as? SQLIdentifier)?.string ?? "" } set { self.realExpression.column = SQLIdentifier(newValue) } } - + public var path: [String] { get { self.realExpression.path } set { self.realExpression.path = newValue } diff --git a/Sources/FluentSQL/SQLList+Deprecated.swift b/Sources/FluentSQL/SQLList+Deprecated.swift index 07e1048a..602e20b8 100644 --- a/Sources/FluentSQL/SQLList+Deprecated.swift +++ b/Sources/FluentSQL/SQLList+Deprecated.swift @@ -32,7 +32,7 @@ extension SQLKit.SQLList { get { self.expressions } set { self.expressions = newValue } } - + @available(*, deprecated, message: "Use `init(_:separator:)` and include whitespace in the separator as needed instead.") public init(items: [any SQLExpression], separator: any SQLExpression) { self.init(items, separator: " \(separator) " as SQLQueryString) diff --git a/Sources/FluentSQL/SQLQueryConverter.swift b/Sources/FluentSQL/SQLQueryConverter.swift index 15ed156d..28c222a7 100644 --- a/Sources/FluentSQL/SQLQueryConverter.swift +++ b/Sources/FluentSQL/SQLQueryConverter.swift @@ -6,30 +6,30 @@ public struct SQLQueryConverter { public init(delegate: any SQLConverterDelegate) { self.delegate = delegate } - + public func convert(_ fluent: DatabaseQuery) -> any SQLExpression { let sql: any SQLExpression - + switch fluent.action { - 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 .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 } - + // MARK: Private - + private func delete(_ query: DatabaseQuery) -> any SQLExpression { 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: SQLKit.SQLQualifiedTable(query.schema, space: query.space)) @@ -38,11 +38,11 @@ public struct SQLQueryConverter { } update.values = query.fields.compactMap { field -> (any SQLExpression)? in let key: FieldKey - + switch field { - case let .path(path, schema) where schema == query.schema: + case .path(let path, let schema) where schema == query.schema: key = path[0] - case let .extendedPath(path, schema, space) where schema == query.schema && space == query.space: + case .extendedPath(let path, let schema, let space) where schema == query.schema && space == query.space: key = path[0] default: return nil @@ -55,10 +55,10 @@ public struct SQLQueryConverter { update.predicate = self.filters(query.filters) return update } - + private func select(_ query: DatabaseQuery) -> any SQLExpression { var select = SQLSelect() - + select.tables.append(SQLKit.SQLQualifiedTable(query.schema, space: query.space)) switch query.action { case .read: @@ -89,41 +89,43 @@ public struct SQLQueryConverter { } return select } - + private func insert(_ query: DatabaseQuery) -> any SQLExpression { 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 { + guard let firstInput = query.input.first, case .dictionary(let firstValues) = firstInput else { fatalError("Unexpected query input: \(query.input)") } - + // 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: + let keys = query.fields.compactMap { field -> FieldKey? in + switch field { + case .path(let path, let schema) where schema == query.schema: return path[0] - case let .extendedPath(path, schema, space) where schema == query.schema && space == query.space: + case .extendedPath(let path, let schema, let 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) } - + // 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 case .dictionary(let 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(_:)) } - + // 5. Provide the list of columns and the sets of inserted values to the actual query, always specifying in the // same order as the original field list. insert.columns = usedKeys.map { SQLColumn(self.key($0)) } @@ -131,7 +133,7 @@ public struct SQLQueryConverter { return insert } - + private func filters(_ filters: [DatabaseQuery.Filter]) -> (any SQLExpression)? { guard !filters.isEmpty else { return nil @@ -162,7 +164,7 @@ public struct SQLQueryConverter { custom(any) } } - + private func join(_ join: DatabaseQuery.Join) -> any SQLExpression { switch join { case .custom(let any): @@ -178,21 +180,21 @@ public struct SQLQueryConverter { self.joinCondition(space: space, schema: schema, alias: alias, method: method, filters: filters) } } - + private func joinCondition( space: String? = nil, schema: String, alias: String?, method: DatabaseQuery.Join.Method, filters: [DatabaseQuery.Filter] ) -> any SQLExpression { - let table: any SQLExpression = alias.map { - SQLAlias(SQLKit.SQLQualifiedTable(schema, space: space), as: SQLIdentifier($0)) - } ?? - SQLKit.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)) } - + private func joinMethod(_ method: DatabaseQuery.Join.Method) -> any SQLExpression { switch method { case .inner: @@ -203,7 +205,7 @@ public struct SQLQueryConverter { custom(any) } } - + private func field(_ field: DatabaseQuery.Field, aliased: Bool = false) -> any SQLExpression { switch field { case .custom(let any): @@ -214,10 +216,10 @@ public struct SQLQueryConverter { self.fieldPath(path, space: space, schema: schema, aliased: aliased) } } - + private func fieldPath(_ path: [FieldKey], space: String? = nil, schema: String, aliased: Bool) -> any SQLExpression { let field: any SQLExpression - + switch path.count { case 1: field = SQLColumn(SQLIdentifier(self.key(path[0])), table: SQLKit.SQLQualifiedTable(schema, space: space)) @@ -226,21 +228,21 @@ public struct SQLQueryConverter { default: fatalError("Field path must not be empty.") } - + if aliased { return SQLAlias(field, as: [space, schema, self.key(path[0])].compactMap({ $0 }).joined(separator: "_")) } else { return field } } - + private func aggregate(_ aggregate: DatabaseQuery.Aggregate, isUnique: Bool) -> any SQLExpression { switch aggregate { case .custom(let any): return custom(any) case .field(let field, let method): let name: String - + switch method { case .average: name = "AVG" case .count: name = "COUNT" @@ -249,7 +251,7 @@ public struct SQLQueryConverter { case .minimum: name = "MIN" case .custom(let custom): name = custom as! String } - + return SQLAlias( SQLFunction( name, @@ -261,7 +263,7 @@ public struct SQLQueryConverter { ) } } - + private func filter(_ filter: DatabaseQuery.Filter) -> any SQLExpression { switch filter { case .value(let field, let method, let value): @@ -331,7 +333,7 @@ public struct SQLQueryConverter { return SQLGroupExpression(expression) } } - + private func relation(_ relation: DatabaseQuery.Filter.Relation) -> any SQLExpression { switch relation { case .and: @@ -365,7 +367,7 @@ public struct SQLQueryConverter { custom(any) } } - + private func method(_ method: DatabaseQuery.Filter.Method) -> any SQLExpression { switch method { case .equality(let inverse): diff --git a/Sources/FluentSQL/SQLSchemaConverter.swift b/Sources/FluentSQL/SQLSchemaConverter.swift index 5001ada7..34866999 100644 --- a/Sources/FluentSQL/SQLSchemaConverter.swift +++ b/Sources/FluentSQL/SQLSchemaConverter.swift @@ -1,5 +1,5 @@ -import SQLKit import FluentKit +import SQLKit public protocol SQLConverterDelegate { func customDataType(_ dataType: DatabaseSchema.DataType) -> (any SQLExpression)? @@ -11,7 +11,7 @@ extension SQLConverterDelegate { public func nestedFieldExpression(_ column: String, _ path: [String]) -> any SQLExpression { SQLNestedSubpathExpression(column: column, path: path) } - + public func beforeConvert(_ schema: DatabaseSchema) -> DatabaseSchema { schema } @@ -19,14 +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) - + return switch schema.action { case .create: self.create(schema) @@ -36,12 +36,12 @@ public struct SQLSchemaConverter { self.update(schema) } } - + // MARK: Private 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,14 +53,14 @@ public struct SQLSchemaConverter { } return update } - + private func delete(_ schema: DatabaseSchema) -> any SQLExpression { SQLDropTable(table: self.name(schema.schema, space: schema.space)) } - + 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) @@ -70,11 +70,11 @@ public struct SQLSchemaConverter { } return create } - + private func name(_ string: String, space: String? = nil) -> any SQLExpression { SQLKit.SQLQualifiedTable(string, space: space) } - + private func constraint(_ constraint: DatabaseSchema.Constraint, table: String) -> any SQLExpression { switch constraint { case .constraint(let algorithm, let customName): @@ -118,7 +118,8 @@ public struct SQLSchemaConverter { return SQLDropTypedConstraint(name: SQLIdentifier(name), algorithm: .custom("")) case .custom(let any): if let fkeyExt = any as? DatabaseSchema.ConstraintDelete._ForeignKeyByNameExtension { - return SQLDropTypedConstraint(name: SQLIdentifier(fkeyExt.name), algorithm: .foreignKey([], "", [], onDelete: .noAction, onUpdate: .noAction)) + return SQLDropTypedConstraint( + name: SQLIdentifier(fkeyExt.name), algorithm: .foreignKey([], "", [], onDelete: .noAction, onUpdate: .noAction)) } return custom(any) } @@ -151,7 +152,6 @@ public struct SQLSchemaConverter { return "\(prefix):\(fieldsString)" } - private func foreignKeyAction(_ action: DatabaseSchema.ForeignKeyAction) -> SQLForeignKeyAction { switch action { case .noAction: @@ -166,7 +166,7 @@ public struct SQLSchemaConverter { .setDefault } } - + private func fieldDefinition(_ fieldDefinition: DatabaseSchema.FieldDefinition) -> any SQLExpression { switch fieldDefinition { case .custom(let any): @@ -191,7 +191,7 @@ public struct SQLSchemaConverter { ) } } - + private func fieldName(_ fieldName: DatabaseSchema.FieldName) -> any SQLExpression { switch fieldName { case .key(let key): @@ -200,12 +200,12 @@ public struct SQLSchemaConverter { custom(any) } } - + private func dataType(_ dataType: DatabaseSchema.DataType) -> any SQLExpression { if let custom = self.delegate.customDataType(dataType) { return custom } - + return switch dataType { case .bool: SQLDataType.int @@ -249,7 +249,7 @@ public struct SQLSchemaConverter { custom(any) } } - + private func fieldConstraint(_ fieldConstraint: DatabaseSchema.FieldConstraint) -> any SQLExpression { switch fieldConstraint { case .required: @@ -277,15 +277,15 @@ public struct SQLSchemaConverter { public struct SQLDropTypedConstraint: SQLExpression { public let name: any SQLExpression public let algorithm: DatabaseSchema.ConstraintAlgorithm - + public init(name: any SQLExpression, algorithm: DatabaseSchema.ConstraintAlgorithm) { self.name = name self.algorithm = algorithm } - + public func serialize(to serializer: inout SQLSerializer) { serializer.statement { - if $0.dialect.name == "mysql" { // TODO: Add an SQLDialect setting for this branch + if $0.dialect.name == "mysql" { // TODO: Add an SQLDialect setting for this branch // MySQL 5.7 does not support the type-generic "DROP CONSTRAINT" syntax. switch algorithm { case .foreignKey(_, _, _, _, _, _): diff --git a/Sources/XCTFluent/DummyDatabase.swift b/Sources/XCTFluent/DummyDatabase.swift index 7df9190a..29f64ce9 100644 --- a/Sources/XCTFluent/DummyDatabase.swift +++ b/Sources/XCTFluent/DummyDatabase.swift @@ -1,39 +1,41 @@ import FluentKit import Foundation -import NIOEmbedded -import NIOCore import NIOConcurrencyHelpers +import NIOCore +import NIOEmbedded public struct DummyDatabase: Database { public var context: DatabaseContext - + public init(context: DatabaseContext? = nil) { - self.context = context ?? .init( - configuration: DummyDatabaseConfiguration(middleware: []), - logger: .init(label: "codes.vapor.test"), - eventLoop: EmbeddedEventLoop() - ) + self.context = + context + ?? .init( + configuration: DummyDatabaseConfiguration(middleware: []), + logger: .init(label: "codes.vapor.test"), + eventLoop: EmbeddedEventLoop() + ) } public var inTransaction: Bool { false } - - public func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) -> EventLoopFuture { + + public func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) -> EventLoopFuture { for _ in 0..(_ closure: @escaping @Sendable(any Database) -> EventLoopFuture) -> EventLoopFuture { + public func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { closure(self) } - + public func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { closure(self) } - + public func execute(schema: DatabaseSchema) -> EventLoopFuture { self.eventLoop.makeSucceededFuture(()) } @@ -54,7 +56,7 @@ public struct DummyDatabaseConfiguration: DatabaseConfiguration { public final class DummyDatabaseDriver: DatabaseDriver { public let eventLoopGroup: any EventLoopGroup let didShutdown: NIOLockedValueBox - + public var fieldDecoder: any Decoder { DummyDecoder() } @@ -63,7 +65,7 @@ public final class DummyDatabaseDriver: DatabaseDriver { self.eventLoopGroup = eventLoopGroup self.didShutdown = .init(false) } - + public func makeDatabase(with context: DatabaseContext) -> any Database { DummyDatabase(context: context) } @@ -79,7 +81,7 @@ public final class DummyDatabaseDriver: DatabaseDriver { // MARK: Private public struct DummyRow: DatabaseOutput { - public init() { } + public init() {} public func schema(_ schema: String) -> any DatabaseOutput { self @@ -92,10 +94,9 @@ public struct DummyRow: DatabaseOutput { public func decodeNil(_ key: FieldKey) throws -> Bool { false } - + public func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T: Decodable - { + where T: Decodable { if T.self is UUID.Type { return UUID() as! T } else if T.self is Int.Type, key == .aggregate { @@ -108,7 +109,7 @@ public struct DummyRow: DatabaseOutput { public func contains(_ key: FieldKey) -> Bool { true } - + public var description: String { "" } @@ -118,34 +119,34 @@ private struct DummyDecoder: Decoder { var codingPath: [any CodingKey] { [] } - + var userInfo: [CodingUserInfoKey: Any] { [:] } - + init() { - + } - + struct KeyedDecoder: KeyedDecodingContainerProtocol { var codingPath: [any CodingKey] { [] } - + var allKeys: [Key] { [Key(stringValue: "test")!] } - + init() {} - + func contains(_ key: Key) -> Bool { false } - + func decodeNil(forKey key: Key) throws -> Bool { false } - + func decode(_ type: T.Type, forKey key: Key) throws -> T { if T.self is UUID.Type { return UUID() as! T @@ -153,24 +154,26 @@ private struct DummyDecoder: Decoder { return try T.init(from: DummyDecoder()) } } - - func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer { + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer< + NestedKey + > { .init(KeyedDecoder()) } - + func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer { UnkeyedDecoder() } - + func superDecoder() throws -> any Decoder { DummyDecoder() } - + func superDecoder(forKey key: Key) throws -> any Decoder { DummyDecoder() } } - + struct UnkeyedDecoder: UnkeyedDecodingContainer { var codingPath: [any CodingKey] var count: Int? @@ -181,101 +184,101 @@ private struct DummyDecoder: Decoder { return self.currentIndex >= count } var currentIndex: Int - + init() { self.codingPath = [] self.count = 1 self.currentIndex = 0 } - + mutating func decodeNil() throws -> Bool { true } - - mutating func decode(_ type: T.Type) throws -> T where T : Decodable { + + mutating func decode(_ type: T.Type) throws -> T where T: Decodable { try T.init(from: DummyDecoder()) } - + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer { KeyedDecodingContainer(KeyedDecoder()) } - + mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { UnkeyedDecoder() } - + mutating func superDecoder() throws -> any Decoder { DummyDecoder() } } - + struct SingleValueDecoder: SingleValueDecodingContainer { var codingPath: [any CodingKey] { [] } - + init() {} - + func decodeNil() -> Bool { false } - + func decode(_ type: Bool.Type) throws -> Bool { false } - + func decode(_ type: String.Type) throws -> String { "foo" } - + func decode(_ type: Double.Type) throws -> Double { 3.14 } - + func decode(_ type: Float.Type) throws -> Float { 1.59 } - + func decode(_ type: Int.Type) throws -> Int { -42 } - + func decode(_ type: Int8.Type) throws -> Int8 { -8 } - + func decode(_ type: Int16.Type) throws -> Int16 { -16 } - + func decode(_ type: Int32.Type) throws -> Int32 { -32 } - + func decode(_ type: Int64.Type) throws -> Int64 { -64 } - + func decode(_ type: UInt.Type) throws -> UInt { 42 } - + func decode(_ type: UInt8.Type) throws -> UInt8 { 8 } - + func decode(_ type: UInt16.Type) throws -> UInt16 { 16 } - + func decode(_ type: UInt32.Type) throws -> UInt32 { 32 } - + func decode(_ type: UInt64.Type) throws -> UInt64 { 64 } - + func decode(_ type: T.Type) throws -> T { if T.self is UUID.Type { return UUID() as! T @@ -284,15 +287,15 @@ private struct DummyDecoder: Decoder { } } } - + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { .init(KeyedDecoder()) } - + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { UnkeyedDecoder() } - + func singleValueContainer() throws -> any SingleValueDecodingContainer { SingleValueDecoder() } diff --git a/Sources/XCTFluent/TestDatabase.swift b/Sources/XCTFluent/TestDatabase.swift index a5eea206..bd917ae2 100644 --- a/Sources/XCTFluent/TestDatabase.swift +++ b/Sources/XCTFluent/TestDatabase.swift @@ -1,8 +1,8 @@ import FluentKit -import NIOEmbedded import Logging -import NIOCore import NIOConcurrencyHelpers +import NIOCore +import NIOEmbedded /// Lets you mock the row results for each query. /// @@ -54,13 +54,12 @@ public final class ArrayTestDatabase: TestDatabase { self.results.withLockedValue { $0.append(result) } } - public func append(_ result: [M]) - where M: Model - { + public func append(_ result: [M]) + where M: Model { self.results.withLockedValue { $0.append(result.map { TestOutput($0) }) } } - public func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) throws { + public func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) throws { guard !self.results.withLockedValue({ $0.isEmpty }) else { throw TestDatabaseError.ranOutOfResults } @@ -79,7 +78,7 @@ public final class CallbackTestDatabase: TestDatabase { self.callback = callback } - public func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) throws { + public func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) throws { for output in self.callback(query) { onOutput(output) } @@ -89,17 +88,18 @@ public final class CallbackTestDatabase: TestDatabase { public protocol TestDatabase: Sendable { func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) throws } extension TestDatabase { public var db: any Database { - self.database(context: .init( - configuration: self.configuration, - logger: Logger(label: "codes.vapor.fluent.test"), - eventLoop: EmbeddedEventLoop() - )) + self.database( + context: .init( + configuration: self.configuration, + logger: Logger(label: "codes.vapor.fluent.test"), + eventLoop: EmbeddedEventLoop() + )) } public func database(context: DatabaseContext) -> any Database { @@ -116,7 +116,7 @@ private struct _TestDatabase: Database { func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) -> EventLoopFuture { guard context.eventLoop.inEventLoop else { return self.eventLoop.flatSubmit { @@ -154,7 +154,6 @@ extension TestDatabase { } } - private struct _TestConfiguration: DatabaseConfiguration { let test: any TestDatabase var middleware: [any AnyModelMiddleware] = [] @@ -186,8 +185,7 @@ public struct TestOutput: DatabaseOutput { } public func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T: Decodable - { + where T: Decodable { if let res = dummyDecodedFields[key] as? T { return res } @@ -198,7 +196,6 @@ public struct TestOutput: DatabaseOutput { true } - public func nested(_ key: FieldKey) throws -> any DatabaseOutput { self } @@ -207,7 +204,6 @@ public struct TestOutput: DatabaseOutput { false } - public var description: String { "" } @@ -223,8 +219,7 @@ public struct TestOutput: DatabaseOutput { } public init(_ model: TestModel) - where TestModel: Model - { + where TestModel: Model { func unpack(_ dbValue: DatabaseQuery.Value) -> Any? { switch dbValue { case .null: @@ -258,11 +253,11 @@ public struct TestOutput: DatabaseOutput { private final class CollectInput: DatabaseInput { var storage: [FieldKey: DatabaseQuery.Value] - + init() { self.storage = [:] } - + func set(_ value: DatabaseQuery.Value, at key: FieldKey) { self.storage[key] = value } diff --git a/Tests/FluentKitTests/AsyncTests/AsyncFilterQueryTests.swift b/Tests/FluentKitTests/AsyncTests/AsyncFilterQueryTests.swift index 008c33cd..f917ab86 100644 --- a/Tests/FluentKitTests/AsyncTests/AsyncFilterQueryTests.swift +++ b/Tests/FluentKitTests/AsyncTests/AsyncFilterQueryTests.swift @@ -1,8 +1,8 @@ -import FluentKit import FluentBenchmark -import XCTest -import Foundation +import FluentKit import FluentSQL +import Foundation +import XCTest final class AsyncFilterQueryTests: XCTestCase { override class func setUp() { @@ -15,7 +15,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$status == .done).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" = 'done'"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" = 'done'"# + ) db.reset() } @@ -23,7 +26,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$status != .done).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"# + ) db.reset() } @@ -31,7 +37,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$status ~~ [.done, .notDone]).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"# + ) db.reset() } @@ -39,7 +48,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$status !~ [.done, .notDone]).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"# + ) db.reset() } @@ -48,7 +60,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$optionalStatus == .done).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" = 'done'"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" = 'done'"# + ) db.reset() } @@ -56,7 +71,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$optionalStatus != .done).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" <> 'done'"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" <> 'done'"# + ) db.reset() } @@ -64,7 +82,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$optionalStatus ~~ [.done, .notDone]).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" IN ('done','notDone')"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" IN ('done','notDone')"# + ) db.reset() } @@ -72,7 +93,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$optionalStatus !~ [.done, .notDone]).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" NOT IN ('done','notDone')"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" NOT IN ('done','notDone')"# + ) db.reset() } @@ -81,7 +105,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$description == "hello").all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" = $1"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" = $1"# + ) db.reset() } @@ -89,7 +116,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$description != "hello").all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" <> $1"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" <> $1"# + ) db.reset() } @@ -97,7 +127,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$description ~~ ["hello"]).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"# + ) db.reset() } @@ -105,7 +138,10 @@ final class AsyncFilterQueryTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Task.query(on: db).filter(\.$description !~ ["hello"]).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"# + ) db.reset() } } diff --git a/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift b/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift index 3fcf9199..40401fe6 100644 --- a/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift +++ b/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift @@ -1,9 +1,9 @@ -import FluentKit import FluentBenchmark -import XCTest -import Foundation +import FluentKit import FluentSQL +import Foundation import XCTFluent +import XCTest final class AsyncFluentKitTests: XCTestCase { override class func setUp() { @@ -90,7 +90,8 @@ final class AsyncFluentKitTests: XCTestCase { _ = try await Planet.query(on: db).join(siblings: \Planet.$tags).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "planet+tag" ON "planet+tag"."planet_id" = "planets"."id""#), true) + XCTAssertEqual( + db.sqlSerializers.first?.sql.contains(#"INNER JOIN "planet+tag" ON "planet+tag"."planet_id" = "planets"."id""#), true) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "tags" ON "planet+tag"."tag_id" = "tags"."id""#), true) db.reset() } @@ -100,7 +101,10 @@ final class AsyncFluentKitTests: XCTestCase { _ = try await Planet.query(on: db).all(\.$name) XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() } @@ -109,7 +113,10 @@ final class AsyncFluentKitTests: XCTestCase { _ = try await Planet.query(on: db).unique().all(\.$name) XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT DISTINCT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT DISTINCT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() _ = try await Planet.query(on: db).unique().all() @@ -120,13 +127,19 @@ final class AsyncFluentKitTests: XCTestCase { db.fakedRows.append([.init(["aggregate": 1])]) _ = try await Planet.query(on: db).unique().count(\.$name) XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT COUNT(DISTINCT "planets"."name") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT COUNT(DISTINCT "planets"."name") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() db.fakedRows.append([.init(["aggregate": 1])]) _ = try await Planet.query(on: db).unique().aggregate(.sum, \.$id, as: Int.self) XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT SUM(DISTINCT "planets"."id") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT SUM(DISTINCT "planets"."id") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() } @@ -172,7 +185,9 @@ final class AsyncFluentKitTests: XCTestCase { .create() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"#) db.reset() try await db.schema("planets") @@ -180,7 +195,9 @@ final class AsyncFluentKitTests: XCTestCase { .create() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"#) } func testMultipleFieldConstraint() async throws { @@ -190,7 +207,8 @@ final class AsyncFluentKitTests: XCTestCase { .create() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY)"#) } func testUniqueTableConstraint() async throws { @@ -211,7 +229,9 @@ final class AsyncFluentKitTests: XCTestCase { .create() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("id" BIGINT, "name" TEXT, CONSTRAINT "uq:planets.id+planets.name" UNIQUE ("id", "name"))"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("id" BIGINT, "name" TEXT, CONSTRAINT "uq:planets.id+planets.name" UNIQUE ("id", "name"))"#) } func testForeignKeyTableConstraint() async throws { @@ -222,7 +242,10 @@ final class AsyncFluentKitTests: XCTestCase { .create() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"# + ) db.reset() try await db.schema("planets") @@ -236,7 +259,10 @@ final class AsyncFluentKitTests: XCTestCase { .create() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"# + ) } func testIfNotExistsTableCreate() async throws { @@ -246,7 +272,6 @@ final class AsyncFluentKitTests: XCTestCase { .ignoreExisting() .create() - XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE IF NOT EXISTS "planets" ("galaxy_id" BIGINT)"#) db.reset() @@ -255,7 +280,6 @@ final class AsyncFluentKitTests: XCTestCase { .field("galaxy_id", .int64) .create() - XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT)"#) } @@ -269,7 +293,8 @@ final class AsyncFluentKitTests: XCTestCase { func testPlanet2FilterPlaceholder1() async throws { let db = DummyDatabaseForTestSQLSerializer() db.fakedRows.append([.init(["aggregate": 1])]) - _ = try await Planet2 + _ = + try await Planet2 .query(on: db) .filter(\.$nickName != "first") .count() @@ -284,7 +309,8 @@ final class AsyncFluentKitTests: XCTestCase { func testPlanet2FilterPlaceholder2() async throws { let db = DummyDatabaseForTestSQLSerializer() db.fakedRows.append([.init(["aggregate": 1])]) - _ = try await Planet2 + _ = + try await Planet2 .query(on: db) .filter(\.$nickName != nil) .count() @@ -299,7 +325,8 @@ final class AsyncFluentKitTests: XCTestCase { func testPlanet2FilterPlaceholder3() async throws { let db = DummyDatabaseForTestSQLSerializer() db.fakedRows.append([.init(["aggregate": 1])]) - _ = try await Planet2 + _ = + try await Planet2 .query(on: db) .filter(\.$nickName != "first") .filter(\.$nickName == "second") @@ -308,7 +335,8 @@ final class AsyncFluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) let result: String = (db.sqlSerializers.first?.sql)! - let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" = $2 AND "planets"."nickname" <> $3"# + let expected = + #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" = $2 AND "planets"."nickname" <> $3"# XCTAssertEqual(result, expected) db.reset() } @@ -316,7 +344,8 @@ final class AsyncFluentKitTests: XCTestCase { func testPlanet2FilterPlaceholder4() async throws { let db = DummyDatabaseForTestSQLSerializer() db.fakedRows.append([.init(["aggregate": 1])]) - _ = try await Planet2 + _ = + try await Planet2 .query(on: db) .filter(\.$nickName != "first") .filter(\.$nickName != nil) @@ -325,7 +354,8 @@ final class AsyncFluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) let result: String = (db.sqlSerializers.first?.sql)! - let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" IS NOT NULL AND "planets"."nickname" = $2"# + let expected = + #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" IS NOT NULL AND "planets"."nickname" = $2"# XCTAssertEqual(result, expected) db.reset() } @@ -334,7 +364,7 @@ final class AsyncFluentKitTests: XCTestCase { final class DGOFoo: Model, @unchecked Sendable { static let schema = "foos" @ID(custom: .id) var id: Int? - init() { } + init() {} init(id: Int?) { self.id = id } @@ -363,16 +393,18 @@ final class AsyncFluentKitTests: XCTestCase { func testPaginationDoesNotCrashWithNegativeNumbers() async throws { let db = DummyDatabaseForTestSQLSerializer() - + db.fakedRows.append([.init(["aggregate": 1])]) let pageRequest1 = PageRequest(page: -1, per: 10) - _ = try await Planet2 + _ = + try await Planet2 .query(on: db) .paginate(pageRequest1) db.fakedRows.append([.init(["aggregate": 1])]) let pageRequest2 = PageRequest(page: 1, per: -10) - _ = try await Planet2 + _ = + try await Planet2 .query(on: db) .paginate(pageRequest2) } diff --git a/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift b/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift index 1742880c..5d5a85bc 100644 --- a/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift +++ b/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift @@ -1,5 +1,5 @@ -import FluentKit import FluentBenchmark +import FluentKit import Foundation import SQLKit import XCTFluent @@ -27,7 +27,7 @@ final class AsyncQueryBuilderTests: XCTestCase { TestOutput([ "id": planet.id as any Sendable, "name": planet.name, - "star_id": UUID() + "star_id": UUID(), ]) ]) @@ -44,13 +44,13 @@ final class AsyncQueryBuilderTests: XCTestCase { TestOutput([ "id": planet.id as any Sendable, "name": planet.name, - "star_id": UUID() + "star_id": UUID(), ]), TestOutput([ "id": UUID(), "name": "Nupeter", - "star_id": UUID() - ]) + "star_id": UUID(), + ]), ]) let retrievedPlanet = try await Planet.query(on: test.db).first() @@ -64,7 +64,7 @@ final class AsyncQueryBuilderTests: XCTestCase { let planets = [ Planet(id: UUID(), name: "P1", starId: starId), Planet(id: UUID(), name: "P2", starId: starId), - Planet(id: UUID(), name: "P3", starId: starId) + Planet(id: UUID(), name: "P3", starId: starId), ] let test = ArrayTestDatabase() test.append(planets.map(TestOutput.init)) @@ -80,10 +80,11 @@ final class AsyncQueryBuilderTests: XCTestCase { let planets = [ Planet(id: UUID(), name: "P1", starId: starId), Planet(id: UUID(), name: "P2", starId: starId), - Planet(id: UUID(), name: "P3", starId: starId) + Planet(id: UUID(), name: "P3", starId: starId), ] let test = ArrayTestDatabase() - let db = test.database(context: .init(configuration: test.configuration, logger: test.db.logger, eventLoop: test.db.eventLoop, history: .init())) + let db = test.database( + context: .init(configuration: test.configuration, logger: test.db.logger, eventLoop: test.db.eventLoop, history: .init())) test.append(planets.map(TestOutput.init)) let retrievedPlanets = try await Planet.query(on: db).all() @@ -105,11 +106,10 @@ final class AsyncQueryBuilderTests: XCTestCase { let test = CallbackTestDatabase { query in XCTAssertEqual(query.schema, "planets") let result: [TestOutput] - if - let limit = query.limits.first, - case let DatabaseQuery.Limit.count(limitValue) = limit, + if let limit = query.limits.first, + case DatabaseQuery.Limit.count(let limitValue) = limit, let offset = query.offsets.first, - case let DatabaseQuery.Offset.count(offsetValue) = offset + case DatabaseQuery.Offset.count(let offsetValue) = offset { result = [TestOutput](rows[min(offsetValue, rows.count - 1).. "planets"."name" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"# + ) } } diff --git a/Tests/FluentKitTests/CompositeIDTests.swift b/Tests/FluentKitTests/CompositeIDTests.swift index 3d52a894..a860f543 100644 --- a/Tests/FluentKitTests/CompositeIDTests.swift +++ b/Tests/FluentKitTests/CompositeIDTests.swift @@ -1,10 +1,10 @@ -import FluentKit import FluentBenchmark -import XCTest -import Foundation +import FluentKit import FluentSQL -import XCTFluent +import Foundation import NIOCore +import XCTFluent +import XCTest extension Collection { func xctAt(_ idx: Self.Index, file: StaticString = #fileID, line: UInt = #line) throws -> Self.Element { @@ -25,85 +25,120 @@ final class CompositeIDTests: XCTestCase { planetID: .init(uuidString: "00000000-0000-0000-0000-000000000000")!, tagID: .init(uuidString: "11111111-1111-1111-1111-111111111111")! ) - + model.notation = "composition" _ = try model.create(on: db).wait() XCTAssertTrue(model.$id.exists) - + Thread.sleep(forTimeInterval: 0.1) - + model.notation = "revision" XCTAssertTrue(model.hasChanges) try model.update(on: db).wait() XCTAssertNotEqual(model.createdAt, model.updatedAt) XCTAssertFalse(model.hasChanges) - + model.$id.$planet.id = .init(uuidString: "22222222-2222-2222-2222-222222222222")! XCTAssertTrue(model.hasChanges) try model.update(on: db).wait() XCTAssertNotEqual(model.createdAt, model.updatedAt) XCTAssertFalse(model.hasChanges) - + try model.delete(force: false, on: db).wait() try model.restore(on: db).wait() try model.delete(force: true, on: db).wait() XCTAssertEqual(db.sqlSerializers.count, 6) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, $3, $4, $5, DEFAULT)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(1).sql, #"UPDATE "composite+planet+tag" SET "notation" = $1, "updatedAt" = $2 WHERE ("composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(2).sql, #"UPDATE "composite+planet+tag" SET "planet_id" = $1, "updatedAt" = $2 WHERE ("composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(3).sql, #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = $2 WHERE ("composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(4).sql, #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = NULL WHERE ("composite+planet+tag"."planet_id" = $2 AND "composite+planet+tag"."tag_id" = $3)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(5).sql, #"DELETE FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2)"#) + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, $3, $4, $5, DEFAULT)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(1).sql, + #"UPDATE "composite+planet+tag" SET "notation" = $1, "updatedAt" = $2 WHERE ("composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(2).sql, + #"UPDATE "composite+planet+tag" SET "planet_id" = $1, "updatedAt" = $2 WHERE ("composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(3).sql, + #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = $2 WHERE ("composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(4).sql, + #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = NULL WHERE ("composite+planet+tag"."planet_id" = $2 AND "composite+planet+tag"."tag_id" = $3)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(5).sql, + #"DELETE FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2)"#) } - + func testCompositeModelQuery() throws { let db = DummyDatabaseForTestSQLSerializer() let model = CompositePlanetTag( planetID: .init(uuidString: "00000000-0000-0000-0000-000000000000")!, tagID: .init(uuidString: "11111111-1111-1111-1111-111111111111")! ) - + _ = try CompositePlanetTag.find(.init(planetID: model.id!.$planet.id, tagID: model.id!.$tag.id), on: db).wait() - _ = try CompositePlanetTag.query(on: db).filter(\.$id.$planet.$id == model.id!.$planet.id).filter(\.$id.$tag.$id == model.id!.$tag.id).all().wait() + _ = try CompositePlanetTag.query(on: db).filter(\.$id.$planet.$id == model.id!.$planet.id).filter( + \.$id.$tag.$id == model.id!.$tag.id + ).all().wait() _ = try CompositePlanetTag.query(on: db).filter(\.$id.$planet.$id == model.id!.$planet.id).all().wait() _ = try CompositePlanetTag.query(on: db).filter(\.$id.$tag.$id == model.id!.$tag.id).withDeleted().all().wait() XCTAssertEqual(db.sqlSerializers.count, 4) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3) LIMIT 1"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(1).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(2).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(3).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."tag_id" = $1"#) + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3) LIMIT 1"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(1).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(2).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(3).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."tag_id" = $1"# + ) } - + func testCompositeIDMigration() throws { let db = DummyDatabaseForTestSQLSerializer() try CompositePlanetTagMigration().prepare(on: db).wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "composite+planet+tag" ("planet_id" UUID NOT NULL REFERENCES "planets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, "tag_id" UUID NOT NULL REFERENCES "tags" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, "notation" TEXT NOT NULL, "createdAt" TIMESTAMPTZ, "updatedAt" TIMESTAMPTZ, "deletedAt" TIMESTAMPTZ, PRIMARY KEY ("planet_id", "tag_id"))"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "composite+planet+tag" ("planet_id" UUID NOT NULL REFERENCES "planets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, "tag_id" UUID NOT NULL REFERENCES "tags" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, "notation" TEXT NOT NULL, "createdAt" TIMESTAMPTZ, "updatedAt" TIMESTAMPTZ, "deletedAt" TIMESTAMPTZ, PRIMARY KEY ("planet_id", "tag_id"))"# + ) } - + func testCompositeIDRelations() throws { let db = DummyDatabaseForTestSQLSerializer() let model = CompositePlanetTag( planetID: .init(uuidString: "00000000-0000-0000-0000-000000000000")!, tagID: .init(uuidString: "11111111-1111-1111-1111-111111111111")! ) - let planet = PlanetUsingCompositePivot(id: model.$id.$planet.id, name: "Planet", starId: .init(uuidString: "22222222-2222-2222-2222-222222222222")!) + let planet = PlanetUsingCompositePivot( + id: model.$id.$planet.id, name: "Planet", starId: .init(uuidString: "22222222-2222-2222-2222-222222222222")!) planet.$planetTags.fromId = planet.id! planet.$tags.fromId = planet.id! let tag = Tag(id: .init(uuidString: "33333333-3333-3333-3333-333333333333")!, name: "Tag") - + db.fakedRows.append([.init(["id": UUID()])]) _ = try model.$id.$planet.get(on: db).wait() db.fakedRows.append([.init(["id": UUID()])]) _ = try planet.$planetTags.get(on: db).wait() db.fakedRows.append([.init(["id": UUID()])]) _ = try planet.$tags.get(on: db).wait() - + db.fakedRows.append([.init(["id": UUID()])]) try planet.$planetTags.create(model, on: db).wait() db.fakedRows.append([.init(["id": UUID()])]) @@ -116,57 +151,103 @@ final class CompositeIDTests: XCTestCase { try planet.$tags.detach(tag, on: db).wait() db.fakedRows.append([.init(["id": UUID()])]) try planet.$tags.detachAll(on: db).wait() - + XCTAssertEqual(db.sqlSerializers.count, 9) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id" FROM "planets" WHERE "planets"."id" = $1 LIMIT 1"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(1).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(2).sql, #"SELECT "tags"."id" AS "tags_id", "tags"."name" AS "tags_name", "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "tags" INNER JOIN "composite+planet+tag" ON "tags"."id" = "composite+planet+tag"."tag_id" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(3).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(4).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(5).sql, #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(6).sql, #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(7).sql, #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = $2 WHERE "composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(8).sql, #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = $2 WHERE "composite+planet+tag"."planet_id" = $3 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $4)"#) + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id" FROM "planets" WHERE "planets"."id" = $1 LIMIT 1"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(1).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(2).sql, + #"SELECT "tags"."id" AS "tags_id", "tags"."name" AS "tags_name", "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "tags" INNER JOIN "composite+planet+tag" ON "tags"."id" = "composite+planet+tag"."tag_id" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(3).sql, + #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(4).sql, + #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(5).sql, + #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(6).sql, + #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(7).sql, + #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = $2 WHERE "composite+planet+tag"."planet_id" = $3 AND "composite+planet+tag"."tag_id" = $4 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $5)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(8).sql, + #"UPDATE "composite+planet+tag" SET "updatedAt" = $1, "deletedAt" = $2 WHERE "composite+planet+tag"."planet_id" = $3 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $4)"# + ) } - + func testSiblingAttachWithAsyncEditClosures() async throws { let db = DummyDatabaseForTestSQLSerializer() - + let model = CompositePlanetTag( planetID: .init(uuidString: "00000000-0000-0000-0000-000000000000")!, tagID: .init(uuidString: "11111111-1111-1111-1111-111111111111")! ) - let planet = PlanetUsingCompositePivot(id: model.$id.$planet.id, name: "Planet", starId: .init(uuidString: "22222222-2222-2222-2222-222222222222")!) + let planet = PlanetUsingCompositePivot( + id: model.$id.$planet.id, name: "Planet", starId: .init(uuidString: "22222222-2222-2222-2222-222222222222")!) planet.$planetTags.fromId = planet.id! planet.$tags.fromId = planet.id! let tag = Tag(id: .init(uuidString: "33333333-3333-3333-3333-333333333333")!, name: "Tag") - + db.fakedRows.append([.init(["id": UUID()])]) try await planet.$planetTags.create(model, on: db) db.fakedRows.append([.init(["id": UUID()])]) try await planet.$tags.attach([tag], on: db) { pivot in - _ = try await Planet.query(on: db).all() // just to make there be something async happening + _ = try await Planet.query(on: db).all() // just to make there be something async happening pivot.notation = "notation" } db.fakedRows.append([.init(["id": UUID()])]) try await planet.$tags.attach(tag, on: db) { pivot in - _ = try await Planet.query(on: db).all() // just to make there be something async happening + _ = try await Planet.query(on: db).all() // just to make there be something async happening pivot.notation = "notation" } db.fakedRows.append(contentsOf: [[.init(["aggregate": 1])], [.init(["id": UUID()])]]) try await planet.$tags.attach(tag, method: .ifNotExists, on: db) { pivot in - _ = try await Planet.query(on: db).all() // just to make there be something async happening + _ = try await Planet.query(on: db).all() // just to make there be something async happening pivot.notation = "notation" } - + XCTAssertEqual(db.sqlSerializers.count, 6) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(1).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(2).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, $3, $4, $5, DEFAULT)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(3).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(4).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, $3, $4, $5, DEFAULT)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(5).sql, #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) - + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(1).sql, + #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(2).sql, + #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, $3, $4, $5, DEFAULT)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(3).sql, + #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(4).sql, + #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, $3, $4, $5, DEFAULT)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(5).sql, + #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"# + ) + planet.$tags.fromId = nil XCTAssertThrowsError(try planet.$tags.attach(tag, on: db).wait()) { guard case .owningModelIdRequired(_) = $0 as? SiblingsPropertyError else { @@ -177,42 +258,79 @@ final class CompositeIDTests: XCTestCase { func testCompositeIDFilterByID() throws { let db = DummyDatabaseForTestSQLSerializer() - let planetId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, - tagId = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! - - _ = try CompositePlanetTag.query(on: db).filter(\.$id == CompositePlanetTag.IDValue.init(planetID: planetId, tagID: tagId)).all().wait() - _ = try CompositePlanetTag.query(on: db).filter(\.$id != CompositePlanetTag.IDValue.init(planetID: planetId, tagID: tagId)).all().wait() - + let planetId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let tagId = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! + + _ = try CompositePlanetTag.query(on: db).filter(\.$id == CompositePlanetTag.IDValue.init(planetID: planetId, tagID: tagId)).all() + .wait() + _ = try CompositePlanetTag.query(on: db).filter(\.$id != CompositePlanetTag.IDValue.init(planetID: planetId, tagID: tagId)).all() + .wait() + XCTAssertEqual(db.sqlSerializers.count, 2) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(1).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" <> $1 OR "composite+planet+tag"."tag_id" <> $2) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"# + ) + XCTAssertEqual( + try db.sqlSerializers.xctAt(1).sql, + #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE ("composite+planet+tag"."planet_id" <> $1 OR "composite+planet+tag"."tag_id" <> $2) AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"# + ) } - + func testCompositeParentAndChildQuerying() throws { let db = DummyDatabaseForTestSQLSerializer() let systemId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! - + _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1).$orbiting.query(on: db).all().wait() - _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: .init(solarSystemId: systemId, normalizedOrdinal: 2)).$progenitor.query(on: db).all().wait() - _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: nil).$progenitor.query(on: db).all().wait() - _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: nil, planetoidId: .init(solarSystemId: systemId, normalizedOrdinal: 3)).$planetoid.query(on: db).all().wait() - _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: nil, planetoidId: nil).$planetoid.query(on: db).all().wait() + _ = try CompositeMoon( + name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, + progenitorId: .init(solarSystemId: systemId, normalizedOrdinal: 2) + ).$progenitor.query(on: db).all().wait() + _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: nil).$progenitor.query( + on: db + ).all().wait() + _ = try CompositeMoon( + name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: nil, + planetoidId: .init(solarSystemId: systemId, normalizedOrdinal: 3) + ).$planetoid.query(on: db).all().wait() + _ = try CompositeMoon(name: "", planetSolarSystemId: systemId, planetNormalizedOrdinal: 1, progenitorId: nil, planetoidId: nil) + .$planetoid.query(on: db).all().wait() _ = try CompositePlanet(name: "", solarSystemId: systemId, normalizedOrdinal: 1).$moons.query(on: db).all().wait() _ = try CompositePlanet(name: "", solarSystemId: systemId, normalizedOrdinal: 2).$moonsMade.query(on: db).all().wait() _ = try CompositePlanet(name: "", solarSystemId: systemId, normalizedOrdinal: 3).$fragment.query(on: db).all().wait() - - let allPlanetFields = #""composite+planet"."system_id" AS "composite+planet_system_id", "composite+planet"."nrm_ord" AS "composite+planet_nrm_ord", "composite+planet"."name" AS "composite+planet_name" FROM "composite+planet""# - let allMoonFields = #""composite+moon"."id" AS "composite+moon_id", "composite+moon"."name" AS "composite+moon_name", "composite+moon"."planet_system_id" AS "composite+moon_planet_system_id", "composite+moon"."planet_nrm_ord" AS "composite+moon_planet_nrm_ord", "composite+moon"."progenitorSystem_id" AS "composite+moon_progenitorSystem_id", "composite+moon"."progenitorNrm_ord" AS "composite+moon_progenitorNrm_ord", "composite+moon"."planetoid_system_id" AS "composite+moon_planetoid_system_id", "composite+moon"."planetoid_nrm_ord" AS "composite+moon_planetoid_nrm_ord" FROM "composite+moon""# - + + let allPlanetFields = + #""composite+planet"."system_id" AS "composite+planet_system_id", "composite+planet"."nrm_ord" AS "composite+planet_nrm_ord", "composite+planet"."name" AS "composite+planet_name" FROM "composite+planet""# + let allMoonFields = + #""composite+moon"."id" AS "composite+moon_id", "composite+moon"."name" AS "composite+moon_name", "composite+moon"."planet_system_id" AS "composite+moon_planet_system_id", "composite+moon"."planet_nrm_ord" AS "composite+moon_planet_nrm_ord", "composite+moon"."progenitorSystem_id" AS "composite+moon_progenitorSystem_id", "composite+moon"."progenitorNrm_ord" AS "composite+moon_progenitorNrm_ord", "composite+moon"."planetoid_system_id" AS "composite+moon_planetoid_system_id", "composite+moon"."planetoid_nrm_ord" AS "composite+moon_planetoid_nrm_ord" FROM "composite+moon""# + let expectedQueries: [(String, [any Encodable])] = [ - (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" = $1 AND "composite+planet"."nrm_ord" = $2)"#, [systemId, 1]), - (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" = $1 AND "composite+planet"."nrm_ord" = $2)"#, [systemId, 2]), - (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" IS NULL AND "composite+planet"."nrm_ord" IS NULL)"#, []), - (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" = $1 AND "composite+planet"."nrm_ord" = $2)"#, [systemId, 3]), - (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" IS NULL AND "composite+planet"."nrm_ord" IS NULL)"#, []), - (#"SELECT \#(allMoonFields) WHERE ("composite+moon"."planet_system_id" = $1 AND "composite+moon"."planet_nrm_ord" = $2)"#, [systemId, 1]), - (#"SELECT \#(allMoonFields) WHERE ("composite+moon"."progenitorSystem_id" = $1 AND "composite+moon"."progenitorNrm_ord" = $2)"#, [systemId, 2]), - (#"SELECT \#(allMoonFields) WHERE ("composite+moon"."planetoid_system_id" = $1 AND "composite+moon"."planetoid_nrm_ord" = $2)"#, [systemId, 3]), + ( + #"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" = $1 AND "composite+planet"."nrm_ord" = $2)"#, + [systemId, 1] + ), + ( + #"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" = $1 AND "composite+planet"."nrm_ord" = $2)"#, + [systemId, 2] + ), + (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" IS NULL AND "composite+planet"."nrm_ord" IS NULL)"#, []), + ( + #"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" = $1 AND "composite+planet"."nrm_ord" = $2)"#, + [systemId, 3] + ), + (#"SELECT \#(allPlanetFields) WHERE ("composite+planet"."system_id" IS NULL AND "composite+planet"."nrm_ord" IS NULL)"#, []), + ( + #"SELECT \#(allMoonFields) WHERE ("composite+moon"."planet_system_id" = $1 AND "composite+moon"."planet_nrm_ord" = $2)"#, + [systemId, 1] + ), + ( + #"SELECT \#(allMoonFields) WHERE ("composite+moon"."progenitorSystem_id" = $1 AND "composite+moon"."progenitorNrm_ord" = $2)"#, + [systemId, 2] + ), + ( + #"SELECT \#(allMoonFields) WHERE ("composite+moon"."planetoid_system_id" = $1 AND "composite+moon"."planetoid_nrm_ord" = $2)"#, + [systemId, 3] + ), ] XCTAssertEqual(db.sqlSerializers.count, expectedQueries.count) for ((query, binds), serializer) in zip(expectedQueries, db.sqlSerializers) { @@ -223,46 +341,70 @@ final class CompositeIDTests: XCTestCase { } } } - + func testCompositeParentChildMutating() throws { let db = DummyDatabaseForTestSQLSerializer() - let sysId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, sys2Id = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! - + let sysId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let sys2Id = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! + let planet1 = CompositePlanet(name: "A", solarSystemId: sysId, normalizedOrdinal: 1) let moon1 = CompositeMoon(name: "B", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1) - let moon2 = CompositeMoon(name: "C", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1, progenitorId: .init(solarSystemId: sysId, normalizedOrdinal: 2)) + let moon2 = CompositeMoon( + name: "C", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1, + progenitorId: .init(solarSystemId: sysId, normalizedOrdinal: 2)) let moon3 = CompositeMoon(name: "D", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1) - let moon4 = CompositeMoon(name: "E", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1, planetoidId: .init(solarSystemId: sysId, normalizedOrdinal: 3)) - - db.fakedRows.append(contentsOf: [[.init(["id": UUID()])], [.init(["id": UUID()])], [.init(["id": UUID()])], [.init(["id": UUID()])], [.init(["id": UUID()])]]) + let moon4 = CompositeMoon( + name: "E", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1, + planetoidId: .init(solarSystemId: sysId, normalizedOrdinal: 3)) + + db.fakedRows.append(contentsOf: [ + [.init(["id": UUID()])], [.init(["id": UUID()])], [.init(["id": UUID()])], [.init(["id": UUID()])], [.init(["id": UUID()])], + ]) try planet1.create(on: db).wait() try [moon1, moon2, moon3, moon4].forEach { try $0.create(on: db).wait() } - + planet1.name = "AA" try planet1.update(on: db).wait() - + moon1.$orbiting.id.$solarSystem.id = sys2Id moon1.$orbiting.id.normalizedOrdinal = 2 moon2.$progenitor.id = nil moon3.$planetoid.id = .init(solarSystemId: sys2Id, normalizedOrdinal: 3) moon4.$planetoid.id = nil try [moon1, moon2, moon3, moon4].forEach { try $0.update(on: db).wait() } - - let moonCols = #""id", "name", "planet_system_id", "planet_nrm_ord", "progenitorSystem_id", "progenitorNrm_ord", "planetoid_system_id", "planetoid_nrm_ord""# + + let moonCols = + #""id", "name", "planet_system_id", "planet_nrm_ord", "progenitorSystem_id", "progenitorNrm_ord", "planetoid_system_id", "planetoid_nrm_ord""# let fourVals = "$1, $2, $3, $4, NULL, NULL, NULL, NULL" - let sixVals1 = "$1, $2, $3, $4, $5, $6, NULL, NULL", sixVals2 = "$1, $2, $3, $4, NULL, NULL, $5, $6" + let sixVals1 = "$1, $2, $3, $4, $5, $6, NULL, NULL" + let sixVals2 = "$1, $2, $3, $4, NULL, NULL, $5, $6" let expectedQueries: [(String, [any Encodable], UInt)] = [ - (#"INSERT INTO "composite+planet" ("system_id", "nrm_ord", "name") VALUES ($1, $2, $3)"#, [sysId, 1, "A"], #line), - (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(fourVals))"#, [moon1.id!, "B", sysId, 1], #line), - (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(sixVals1))"#, [moon2.id!, "C", sysId, 1, sysId, 2], #line), - (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(fourVals))"#, [moon3.id!, "D", sysId, 1], #line), - (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(sixVals2))"#, [moon4.id!, "E", sysId, 1, sysId, 3], #line), - (#"UPDATE "composite+planet" SET "name" = $1 WHERE ("composite+planet"."system_id" = $2 AND "composite+planet"."nrm_ord" = $3)"#, ["AA", sysId, 1], #line), - (#"UPDATE "composite+moon" SET "planet_system_id" = $1, "planet_nrm_ord" = $2 WHERE "composite+moon"."id" = $3"#, [sys2Id, 2, moon1.id!], #line), - (#"UPDATE "composite+moon" SET "progenitorSystem_id" = NULL, "progenitorNrm_ord" = NULL WHERE "composite+moon"."id" = $1"#, [moon2.id!], #line), - (#"UPDATE "composite+moon" SET "planetoid_system_id" = $1, "planetoid_nrm_ord" = $2 WHERE "composite+moon"."id" = $3"#, [sys2Id, 3, moon3.id!], #line), - (#"UPDATE "composite+moon" SET "planetoid_system_id" = NULL, "planetoid_nrm_ord" = NULL WHERE "composite+moon"."id" = $1"#, [moon4.id!], #line), + (#"INSERT INTO "composite+planet" ("system_id", "nrm_ord", "name") VALUES ($1, $2, $3)"#, [sysId, 1, "A"], #line), + (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(fourVals))"#, [moon1.id!, "B", sysId, 1], #line), + (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(sixVals1))"#, [moon2.id!, "C", sysId, 1, sysId, 2], #line), + (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(fourVals))"#, [moon3.id!, "D", sysId, 1], #line), + (#"INSERT INTO "composite+moon" (\#(moonCols)) VALUES (\#(sixVals2))"#, [moon4.id!, "E", sysId, 1, sysId, 3], #line), + ( + #"UPDATE "composite+planet" SET "name" = $1 WHERE ("composite+planet"."system_id" = $2 AND "composite+planet"."nrm_ord" = $3)"#, + ["AA", sysId, 1], #line + ), + ( + #"UPDATE "composite+moon" SET "planet_system_id" = $1, "planet_nrm_ord" = $2 WHERE "composite+moon"."id" = $3"#, + [sys2Id, 2, moon1.id!], #line + ), + ( + #"UPDATE "composite+moon" SET "progenitorSystem_id" = NULL, "progenitorNrm_ord" = NULL WHERE "composite+moon"."id" = $1"#, + [moon2.id!], #line + ), + ( + #"UPDATE "composite+moon" SET "planetoid_system_id" = $1, "planetoid_nrm_ord" = $2 WHERE "composite+moon"."id" = $3"#, + [sys2Id, 3, moon3.id!], #line + ), + ( + #"UPDATE "composite+moon" SET "planetoid_system_id" = NULL, "planetoid_nrm_ord" = NULL WHERE "composite+moon"."id" = $1"#, + [moon4.id!], #line + ), ] XCTAssertEqual(db.sqlSerializers.count, expectedQueries.count) @@ -274,52 +416,68 @@ final class CompositeIDTests: XCTestCase { } } } - + func testCompositeParentChildEncoding() throws { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] func jsonString(_ value: E) throws -> String { try String(decoding: encoder.encode(value), as: UTF8.self) } - - let sysId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, moonId = UUID(), moonJId = #""\#(moonId.uuidString)""# + + let sysId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let moonId = UUID() + let moonJId = #""\#(moonId.uuidString)""# let planet = CompositePlanet(name: "A", solarSystemId: sysId, normalizedOrdinal: 1) let moon = CompositeMoon(id: moonId, name: "B", planetSolarSystemId: sysId, planetNormalizedOrdinal: 1) - + let sysJId = #"{"normalizedOrdinal":1,"solarSystem":{"id":"\#(sysId.uuidString)"}}"# - let moonJDat = #"{"id":\#(moonJId),"name":"B","orbiting":{"id":\#(sysJId)},"planetoid":{"id":\#(sysJId)},"progenitor":{"id":\#(sysJId)}}"# - + let moonJDat = + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":\#(sysJId)},"planetoid":{"id":\#(sysJId)},"progenitor":{"id":\#(sysJId)}}"# + // Unloaded children properties XCTAssertEqual(try jsonString(planet), #"{"id":\#(sysJId),"name":"A"}"#) // Unset optional parent properties - XCTAssertEqual(try jsonString(moon), #"{"id":\#(moonJId),"name":"B","orbiting":{"id":\#(sysJId)},"planetoid":{"id":null},"progenitor":{"id":null}}"#) + XCTAssertEqual( + try jsonString(moon), + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":\#(sysJId)},"planetoid":{"id":null},"progenitor":{"id":null}}"#) // OptionalChild loaded as NULL, Children properties loaded empty (planet.$moons.value, planet.$moonsMade.value, planet.$fragment.value) = ([], [], .some(.none)) XCTAssertEqual(try jsonString(planet), #"{"fragment":null,"id":\#(sysJId),"moons":[],"moonsMade":[],"name":"A"}"#) - + // Parent unloaded, OptionalParent set with ID and unset/explicit null value respectively moon.$orbiting.value = nil (moon.$progenitor.id, moon.$progenitor.value) = (planet.id, .none) (moon.$planetoid.id, moon.$planetoid.value) = (planet.id, .some(.none)) XCTAssertEqual(try jsonString(moon), moonJDat) - + // Children properties loaded with value(s) (planet.$moons.value, planet.$moonsMade.value, planet.$fragment.value) = ([moon], [moon], .some(.some(moon))) - XCTAssertEqual(try jsonString(planet), #"{"fragment":\#(moonJDat),"id":\#(sysJId),"moons":[\#(moonJDat)],"moonsMade":[\#(moonJDat)],"name":"A"}"#) + XCTAssertEqual( + try jsonString(planet), + #"{"fragment":\#(moonJDat),"id":\#(sysJId),"moons":[\#(moonJDat)],"moonsMade":[\#(moonJDat)],"name":"A"}"#) // Parent properties set with IDs and values (moon.$orbiting.value, planet.$moons.value) = (planet, nil) (moon.$progenitor.value, planet.$moonsMade.value) = (.some(.some(planet)), nil) (moon.$planetoid.value, planet.$fragment.value) = (.some(.some(planet)), .none) - XCTAssertEqual(try jsonString(moon), #"{"id":\#(moonJId),"name":"B","orbiting":{"id":\#(sysJId),"name":"A"},"planetoid":{"id":\#(sysJId),"name":"A"},"progenitor":{"id":\#(sysJId),"name":"A"}}"#) + XCTAssertEqual( + try jsonString(moon), + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":\#(sysJId),"name":"A"},"planetoid":{"id":\#(sysJId),"name":"A"},"progenitor":{"id":\#(sysJId),"name":"A"}}"# + ) } - + func testCompositeParentChildDecoding() throws { let decoder = JSONDecoder() - func unjsonString(_ json: String, as: D.Type = D.self) throws -> D { try decoder.decode(D.self, from: json.data(using: .utf8)!) } - - let sysId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, moonId = UUID(), sysJId = #""\#(sysId.uuidString)""#, moonJId = #""\#(moonId.uuidString)""# - - let planet1 = try unjsonString(#"{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"}"#, as: CompositePlanet.self) + func unjsonString(_ json: String, as: D.Type = D.self) throws -> D { + try decoder.decode(D.self, from: json.data(using: .utf8)!) + } + + let sysId = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let moonId = UUID() + let sysJId = #""\#(sysId.uuidString)""# + let moonJId = #""\#(moonId.uuidString)""# + + let planet1 = try unjsonString( + #"{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"}"#, as: CompositePlanet.self) XCTAssertEqual(planet1.$id.$solarSystem.id, sysId) XCTAssertEqual(planet1.id!.normalizedOrdinal, 1) XCTAssertEqual(planet1.name, "A") @@ -329,8 +487,10 @@ final class CompositeIDTests: XCTestCase { XCTAssertNil(planet1.$moonsMade.value) XCTAssertNil(planet1.$fragment.fromId) XCTAssertNilNil(planet1.$fragment.value) - - let moon1 = try unjsonString(#"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":null},"progenitor":{"id":null}}"#, as: CompositeMoon.self) + + let moon1 = try unjsonString( + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":null},"progenitor":{"id":null}}"#, + as: CompositeMoon.self) XCTAssertEqual(moon1.id, moonId) XCTAssertEqual(moon1.name, "B") XCTAssertEqual(moon1.$orbiting.id, planet1.id!) @@ -339,10 +499,14 @@ final class CompositeIDTests: XCTestCase { XCTAssertNilNil(moon1.$progenitor.value) XCTAssertNil(moon1.$planetoid.id) XCTAssertNilNil(moon1.$planetoid.value) - let moon1_1 = try unjsonString(#"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"},"planetoid":{"id":null},"progenitor":{"id":null}}"#, as: CompositeMoon.self) + let moon1_1 = try unjsonString( + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"},"planetoid":{"id":null},"progenitor":{"id":null}}"#, + as: CompositeMoon.self) XCTAssertNil(moon1_1.$orbiting.value) - let moon2 = try unjsonString(#"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":null},"progenitor":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}}}"#, as: CompositeMoon.self) + let moon2 = try unjsonString( + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":null},"progenitor":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}}}"#, + as: CompositeMoon.self) XCTAssertEqual(moon2.id, moonId) XCTAssertEqual(moon2.name, "B") XCTAssertEqual(moon2.$orbiting.id, planet1.id!) @@ -351,10 +515,14 @@ final class CompositeIDTests: XCTestCase { XCTAssertNilNil(moon2.$progenitor.value) XCTAssertNil(moon2.$planetoid.id) XCTAssertNilNil(moon2.$planetoid.value) - let moon2_1 = try unjsonString(#"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":null},"progenitor":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"}}"#, as: CompositeMoon.self) + let moon2_1 = try unjsonString( + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":null},"progenitor":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"}}"#, + as: CompositeMoon.self) XCTAssertNilNil(moon2_1.$progenitor.value) - let moon3 = try unjsonString(#"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"progenitor":{"id":null}}"#, as: CompositeMoon.self) + let moon3 = try unjsonString( + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"progenitor":{"id":null}}"#, + as: CompositeMoon.self) XCTAssertEqual(moon3.id, moonId) XCTAssertEqual(moon3.name, "B") XCTAssertEqual(moon3.$orbiting.id, planet1.id!) @@ -363,7 +531,9 @@ final class CompositeIDTests: XCTestCase { XCTAssertNilNil(moon3.$progenitor.value) XCTAssertEqual(moon3.$planetoid.id, planet1.id!) XCTAssertNilNil(moon3.$planetoid.value) - let moon3_1 = try unjsonString(#"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"},"progenitor":{"id":null}}"#, as: CompositeMoon.self) + let moon3_1 = try unjsonString( + #"{"id":\#(moonJId),"name":"B","orbiting":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}}},"planetoid":{"id":{"normalizedOrdinal":1,"solarSystem":{"id":\#(sysJId)}},"name":"A"},"progenitor":{"id":null}}"#, + as: CompositeMoon.self) XCTAssertNilNil(moon3_1.$planetoid.value) } @@ -372,25 +542,33 @@ final class CompositeIDTests: XCTestCase { } } -fileprivate func XCTAssertNilNil(_ expression: @autoclosure () throws -> Optional>, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { - func messageIfGiven() -> String { let m = message(); guard !m.isEmpty else { return m }; return " - \(m)" } +private func XCTAssertNilNil( + _ expression: @autoclosure () throws -> V??, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, + line: UInt = #line +) { + func messageIfGiven() -> String { + let m = message() + guard !m.isEmpty else { return m } + return " - \(m)" + } switch Result(catching: { try expression() }) { case .success(.none): return case .success(.some(.none)): return XCTFail("XCTAssertNilNil failed: \".some(nil)\"\(messageIfGiven())", file: file, line: line) - case .success(.some(.some(let value))): return XCTFail("XCTAssertNilNil failed: \".some(.some(\(value)))\"\(messageIfGiven())", file: file, line: line) + case .success(.some(.some(let value))): + return XCTFail("XCTAssertNilNil failed: \".some(.some(\(value)))\"\(messageIfGiven())", file: file, line: line) case .failure(let error): return XCTFail("XCTAssertNilNil failed: threw error \"\(error)\"\(messageIfGiven())", file: file, line: line) } } final class PlanetUsingCompositePivot: Model, @unchecked Sendable { static let schema = Planet.schema - + @ID(key: .id) var id: UUID? @Field(key: "name") var name: String @Parent(key: "star_id") var star: Star @Children(for: \.$id.$planet) var planetTags: [CompositePlanetTag] @Siblings(through: CompositePlanetTag.self, from: \.$id.$planet, to: \.$id.$tag) var tags: [Tag] - + init() {} init(id: IDValue? = nil, name: String) { (self.id, self.name) = (id, name) } init(id: IDValue? = nil, name: String, starId: UUID) { @@ -402,17 +580,20 @@ final class PlanetUsingCompositePivot: Model, @unchecked Sendable { final class CompositePlanetTag: Model, @unchecked Sendable { static let schema = "composite+planet+tag" - + final class IDValue: Fields, Hashable, @unchecked Sendable { @Parent(key: "planet_id") var planet: PlanetUsingCompositePivot @Parent(key: "tag_id") var tag: Tag - + init() {} init(planetID: PlanetUsingCompositePivot.IDValue, tagID: Tag.IDValue) { (self.$planet.id, self.$tag.id) = (planetID, tagID) } static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.$planet.id == rhs.$planet.id && lhs.$tag.id == rhs.$tag.id } - func hash(into hasher: inout Hasher) { hasher.combine(self.$planet.id); hasher.combine(self.$tag.id) } + func hash(into hasher: inout Hasher) { + hasher.combine(self.$planet.id) + hasher.combine(self.$tag.id) + } } - + @CompositeID var id: IDValue? @Field(key: "notation") var notation: String @Timestamp(key: "createdAt", on: .create) var createdAt: Date? @@ -445,11 +626,11 @@ struct CompositePlanetTagMigration: Migration { final class SolarSystem: Model, @unchecked Sendable { static let schema = "solar_system" - + @ID(key: .id) var id: UUID? @Field(key: "name") var name: String @Children(for: \.$id.$solarSystem) var planets: [CompositePlanet] - + init() {} init(id: IDValue? = nil, name: String) { if let id = id { self.id = id } @@ -460,26 +641,31 @@ final class SolarSystem: Model, @unchecked Sendable { final class CompositePlanet: Model, @unchecked Sendable { static let schema = "composite+planet" - + // Note for the curious: "normalized ordinal" means "how many orbits from the center if a unique value was chosen for every planet despite overlapping or shared orbits" final class IDValue: Fields, Hashable, @unchecked Sendable { @Parent(key: "system_id") var solarSystem: SolarSystem @Field(key: "nrm_ord") var normalizedOrdinal: Int - + init() {} init(solarSystemId: SolarSystem.IDValue, normalizedOrdinal: Int) { (self.$solarSystem.id, self.normalizedOrdinal) = (solarSystemId, normalizedOrdinal) } - static func ==(lhs: IDValue, rhs: IDValue) -> Bool { lhs.$solarSystem.id == rhs.$solarSystem.id && lhs.normalizedOrdinal == rhs.normalizedOrdinal } - func hash(into hasher: inout Hasher) { hasher.combine(self.$solarSystem.id); hasher.combine(self.normalizedOrdinal) } + static func == (lhs: IDValue, rhs: IDValue) -> Bool { + lhs.$solarSystem.id == rhs.$solarSystem.id && lhs.normalizedOrdinal == rhs.normalizedOrdinal + } + func hash(into hasher: inout Hasher) { + hasher.combine(self.$solarSystem.id) + hasher.combine(self.normalizedOrdinal) + } } - + @CompositeID var id: IDValue? @Field(key: "name") var name: String @CompositeChildren(for: \.$orbiting) var moons: [CompositeMoon] @CompositeChildren(for: \.$progenitor) var moonsMade: [CompositeMoon] @CompositeOptionalChild(for: \.$planetoid) var fragment: CompositeMoon? - + init() {} init(name: String, solarSystemId: SolarSystem.IDValue, normalizedOrdinal: Int) { self.id = .init(solarSystemId: solarSystemId, normalizedOrdinal: normalizedOrdinal) @@ -492,15 +678,18 @@ final class CompositePlanet: Model, @unchecked Sendable { final class CompositeMoon: Model, @unchecked Sendable { static let schema = "composite+moon" - + @ID(key: .id) var id: UUID? @Field(key: "name") var name: String @CompositeParent(prefix: "planet") var orbiting: CompositePlanet @CompositeOptionalParent(prefix: "progenitor", strategy: .camelCase) var progenitor: CompositePlanet? @CompositeOptionalParent(prefix: "planetoid") var planetoid: CompositePlanet? - + init() {} - init(id: UUID? = nil, name: String, planetSolarSystemId: SolarSystem.IDValue, planetNormalizedOrdinal: Int, progenitorId: CompositePlanet.IDValue? = nil, planetoidId: CompositePlanet.IDValue? = nil) { + init( + id: UUID? = nil, name: String, planetSolarSystemId: SolarSystem.IDValue, planetNormalizedOrdinal: Int, + progenitorId: CompositePlanet.IDValue? = nil, planetoidId: CompositePlanet.IDValue? = nil + ) { if let id = id { self.id = id } self.name = name self.$orbiting.id = .init(solarSystemId: planetSolarSystemId, normalizedOrdinal: planetNormalizedOrdinal) diff --git a/Tests/FluentKitTests/DummyDatabaseForTestSQLSerializer.swift b/Tests/FluentKitTests/DummyDatabaseForTestSQLSerializer.swift index a03d80f8..21e53796 100644 --- a/Tests/FluentKitTests/DummyDatabaseForTestSQLSerializer.swift +++ b/Tests/FluentKitTests/DummyDatabaseForTestSQLSerializer.swift @@ -1,31 +1,33 @@ import FluentKit import FluentSQL +import NIOConcurrencyHelpers import NIOCore import SQLKit import XCTFluent -import NIOConcurrencyHelpers struct FakedDatabaseRow: DatabaseOutput, SQLRow { let data: [String: (any Sendable)?] let schema: String? - + private func column(for key: FieldKey) -> String { "\(self.schema.map { "\($0)_" } ?? "")\(key.description)" } func schema(_ schema: String) -> any DatabaseOutput { FakedDatabaseRow(self.data, schema: schema) } func contains(_ key: FieldKey) -> Bool { self.contains(column: self.column(for: key)) } func decodeNil(_ key: FieldKey) throws -> Bool { try self.decodeNil(column: self.column(for: key)) } func decode(_ key: FieldKey, as: T.Type) throws -> T { try self.decode(column: self.column(for: key), as: T.self) } - + var allColumns: [String] { .init(self.data.keys) } func contains(column: String) -> Bool { self.data.keys.contains(column) } func decodeNil(column: String) throws -> Bool { self.data[column].map { $0 == nil } ?? true } func decode(column c: String, as: D.Type) throws -> D { - guard case .some(.some(let v)) = self.data[c] else { throw DecodingError.keyNotFound(SomeCodingKey(stringValue: c), .init(codingPath: [], debugDescription: "")) } + guard case .some(.some(let v)) = self.data[c] else { + throw DecodingError.keyNotFound(SomeCodingKey(stringValue: c), .init(codingPath: [], debugDescription: "")) + } guard let value = v as? D else { throw DecodingError.typeMismatch(D.self, .init(codingPath: [], debugDescription: "")) } return value } - + var description: String { "" } - + init(_ data: [String: (any Sendable)?], schema: String? = nil) { self.data = data self.schema = schema @@ -49,7 +51,7 @@ final class DummyDatabaseForTestSQLSerializer: Database, SQLDatabase { get { self._sqlSerializers.withLockedValue { $0 } } set { self._sqlSerializers.withLockedValue { $0 = newValue } } } - + let _fakedRows = NIOLockedValueBox<[[FakedDatabaseRow]]>([]) var fakedRows: [[FakedDatabaseRow]] { get { self._fakedRows.withLockedValue { $0 } } @@ -67,17 +69,17 @@ final class DummyDatabaseForTestSQLSerializer: Database, SQLDatabase { func reset() { self.sqlSerializers = [] } - + func execute( query: DatabaseQuery, - onOutput: @escaping @Sendable (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> Void ) -> EventLoopFuture { let sqlExpression = SQLQueryConverter(delegate: DummyDatabaseConverterDelegate()).convert(query) - + return self.execute(sql: sqlExpression, { row in onOutput(row as! any DatabaseOutput) }) } - 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 { var sqlSerializer = SQLSerializer(database: self) query.serialize(to: &sqlSerializer) self._sqlSerializers.withLockedValue { $0.append(sqlSerializer) } @@ -94,10 +96,10 @@ final class DummyDatabaseForTestSQLSerializer: Database, SQLDatabase { func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { closure(self) } - + func execute(schema: DatabaseSchema) -> EventLoopFuture { let sqlExpression = SQLSchemaConverter(delegate: DummyDatabaseConverterDelegate()).convert(schema) - + return self.execute(sql: sqlExpression, { _ in }) } @@ -105,7 +107,7 @@ final class DummyDatabaseForTestSQLSerializer: Database, SQLDatabase { // do nothing self.eventLoop.makeSucceededVoidFuture() } - + func withConnection( _ closure: @escaping @Sendable (any Database) -> EventLoopFuture ) -> EventLoopFuture { @@ -150,7 +152,7 @@ struct DummyDatabaseDialect: SQLDialect { var autoIncrementClause: any SQLExpression { SQLRaw("GENERATED BY DEFAULT AS IDENTITY") } - + var sharedSelectLockExpression: (any SQLExpression)? { SQLRaw("FOR SHARE") } diff --git a/Tests/FluentKitTests/FilterQueryTests.swift b/Tests/FluentKitTests/FilterQueryTests.swift index 5296dfb9..a3667a9f 100644 --- a/Tests/FluentKitTests/FilterQueryTests.swift +++ b/Tests/FluentKitTests/FilterQueryTests.swift @@ -1,78 +1,114 @@ // // FilterQueryTests.swift -// +// // // Created by Mathew Polzin on 3/8/20. // -import FluentKit import FluentBenchmark -import XCTest -import Foundation +import FluentKit import FluentSQL +import Foundation +import XCTest final class FilterQueryTests: DbQueryTestCase { // MARK: Enum func test_enumEquals() throws { _ = try Task.query(on: self.db).filter(\.$status == .done).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" = 'done'"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" = 'done'"# + ) } func test_enumNotEquals() throws { _ = try Task.query(on: self.db).filter(\.$status != .done).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"# + ) } func test_enumIn() throws { _ = try Task.query(on: self.db).filter(\.$status ~~ [.done, .notDone]).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"# + ) } func test_enumNotIn() throws { _ = try Task.query(on: self.db).filter(\.$status !~ [.done, .notDone]).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"# + ) } - + // MARK: OptionalEnum func test_optionalEnumEquals() throws { _ = try Task.query(on: self.db).filter(\.$optionalStatus == .done).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" = 'done'"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" = 'done'"# + ) } func test_optionalEnumNotEquals() throws { _ = try Task.query(on: self.db).filter(\.$optionalStatus != .done).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" <> 'done'"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" <> 'done'"# + ) } func test_optionalEnumIn() throws { _ = try Task.query(on: self.db).filter(\.$optionalStatus ~~ [.done, .notDone]).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" IN ('done','notDone')"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" IN ('done','notDone')"# + ) } func test_optionalEnumNotIn() throws { _ = try Task.query(on: self.db).filter(\.$optionalStatus !~ [.done, .notDone]).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" NOT IN ('done','notDone')"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" NOT IN ('done','notDone')"# + ) } // MARK: String func test_stringEquals() throws { _ = try Task.query(on: self.db).filter(\.$description == "hello").all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" = $1"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" = $1"# + ) } func test_stringNotEquals() throws { _ = try Task.query(on: self.db).filter(\.$description != "hello").all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" <> $1"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" <> $1"# + ) } func test_stringIn() throws { _ = try Task.query(on: self.db).filter(\.$description ~~ ["hello"]).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"# + ) } func test_stringNotIn() throws { _ = try Task.query(on: self.db).filter(\.$description !~ ["hello"]).all().wait() - assertQuery(self.db, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"#) + assertQuery( + self.db, + #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"# + ) } } @@ -91,7 +127,7 @@ final class Task: Model, @unchecked Sendable { @Enum(key: "status") var status: Diggity - + @OptionalEnum(key: "optional_status") var optionalStatus: Diggity? diff --git a/Tests/FluentKitTests/FluentKitTests.swift b/Tests/FluentKitTests/FluentKitTests.swift index 46b33006..642b313f 100644 --- a/Tests/FluentKitTests/FluentKitTests.swift +++ b/Tests/FluentKitTests/FluentKitTests.swift @@ -1,18 +1,18 @@ -import Logging -import FluentKit import FluentBenchmark -import XCTest -import Foundation +import FluentKit import FluentSQL -import XCTFluent +import Foundation +import Logging import SQLKit +import XCTFluent +import XCTest final class FluentKitTests: XCTestCase { override class func setUp() { super.setUp() XCTAssertTrue(isLoggingConfigured) } - + /// This test is a deliberate code smell put in place to prevent an even worse one from /// causing problems without at least some warning. Specifically, the output of /// ``AnyModel/description`` is rather precise when it comes to labeling the input and @@ -45,7 +45,7 @@ final class FluentKitTests: XCTestCase { let modelOutputDesc = model.description model.num += 1 let modelBothDesc = model.description - + XCTAssertEqual(modelEmptyDesc, "Foo(:)") XCTAssertEqual(modelInputDesc, "Foo(input: [name: \"Test\", num: 42])") XCTAssertEqual(modelOutputDesc, "Foo(output: [num: 42, name: \"Test\", id: \(model.id!)])") @@ -76,7 +76,7 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"ORDER BY "planets"."name" DESC"#), true) db.reset() - + _ = try Planet.query(on: db) .join(parent: \Planet.$star) .sort(Star.self, \.$name, .ascending) @@ -84,12 +84,12 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"ORDER BY "stars"."name" ASC"#), true) db.reset() - + _ = try Planet.query(on: db).sort(\.$id, .descending).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"ORDER BY "planets"."id" DESC"#), true) db.reset() - + _ = try Planet.query(on: db) .join(parent: \.$star) .sort(Star.self, \.$id, .ascending) @@ -97,12 +97,12 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"ORDER BY "stars"."id" ASC"#), true) db.reset() - + _ = try Planet.query(on: db).sort("name", .descending).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"ORDER BY "planets"."name" DESC"#), true) db.reset() - + _ = try Planet.query(on: db) .join(parent: \Planet.$star) .sort(Star.self, "name", .ascending) @@ -136,7 +136,7 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "governors" ON "planets"."id" = "governors"."planet_id"#), true) db.reset() - + _ = try Planet.query(on: db).join(children: \Planet.$moons).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "moons" ON "planets"."id" = "moons"."planet_id"#), true) @@ -154,43 +154,57 @@ final class FluentKitTests: XCTestCase { _ = try Planet.query(on: db).join(siblings: \Planet.$tags).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "planet+tag" ON "planet+tag"."planet_id" = "planets"."id""#), true, db.sqlSerializers.first?.sql ?? "") + XCTAssertEqual( + db.sqlSerializers.first?.sql.contains(#"INNER JOIN "planet+tag" ON "planet+tag"."planet_id" = "planets"."id""#), true, + db.sqlSerializers.first?.sql ?? "") XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "tags" ON "planet+tag"."tag_id" = "tags"."id""#), true) db.reset() } - + func testSingleColumnSelect() throws { let db = DummyDatabaseForTestSQLSerializer() - + _ = try Planet.query(on: db).all(\.$name).wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() } - + func testSQLDistinct() throws { let db = DummyDatabaseForTestSQLSerializer() - + _ = try Planet.query(on: db).unique().all(\.$name).wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT DISTINCT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT DISTINCT "planets"."name" AS "planets_name" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() - + _ = try Planet.query(on: db).unique().all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql.starts(with: #"SELECT DISTINCT "planets"."#), true) db.reset() - + db.fakedRows.append([.init(["aggregate": 1])]) _ = try? Planet.query(on: db).unique().count(\.$name).wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT COUNT(DISTINCT "planets"."name") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT COUNT(DISTINCT "planets"."name") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() - + db.fakedRows.append([.init(["aggregate": Int?(1)])]) _ = try? Planet.query(on: db).unique().sum(\.$id).wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT SUM(DISTINCT "planets"."id") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT SUM(DISTINCT "planets"."id") AS "aggregate" FROM "planets" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1)"# + ) db.reset() } @@ -200,7 +214,7 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql, #"ALTER TABLE "foo" ADD INDEX i_foo (foo)"#) } - + func testRequiredFieldConstraint() throws { let db = DummyDatabaseForTestSQLSerializer() try db.schema("planets") @@ -236,7 +250,9 @@ final class FluentKitTests: XCTestCase { .create() .wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"#) db.reset() try db.schema("planets") @@ -244,7 +260,9 @@ final class FluentKitTests: XCTestCase { .create() .wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"#) } func testMultipleFieldConstraint() throws { @@ -254,7 +272,8 @@ final class FluentKitTests: XCTestCase { .create() .wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY)"#) } func testUniqueTableConstraint() throws { @@ -275,7 +294,9 @@ final class FluentKitTests: XCTestCase { .create() .wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("id" BIGINT, "name" TEXT, CONSTRAINT "uq:planets.id+planets.name" UNIQUE ("id", "name"))"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("id" BIGINT, "name" TEXT, CONSTRAINT "uq:planets.id+planets.name" UNIQUE ("id", "name"))"#) } func testForeignKeyTableConstraint() throws { @@ -286,7 +307,10 @@ final class FluentKitTests: XCTestCase { .create() .wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)"# + ) db.reset() try db.schema("planets") @@ -300,23 +324,29 @@ final class FluentKitTests: XCTestCase { .create() .wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT, CONSTRAINT "fk:planets.galaxy_id+planets.id" FOREIGN KEY ("galaxy_id") REFERENCES "galaxies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE)"# + ) db.reset() - try db.schema("planets") - .field("galaxy_id", .int64) - .field("galaxy_name", .string) - .foreignKey( - ["galaxy_id", "galaxy_name"], - references: "galaxies", ["id", "name"], - onUpdate: .cascade - ) - .create() - .wait() - XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT, "galaxy_name" TEXT, CONSTRAINT "fk:planets.galaxy_id+planets.galaxy_name+planets.id+planets.name" FOREIGN KEY ("galaxy_id", "galaxy_name") REFERENCES "galaxies" ("id", "name") ON DELETE NO ACTION ON UPDATE CASCADE)"#) + try db.schema("planets") + .field("galaxy_id", .int64) + .field("galaxy_name", .string) + .foreignKey( + ["galaxy_id", "galaxy_name"], + references: "galaxies", ["id", "name"], + onUpdate: .cascade + ) + .create() + .wait() + XCTAssertEqual(db.sqlSerializers.count, 1) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"CREATE TABLE "planets" ("galaxy_id" BIGINT, "galaxy_name" TEXT, CONSTRAINT "fk:planets.galaxy_id+planets.galaxy_name+planets.id+planets.name" FOREIGN KEY ("galaxy_id", "galaxy_name") REFERENCES "galaxies" ("id", "name") ON DELETE NO ACTION ON UPDATE CASCADE)"# + ) } - + func testIfNotExistsTableCreate() throws { let db = DummyDatabaseForTestSQLSerializer() try db.schema("planets") @@ -324,7 +354,7 @@ final class FluentKitTests: XCTestCase { .ignoreExisting() .create() .wait() - + XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE IF NOT EXISTS "planets" ("galaxy_id" BIGINT)"#) db.reset() @@ -333,49 +363,49 @@ final class FluentKitTests: XCTestCase { .field("galaxy_id", .int64) .create() .wait() - + XCTAssertEqual(db.sqlSerializers.count, 1) XCTAssertEqual(db.sqlSerializers.first?.sql, #"CREATE TABLE "planets" ("galaxy_id" BIGINT)"#) } - + func testDecodeWithoutID() throws { let json = """ - {"name": "Earth", "moonCount": 1} - """ + {"name": "Earth", "moonCount": 1} + """ let planet = try JSONDecoder().decode(Planet2.self, from: Data(json.utf8)) XCTAssertEqual(planet.id, nil) XCTAssertEqual(planet.name, "Earth") XCTAssertEqual(planet.nickName, nil) XCTAssertEqual(planet.moonCount, 1) } - + func testDecodeWithID() throws { let json = """ - {"id": 42, "name": "Earth", "moonCount": 1} - """ + {"id": 42, "name": "Earth", "moonCount": 1} + """ let planet = try JSONDecoder().decode(Planet2.self, from: Data(json.utf8)) XCTAssertEqual(planet.id, 42) XCTAssertEqual(planet.name, "Earth") XCTAssertEqual(planet.nickName, nil) XCTAssertEqual(planet.moonCount, 1) } - + func testDecodeWithOptional() throws { let json = """ - {"id": 42, "name": "Earth", "moonCount": 1, "nickName": "The Blue Marble"} - """ + {"id": 42, "name": "Earth", "moonCount": 1, "nickName": "The Blue Marble"} + """ let planet = try JSONDecoder().decode(Planet2.self, from: Data(json.utf8)) XCTAssertEqual(planet.id, 42) XCTAssertEqual(planet.name, "Earth") XCTAssertEqual(planet.nickName, "The Blue Marble") XCTAssertEqual(planet.moonCount, 1) } - + func testDecodeMissingRequired() throws { let json = """ - {"name": "Earth"} - """ - + {"name": "Earth"} + """ + XCTAssertThrowsError(try JSONDecoder().decode(Planet2.self, from: Data(json.utf8))) { guard case .typeMismatch(let type, _) = $0 as? DecodingError else { return XCTFail("Expected DecodingError.typeMismatch but got \(String(reflecting: $0))") @@ -387,8 +417,8 @@ final class FluentKitTests: XCTestCase { // GitHub PR: https://github.com/vapor/fluent-kit/pull/209 func testDecodeEnumProperty() throws { let json = """ - {"name": "Squeeky", "type": "mouse"} - """ + {"name": "Squeeky", "type": "mouse"} + """ do { let toy = try JSONDecoder().decode(Toy.self, from: Data(json.utf8)) XCTAssertNotNil(toy.$type.value) @@ -425,63 +455,69 @@ final class FluentKitTests: XCTestCase { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys let encoded = try encoder.encode(tanner) - let result = String(data:encoded, encoding: .utf8)! - let expected = #"{"deletedAt":null,"id":null,"name":"Tanner","pet":{"name":"Ziz","toy":{"foo":{"bar":42,"baz":"hello"},"name":"Foo","type":"mouse"},"type":"cat"}}"# + let result = String(data: encoded, encoding: .utf8)! + let expected = + #"{"deletedAt":null,"id":null,"name":"Tanner","pet":{"name":"Ziz","toy":{"foo":{"bar":42,"baz":"hello"},"name":"Foo","type":"mouse"},"type":"cat"}}"# XCTAssertEqual(result, expected) } } func testPlanet2FilterPlaceholder1() throws { - let db = DummyDatabaseForTestSQLSerializer() - db.fakedRows.append([.init(["aggregate": 1])]) - _ = try Planet2 - .query(on: db) - .filter(\.$nickName != "first") - .count() - .wait() - XCTAssertEqual(db.sqlSerializers.count, 1) - let result: String = (db.sqlSerializers.first?.sql)! - let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1"# - XCTAssertEqual(result, expected) - db.reset() - } + let db = DummyDatabaseForTestSQLSerializer() + db.fakedRows.append([.init(["aggregate": 1])]) + _ = + try Planet2 + .query(on: db) + .filter(\.$nickName != "first") + .count() + .wait() + XCTAssertEqual(db.sqlSerializers.count, 1) + let result: String = (db.sqlSerializers.first?.sql)! + let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1"# + XCTAssertEqual(result, expected) + db.reset() + } func testPlanet2FilterPlaceholder2() throws { - let db = DummyDatabaseForTestSQLSerializer() - db.fakedRows.append([.init(["aggregate": 1])]) - _ = try Planet2 - .query(on: db) - .filter(\.$nickName != nil) - .count() - .wait() - XCTAssertEqual(db.sqlSerializers.count, 1) - let result: String = (db.sqlSerializers.first?.sql)! - let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" IS NOT NULL"# - XCTAssertEqual(result, expected) - db.reset() - } + let db = DummyDatabaseForTestSQLSerializer() + db.fakedRows.append([.init(["aggregate": 1])]) + _ = + try Planet2 + .query(on: db) + .filter(\.$nickName != nil) + .count() + .wait() + XCTAssertEqual(db.sqlSerializers.count, 1) + let result: String = (db.sqlSerializers.first?.sql)! + let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" IS NOT NULL"# + XCTAssertEqual(result, expected) + db.reset() + } func testPlanet2FilterPlaceholder3() throws { - let db = DummyDatabaseForTestSQLSerializer() - db.fakedRows.append([.init(["aggregate": 1])]) - _ = try Planet2 - .query(on: db) - .filter(\.$nickName != "first") - .filter(\.$nickName == "second") - .filter(\.$nickName != "third") - .count() - .wait() - XCTAssertEqual(db.sqlSerializers.count, 1) - let result: String = (db.sqlSerializers.first?.sql)! - let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" = $2 AND "planets"."nickname" <> $3"# - XCTAssertEqual(result, expected) - db.reset() - } + let db = DummyDatabaseForTestSQLSerializer() + db.fakedRows.append([.init(["aggregate": 1])]) + _ = + try Planet2 + .query(on: db) + .filter(\.$nickName != "first") + .filter(\.$nickName == "second") + .filter(\.$nickName != "third") + .count() + .wait() + XCTAssertEqual(db.sqlSerializers.count, 1) + let result: String = (db.sqlSerializers.first?.sql)! + let expected = + #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" = $2 AND "planets"."nickname" <> $3"# + XCTAssertEqual(result, expected) + db.reset() + } func testPlanet2FilterPlaceholder4() throws { let db = DummyDatabaseForTestSQLSerializer() - db.fakedRows.append([.init(["aggregate": 1])]) - _ = try Planet2 + db.fakedRows.append([.init(["aggregate": 1])]) + _ = + try Planet2 .query(on: db) .filter(\.$nickName != "first") .filter(\.$nickName != nil) @@ -490,7 +526,8 @@ final class FluentKitTests: XCTestCase { .wait() XCTAssertEqual(db.sqlSerializers.count, 1) let result: String = (db.sqlSerializers.first?.sql)! - let expected = #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" IS NOT NULL AND "planets"."nickname" = $2"# + let expected = + #"SELECT COUNT("planets"."id") AS "aggregate" FROM "planets" WHERE "planets"."nickname" <> $1 AND "planets"."nickname" IS NOT NULL AND "planets"."nickname" = $2"# XCTAssertEqual(result, expected) db.reset() } @@ -512,19 +549,25 @@ final class FluentKitTests: XCTestCase { static let schema = "foos" @ID var id: UUID? @Enum(key: "bar") var bar: Bar - init() { } + init() {} } do { - let foo = try JSONDecoder().decode(EFoo.self, from: Data(""" - {"bar": "baz"} - """.utf8)) + let foo = try JSONDecoder().decode( + EFoo.self, + from: Data( + """ + {"bar": "baz"} + """.utf8)) XCTAssertEqual(foo.bar, .baz) } do { - _ = try JSONDecoder().decode(EFoo.self, from: Data(""" - {"bar": "qux"} - """.utf8)) + _ = try JSONDecoder().decode( + EFoo.self, + from: Data( + """ + {"bar": "qux"} + """.utf8)) XCTFail("should not have passed") } catch DecodingError.typeMismatch(let foo, let context) { XCTAssert(foo is Bar.Type) @@ -540,51 +583,64 @@ final class FluentKitTests: XCTestCase { static let schema = "foos" @ID var id: UUID? @OptionalEnum(key: "bar") var bar: Bar? - init() { } + init() {} } do { - let foo = try JSONDecoder().decode(OEFoo.self, from: Data(""" - {"bar": "baz"} - """.utf8)) + let foo = try JSONDecoder().decode( + OEFoo.self, + from: Data( + """ + {"bar": "baz"} + """.utf8)) XCTAssertEqual(foo.bar, .baz) } do { - _ = try JSONDecoder().decode(OEFoo.self, from: Data(""" - {"bar": "qux"} - """.utf8)) + _ = try JSONDecoder().decode( + OEFoo.self, + from: Data( + """ + {"bar": "qux"} + """.utf8)) XCTFail("should not have passed") } catch DecodingError.typeMismatch(let foo, let context) { XCTAssert(foo is Bar?.Type) XCTAssertEqual(context.codingPath.map(\.stringValue), ["bar"]) } } - + func testOptionalParentCoding() throws { let db = DummyDatabaseForTestSQLSerializer() db.fakedRows.append([.init(["id": 1])]) - let prefoo = PreFoo(boo: true); try prefoo.create(on: db).wait() + let prefoo = PreFoo(boo: true) + try prefoo.create(on: db).wait() db.fakedRows.append([.init(["id": 2])]) - let foo1 = AtFoo(preFoo: prefoo); try foo1.create(on: db).wait() + let foo1 = AtFoo(preFoo: prefoo) + try foo1.create(on: db).wait() db.fakedRows.append([.init(["id": 3])]) - let foo2 = AtFoo(preFoo: nil); try foo2.create(on: db).wait() - prefoo.$foos.fromId = prefoo.id//; prefoo.$foos.value = [] - + let foo2 = AtFoo(preFoo: nil) + try foo2.create(on: db).wait() + prefoo.$foos.fromId = prefoo.id //; prefoo.$foos.value = [] + let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes, .prettyPrinted] - + let prefooEncoded = try String(decoding: encoder.encode(prefoo), as: UTF8.self) let foo1Encoded = try String(decoding: encoder.encode(foo1), as: UTF8.self) let foo2Encoded = try String(decoding: encoder.encode(foo2), as: UTF8.self) - - XCTAssertEqual(prefooEncoded, """ + + XCTAssertEqual( + prefooEncoded, + """ { "boo" : true, "id" : \(prefoo.id!) } """) - XCTAssertEqual(foo1Encoded, """ + XCTAssertEqual( + foo1Encoded, + """ { "id" : \(foo1.id!), "preFoo" : { @@ -593,7 +649,9 @@ final class FluentKitTests: XCTestCase { } } """) - XCTAssertEqual(foo2Encoded, """ + XCTAssertEqual( + foo2Encoded, + """ { "id" : \(foo2.id!), "preFoo" : { @@ -604,57 +662,65 @@ final class FluentKitTests: XCTestCase { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 - + let decodedPrefoo = try decoder.decode(PreFoo.self, from: prefooEncoded.data(using: .utf8)!) let decodedFoo1 = try decoder.decode(AtFoo.self, from: foo1Encoded.data(using: .utf8)!) let decodedFoo2 = try decoder.decode(AtFoo.self, from: foo2Encoded.data(using: .utf8)!) - + XCTAssertEqual(decodedPrefoo.id, prefoo.id) XCTAssertEqual(decodedPrefoo.boo, prefoo.boo) XCTAssertEqual(decodedFoo1.id, foo1.id) XCTAssertEqual(decodedFoo1.$preFoo.id, foo1.$preFoo.id) - XCTAssert({ guard case .none = decodedFoo1.$preFoo.value else { return false }; return true }()) + XCTAssert( + { + guard case .none = decodedFoo1.$preFoo.value else { return false } + return true + }()) XCTAssertEqual(decodedFoo2.id, foo2.id) XCTAssertEqual(decodedFoo2.$preFoo.id, foo2.$preFoo.id) - XCTAssert({ guard case .none = decodedFoo2.$preFoo.value else { return false }; return true }()) + XCTAssert( + { + guard case .none = decodedFoo2.$preFoo.value else { return false } + return true + }()) } - + func testGroupCoding() throws { final class GroupedFoo: Fields, @unchecked Sendable { @Field(key: "hello") var string: String - + init() {} } - + final class GroupFoo: Model, @unchecked Sendable { static let schema = "group_foos" - + @ID(key: .id) var id: UUID? @Group(key: "group") var group: GroupedFoo - + init() {} } let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys] let decoder = JSONDecoder() - + let groupFoo = GroupFoo() groupFoo.id = UUID() groupFoo.group.string = "hi" let encoded = try encoder.encode(groupFoo) XCTAssertEqual(String(decoding: encoded, as: UTF8.self), #"{"group":{"string":"hi"},"id":"\#(groupFoo.id!.uuidString)"}"#) - + let missingGroupFoo = GroupFoo() missingGroupFoo.id = UUID() missingGroupFoo.$group.value = nil let missingEncoded = try encoder.encode(missingGroupFoo) XCTAssertEqual(String(decoding: missingEncoded, as: UTF8.self), #"{"id":"\#(missingGroupFoo.id!.uuidString)"}"#) - + let decoded = try decoder.decode(GroupFoo.self, from: encoded) XCTAssertEqual(decoded.id?.uuidString, groupFoo.id?.uuidString) XCTAssertEqual(decoded.group.string, groupFoo.group.string) - + let decodedMissing = try decoder.decode(GroupFoo.self, from: #"{"id":"\#(groupFoo.id!.uuidString)"}"#.data(using: .utf8)!) XCTAssertEqual(decodedMissing.id?.uuidString, groupFoo.id?.uuidString) XCTAssertNotNil(decodedMissing.$group.value) @@ -664,7 +730,7 @@ final class FluentKitTests: XCTestCase { final class DGOFoo: Model, @unchecked Sendable { static let schema = "foos" @ID(custom: .id) var id: Int? - init() { } + init() {} init(id: Int?) { self.id = id } @@ -691,7 +757,6 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(foo.id, 1) } - func testQueryBuilderFieldsFor() throws { let test = ArrayTestDatabase() let builder = User.query(on: test.db) @@ -704,31 +769,34 @@ final class FluentKitTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() let pageRequest1 = PageRequest(page: -1, per: 10) db.fakedRows.append([.init(["aggregate": 1])]) - XCTAssertNoThrow(try Planet2 - .query(on: db) - .paginate(pageRequest1) - .wait()) + XCTAssertNoThrow( + try Planet2 + .query(on: db) + .paginate(pageRequest1) + .wait()) let pageRequest2 = PageRequest(page: 1, per: -10) db.fakedRows.append([.init(["aggregate": 1])]) - XCTAssertNoThrow(try Planet2 - .query(on: db) - .paginate(pageRequest2) - .wait()) + XCTAssertNoThrow( + try Planet2 + .query(on: db) + .paginate(pageRequest2) + .wait()) } - + func testPaginationDoesntCrashOnOverflow() async throws { let db = DummyDatabaseForTestSQLSerializer() - let pageRequest1 = PageRequest(page: 1184467440737095516, per: 1184467440737095516) + let pageRequest1 = PageRequest(page: 1_184_467_440_737_095_516, per: 1_184_467_440_737_095_516) db.fakedRows.append([.init(["aggregate": 1])]) - let result = try await Planet2 + let result = + try await Planet2 .query(on: db) .paginate(pageRequest1) - XCTAssertEqual(result.metadata.page, 1184467440737095516) - XCTAssertEqual(result.metadata.per, 1184467440737095516) + XCTAssertEqual(result.metadata.page, 1_184_467_440_737_095_516) + XCTAssertEqual(result.metadata.per, 1_184_467_440_737_095_516) XCTAssertEqual(result.metadata.total, 1) } - + func testModelsWithSpacesSpecified() throws { let db = DummyDatabaseForTestSQLSerializer() try db.schema(AltPlanet.schema, space: AltPlanet.space) @@ -748,22 +816,40 @@ final class FluentKitTests: XCTestCase { updateMe.$id.exists = true try updateMe.update(on: db).wait() try AltPlanet.query(on: db).filter(\.$name != "Arret").delete(force: true).wait() - _ = try Star.query(on: db).join(AltPlanet.self, on: \AltPlanet.$star.$id == \Star.$id).fields(for: Star.self).withDeleted().first().wait() - + _ = try Star.query(on: db).join(AltPlanet.self, on: \AltPlanet.$star.$id == \Star.$id).fields(for: Star.self).withDeleted().first() + .wait() + XCTAssertEqual(db.sqlSerializers.count, 6) - XCTAssertEqual(db.sqlSerializers.dropFirst(0).first?.sql, #"CREATE TABLE "mirror_universe"."planets" ("id" UUID PRIMARY KEY, "name" TEXT NOT NULL, "star_id" UUID REFERENCES "stars" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION NOT NULL, "possible_star_id" UUID REFERENCES "stars" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, "createdAt" TIMESTAMPTZ, "updatedAt" TIMESTAMPTZ, "deletedAt" TIMESTAMPTZ DEFAULT NULL)"#) - XCTAssertEqual(db.sqlSerializers.dropFirst(1).first?.sql, #"SELECT "mirror_universe"."planets"."id" AS "mirror_universe_planets_id", "mirror_universe"."planets"."name" AS "mirror_universe_planets_name", "mirror_universe"."planets"."star_id" AS "mirror_universe_planets_star_id", "mirror_universe"."planets"."possible_star_id" AS "mirror_universe_planets_possible_star_id", "mirror_universe"."planets"."createdAt" AS "mirror_universe_planets_createdAt", "mirror_universe"."planets"."updatedAt" AS "mirror_universe_planets_updatedAt", "mirror_universe"."planets"."deletedAt" AS "mirror_universe_planets_deletedAt" FROM "mirror_universe"."planets" WHERE "mirror_universe"."planets"."name" = $1 AND ("mirror_universe"."planets"."deletedAt" IS NULL OR "mirror_universe"."planets"."deletedAt" > $2)"#) - XCTAssertEqual(db.sqlSerializers.dropFirst(2).first?.sql, #"INSERT INTO "mirror_universe"."planets" ("id", "name", "star_id", "possible_star_id", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, DEFAULT, $3, $4, DEFAULT)"#) - XCTAssertEqual(db.sqlSerializers.dropFirst(3).first?.sql, #"UPDATE "mirror_universe"."planets" SET "id" = $1, "name" = $2, "updatedAt" = $3 WHERE "mirror_universe"."planets"."id" = $4 AND ("mirror_universe"."planets"."deletedAt" IS NULL OR "mirror_universe"."planets"."deletedAt" > $5)"#) - XCTAssertEqual(db.sqlSerializers.dropFirst(4).first?.sql, #"DELETE FROM "mirror_universe"."planets" WHERE "mirror_universe"."planets"."name" <> $1"#) - XCTAssertEqual(db.sqlSerializers.dropFirst(5).first?.sql, #"SELECT "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "stars" INNER JOIN "mirror_universe"."planets" ON "mirror_universe"."planets"."star_id" = "stars"."id" LIMIT 1"#) + XCTAssertEqual( + db.sqlSerializers.dropFirst(0).first?.sql, + #"CREATE TABLE "mirror_universe"."planets" ("id" UUID PRIMARY KEY, "name" TEXT NOT NULL, "star_id" UUID REFERENCES "stars" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION NOT NULL, "possible_star_id" UUID REFERENCES "stars" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, "createdAt" TIMESTAMPTZ, "updatedAt" TIMESTAMPTZ, "deletedAt" TIMESTAMPTZ DEFAULT NULL)"# + ) + XCTAssertEqual( + db.sqlSerializers.dropFirst(1).first?.sql, + #"SELECT "mirror_universe"."planets"."id" AS "mirror_universe_planets_id", "mirror_universe"."planets"."name" AS "mirror_universe_planets_name", "mirror_universe"."planets"."star_id" AS "mirror_universe_planets_star_id", "mirror_universe"."planets"."possible_star_id" AS "mirror_universe_planets_possible_star_id", "mirror_universe"."planets"."createdAt" AS "mirror_universe_planets_createdAt", "mirror_universe"."planets"."updatedAt" AS "mirror_universe_planets_updatedAt", "mirror_universe"."planets"."deletedAt" AS "mirror_universe_planets_deletedAt" FROM "mirror_universe"."planets" WHERE "mirror_universe"."planets"."name" = $1 AND ("mirror_universe"."planets"."deletedAt" IS NULL OR "mirror_universe"."planets"."deletedAt" > $2)"# + ) + XCTAssertEqual( + db.sqlSerializers.dropFirst(2).first?.sql, + #"INSERT INTO "mirror_universe"."planets" ("id", "name", "star_id", "possible_star_id", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, DEFAULT, $3, $4, DEFAULT)"# + ) + XCTAssertEqual( + db.sqlSerializers.dropFirst(3).first?.sql, + #"UPDATE "mirror_universe"."planets" SET "id" = $1, "name" = $2, "updatedAt" = $3 WHERE "mirror_universe"."planets"."id" = $4 AND ("mirror_universe"."planets"."deletedAt" IS NULL OR "mirror_universe"."planets"."deletedAt" > $5)"# + ) + XCTAssertEqual( + db.sqlSerializers.dropFirst(4).first?.sql, + #"DELETE FROM "mirror_universe"."planets" WHERE "mirror_universe"."planets"."name" <> $1"#) + XCTAssertEqual( + db.sqlSerializers.dropFirst(5).first?.sql, + #"SELECT "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "stars" INNER JOIN "mirror_universe"."planets" ON "mirror_universe"."planets"."star_id" = "stars"."id" LIMIT 1"# + ) } func testKeyPrefixingStrategies() throws { XCTAssertEqual(KeyPrefixingStrategy.none.apply(prefix: "abc", to: "def").description, "abcdef") XCTAssertEqual(KeyPrefixingStrategy.none.apply(prefix: "abc", to: .prefix("def", "ghi")).description, "abcdefghi") XCTAssertEqual(KeyPrefixingStrategy.none.apply(prefix: .prefix("abc", "def"), to: "ghi").description, "abcdefghi") - + XCTAssertEqual(KeyPrefixingStrategy.camelCase.apply(prefix: "abc", to: "def").description, "abcDef") XCTAssertEqual(KeyPrefixingStrategy.camelCase.apply(prefix: "abc", to: .prefix("def", "ghi")).description, "abcDefghi") XCTAssertEqual(KeyPrefixingStrategy.camelCase.apply(prefix: .prefix("abc", "def"), to: "ghi").description, "abcdefGhi") @@ -772,27 +858,28 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(KeyPrefixingStrategy.camelCase.apply(prefix: "", to: "ABC").description, "ABC") XCTAssertEqual(KeyPrefixingStrategy.camelCase.apply(prefix: "abc", to: "_def").description, "abc_def") XCTAssertEqual(KeyPrefixingStrategy.camelCase.apply(prefix: "abc_", to: "def").description, "abc_Def") - + XCTAssertEqual(KeyPrefixingStrategy.snakeCase.apply(prefix: "abc", to: "def").description, "abc_def") XCTAssertEqual(KeyPrefixingStrategy.snakeCase.apply(prefix: "abc", to: .prefix("def", "ghi")).description, "abc_defghi") XCTAssertEqual(KeyPrefixingStrategy.snakeCase.apply(prefix: .prefix("abc", "def"), to: "ghi").description, "abcdef_ghi") XCTAssertEqual(KeyPrefixingStrategy.snakeCase.apply(prefix: "abc_", to: "def").description, "abc__def") XCTAssertEqual(KeyPrefixingStrategy.snakeCase.apply(prefix: "abc", to: "_def").description, "abc__def") - - XCTAssertEqual(KeyPrefixingStrategy.custom({ .prefix($0, .prefix("+", $1)) }).apply(prefix: "abc", to: "def").description, "abc+def") + + XCTAssertEqual( + KeyPrefixingStrategy.custom({ .prefix($0, .prefix("+", $1)) }).apply(prefix: "abc", to: "def").description, "abc+def") } - + func testCreatingModelArraysWithUnsetOptionalProperties() throws { final class Foo: Model, @unchecked Sendable { static let schema = "foos" - + @ID var id: UUID? @OptionalField(key: "opt") var opt: String? - + init() {} init(id: UUID? = nil, opt: String? = nil) { (self.id, self.opt) = (id, opt) } } - + let foos = [ Foo(), Foo(opt: nil), @@ -832,7 +919,7 @@ final class User: Model, @unchecked Sendable { @Group(key: "pet") var pet: Pet - init() { } + init() {} init(id: UUID? = nil, name: String, pet: Pet) { self.id = id @@ -855,7 +942,7 @@ final class Pet: Fields, @unchecked Sendable { @Group(key: "toy") var toy: Toy - init() { } + init() {} init(name: String, type: Animal, toy: Toy) { self.name = name @@ -878,7 +965,7 @@ final class Toy: Fields, @unchecked Sendable { @Group(key: "foo") var foo: ToyFoo - init() { } + init() {} init(name: String, type: ToyType, foo: ToyFoo) { self.name = name @@ -894,7 +981,7 @@ final class ToyFoo: Fields, @unchecked Sendable { @Field(key: "baz") var baz: String - init() { } + init() {} init(bar: Int, baz: String) { self.bar = bar @@ -904,21 +991,21 @@ final class ToyFoo: Fields, @unchecked Sendable { final class Planet2: Model, @unchecked Sendable { static let schema = "planets" - + @ID(custom: "id", generatedBy: .database) var id: Int? - + @Field(key: "name") var name: String - + @Field(key: "nickname") var nickName: String? - + @Field(key: "moon_count") var moonCount: Int - - init() { } - + + init() {} + init(id: Int? = nil, name: String, moonCount: Int) { self.id = id self.name = name @@ -941,7 +1028,7 @@ final class AltPlanet: Model, @unchecked Sendable { @OptionalParent(key: "possible_star_id") public var possibleStar: Star? - + @Timestamp(key: "createdAt", on: .create) public var createdAt: Date? @@ -967,116 +1054,126 @@ final class AltPlanet: Model, @unchecked Sendable { final class LotsOfFields: Model, @unchecked Sendable { static let schema = "never_used" - + @ID(custom: "id") var id: Int? - + @Field(key: "field1") var field1: String - + @Field(key: "field2") var field2: String - + @Field(key: "field3") var field3: String - + @Field(key: "field4") var field4: String - + @Field(key: "field5") var field5: String - + @Field(key: "field6") var field6: String - + @Field(key: "field7") var field7: String - + @Field(key: "field8") var field8: String - + @Field(key: "field9") var field9: String - + @Field(key: "field10") var field10: String - + @Field(key: "field11") var field11: String - + @Field(key: "field12") var field12: String - + @Field(key: "field13") var field13: String - + @Field(key: "field14") var field14: String - + @Field(key: "field15") var field15: String - + @Field(key: "field16") var field16: String - + @Field(key: "field17") var field17: String - + @Field(key: "field18") var field18: String - + @Field(key: "field19") var field19: String - + @Field(key: "field20") var field20: String } final class AtFoo: Model, @unchecked Sendable { static let schema = "foos" - + @ID(custom: .id) var id: Int? @OptionalParent(key: "pre_foo_id") var preFoo: PreFoo? - + init() {} - init(id: Int? = nil, preFoo: PreFoo?) { self.id = id; self.$preFoo.id = preFoo?.id; self.$preFoo.value = preFoo } + init(id: Int? = nil, preFoo: PreFoo?) { + self.id = id + self.$preFoo.id = preFoo?.id + self.$preFoo.value = preFoo + } } final class PostFoo: Model, @unchecked Sendable { static let schema = "postfoos" - + @ID(custom: .id) var id: Int? - + init() {} init(id: Int? = nil) { self.id = id } } final class PreFoo: Model, @unchecked Sendable { static let schema = "prefoos" - + @ID(custom: .id) var id: Int? @Field(key: "boo") var boo: Bool - + @Children(for: \AtFoo.$preFoo) var foos: [AtFoo] @OptionalChild(for: \AtFoo.$preFoo) var afoo: AtFoo? @Siblings(through: MidFoo.self, from: \.$id.$prefoo, to: \.$id.$postfoo) var postfoos: [PostFoo] - + init() {} - init(id: Int? = nil, boo: Bool) { self.id = id; self.boo = boo } + init(id: Int? = nil, boo: Bool) { + self.id = id + self.boo = boo + } } final class MidFoo: Model, @unchecked Sendable { static let schema = "midfoos" - + final class IDValue: Fields, Hashable, @unchecked Sendable { @Parent(key: "prefoo_id") var prefoo: PreFoo @Parent(key: "postfoo_id") var postfoo: PostFoo - + init() {} init(prefooId: PreFoo.IDValue, postfooId: PostFoo.IDValue) { (self.$prefoo.id, self.$postfoo.id) = (prefooId, postfooId) } static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.$prefoo.id == rhs.$prefoo.id && lhs.$postfoo.id == rhs.$postfoo.id } - func hash(into hasher: inout Hasher) { hasher.combine(self.$prefoo.id); hasher.combine(self.$postfoo.id) } + func hash(into hasher: inout Hasher) { + hasher.combine(self.$prefoo.id) + hasher.combine(self.$postfoo.id) + } } @CompositeID var id: IDValue? diff --git a/Tests/FluentKitTests/OptionalEnumQueryTests.swift b/Tests/FluentKitTests/OptionalEnumQueryTests.swift index 75a00b6b..bf94ba8a 100644 --- a/Tests/FluentKitTests/OptionalEnumQueryTests.swift +++ b/Tests/FluentKitTests/OptionalEnumQueryTests.swift @@ -9,13 +9,13 @@ final class OptionalEnumQueryTests: DbQueryTestCase { _ = try Thing(id: 1, fb: .fizz).create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "fb") VALUES ($1, 'fizz')"#) } - + func testInsertNull() throws { db.fakedRows.append([.init(["id": UUID()])]) _ = try Thing(id: 1, fb: nil).create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "fb") VALUES ($1, NULL)"#) } - + func testBulkUpdateDoesntOverkill() throws { let thing = Thing(id: 1, fb: .buzz) db.fakedRows.append([.init(["id": UUID()])]) @@ -23,21 +23,21 @@ final class OptionalEnumQueryTests: DbQueryTestCase { try Thing.query(on: db).filter(\.$id != thing.id!).set(\.$id, to: 99).update().wait() assertLastQuery(db, #"UPDATE "things" SET "id" = $1 WHERE "things"."id" <> $2"#) } - + func testInsertAfterMutatingNullableField() throws { let thing = Thing(id: 1, fb: nil) thing.fb = .fizz db.fakedRows.append([.init(["id": UUID()])]) _ = try thing.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "fb") VALUES ($1, 'fizz')"#) - + let thing2 = Thing(id: 1, fb: .buzz) thing2.fb = nil db.fakedRows.append([.init(["id": UUID()])]) _ = try thing2.create(on: db).wait() assertLastQuery(db, #"INSERT INTO "things" ("id", "fb") VALUES ($1, NULL)"#) } - + func testSaveReplacingNonNull() throws { let thing = Thing(id: 1, fb: .fizz) db.fakedRows.append([.init(["id": UUID()])]) @@ -46,7 +46,7 @@ final class OptionalEnumQueryTests: DbQueryTestCase { _ = try thing.save(on: db).wait() assertLastQuery(db, #"UPDATE "things" SET "fb" = 'buzz' WHERE "things"."id" = $1"#) } - + func testSaveReplacingNull() throws { let thing = Thing(id: 1, fb: nil) db.fakedRows.append([.init(["id": UUID()])]) @@ -55,7 +55,7 @@ final class OptionalEnumQueryTests: DbQueryTestCase { _ = try thing.save(on: db).wait() assertLastQuery(db, #"UPDATE "things" SET "fb" = 'fizz' WHERE "things"."id" = $1"#) } - + // @see https://github.com/vapor/fluent-kit/issues/444 func testSaveNullReplacingNonNull() throws { let thing = Thing(id: 1, fb: .fizz) @@ -66,21 +66,21 @@ final class OptionalEnumQueryTests: DbQueryTestCase { XCTAssertNil(thing.fb) assertLastQuery(db, #"UPDATE "things" SET "fb" = NULL WHERE "things"."id" = $1"#) } - + func testBulkInsertWithoutNulls() throws { let things = [Thing(id: 1, fb: .fizz), Thing(id: 2, fb: .buzz)] db.fakedRows.append([.init(["id": UUID()])]) _ = try things.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "fb") VALUES ($1, 'fizz'), ($2, 'buzz')"#) } - + func testBulkInsertWithOnlyNulls() throws { let things = [Thing(id: 1, fb: nil), Thing(id: 2, fb: nil)] db.fakedRows.append([.init(["id": UUID()])]) _ = try things.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "fb") VALUES ($1, NULL), ($2, NULL)"#) } - + // @see https://github.com/vapor/fluent-kit/issues/396 func testBulkInsertWithMixedNulls() throws { let things = [Thing(id: 1, fb: nil), Thing(id: 2, fb: .fizz)] @@ -102,15 +102,15 @@ private final class Thing: Model, @unchecked Sendable { } static let schema = "things" - + @ID(custom: "id", generatedBy: .user) var id: Int? - + @OptionalEnum(key: "fb") var fb: FizzBuzz? - + init() {} - + init(id: Int, fb: FizzBuzz? = nil) { self.id = id self.fb = fb diff --git a/Tests/FluentKitTests/OptionalFieldQueryTests.swift b/Tests/FluentKitTests/OptionalFieldQueryTests.swift index 66647876..3acfe9b4 100644 --- a/Tests/FluentKitTests/OptionalFieldQueryTests.swift +++ b/Tests/FluentKitTests/OptionalFieldQueryTests.swift @@ -9,29 +9,29 @@ final class OptionalFieldQueryTests: DbQueryTestCase { _ = try Thing(id: 1, name: "Jared").create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2)"#) } - + func testInsertNull() throws { db.fakedRows.append([.init(["id": UUID()])]) _ = try Thing(id: 1, name: nil).create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, NULL)"#) } - + func testInsertAfterMutatingNullableField() throws { let thing = Thing(id: 1, name: nil) thing.name = "Jared" db.fakedRows.append([.init(["id": UUID()])]) _ = try thing.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2)"#) - + db.reset() - + let thing2 = Thing(id: 1, name: "Jared") thing2.name = nil db.fakedRows.append([.init(["id": UUID()])]) _ = try thing2.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, NULL)"#) } - + func testSaveReplacingNonNull() throws { let thing = Thing(id: 1, name: "Jared") db.fakedRows.append([.init(["id": UUID()])]) @@ -40,7 +40,7 @@ final class OptionalFieldQueryTests: DbQueryTestCase { _ = try thing.save(on: db).wait() assertLastQuery(db, #"UPDATE "things" SET "name" = $1 WHERE "things"."id" = $2"#) } - + func testSaveReplacingNull() throws { let thing = Thing(id: 1, name: nil) db.fakedRows.append([.init(["id": UUID()])]) @@ -49,7 +49,7 @@ final class OptionalFieldQueryTests: DbQueryTestCase { _ = try thing.save(on: db).wait() assertLastQuery(db, #"UPDATE "things" SET "name" = $1 WHERE "things"."id" = $2"#) } - + func testSaveNullReplacingNonNull() throws { let thing = Thing(id: 1, name: "Jared") db.fakedRows.append([.init(["id": UUID()])]) @@ -58,21 +58,21 @@ final class OptionalFieldQueryTests: DbQueryTestCase { _ = try thing.save(on: db).wait() assertLastQuery(db, #"UPDATE "things" SET "name" = NULL WHERE "things"."id" = $1"#) } - + func testBulkInsertWithoutNulls() throws { let things = [Thing(id: 1, name: "Jared"), Thing(id: 2, name: "Bob")] db.fakedRows.append([.init(["id": UUID()])]) _ = try things.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2), ($3, $4)"#) } - + func testBulkInsertWithOnlyNulls() throws { let things = [Thing(id: 1, name: nil), Thing(id: 2, name: nil)] db.fakedRows.append([.init(["id": UUID()])]) _ = try things.create(on: db).wait() assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, NULL), ($2, NULL)"#) } - + func testBulkInsertWithMixedNulls() throws { let things = [Thing(id: 1, name: "Jared"), Thing(id: 2, name: nil)] db.fakedRows.append([.init(["id": UUID()])]) diff --git a/Tests/FluentKitTests/QueryBuilderTests.swift b/Tests/FluentKitTests/QueryBuilderTests.swift index b5963469..98a5a2b0 100644 --- a/Tests/FluentKitTests/QueryBuilderTests.swift +++ b/Tests/FluentKitTests/QueryBuilderTests.swift @@ -1,5 +1,5 @@ -import FluentKit import FluentBenchmark +import FluentKit import Foundation import SQLKit import XCTFluent @@ -27,7 +27,7 @@ final class QueryBuilderTests: XCTestCase { TestOutput([ "id": planet.id as any Sendable, "name": planet.name, - "star_id": UUID() + "star_id": UUID(), ]) ]) @@ -44,13 +44,13 @@ final class QueryBuilderTests: XCTestCase { TestOutput([ "id": planet.id as any Sendable, "name": planet.name, - "star_id": UUID() + "star_id": UUID(), ]), TestOutput([ "id": UUID(), "name": "Nupeter", - "star_id": UUID() - ]) + "star_id": UUID(), + ]), ]) let retrievedPlanet = try Planet.query(on: test.db).first().wait() @@ -64,7 +64,7 @@ final class QueryBuilderTests: XCTestCase { let planets = [ Planet(id: UUID(), name: "P1", starId: starId), Planet(id: UUID(), name: "P2", starId: starId), - Planet(id: UUID(), name: "P3", starId: starId) + Planet(id: UUID(), name: "P3", starId: starId), ] let test = ArrayTestDatabase() test.append(planets.map(TestOutput.init)) @@ -80,10 +80,11 @@ final class QueryBuilderTests: XCTestCase { let planets = [ Planet(id: UUID(), name: "P1", starId: starId), Planet(id: UUID(), name: "P2", starId: starId), - Planet(id: UUID(), name: "P3", starId: starId) + Planet(id: UUID(), name: "P3", starId: starId), ] let test = ArrayTestDatabase() - let db = test.database(context: .init(configuration: test.configuration, logger: test.db.logger, eventLoop: test.db.eventLoop, history: .init())) + let db = test.database( + context: .init(configuration: test.configuration, logger: test.db.logger, eventLoop: test.db.eventLoop, history: .init())) test.append(planets.map(TestOutput.init)) let retrievedPlanets = try Planet.query(on: db).all().wait() @@ -101,15 +102,14 @@ final class QueryBuilderTests: XCTestCase { TestOutput(["id": UUID(), "name": "d", "star_id": starId]), TestOutput(["id": UUID(), "name": "e", "star_id": starId]), ] - + let test = CallbackTestDatabase { query in XCTAssertEqual(query.schema, "planets") let result: [TestOutput] - if - let limit = query.limits.first, - case let DatabaseQuery.Limit.count(limitValue) = limit, + if let limit = query.limits.first, + case DatabaseQuery.Limit.count(let limitValue) = limit, let offset = query.offsets.first, - case let DatabaseQuery.Offset.count(offsetValue) = offset + case DatabaseQuery.Offset.count(let offsetValue) = offset { result = [TestOutput](rows[min(offsetValue, rows.count - 1).. $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) + XCTAssertEqual( + db.sqlSerializers.first?.sql, + #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" LEFT JOIN "stars" ON "stars"."id" = "planets"."id" AND "stars"."name" = 'Sol' WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"# + ) } - + func testComplexJoinOperators() throws { let db = DummyDatabaseForTestSQLSerializer() - + _ = try Planet.query(on: db) .join(Star.self, on: \Star.$id == \Planet.$star.$id && \Star.$name != \Planet.$name) .all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) + XCTAssertEqual( + try db.sqlSerializers.xctAt(0).sql, + #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"# + ) } } diff --git a/Tests/FluentKitTests/SQLTests.swift b/Tests/FluentKitTests/SQLTests.swift index 9a12c0cb..bb85034f 100644 --- a/Tests/FluentKitTests/SQLTests.swift +++ b/Tests/FluentKitTests/SQLTests.swift @@ -1,13 +1,16 @@ import FluentKit import FluentSQL import SQLKit -import XCTest import XCTFluent +import XCTest final class SQLTests: DbQueryTestCase { func testFetchFluentModels() async throws { - let date1 = Date(), date2 = Date(), uuid1 = UUID(), uuid2 = UUID() - + let date1 = Date() + let date2 = Date() + let uuid1 = UUID() + let uuid2 = UUID() + self.db.fakedRows.append(contentsOf: [ [ .init([ @@ -26,11 +29,17 @@ final class SQLTests: DbQueryTestCase { .init(["id": 2, "field": Data([4, 5, 6, 7]), "model1_id": 2, "othermodel1_id": nil]), ], [ - .init(["model1_id": 1, "model2_id": 1]), + .init(["model1_id": 1, "model2_id": 1]) ], [ - .init(["field1": 1.0, "field2": uuid1, "pivot_model1_id": 1, "pivot_model2_id": 1, "optpivot_model1_id": 2, "optpivot_model2_id": 2]), - .init(["field1": 2.0, "field2": uuid2, "pivot_model1_id": 2, "pivot_model2_id": 2, "optpivot_model1_id": nil, "optpivot_model2_id": nil]), + .init([ + "field1": 1.0, "field2": uuid1, "pivot_model1_id": 1, "pivot_model2_id": 1, "optpivot_model1_id": 2, + "optpivot_model2_id": 2, + ]), + .init([ + "field1": 2.0, "field2": uuid2, "pivot_model1_id": 2, "pivot_model2_id": 2, "optpivot_model1_id": nil, + "optpivot_model2_id": nil, + ]), ], ]) @@ -38,12 +47,12 @@ final class SQLTests: DbQueryTestCase { let model2s = try await self.db.select().columns("*").from(Model2.schema).all(decodingFluent: Model2.self) let pivots = try await self.db.select().columns("*").from(Pivot.schema).all(decodingFluent: Pivot.self) let fromPivots = try await self.db.select().columns("*").from(FromPivot.schema).all(decodingFluent: FromPivot.self) - + XCTAssertEqual(model1s.count, 2) XCTAssertEqual(model2s.count, 2) XCTAssertEqual(pivots.count, 1) XCTAssertEqual(fromPivots.count, 2) - + let model1_1 = try XCTUnwrap(model1s.dropFirst(0).first) let model1_2 = try XCTUnwrap(model1s.dropFirst(1).first) let model2_1 = try XCTUnwrap(model2s.dropFirst(0).first) @@ -51,7 +60,7 @@ final class SQLTests: DbQueryTestCase { let pivot = try XCTUnwrap(pivots.first) let fromPivot1 = try XCTUnwrap(fromPivots.dropFirst(0).first) let fromPivot2 = try XCTUnwrap(fromPivots.dropFirst(1).first) - + XCTAssertEqual(model1_1.$id.value, 1) XCTAssertEqual(model1_1.$field.value, "a") XCTAssertEqual(model1_1.$optField.value, .some(.some(0))) @@ -79,7 +88,7 @@ final class SQLTests: DbQueryTestCase { XCTAssertEqual(model1_2.$model2s.fromId, model1_2.$id.value) XCTAssertEqual(model1_2.$otherModel2.fromId, model1_2.$id.value) XCTAssertEqual(model1_2.$pivotedModels2.fromId, model1_2.$id.value) - + XCTAssertEqual(model2_1.$id.value, 1) XCTAssertEqual(model2_1.$field.value, Data([0, 1, 2, 3])) XCTAssertEqual(model2_1.$model1.id, 1) @@ -89,10 +98,10 @@ final class SQLTests: DbQueryTestCase { XCTAssertEqual(model2_2.$field.value, Data([4, 5, 6, 7])) XCTAssertEqual(model2_2.$model1.id, 2) XCTAssertEqual(model2_2.$otherModel1.id, nil) - + XCTAssertEqual(pivot.$id.$model1.id, 1) XCTAssertEqual(pivot.$id.$model2.id, 1) - + XCTAssertEqual(fromPivot1.$id.$field1.value, 1.0) XCTAssertEqual(fromPivot1.$id.$field2.value, uuid1) XCTAssertEqual(fromPivot1.$pivot.id.$model1.$id.value, 1) @@ -107,9 +116,10 @@ final class SQLTests: DbQueryTestCase { XCTAssertEqual(fromPivot2.$optPivot.id?.$model1.$id.value, nil) XCTAssertEqual(fromPivot2.$optPivot.id?.$model2.$id.value, nil) } - + func testInsertFluentModels() async throws { - let model1_1 = Model1(), model1_2 = Model1() + let model1_1 = Model1() + let model1_2 = Model1() model1_1.field = "a" model1_1.optField = 1 model1_1.bool = true @@ -129,7 +139,8 @@ final class SQLTests: DbQueryTestCase { model1_2.optEnum = nil model1_2.group.groupfield1 = 32 model1_2.group.groupfield2 = 64 - let model2_1 = Model2(), model2_2 = Model2() + let model2_1 = Model2() + let model2_2 = Model2() model2_1.field = Data([0]) model2_1.$model1.id = 1 model2_1.$otherModel1.id = 2 @@ -140,7 +151,8 @@ final class SQLTests: DbQueryTestCase { let pivot = Pivot() pivot.id?.$model1.id = 1 pivot.id?.$model2.id = 1 - let fromPivot1 = FromPivot(), fromPivot2 = FromPivot() + let fromPivot1 = FromPivot() + let fromPivot2 = FromPivot() fromPivot1.id?.field1 = 1.0 fromPivot1.id?.field2 = UUID() fromPivot1.$pivot.id.$model1.id = 1 @@ -151,20 +163,28 @@ final class SQLTests: DbQueryTestCase { fromPivot2.$pivot.id.$model1.id = 2 fromPivot2.$pivot.id.$model2.id = 2 fromPivot2.$optPivot.id = nil - + try await self.db.insert(into: Model1.schema).fluentModels([model1_1, model1_2]).run() try await self.db.insert(into: Model2.schema).fluentModels([model2_1, model2_2]).run() try await self.db.insert(into: Pivot.schema).fluentModel(pivot).run() try await self.db.insert(into: FromPivot.schema).fluentModels([fromPivot1, fromPivot2]).run() - + XCTAssertEqual(self.db.sqlSerializers.count, 4) - XCTAssertEqual(self.db.sqlSerializers.dropFirst(0).first?.sql, #"INSERT INTO "model1s" ("bool", "created_at", "enum", "field", "group_groupfield1", "group_groupfield2", "id", "optbool", "optenum", "optfield") VALUES ($1, $2, 'foo', $3, $4, $5, DEFAULT, $6, 'bar', $7), ($8, $9, 'foo', $10, $11, $12, $13, NULL, NULL, NULL)"#) + XCTAssertEqual( + self.db.sqlSerializers.dropFirst(0).first?.sql, + #"INSERT INTO "model1s" ("bool", "created_at", "enum", "field", "group_groupfield1", "group_groupfield2", "id", "optbool", "optenum", "optfield") VALUES ($1, $2, 'foo', $3, $4, $5, DEFAULT, $6, 'bar', $7), ($8, $9, 'foo', $10, $11, $12, $13, NULL, NULL, NULL)"# + ) XCTAssertEqual(self.db.sqlSerializers.dropFirst(0).first?.binds.count, 13) - XCTAssertEqual(self.db.sqlSerializers.dropFirst(1).first?.sql, #"INSERT INTO "model2s" ("field", "id", "model1_id", "othermodel1_id") VALUES ($1, DEFAULT, $2, $3), ($4, $5, $6, NULL)"#) + XCTAssertEqual( + self.db.sqlSerializers.dropFirst(1).first?.sql, + #"INSERT INTO "model2s" ("field", "id", "model1_id", "othermodel1_id") VALUES ($1, DEFAULT, $2, $3), ($4, $5, $6, NULL)"#) XCTAssertEqual(self.db.sqlSerializers.dropFirst(1).first?.binds.count, 6) XCTAssertEqual(self.db.sqlSerializers.dropFirst(2).first?.sql, #"INSERT INTO "pivots" ("model1_id", "model2_id") VALUES ($1, $2)"#) XCTAssertEqual(self.db.sqlSerializers.dropFirst(2).first?.binds.count, 2) - XCTAssertEqual(self.db.sqlSerializers.dropFirst(3).first?.sql, #"INSERT INTO "from_pivots" ("field1", "field2", "optpivot_model1_id", "optpivot_model2_id", "pivot_model1_id", "pivot_model2_id") VALUES ($1, $2, $3, $4, $5, $6), ($7, $8, NULL, NULL, $9, $10)"#) + XCTAssertEqual( + self.db.sqlSerializers.dropFirst(3).first?.sql, + #"INSERT INTO "from_pivots" ("field1", "field2", "optpivot_model1_id", "optpivot_model2_id", "pivot_model1_id", "pivot_model2_id") VALUES ($1, $2, $3, $4, $5, $6), ($7, $8, NULL, NULL, $9, $10)"# + ) XCTAssertEqual(self.db.sqlSerializers.dropFirst(3).first?.binds.count, 10) } } @@ -220,7 +240,10 @@ final class Pivot: Model, @unchecked Sendable { init(model1Id: Model1.IDValue, model2Id: Model2.IDValue) { (self.$model1.id, self.$model2.id) = (model1Id, model2Id) } static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.$model1.id == rhs.$model1.id && lhs.$model2.id == rhs.$model2.id } - func hash(into hasher: inout Hasher) { hasher.combine(self.$model1.id); hasher.combine(self.$model2.id) } + func hash(into hasher: inout Hasher) { + hasher.combine(self.$model1.id) + hasher.combine(self.$model2.id) + } } @CompositeID var id: IDValue? @@ -233,7 +256,7 @@ final class Pivot: Model, @unchecked Sendable { final class FromPivot: Model, @unchecked Sendable { static let schema = "from_pivots" - + final class IDValue: Fields, Hashable, @unchecked Sendable { @Field(key: "field1") var field1: Double @Field(key: "field2") var field2: UUID @@ -242,13 +265,16 @@ final class FromPivot: Model, @unchecked Sendable { init(field1: Double, field2: UUID) { (self.field1, self.field2) = (field1, field2) } static func == (lhs: IDValue, rhs: IDValue) -> Bool { lhs.field1 == rhs.field1 && lhs.field2 == rhs.field2 } - func hash(into hasher: inout Hasher) { hasher.combine(self.field1); hasher.combine(self.field2) } + func hash(into hasher: inout Hasher) { + hasher.combine(self.field1) + hasher.combine(self.field2) + } } - + @CompositeID var id: IDValue? @CompositeParent(prefix: "pivot", strategy: .snakeCase) var pivot: Pivot @CompositeOptionalParent(prefix: "optpivot", strategy: .snakeCase) var optPivot: Pivot? - + init() {} init(field1: Double, field2: UUID) { self.id = .init(field1: field1, field2: field2) } } diff --git a/Tests/FluentKitTests/TestUtilities.swift b/Tests/FluentKitTests/TestUtilities.swift index b2cd5912..0477af8e 100644 --- a/Tests/FluentKitTests/TestUtilities.swift +++ b/Tests/FluentKitTests/TestUtilities.swift @@ -4,7 +4,7 @@ import XCTest class DbQueryTestCase: XCTestCase { var db = DummyDatabaseForTestSQLSerializer() - + override class func setUp() { super.setUp() XCTAssertTrue(isLoggingConfigured) @@ -13,7 +13,7 @@ class DbQueryTestCase: XCTestCase { override func setUp() { self.db = DummyDatabaseForTestSQLSerializer() } - + override func tearDown() { self.db.reset() } From 82acaec02bf39e6a083d254c7b00da855c910787 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 3 Dec 2025 11:05:52 +0100 Subject: [PATCH 4/5] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63379973..36bee273 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Team Chat MIT License Continuous Integration - -Swift 5.10+ +Code Coverage +Swift 6.0+


From c4a5296e10ceb45ad45241c2cdff7624ac88d593 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 3 Dec 2025 11:10:39 +0100 Subject: [PATCH 5/5] Add linting in CI --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13961d74..b5bab0a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -119,3 +119,4 @@ jobs: secrets: inherit with: with_android: true + with_linting: true