+
+
+
+An Object-Relational Mapper (ORM) for Swift. It allows you to write type safe, database agnostic models and queries. It takes advantage of Swift's type system to provide a powerful, yet easy to use API.
+
+An example query looks like:
+
+```swift
+let planets = try await Planet.query(on: database)
+ .filter(\.$type == .gasGiant)
+ .sort(\.$name)
+ .with(\.$star)
+ .all()
+```
+
+For more information, see the [Fluent documentation](https://docs.vapor.codes/fluent/overview/).
diff --git a/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift b/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift
index d172cf8b..94421edb 100644
--- a/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift
+++ b/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift
@@ -2,10 +2,10 @@ import FluentKit
import Foundation
import NIOCore
-public final class GalacticJurisdiction: Model {
+public final class GalacticJurisdiction: Model, @unchecked Sendable {
public static let schema = "galaxy_jurisdictions"
- public final class IDValue: Fields, Hashable {
+ public final class IDValue: Fields, Hashable, @unchecked Sendable {
@Parent(key: "galaxy_id")
public var galaxy: Galaxy
diff --git a/Sources/FluentBenchmark/SolarSystem/Galaxy.swift b/Sources/FluentBenchmark/SolarSystem/Galaxy.swift
index 93411087..1b937ed9 100644
--- a/Sources/FluentBenchmark/SolarSystem/Galaxy.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Galaxy.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class Galaxy: Model {
+public final class Galaxy: Model, @unchecked Sendable {
public static let schema = "galaxies"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/Governor.swift b/Sources/FluentBenchmark/SolarSystem/Governor.swift
index a2b94c11..59a243d2 100644
--- a/Sources/FluentBenchmark/SolarSystem/Governor.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Governor.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class Governor: Model {
+public final class Governor: Model, @unchecked Sendable {
public static let schema = "governors"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift b/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift
index c3cb325f..a48c0f4a 100644
--- a/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class Jurisdiction: Model {
+public final class Jurisdiction: Model, @unchecked Sendable {
public static let schema = "jurisdictions"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/Moon.swift b/Sources/FluentBenchmark/SolarSystem/Moon.swift
index 7a5e0a91..3900518b 100644
--- a/Sources/FluentBenchmark/SolarSystem/Moon.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Moon.swift
@@ -2,7 +2,7 @@ import FluentKit
import Foundation
import NIOCore
-public final class Moon: Model {
+public final class Moon: Model, @unchecked Sendable {
public static let schema = "moons"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/Planet.swift b/Sources/FluentBenchmark/SolarSystem/Planet.swift
index 7e5da08d..01c97467 100644
--- a/Sources/FluentBenchmark/SolarSystem/Planet.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Planet.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class Planet: Model {
+public final class Planet: Model, @unchecked Sendable {
public static let schema = "planets"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift b/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift
index 8b38ec43..8dc2e74d 100644
--- a/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift
+++ b/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class PlanetTag: Model {
+public final class PlanetTag: Model, @unchecked Sendable {
public static let schema = "planet+tag"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/Star.swift b/Sources/FluentBenchmark/SolarSystem/Star.swift
index 45ab4097..9ef087e6 100644
--- a/Sources/FluentBenchmark/SolarSystem/Star.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Star.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class Star: Model {
+public final class Star: Model, @unchecked Sendable {
public static let schema = "stars"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/SolarSystem/Tag.swift b/Sources/FluentBenchmark/SolarSystem/Tag.swift
index ff2feae7..e99342a0 100644
--- a/Sources/FluentBenchmark/SolarSystem/Tag.swift
+++ b/Sources/FluentBenchmark/SolarSystem/Tag.swift
@@ -3,7 +3,7 @@ import Foundation
import NIOCore
import XCTest
-public final class Tag: Model {
+public final class Tag: Model, @unchecked Sendable {
public static let schema = "tags"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/ArrayTests.swift b/Sources/FluentBenchmark/Tests/ArrayTests.swift
index 5de5b35a..8ca7b7d4 100644
--- a/Sources/FluentBenchmark/Tests/ArrayTests.swift
+++ b/Sources/FluentBenchmark/Tests/ArrayTests.swift
@@ -68,7 +68,7 @@ private struct Qux: Codable {
var foo: String
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -114,7 +114,7 @@ private enum Role: String, Codable, Equatable {
case client
}
-private final class User: Model {
+private final class User: Model, @unchecked Sendable {
static let schema = "users"
@ID(key: .id)
@@ -144,7 +144,7 @@ private struct UserMigration: Migration {
}
}
-private final class FooSet: Model {
+private final class FooSet: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/ChildTests.swift b/Sources/FluentBenchmark/Tests/ChildTests.swift
index f4a7d7b0..166e18f7 100644
--- a/Sources/FluentBenchmark/Tests/ChildTests.swift
+++ b/Sources/FluentBenchmark/Tests/ChildTests.swift
@@ -107,7 +107,7 @@ extension FluentBenchmarker {
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -143,7 +143,7 @@ private struct FooMigration: Migration {
}
}
-private final class Bar: Model {
+private final class Bar: Model, @unchecked Sendable {
static let schema = "bars"
@ID(key: .id)
@@ -179,7 +179,7 @@ private struct BarMigration: Migration {
}
}
-private final class Baz: Model {
+private final class Baz: Model, @unchecked Sendable {
static let schema = "bazs"
@ID(key: .id)
@@ -214,7 +214,7 @@ private struct BazMigration: Migration {
}
}
-private final class Game: Model {
+private final class Game: Model, @unchecked Sendable {
static let schema = "games"
@ID(custom: .id, generatedBy: .database)
@@ -251,7 +251,7 @@ private struct GameMigration: Migration {
}
}
-private final class Player: Model {
+private final class Player: Model, @unchecked Sendable {
static let schema = "players"
@ID(custom: .id, generatedBy: .database)
diff --git a/Sources/FluentBenchmark/Tests/ChildrenTests.swift b/Sources/FluentBenchmark/Tests/ChildrenTests.swift
index ce569ca4..62b3d0f0 100644
--- a/Sources/FluentBenchmark/Tests/ChildrenTests.swift
+++ b/Sources/FluentBenchmark/Tests/ChildrenTests.swift
@@ -35,7 +35,7 @@ extension FluentBenchmarker {
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -71,7 +71,7 @@ private struct FooMigration: Migration {
}
}
-private final class Bar: Model {
+private final class Bar: Model, @unchecked Sendable {
static let schema = "bars"
@ID(key: .id)
@@ -106,7 +106,7 @@ private struct BarMigration: Migration {
}
}
-private final class Baz: Model {
+private final class Baz: Model, @unchecked Sendable {
static let schema = "bazs"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/ChunkTests.swift b/Sources/FluentBenchmark/Tests/ChunkTests.swift
index 9877834b..cd7f136a 100644
--- a/Sources/FluentBenchmark/Tests/ChunkTests.swift
+++ b/Sources/FluentBenchmark/Tests/ChunkTests.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
import XCTest
extension FluentBenchmarker {
@@ -10,8 +11,6 @@ extension FluentBenchmarker {
try runTest(#function, [
GalaxyMigration(),
]) {
- var fetched64: [Result] = []
- var fetched2047: [Result] = []
let saves = (1...512).map { i -> EventLoopFuture in
return Galaxy(name: "Milky Way \(i)")
@@ -19,29 +18,33 @@ extension FluentBenchmarker {
}
try EventLoopFuture.andAllSucceed(saves, on: self.database.eventLoop).wait()
+ let fetched64 = NIOLockedValueBox(0)
+
try Galaxy.query(on: self.database).chunk(max: 64) { chunk in
guard chunk.count == 64 else {
XCTFail("bad chunk count")
return
}
- fetched64 += chunk
+ fetched64.withLockedValue { $0 += chunk.count }
}.wait()
- guard fetched64.count == 512 else {
- XCTFail("did not fetch all - only \(fetched64.count) out of 512")
+ guard fetched64.withLockedValue({ $0 }) == 512 else {
+ XCTFail("did not fetch all - only \(fetched64.withLockedValue { $0 }) out of 512")
return
}
+ let fetched511 = NIOLockedValueBox(0)
+
try Galaxy.query(on: self.database).chunk(max: 511) { chunk in
guard chunk.count == 511 || chunk.count == 1 else {
XCTFail("bad chunk count")
return
}
- fetched2047 += chunk
+ fetched511.withLockedValue { $0 += chunk.count }
}.wait()
- guard fetched2047.count == 512 else {
- XCTFail("did not fetch all - only \(fetched2047.count) out of 512")
+ guard fetched511.withLockedValue({ $0 }) == 512 else {
+ XCTFail("did not fetch all - only \(fetched511.withLockedValue { $0 }) out of 512")
return
}
}
diff --git a/Sources/FluentBenchmark/Tests/CodableTests.swift b/Sources/FluentBenchmark/Tests/CodableTests.swift
index 68c2429c..90b0d71c 100644
--- a/Sources/FluentBenchmark/Tests/CodableTests.swift
+++ b/Sources/FluentBenchmark/Tests/CodableTests.swift
@@ -28,7 +28,7 @@ extension FluentBenchmarker {
}
}
-final class Question: Model {
+final class Question: Model, @unchecked Sendable {
static let schema = "questions"
@ID(custom: "id")
@@ -57,7 +57,7 @@ final class Question: Model {
}
}
-final class Project: Model {
+final class Project: Model, @unchecked Sendable {
static let schema = "projects"
@ID(custom: "id")
diff --git a/Sources/FluentBenchmark/Tests/CompositeIDTests.swift b/Sources/FluentBenchmark/Tests/CompositeIDTests.swift
index 7f178081..b44d4b8d 100644
--- a/Sources/FluentBenchmark/Tests/CompositeIDTests.swift
+++ b/Sources/FluentBenchmark/Tests/CompositeIDTests.swift
@@ -175,10 +175,10 @@ extension FluentBenchmarker {
}
}
-public final class CompositeIDModel: Model {
+public final class CompositeIDModel: Model, @unchecked Sendable {
public static let schema = "composite_id_models"
- public final class IDValue: Fields, Hashable {
+ public final class IDValue: Fields, Hashable, @unchecked Sendable {
@Field(key: "name")
public var name: String
diff --git a/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift b/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift
index 22b809d6..278d3f1b 100644
--- a/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift
+++ b/Sources/FluentBenchmark/Tests/CompositeRelationTests.swift
@@ -146,10 +146,10 @@ extension FluentBenchmarker {
}
}
-final class CompositeIDParentModel: Model {
+final class CompositeIDParentModel: Model, @unchecked Sendable {
static let schema = "composite_id_parent_models"
- final class IDValue: Fields, Hashable {
+ final class IDValue: Fields, Hashable, @unchecked Sendable {
@Field(key: "name")
var name: String
@@ -228,7 +228,7 @@ final class CompositeIDParentModel: Model {
}
}
-final class CompositeIDChildModel: Model {
+final class CompositeIDChildModel: Model, @unchecked Sendable {
static let schema = "composite_id_child_models"
@ID(custom: .id)
@@ -339,10 +339,10 @@ extension DatabaseSchema.Constraint {
}
}
-final class CompositeParentTheFirst: Model {
+final class CompositeParentTheFirst: Model, @unchecked Sendable {
static let schema = "composite_parent_the_first"
- final class IDValue: Fields, Hashable {
+ final class IDValue: Fields, Hashable, @unchecked Sendable {
@Parent(key: "parent_id")
var parent: Galaxy
@@ -389,10 +389,10 @@ final class CompositeParentTheFirst: Model {
}
}
-final class CompositeParentTheSecond: Model {
+final class CompositeParentTheSecond: Model, @unchecked Sendable {
static let schema = "composite_parent_the_second"
- final class IDValue: Fields, Hashable {
+ final class IDValue: Fields, Hashable, @unchecked Sendable {
@CompositeParent(prefix: "ref", strategy: .snakeCase)
var parent: CompositeParentTheFirst
diff --git a/Sources/FluentBenchmark/Tests/EagerLoadTests.swift b/Sources/FluentBenchmark/Tests/EagerLoadTests.swift
index 6ee0779f..318cda72 100644
--- a/Sources/FluentBenchmark/Tests/EagerLoadTests.swift
+++ b/Sources/FluentBenchmark/Tests/EagerLoadTests.swift
@@ -278,7 +278,7 @@ extension FluentBenchmarker {
}
}
-private final class A: Model {
+private final class A: Model, @unchecked Sendable {
static let schema = "a"
@ID
@@ -290,7 +290,7 @@ private final class A: Model {
init() { }
}
-private final class B: Model {
+private final class B: Model, @unchecked Sendable {
static let schema = "b"
@ID
@@ -302,7 +302,7 @@ private final class B: Model {
init() { }
}
-private final class C: Model {
+private final class C: Model, @unchecked Sendable {
static let schema = "c"
@ID
diff --git a/Sources/FluentBenchmark/Tests/EnumTests.swift b/Sources/FluentBenchmark/Tests/EnumTests.swift
index e294ec15..aa648f4f 100644
--- a/Sources/FluentBenchmark/Tests/EnumTests.swift
+++ b/Sources/FluentBenchmark/Tests/EnumTests.swift
@@ -310,7 +310,7 @@ private enum Bar: String, Codable {
case baz, qux, quz, quzz
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -389,7 +389,7 @@ private enum Animal: UInt8, Codable {
case dog, cat
}
-private final class Pet: Model {
+private final class Pet: Model, @unchecked Sendable {
static let schema = "pets"
@ID(key: .id)
@@ -420,7 +420,7 @@ private struct PetMigration: Migration {
}
}
-private final class Flags: Model {
+private final class Flags: Model, @unchecked Sendable {
static let schema = "flags"
@ID(key: .id)
@@ -457,7 +457,7 @@ private final class Flags: Model {
}
}
-private final class RawFlags: Model {
+private final class RawFlags: Model, @unchecked Sendable {
static let schema = "flags"
@ID(key: .id) var id: UUID?
diff --git a/Sources/FluentBenchmark/Tests/FilterTests.swift b/Sources/FluentBenchmark/Tests/FilterTests.swift
index b375ed59..94f73006 100644
--- a/Sources/FluentBenchmark/Tests/FilterTests.swift
+++ b/Sources/FluentBenchmark/Tests/FilterTests.swift
@@ -247,7 +247,7 @@ extension FluentBenchmarker {
}
}
-private final class FooOwner: Model {
+private final class FooOwner: Model, @unchecked Sendable {
static let schema = "foo_owners"
@ID var id: UUID?
@Field(key: "name") var name: String
@@ -263,7 +263,7 @@ private enum FooEnumType: String, Codable {
case baz
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID var id: UUID?
@OptionalField(key: "bar") var bar: String?
diff --git a/Sources/FluentBenchmark/Tests/GroupTests.swift b/Sources/FluentBenchmark/Tests/GroupTests.swift
index 219272ef..e29a2418 100644
--- a/Sources/FluentBenchmark/Tests/GroupTests.swift
+++ b/Sources/FluentBenchmark/Tests/GroupTests.swift
@@ -90,7 +90,7 @@ extension FluentBenchmarker {
// MARK: Flat
-private final class FlatMoon: Model {
+private final class FlatMoon: Model, @unchecked Sendable {
static let schema = "moons"
@ID(key: .id)
@@ -99,7 +99,7 @@ private final class FlatMoon: Model {
@Field(key: "name")
var name: String
- final class Planet: Fields {
+ final class Planet: Fields, @unchecked Sendable {
@Field(key: "name")
var name: String
@@ -110,11 +110,11 @@ private final class FlatMoon: Model {
@Field(key: "type")
var type: PlanetType
- final class Star: Fields {
+ final class Star: Fields, @unchecked Sendable {
@Field(key: "name")
var name: String
- final class Galaxy: Fields {
+ final class Galaxy: Fields, @unchecked Sendable {
@Field(key: "name")
var name: String
diff --git a/Sources/FluentBenchmark/Tests/IDTests.swift b/Sources/FluentBenchmark/Tests/IDTests.swift
index 41c41c81..9407507a 100644
--- a/Sources/FluentBenchmark/Tests/IDTests.swift
+++ b/Sources/FluentBenchmark/Tests/IDTests.swift
@@ -76,7 +76,7 @@ extension FluentBenchmarker {
}
// Model recommended, default @ID configuration.
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID
@@ -101,7 +101,7 @@ private struct FooMigration: Migration {
}
// Model with custom id key and type.
-private final class StringFoo: Model {
+private final class StringFoo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(custom: .id, generatedBy: .user)
@@ -126,7 +126,7 @@ private struct StringFooMigration: Migration {
}
// Model with auto-incrementing id.
-private final class AutoincrementingFoo: Model {
+private final class AutoincrementingFoo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(custom: .id, generatedBy: .database)
@@ -151,7 +151,7 @@ private struct AutoincrementingFooMigration: Migration {
}
// Model with auto-incrementing and custom key.
-private final class CustomAutoincrementingFoo: Model {
+private final class CustomAutoincrementingFoo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(custom: "bar", generatedBy: .database)
diff --git a/Sources/FluentBenchmark/Tests/JoinTests.swift b/Sources/FluentBenchmark/Tests/JoinTests.swift
index fb5a7997..ead55465 100644
--- a/Sources/FluentBenchmark/Tests/JoinTests.swift
+++ b/Sources/FluentBenchmark/Tests/JoinTests.swift
@@ -156,7 +156,7 @@ extension FluentBenchmarker {
}
private func testJoin_aliasNesting() throws {
- final class ChatParticipant: Model {
+ final class ChatParticipant: Model, @unchecked Sendable {
static let schema = "chat_participants"
@ID(key: .id)
@@ -166,7 +166,7 @@ extension FluentBenchmarker {
var user: User
}
- final class User: Model {
+ final class User: Model, @unchecked Sendable {
static let schema = "users"
@ID(key: .id)
@@ -179,7 +179,7 @@ extension FluentBenchmarker {
}
final class OtherParticipant: ModelAlias {
static let name: String = "other_participant"
- var model = ChatParticipant()
+ let model = ChatParticipant()
}
_ = User.query(on: self.database)
@@ -234,7 +234,7 @@ extension FluentBenchmarker {
}
}
-private final class Team: Model {
+private final class Team: Model, @unchecked Sendable {
static let schema = "teams"
@ID(key: .id)
@@ -270,7 +270,7 @@ private struct TeamMigration: Migration {
}
}
-private final class Match: Model {
+private final class Match: Model, @unchecked Sendable {
static let schema = "matches"
@ID(key: .id)
@@ -316,7 +316,7 @@ 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
- return .andAllSucceed([
+ .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),
@@ -337,7 +337,7 @@ private struct TeamMatchSeed: Migration {
}
-private final class School: Model {
+private final class School: Model, @unchecked Sendable {
static let schema = "schools"
@ID(key: .id)
@@ -423,7 +423,7 @@ private struct SchoolSeed: Migration {
}
}
-private final class City: Model {
+private final class City: Model, @unchecked Sendable {
static let schema = "cities"
@ID(key: .id)
@@ -475,6 +475,6 @@ private struct CitySeed: Migration {
}
func revert(on database: any Database) -> EventLoopFuture {
- return database.eventLoop.makeSucceededFuture(())
+ database.eventLoop.makeSucceededFuture(())
}
}
diff --git a/Sources/FluentBenchmark/Tests/MiddlewareTests.swift b/Sources/FluentBenchmark/Tests/MiddlewareTests.swift
index b3b886bb..aace231d 100644
--- a/Sources/FluentBenchmark/Tests/MiddlewareTests.swift
+++ b/Sources/FluentBenchmark/Tests/MiddlewareTests.swift
@@ -140,7 +140,7 @@ private struct TestError: Error {
var string: String
}
-private final class User: Model {
+private final class User: Model, @unchecked Sendable {
static let schema = "users"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/MigratorTests.swift b/Sources/FluentBenchmark/Tests/MigratorTests.swift
index c4e74605..f8dfdfa7 100644
--- a/Sources/FluentBenchmark/Tests/MigratorTests.swift
+++ b/Sources/FluentBenchmark/Tests/MigratorTests.swift
@@ -17,9 +17,10 @@ extension FluentBenchmarker {
let migrations = Migrations()
migrations.add(GalaxyMigration())
migrations.add(StarMigration())
-
+
+ let database = self.database
let migrator = Migrator(
- databaseFactory: { _ in self.database },
+ databaseFactory: { _ in database },
migrations: migrations,
on: self.database.eventLoop
)
@@ -45,9 +46,10 @@ extension FluentBenchmarker {
migrations.add(GalaxyMigration())
migrations.add(ErrorMigration())
migrations.add(StarMigration())
-
+
+ let database = self.database
let migrator = Migrator(
- databaseFactory: { _ in self.database },
+ databaseFactory: { _ in database },
migrations: migrations,
on: self.database.eventLoop
)
diff --git a/Sources/FluentBenchmark/Tests/ModelTests.swift b/Sources/FluentBenchmark/Tests/ModelTests.swift
index 83d6a4d5..611099ac 100644
--- a/Sources/FluentBenchmark/Tests/ModelTests.swift
+++ b/Sources/FluentBenchmark/Tests/ModelTests.swift
@@ -183,12 +183,12 @@ extension FluentBenchmarker {
private func testModel_useOfFieldsWithoutGroup() throws {
try runTest(#function, []) {
- final class Contained: Fields {
+ final class Contained: Fields, @unchecked Sendable {
@Field(key: "something") var something: String
@Field(key: "another") var another: Int
init() {}
}
- final class Enclosure: Model {
+ final class Enclosure: Model, @unchecked Sendable {
static let schema = "enclosures"
@ID(custom: .id) var id: Int?
@Field(key: "primary") var primary: Contained
@@ -253,7 +253,7 @@ struct BadFooOutput: DatabaseOutput {
}
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -283,7 +283,7 @@ private struct FooMigration: Migration {
}
}
-private final class User: Model {
+private final class User: Model, @unchecked Sendable {
static let schema = "users"
@ID(key: .id)
@@ -312,7 +312,7 @@ private struct UserMigration: Migration {
}
}
-private final class Todo: Model {
+private final class Todo: Model, @unchecked Sendable {
static let schema = "todos"
@ID(key: .id)
@@ -341,7 +341,7 @@ private struct TodoMigration: Migration {
}
}
-private final class Bar: Model {
+private final class Bar: Model, @unchecked Sendable {
static let schema = "bars"
@ID
diff --git a/Sources/FluentBenchmark/Tests/OptionalParentTests.swift b/Sources/FluentBenchmark/Tests/OptionalParentTests.swift
index 041a6ad0..1f844653 100644
--- a/Sources/FluentBenchmark/Tests/OptionalParentTests.swift
+++ b/Sources/FluentBenchmark/Tests/OptionalParentTests.swift
@@ -89,7 +89,7 @@ extension FluentBenchmarker {
}
}
-private final class User: Model {
+private final class User: Model, @unchecked Sendable {
struct Pet: Codable {
enum Animal: String, Codable {
case cat, dog
diff --git a/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift b/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift
index 6bcb1abe..e6c1be77 100644
--- a/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift
+++ b/Sources/FluentBenchmark/Tests/PerformanceTests+Siblings.swift
@@ -147,7 +147,7 @@ private struct ExpeditionPeopleSeed: Migration {
}
}
-private final class Person: Model {
+private final class Person: Model, @unchecked Sendable {
static let schema = "people"
@ID
@@ -191,7 +191,7 @@ private struct PersonMigration: Migration {
}
}
-private final class Expedition: Model {
+private final class Expedition: Model, @unchecked Sendable {
static let schema = "expeditions"
@ID(key: .id)
@@ -245,7 +245,7 @@ private struct ExpeditionMigration: Migration {
}
}
-private final class ExpeditionOfficer: Model {
+private final class ExpeditionOfficer: Model, @unchecked Sendable {
static let schema = "expedition+officer"
@ID(key: .id)
@@ -274,7 +274,7 @@ private struct ExpeditionOfficerMigration: Migration {
}
}
-private final class ExpeditionScientist: Model {
+private final class ExpeditionScientist: Model, @unchecked Sendable {
static let schema = "expedition+scientist"
@ID(key: .id)
@@ -304,7 +304,7 @@ private struct ExpeditionScientistMigration: Migration {
}
-private final class ExpeditionDoctor: Model {
+private final class ExpeditionDoctor: Model, @unchecked Sendable {
static let schema = "expedition+doctor"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/PerformanceTests.swift b/Sources/FluentBenchmark/Tests/PerformanceTests.swift
index 5504868b..d05a09d5 100644
--- a/Sources/FluentBenchmark/Tests/PerformanceTests.swift
+++ b/Sources/FluentBenchmark/Tests/PerformanceTests.swift
@@ -39,7 +39,7 @@ extension FluentBenchmarker {
}
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
struct Thud: Codable {
diff --git a/Sources/FluentBenchmark/Tests/SQLTests.swift b/Sources/FluentBenchmark/Tests/SQLTests.swift
index 7de7e34b..0c982c02 100644
--- a/Sources/FluentBenchmark/Tests/SQLTests.swift
+++ b/Sources/FluentBenchmark/Tests/SQLTests.swift
@@ -21,7 +21,7 @@ extension FluentBenchmarker {
// test db.first(decoding:)
do {
- let user = try sql.raw("SELECT * FROM users").first(decoding: User.self).wait()
+ let user = try sql.raw("SELECT * FROM users").first(decodingFluent: User.self).wait()
XCTAssertNotNil(user)
if let user = user {
XCTAssertEqual(user.id, tanner.id)
@@ -33,7 +33,7 @@ extension FluentBenchmarker {
// test db.all(decoding:)
do {
- let users = try sql.raw("SELECT * FROM users").all(decoding: User.self).wait()
+ let users = try sql.raw("SELECT * FROM users").all(decodingFluent: User.self).wait()
XCTAssertEqual(users.count, 1)
if let user = users.first {
XCTAssertEqual(user.id, tanner.id)
@@ -46,7 +46,7 @@ extension FluentBenchmarker {
// test row.decode()
do {
let users = try sql.raw("SELECT * FROM users").all().wait().map {
- try $0.decode(model: User.self)
+ try $0.decode(fluentModel: User.self)
}
XCTAssertEqual(users.count, 1)
if let user = users.first {
@@ -61,7 +61,7 @@ extension FluentBenchmarker {
}
-private final class User: Model {
+private final class User: Model, @unchecked Sendable {
static let schema = "users"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/SchemaTests.swift b/Sources/FluentBenchmark/Tests/SchemaTests.swift
index f4bc10da..388b6dc3 100644
--- a/Sources/FluentBenchmark/Tests/SchemaTests.swift
+++ b/Sources/FluentBenchmark/Tests/SchemaTests.swift
@@ -176,7 +176,7 @@ extension FluentBenchmarker {
}
}
-final class Category: Model {
+final class Category: Model, @unchecked Sendable {
static let schema = "categories"
@ID var id: UUID?
@Field(key: "name") var name: String
diff --git a/Sources/FluentBenchmark/Tests/SetTests.swift b/Sources/FluentBenchmark/Tests/SetTests.swift
index 79c24dfc..e543fdff 100644
--- a/Sources/FluentBenchmark/Tests/SetTests.swift
+++ b/Sources/FluentBenchmark/Tests/SetTests.swift
@@ -50,7 +50,7 @@ extension FluentBenchmarker {
}
}
-private final class Test: Model {
+private final class Test: Model, @unchecked Sendable {
static let schema = "test"
@ID(key: .id)
@@ -81,7 +81,7 @@ private enum Foo: String, Codable {
case bar, baz
}
-private final class Test2: Model {
+private final class Test2: Model, @unchecked Sendable {
static let schema = "test"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift b/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift
index 3885684b..1004b32d 100644
--- a/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift
+++ b/Sources/FluentBenchmark/Tests/SoftDeleteTests.swift
@@ -133,7 +133,7 @@ extension FluentBenchmarker {
// Tests eager load of @Parent relation that has been soft-deleted.
private func testSoftDelete_parent() throws {
- final class Foo: Model {
+ final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -158,7 +158,7 @@ extension FluentBenchmarker {
}
}
- final class Bar: Model {
+ final class Bar: Model, @unchecked Sendable {
static let schema = "bars"
@ID(key: .id)
@@ -227,7 +227,7 @@ extension FluentBenchmarker {
}
}
-private final class Trash: Model {
+private final class Trash: Model, @unchecked Sendable {
static let schema = "trash"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/TimestampTests.swift b/Sources/FluentBenchmark/Tests/TimestampTests.swift
index 4bfc8ed3..50d50c66 100644
--- a/Sources/FluentBenchmark/Tests/TimestampTests.swift
+++ b/Sources/FluentBenchmark/Tests/TimestampTests.swift
@@ -169,7 +169,7 @@ extension FluentBenchmarker {
}
}
-private final class User: Model {
+private final class User: Model, @unchecked Sendable {
static let schema = "users"
@ID(key: .id)
@@ -213,7 +213,7 @@ private struct UserMigration: Migration {
}
-private final class Event: Model {
+private final class Event: Model, @unchecked Sendable {
static let schema = "events"
@ID(key: .id)
diff --git a/Sources/FluentBenchmark/Tests/UniqueTests.swift b/Sources/FluentBenchmark/Tests/UniqueTests.swift
index 03fa23af..48310f46 100644
--- a/Sources/FluentBenchmark/Tests/UniqueTests.swift
+++ b/Sources/FluentBenchmark/Tests/UniqueTests.swift
@@ -35,7 +35,7 @@ extension FluentBenchmarker {
}
}
-private final class Foo: Model {
+private final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
diff --git a/Sources/FluentKit/Concurrency/AsyncMigration.swift b/Sources/FluentKit/Concurrency/AsyncMigration.swift
index 37033853..fbeebfde 100644
--- a/Sources/FluentKit/Concurrency/AsyncMigration.swift
+++ b/Sources/FluentKit/Concurrency/AsyncMigration.swift
@@ -7,18 +7,14 @@ public protocol AsyncMigration: Migration {
public extension AsyncMigration {
func prepare(on database: any Database) -> EventLoopFuture {
- let promise = database.eventLoop.makePromise(of: Void.self)
- promise.completeWithTask {
+ database.eventLoop.makeFutureWithTask {
try await self.prepare(on: database)
}
- return promise.futureResult
}
func revert(on database: any Database) -> EventLoopFuture {
- let promise = database.eventLoop.makePromise(of: Void.self)
- promise.completeWithTask {
+ database.eventLoop.makeFutureWithTask {
try await self.revert(on: database)
}
- return promise.futureResult
}
}
diff --git a/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift b/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift
index 7c026153..28bbb707 100644
--- a/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift
+++ b/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift
@@ -17,31 +17,28 @@ extension AsyncModelMiddleware {
on db: any Database,
chainingTo next: any AnyModelResponder
) -> EventLoopFuture {
- let promise = db.eventLoop.makePromise(of: Void.self)
- promise.completeWithTask {
- guard let modelType = model as? Model else {
- try await next.handle(event, model, on: db).get()
- return
- }
+ guard let modelType = (model as? Model).map({ UnsafeTransfer(wrappedValue: $0) }) else {
+ return next.handle(event, model, on: db)
+ }
+ return db.eventLoop.makeFutureWithTask {
let responder = AsyncBasicModelResponder { responderEvent, responderModel, responderDB in
- return try await next.handle(responderEvent, responderModel, on: responderDB).get()
+ try await next.handle(responderEvent, responderModel, on: responderDB).get()
}
switch event {
case .create:
- try await self.create(model: modelType, on: db, next: responder)
+ try await self.create(model: modelType.wrappedValue, on: db, next: responder)
case .update:
- try await self.update(model: modelType, on: db, next: responder)
+ try await self.update(model: modelType.wrappedValue, on: db, next: responder)
case .delete(let force):
- try await self.delete(model: modelType, force: force, on: db, next: responder)
+ try await self.delete(model: modelType.wrappedValue, force: force, on: db, next: responder)
case .softDelete:
- try await self.softDelete(model: modelType, on: db, next: responder)
+ try await self.softDelete(model: modelType.wrappedValue, on: db, next: responder)
case .restore:
- try await self.restore(model: modelType, on: db, next: responder)
+ try await self.restore(model: modelType.wrappedValue, on: db, next: responder)
}
}
- return promise.futureResult
}
public func create(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws {
diff --git a/Sources/FluentKit/Concurrency/Database+Concurrency.swift b/Sources/FluentKit/Concurrency/Database+Concurrency.swift
index 7e422893..676d31bc 100644
--- a/Sources/FluentKit/Concurrency/Database+Concurrency.swift
+++ b/Sources/FluentKit/Concurrency/Database+Concurrency.swift
@@ -1,19 +1,19 @@
import NIOCore
public extension Database {
- func transaction(_ closure: @Sendable @escaping (any Database) async throws -> T) async throws -> T {
+ func transaction(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
try await self.transaction { db -> EventLoopFuture in
- let promise = self.eventLoop.makePromise(of: T.self)
- promise.completeWithTask{ try await closure(db) }
- return promise.futureResult
+ self.eventLoop.makeFutureWithTask {
+ try await closure(db)
+ }
}.get()
}
- func withConnection(_ closure: @Sendable @escaping (any Database) async throws -> T) async throws -> T {
+ func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
try await self.withConnection { db -> EventLoopFuture in
- let promise = self.eventLoop.makePromise(of: T.self)
- promise.completeWithTask{ try await closure(db) }
- return promise.futureResult
+ self.eventLoop.makeFutureWithTask {
+ try await closure(db)
+ }
}.get()
}
}
diff --git a/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift b/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift
index adc78b4c..e2898ed9 100644
--- a/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift
+++ b/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift
@@ -10,11 +10,11 @@ public protocol AnyAsyncModelResponder: AnyModelResponder {
extension AnyAsyncModelResponder {
func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture {
- let promise = db.eventLoop.makePromise(of: Void.self)
- promise.completeWithTask {
- try await self.handle(event, model, on: db)
+ let model = UnsafeTransfer(wrappedValue: model)
+
+ return db.eventLoop.makeFutureWithTask {
+ try await self.handle(event, model.wrappedValue, on: db)
}
- return promise.futureResult
}
}
@@ -41,13 +41,13 @@ extension AnyAsyncModelResponder {
}
internal struct AsyncBasicModelResponder: AnyAsyncModelResponder {
- private let _handle: (ModelEvent, any AnyModel, any Database) async throws -> Void
+ private let _handle: @Sendable (ModelEvent, any AnyModel, any Database) async throws -> Void
internal func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) async throws {
- return try await _handle(event, model, db)
+ try await _handle(event, model, db)
}
- init(handle: @escaping (ModelEvent, any AnyModel, any Database) async throws -> Void) {
+ init(handle: @escaping @Sendable (ModelEvent, any AnyModel, any Database) async throws -> Void) {
self._handle = handle
}
}
diff --git a/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift b/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift
index 92c034e7..aa4469dd 100644
--- a/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift
+++ b/Sources/FluentKit/Concurrency/QueryBuilder+Concurrency.swift
@@ -16,7 +16,7 @@ public extension QueryBuilder {
// MARK: - Fetch
- func chunk(max: Int, closure: @escaping ([Result]) -> ()) async throws {
+ func chunk(max: Int, closure: @escaping @Sendable ([Result]) -> ()) async throws {
try await self.chunk(max: max, closure: closure).get()
}
@@ -47,11 +47,11 @@ public extension QueryBuilder {
try await self.run().get()
}
- func all(_ onOutput: @escaping (Result) -> ()) async throws {
+ func all(_ onOutput: @escaping @Sendable (Result) -> ()) async throws {
try await self.all(onOutput).get()
}
- func run(_ onOutput: @escaping (any DatabaseOutput) -> ()) async throws {
+ func run(_ onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) async throws {
try await self.run(onOutput).get()
}
@@ -61,109 +61,109 @@ public extension QueryBuilder {
}
func count(_ key: KeyPath) async throws -> Int
- where Field: QueryableProperty, Field.Model == Model
+ 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
+ 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
+ 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
+ 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, Field.Model == Model
+ 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, Field.Model == Model.IDValue
+ 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
+ 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
+ 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, Field.Model == Model
+ 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, Field.Model == Model.IDValue
+ 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
+ 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
+ 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, Field.Model == Model
+ 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, Field.Model == Model.IDValue
+ 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
+ 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
+ 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, Field.Model == Model
+ 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, Field.Model == Model.IDValue
+ where Field: QueryableProperty, Field.Value: OptionalType & Sendable, Field.Model == Model.IDValue
{
try await self.max(key).get()
}
@@ -173,7 +173,7 @@ public extension QueryBuilder {
_ field: KeyPath,
as type: Result.Type = Result.self
) async throws -> Result
- where Field: QueryableProperty, Field.Model == Model, Result: Codable
+ where Field: QueryableProperty, Field.Model == Model, Result: Codable & Sendable
{
try await self.aggregate(method, field, as: type).get()
}
@@ -183,7 +183,7 @@ public extension QueryBuilder {
_ field: KeyPath,
as type: Result.Type = Result.self
) async throws -> Result
- where Field: QueryableProperty, Field.Model == Model.IDValue, Result: Codable
+ where Field: QueryableProperty, Field.Model == Model.IDValue, Result: Codable & Sendable
{
try await self.aggregate(method, field, as: type).get()
}
@@ -193,7 +193,7 @@ public extension QueryBuilder {
_ field: FieldKey,
as type: Result.Type = Result.self
) async throws -> Result
- where Result: Codable
+ where Result: Codable & Sendable
{
try await self.aggregate(method, field, as: type).get()
}
@@ -203,7 +203,7 @@ public extension QueryBuilder {
_ path: [FieldKey],
as type: Result.Type = Result.self
) async throws -> Result
- where Result: Codable
+ where Result: Codable & Sendable
{
try await self.aggregate(method, path, as: type).get()
}
diff --git a/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift b/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift
index 01c3db87..88695964 100644
--- a/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift
+++ b/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift
@@ -19,19 +19,19 @@ public extension SiblingsProperty {
// MARK: Operations
/// Attach multiple models with plain edit closure.
- func attach(_ tos: [To], on database: any Database, _ edit: (Through) -> () = { _ in }) async throws {
+ func attach(_ tos: [To], on database: any Database, _ edit: @escaping @Sendable (Through) -> () = { _ 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 (Through) -> () = { _ in }) async throws {
+ func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) -> () = { _ 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(
_ to: To, method: AttachMethod, on database: any Database,
- _ edit: @escaping (Through) -> () = { _ in }
+ _ edit: @escaping @Sendable (Through) -> () = { _ in }
) async throws {
try await self.attach(to, method: method, on: database, edit).get()
}
@@ -45,7 +45,7 @@ public extension SiblingsProperty {
func attach(
_ tos: [To],
on database: any Database,
- _ edit: @Sendable @escaping (Through) async throws -> ()
+ _ edit: @escaping @Sendable (Through) async throws -> ()
) async throws {
guard let fromID = self.idValue else {
throw SiblingsPropertyError.owningModelIdRequired(property: self.name)
@@ -71,7 +71,7 @@ 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: @Sendable @escaping (Through) async throws -> ()) async throws {
+ func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) async throws -> ()) async throws {
try await self.attach(to, method: .always, on: database, edit)
}
@@ -80,7 +80,7 @@ public extension SiblingsProperty {
/// These semantics require us to reimplement, rather than calling through to, the ELF version.
func attach(
_ to: To, method: AttachMethod, on database: any Database,
- _ edit: @Sendable @escaping (Through) async throws -> ()
+ _ edit: @escaping @Sendable (Through) async throws -> ()
) async throws {
switch method {
case .ifNotExists:
diff --git a/Sources/FluentKit/Database/Database+Logging.swift b/Sources/FluentKit/Database/Database+Logging.swift
index 252002d9..c81e6c82 100644
--- a/Sources/FluentKit/Database/Database+Logging.swift
+++ b/Sources/FluentKit/Database/Database+Logging.swift
@@ -25,7 +25,7 @@ extension LoggingOverrideDatabase: Database {
func execute(
query: DatabaseQuery,
- onOutput: @escaping (any DatabaseOutput) -> ()
+ onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
) -> EventLoopFuture {
self.database.execute(query: query, onOutput: onOutput)
}
@@ -47,17 +47,17 @@ extension LoggingOverrideDatabase: Database {
self.database.inTransaction
}
- func transaction(_ closure: @escaping (any Database) -> EventLoopFuture) -> EventLoopFuture {
+ func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
self.database.transaction(closure)
}
- func withConnection(_ closure: @escaping (any Database) -> EventLoopFuture) -> EventLoopFuture {
+ 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 (any SQLRow) -> ()) -> EventLoopFuture {
+ func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture {
self.database.execute(sql: query, onRow)
}
var dialect: any SQLDialect { self.database.dialect }
diff --git a/Sources/FluentKit/Database/Database.swift b/Sources/FluentKit/Database/Database.swift
index 65ca9357..12711d81 100644
--- a/Sources/FluentKit/Database/Database.swift
+++ b/Sources/FluentKit/Database/Database.swift
@@ -1,12 +1,12 @@
import NIOCore
import Logging
-public protocol Database {
+public protocol Database: Sendable {
var context: DatabaseContext { get }
func execute(
query: DatabaseQuery,
- onOutput: @escaping (any DatabaseOutput) -> ()
+ onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
) -> EventLoopFuture
func execute(
@@ -19,9 +19,9 @@ public protocol Database {
var inTransaction: Bool { get }
- func transaction(_ closure: @escaping (any Database) -> EventLoopFuture) -> EventLoopFuture
+ func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture
- func withConnection(_ closure: @escaping (any Database) -> EventLoopFuture) -> EventLoopFuture
+ func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture
}
extension Database {
@@ -54,17 +54,17 @@ extension Database {
}
}
-public protocol DatabaseDriver {
+public protocol DatabaseDriver: Sendable {
func makeDatabase(with context: DatabaseContext) -> any Database
func shutdown()
}
-public protocol DatabaseConfiguration {
+public protocol DatabaseConfiguration: Sendable {
var middleware: [any AnyModelMiddleware] { get set }
func makeDriver(for databases: Databases) -> any DatabaseDriver
}
-public struct DatabaseContext {
+public struct DatabaseContext: Sendable {
public let configuration: any DatabaseConfiguration
public let logger: Logger
public let eventLoop: any EventLoop
diff --git a/Sources/FluentKit/Database/DatabaseInput.swift b/Sources/FluentKit/Database/DatabaseInput.swift
index e4c4542c..19ad36e3 100644
--- a/Sources/FluentKit/Database/DatabaseInput.swift
+++ b/Sources/FluentKit/Database/DatabaseInput.swift
@@ -115,9 +115,9 @@ private struct PrefixedDatabaseInput: DatabaseInput {
/// to a ``QueryBuilder/group(_:_:)`` closure to create an instance of this type.
///
/// > Tip: Applying a query filter via database input is especially useful as a means of providing generic
-/// support for filters involving a ``CompositeIDProperty``. For example, using an instance of this type
-/// as the input for a ``CompositeParentProperty`` filters the query according to the set of appropriately
-/// prefixed field keys the property encapsulates.
+/// > support for filters involving a ``CompositeIDProperty``. For example, using an instance of this type
+/// > as the input for a ``CompositeParentProperty`` filters the query according to the set of appropriately
+/// > prefixed field keys the property encapsulates.
internal struct QueryFilterInput: DatabaseInput {
let builder: QueryBuilder
let inverted: Bool
diff --git a/Sources/FluentKit/Database/DatabaseOutput.swift b/Sources/FluentKit/Database/DatabaseOutput.swift
index c809cab3..781225ff 100644
--- a/Sources/FluentKit/Database/DatabaseOutput.swift
+++ b/Sources/FluentKit/Database/DatabaseOutput.swift
@@ -1,4 +1,4 @@
-public protocol DatabaseOutput: CustomStringConvertible {
+public protocol DatabaseOutput: CustomStringConvertible, Sendable {
func schema(_ schema: String) -> any DatabaseOutput
func contains(_ key: FieldKey) -> Bool
func decodeNil(_ key: FieldKey) throws -> Bool
diff --git a/Sources/FluentKit/Database/Databases.swift b/Sources/FluentKit/Database/Databases.swift
index bdaf3967..e3b4ec61 100644
--- a/Sources/FluentKit/Database/Databases.swift
+++ b/Sources/FluentKit/Database/Databases.swift
@@ -4,15 +4,15 @@ import NIOCore
import NIOPosix
import Logging
-public struct DatabaseConfigurationFactory {
- public let make: () -> any DatabaseConfiguration
+public struct DatabaseConfigurationFactory: Sendable {
+ public let make: @Sendable () -> any DatabaseConfiguration
- public init(make: @escaping () -> any DatabaseConfiguration) {
+ public init(make: @escaping @Sendable () -> any DatabaseConfiguration) {
self.make = make
}
}
-public final class Databases {
+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
diff --git a/Sources/FluentKit/Database/KeyPrefixingStrategy.swift b/Sources/FluentKit/Database/KeyPrefixingStrategy.swift
index 52fd3d68..add18b65 100644
--- a/Sources/FluentKit/Database/KeyPrefixingStrategy.swift
+++ b/Sources/FluentKit/Database/KeyPrefixingStrategy.swift
@@ -1,5 +1,5 @@
/// A strategy describing how to apply a prefix to a ``FieldKey``.
-public enum KeyPrefixingStrategy: CustomStringConvertible {
+public enum KeyPrefixingStrategy: CustomStringConvertible, Sendable {
/// The "do nothing" strategy - the prefix is applied to each key by simple concatenation.
case none
@@ -13,7 +13,7 @@ public enum KeyPrefixingStrategy: CustomStringConvertible {
/// 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((_ prefix: FieldKey, _ idFieldKey: FieldKey) -> FieldKey)
+ case custom(@Sendable (_ prefix: FieldKey, _ idFieldKey: FieldKey) -> FieldKey)
// See `CustomStringConvertible.description`.
public var description: String {
diff --git a/Sources/FluentKit/Docs.docc/Resources/vapor-fluentkit-logo.svg b/Sources/FluentKit/Docs.docc/Resources/vapor-fluentkit-logo.svg
new file mode 100644
index 00000000..3d468c81
--- /dev/null
+++ b/Sources/FluentKit/Docs.docc/Resources/vapor-fluentkit-logo.svg
@@ -0,0 +1,21 @@
+
diff --git a/Sources/FluentKit/Docs.docc/index.md b/Sources/FluentKit/Docs.docc/index.md
index ec8f1015..7628721a 100644
--- a/Sources/FluentKit/Docs.docc/index.md
+++ b/Sources/FluentKit/Docs.docc/index.md
@@ -12,4 +12,4 @@ let planets = try await Planet.query(on: database)
.all()
```
-For more information, see the [Vapor documentation](https://docs.vapor.codes/fluent/overview/).
\ No newline at end of file
+For more information, see the [Fluent documentation](https://docs.vapor.codes/fluent/overview/).
diff --git a/Sources/FluentKit/Docs.docc/theme-settings.json b/Sources/FluentKit/Docs.docc/theme-settings.json
new file mode 100644
index 00000000..ec916e9a
--- /dev/null
+++ b/Sources/FluentKit/Docs.docc/theme-settings.json
@@ -0,0 +1,21 @@
+{
+ "theme": {
+ "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
+ "border-radius": "0",
+ "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
+ "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
+ "color": {
+ "fluentkit": { "dark": "hsl(200, 75%, 85%)", "light": "hsl(200, 75%, 75%)" },
+ "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentkit) 30%, #000 100%)",
+ "documentation-intro-accent": "var(--color-fluentkit)",
+ "logo-base": { "dark": "#fff", "light": "#000" },
+ "logo-shape": { "dark": "#000", "light": "#fff" },
+ "fill": { "dark": "#000", "light": "#fff" }
+ },
+ "icons": { "technology": "/fluentkit/images/vapor-fluentkit-logo.svg" }
+ },
+ "features": {
+ "quickNavigation": { "enable": true },
+ "i18n": { "enable": true }
+ }
+}
diff --git a/Sources/FluentKit/Enum/DatabaseEnum.swift b/Sources/FluentKit/Enum/DatabaseEnum.swift
index 9a1a8a3c..7aa4734e 100644
--- a/Sources/FluentKit/Enum/DatabaseEnum.swift
+++ b/Sources/FluentKit/Enum/DatabaseEnum.swift
@@ -1,5 +1,5 @@
-public struct DatabaseEnum {
- public enum Action {
+public struct DatabaseEnum: Sendable {
+ public enum Action: Sendable {
case create
case update
case delete
diff --git a/Sources/FluentKit/Enum/EnumBuilder.swift b/Sources/FluentKit/Enum/EnumBuilder.swift
index c563a601..4a186462 100644
--- a/Sources/FluentKit/Enum/EnumBuilder.swift
+++ b/Sources/FluentKit/Enum/EnumBuilder.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
import SQLKit
extension Database {
@@ -7,13 +8,18 @@ extension Database {
}
}
-public final class EnumBuilder {
+public final class EnumBuilder: Sendable {
let database: any Database
- public var `enum`: DatabaseEnum
+ let lockedEnum: NIOLockedValueBox
+
+ public var `enum`: DatabaseEnum {
+ get { self.lockedEnum.withLockedValue { $0 } }
+ set { self.lockedEnum.withLockedValue { $0 = newValue } }
+ }
init(database: any Database, name: String) {
self.database = database
- self.enum = .init(name: name)
+ self.lockedEnum = .init(.init(name: name))
}
public func `case`(_ name: String) -> Self {
diff --git a/Sources/FluentKit/Enum/EnumMetadata.swift b/Sources/FluentKit/Enum/EnumMetadata.swift
index 190e4016..b6430e99 100644
--- a/Sources/FluentKit/Enum/EnumMetadata.swift
+++ b/Sources/FluentKit/Enum/EnumMetadata.swift
@@ -1,11 +1,11 @@
import NIOCore
import Foundation
-final class EnumMetadata: Model {
+final class EnumMetadata: Model, @unchecked Sendable {
static let schema = "_fluent_enums"
static var migration: any Migration {
- return EnumMetadataMigration()
+ EnumMetadataMigration()
}
@ID(key: .id)
@@ -17,7 +17,7 @@ final class EnumMetadata: Model {
@Field(key: "case")
var `case`: String
- init() { }
+ init() {}
init(id: IDValue? = nil, name: String, `case`: String) {
self.id = id
@@ -26,10 +26,10 @@ final class EnumMetadata: Model {
}
}
-private struct EnumMetadataMigration: Migration {
- func prepare(on database: any Database) -> EventLoopFuture {
- database.schema("_fluent_enums")
- .field(.id, .uuid, .identifier(auto: false))
+private struct EnumMetadataMigration: AsyncMigration {
+ func prepare(on database: any Database) async throws {
+ try await database.schema(EnumMetadata.schema)
+ .id()
.field("name", .string, .required)
.field("case", .string, .required)
.unique(on: "name", "case")
@@ -37,7 +37,7 @@ private struct EnumMetadataMigration: Migration {
.create()
}
- func revert(on database: any Database) -> EventLoopFuture {
- database.schema("_fluent_enums").delete()
+ func revert(on database: any Database) async throws {
+ try await database.schema(EnumMetadata.schema).delete()
}
}
diff --git a/Sources/FluentKit/Enum/EnumProperty.swift b/Sources/FluentKit/Enum/EnumProperty.swift
index 3e99e5e4..3e07b000 100644
--- a/Sources/FluentKit/Enum/EnumProperty.swift
+++ b/Sources/FluentKit/Enum/EnumProperty.swift
@@ -1,6 +1,6 @@
extension Fields {
public typealias Enum = EnumProperty
- where Value: Codable,
+ where Value: Codable & Sendable,
Value: RawRepresentable,
Value.RawValue == String
}
@@ -10,7 +10,7 @@ extension Fields {
@propertyWrapper
public final class EnumProperty
where Model: FluentKit.Fields,
- Value: Codable,
+ Value: Codable & Sendable,
Value: RawRepresentable,
Value.RawValue == String
{
diff --git a/Sources/FluentKit/Enum/OptionalEnumProperty.swift b/Sources/FluentKit/Enum/OptionalEnumProperty.swift
index 9e27141e..51cddd64 100644
--- a/Sources/FluentKit/Enum/OptionalEnumProperty.swift
+++ b/Sources/FluentKit/Enum/OptionalEnumProperty.swift
@@ -1,6 +1,6 @@
extension Fields {
public typealias OptionalEnum = OptionalEnumProperty
- where Value: Codable,
+ where Value: Codable & Sendable,
Value: RawRepresentable,
Value.RawValue == String
}
@@ -10,7 +10,7 @@ extension Fields {
@propertyWrapper
public final class OptionalEnumProperty
where Model: FluentKit.Fields,
- WrappedValue: Codable,
+ WrappedValue: Codable & Sendable,
WrappedValue: RawRepresentable,
WrappedValue.RawValue == String
{
diff --git a/Sources/FluentKit/Exports.swift b/Sources/FluentKit/Exports.swift
index 9cded41b..28aef3f0 100644
--- a/Sources/FluentKit/Exports.swift
+++ b/Sources/FluentKit/Exports.swift
@@ -1,27 +1,10 @@
-#if swift(>=5.8)
-
@_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 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 class NIO.NIOThreadPool
-
-#else
-
-@_exported import struct Foundation.Date
-@_exported import struct Foundation.UUID
-
-@_exported import Logging
-
-@_exported import protocol NIO.EventLoop
-@_exported import class NIO.EventLoopFuture
-@_exported import struct NIO.EventLoopPromise
-@_exported import protocol NIO.EventLoopGroup
-@_exported import class NIO.NIOThreadPool
-
-#endif
+@_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop
+@_documentation(visibility: internal) @_exported import class NIOCore.EventLoopFuture
+@_documentation(visibility: internal) @_exported import struct NIOCore.EventLoopPromise
+@_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup
+@_documentation(visibility: internal) @_exported import class NIOPosix.NIOThreadPool
diff --git a/Sources/FluentKit/Middleware/ModelMiddleware.swift b/Sources/FluentKit/Middleware/ModelMiddleware.swift
index 5b4dae27..d717ce02 100644
--- a/Sources/FluentKit/Middleware/ModelMiddleware.swift
+++ b/Sources/FluentKit/Middleware/ModelMiddleware.swift
@@ -1,6 +1,6 @@
import NIOCore
-public protocol AnyModelMiddleware {
+public protocol AnyModelMiddleware: Sendable {
func handle(
_ event: ModelEvent,
_ model: any AnyModel,
@@ -67,7 +67,10 @@ extension AnyModelMiddleware {
}
extension Array where Element == any AnyModelMiddleware {
- internal func chainingTo(_ type: Model.Type, closure: @escaping (ModelEvent, Model, any Database) throws -> EventLoopFuture) -> any AnyModelResponder where Model: FluentKit.Model {
+ internal func chainingTo(
+ _ type: Model.Type,
+ closure: @escaping @Sendable (ModelEvent, Model, any Database) throws -> EventLoopFuture
+ ) -> any AnyModelResponder where Model: FluentKit.Model {
var responder: any AnyModelResponder = BasicModelResponder(handle: closure)
for middleware in reversed() {
responder = middleware.makeResponder(chainingTo: responder)
@@ -85,7 +88,7 @@ private struct ModelMiddlewareResponder: AnyModelResponder {
}
}
-public enum ModelEvent {
+public enum ModelEvent: Sendable {
case create
case update
case delete(Bool)
diff --git a/Sources/FluentKit/Middleware/ModelResponder.swift b/Sources/FluentKit/Middleware/ModelResponder.swift
index 33fb7352..cfb49733 100644
--- a/Sources/FluentKit/Middleware/ModelResponder.swift
+++ b/Sources/FluentKit/Middleware/ModelResponder.swift
@@ -1,6 +1,6 @@
import NIOCore
-public protocol AnyModelResponder {
+public protocol AnyModelResponder: Sendable {
func handle(
_ event: ModelEvent,
_ model: any AnyModel,
@@ -31,7 +31,7 @@ extension AnyModelResponder {
}
internal struct BasicModelResponder: AnyModelResponder where Model: FluentKit.Model {
- private let _handle: (ModelEvent, Model, any Database) throws -> EventLoopFuture
+ 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 {
@@ -45,7 +45,7 @@ internal struct BasicModelResponder: AnyModelResponder where Model: Fluen
}
}
- init(handle: @escaping (ModelEvent, Model, any Database) throws -> EventLoopFuture) {
+ 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 49fc90f9..4cab0881 100644
--- a/Sources/FluentKit/Migration/Migration.swift
+++ b/Sources/FluentKit/Migration/Migration.swift
@@ -3,7 +3,7 @@ import NIOCore
/// Fluent's `Migration` can handle database migrations, which can include
/// adding new table, changing existing tables or adding
/// seed data. These actions are executed only once.
-public protocol Migration {
+public protocol Migration: Sendable {
/// The name of the migration which Fluent uses to track the state of.
var name: String { get }
diff --git a/Sources/FluentKit/Migration/MigrationLog.swift b/Sources/FluentKit/Migration/MigrationLog.swift
index b4a884c2..d2e8302f 100644
--- a/Sources/FluentKit/Migration/MigrationLog.swift
+++ b/Sources/FluentKit/Migration/MigrationLog.swift
@@ -2,7 +2,7 @@ import NIOCore
import Foundation
/// Stores information about `Migration`s that have been run.
-public final class MigrationLog: Model {
+public final class MigrationLog: Model, @unchecked Sendable {
public static let schema = "_fluent_migrations"
public static var migration: any Migration {
@@ -24,7 +24,7 @@ public final class MigrationLog: Model {
@Timestamp(key: "updated_at", on: .update)
public var updatedAt: Date?
- public init() { }
+ public init() {}
public init(id: IDValue? = nil, name: String, batch: Int) {
self.id = id
@@ -35,9 +35,9 @@ public final class MigrationLog: Model {
}
}
-private struct MigrationLogMigration: Migration {
- func prepare(on database: any Database) -> EventLoopFuture {
- database.schema("_fluent_migrations")
+private struct MigrationLogMigration: AsyncMigration {
+ func prepare(on database: any Database) async throws {
+ try await database.schema(MigrationLog.schema)
.field(.id, .uuid, .identifier(auto: false))
.field("name", .string, .required)
.field("batch", .int, .required)
@@ -48,7 +48,7 @@ private struct MigrationLogMigration: Migration {
.create()
}
- func revert(on database: any Database) -> EventLoopFuture {
- database.schema("_fluent_migrations").delete()
+ func revert(on database: any Database) async throws {
+ try await database.schema(MigrationLog.schema).delete()
}
}
diff --git a/Sources/FluentKit/Migration/Migrations.swift b/Sources/FluentKit/Migration/Migrations.swift
index cabd8b72..44dd36f3 100644
--- a/Sources/FluentKit/Migration/Migrations.swift
+++ b/Sources/FluentKit/Migration/Migrations.swift
@@ -1,12 +1,14 @@
-public final class Migrations {
- var storage: [DatabaseID?: [any Migration]]
+import NIOConcurrencyHelpers
+
+public final class Migrations: Sendable {
+ let storage: NIOLockedValueBox<[DatabaseID?: [any Migration]]>
public init() {
- self.storage = [:]
+ self.storage = .init([:])
}
public func add(_ migration: any Migration, to id: DatabaseID? = nil) {
- self.storage[id, default: []].append(migration)
+ self.storage.withLockedValue { $0[id, default: []].append(migration) }
}
@inlinable
@@ -15,6 +17,6 @@ public final class Migrations {
}
public func add(_ migrations: [any Migration], to id: DatabaseID? = nil) {
- self.storage[id, default: []].append(contentsOf: migrations)
+ self.storage.withLockedValue { $0[id, default: []].append(contentsOf: migrations) }
}
}
diff --git a/Sources/FluentKit/Migration/Migrator.swift b/Sources/FluentKit/Migration/Migrator.swift
index 87be2968..02755015 100644
--- a/Sources/FluentKit/Migration/Migrator.swift
+++ b/Sources/FluentKit/Migration/Migrator.swift
@@ -3,8 +3,8 @@ import AsyncKit
import Logging
import NIOCore
-public struct Migrator {
- public let databaseFactory: (DatabaseID?) -> (any Database)
+public struct Migrator: Sendable {
+ public let databaseFactory: @Sendable (DatabaseID?) -> (any Database)
public let migrations: Migrations
public let eventLoop: any EventLoop
public let migrationLogLevel: Logger.Level
@@ -27,7 +27,7 @@ public struct Migrator {
}
public init(
- databaseFactory: @escaping (DatabaseID?) -> (any Database),
+ databaseFactory: @escaping @Sendable (DatabaseID?) -> (any Database),
migrations: Migrations,
on eventLoop: any EventLoop,
migrationLogLevel: Logger.Level = .info
@@ -113,31 +113,31 @@ public struct Migrator {
private func migrators(
_ handler: (DatabaseMigrator) -> EventLoopFuture
) -> EventLoopFuture<[Result]> {
- return self.migrations.storage.map {
- handler(.init(id: $0, database: self.databaseFactory($0), migrations: $1, migrationLogLeveL: self.migrationLogLevel))
- }
+ self.migrations.storage.withLockedValue { $0.map {
+ handler(.init(id: $0, database: self.databaseFactory($0), migrations: $1, migrationLogLevel: self.migrationLogLevel))
+ } }
.flatten(on: self.eventLoop)
}
}
-private final class DatabaseMigrator {
+private final class DatabaseMigrator: Sendable {
let migrations: [any Migration]
- let database: any Database
+ let database: any Database & Sendable
let id: DatabaseID?
let migrationLogLevel: Logger.Level
- init(id: DatabaseID?, database: any Database, migrations: [any Migration], migrationLogLeveL: Logger.Level) {
+ init(id: DatabaseID?, database: any Database & Sendable, migrations: [any Migration], migrationLogLevel: Logger.Level) {
self.migrations = migrations
self.database = database
self.id = id
- self.migrationLogLevel = migrationLogLeveL
+ self.migrationLogLevel = migrationLogLevel
}
// MARK: Setup
func setupIfNeeded() -> EventLoopFuture {
- return MigrationLog.migration.prepare(on: self.database)
- .map(self.preventUnstableNames)
+ MigrationLog.migration.prepare(on: self.database)
+ .map { self.preventUnstableNames() }
}
/// An unstable name is a name that is not the same every time migrations
@@ -160,7 +160,7 @@ private final class DatabaseMigrator {
// MARK: Prepare
func prepareBatch() -> EventLoopFuture {
- return self.lastBatchNumber().flatMap { batch in
+ self.lastBatchNumber().flatMap { batch in
self.unpreparedMigrations().sequencedFlatMapEach { self.prepare($0, batch: batch + 1) }
}
}
@@ -168,35 +168,35 @@ private final class DatabaseMigrator {
// MARK: Revert
func revertLastBatch() -> EventLoopFuture {
- return self.lastBatchNumber().flatMap(self.revertBatch(number:))
+ self.lastBatchNumber().flatMap { self.revertBatch(number: $0) }
}
func revertBatch(number: Int) -> EventLoopFuture {
- return self.preparedMigrations(batch: number).sequencedFlatMapEach(self.revert)
+ self.preparedMigrations(batch: number).sequencedFlatMapEach(self.revert)
}
func revertAllBatches() -> EventLoopFuture {
- return self.preparedMigrations().sequencedFlatMapEach(self.revert)
+ self.preparedMigrations().sequencedFlatMapEach(self.revert)
}
// MARK: Preview
func previewPrepareBatch() -> EventLoopFuture<[any Migration]> {
- return self.unpreparedMigrations()
+ self.unpreparedMigrations()
}
func previewRevertLastBatch() -> EventLoopFuture<[any Migration]> {
- return self.lastBatchNumber().flatMap { batch in
- return self.preparedMigrations(batch: batch)
+ self.lastBatchNumber().flatMap { batch in
+ self.preparedMigrations(batch: batch)
}
}
func previewRevertBatch(number: Int) -> EventLoopFuture<[any Migration]> {
- return self.preparedMigrations(batch: number)
+ self.preparedMigrations(batch: number)
}
func previewRevertAllBatches() -> EventLoopFuture<[any Migration]> {
- return self.preparedMigrations()
+ self.preparedMigrations()
}
// MARK: Private
@@ -224,34 +224,34 @@ private final class DatabaseMigrator {
}
private func revertMigrationLog() -> EventLoopFuture {
- return MigrationLog.migration.revert(on: self.database)
+ MigrationLog.migration.revert(on: self.database)
}
private func lastBatchNumber() -> EventLoopFuture {
- return MigrationLog.query(on: self.database).sort(\.$batch, .descending).first().map { log in
+ MigrationLog.query(on: self.database).sort(\.$batch, .descending).first().map { log in
log?.batch ?? 0
}
}
private func preparedMigrations() -> EventLoopFuture<[any Migration]> {
- return MigrationLog.query(on: self.database).all().map { logs in
- return self.migrations.filter { migration in
- return logs.contains(where: { $0.name == migration.name })
+ MigrationLog.query(on: self.database).all().map { logs in
+ self.migrations.filter { migration in
+ logs.contains(where: { $0.name == migration.name })
}.reversed()
}
}
private func preparedMigrations(batch: Int) -> EventLoopFuture<[any Migration]> {
- return MigrationLog.query(on: self.database).filter(\.$batch == batch).all().map { logs in
- return self.migrations.filter { migration in
- return logs.contains(where: { $0.name == migration.name })
+ MigrationLog.query(on: self.database).filter(\.$batch == batch).all().map { logs in
+ self.migrations.filter { migration in
+ logs.contains(where: { $0.name == migration.name })
}.reversed()
}
}
private func unpreparedMigrations() -> EventLoopFuture<[any Migration]> {
- return MigrationLog.query(on: self.database).all().map { logs in
- return self.migrations.compactMap { migration in
+ MigrationLog.query(on: self.database).all().map { logs in
+ self.migrations.compactMap { migration in
if logs.contains(where: { $0.name == migration.name }) { return nil }
return migration
}
diff --git a/Sources/FluentKit/Model/EagerLoad.swift b/Sources/FluentKit/Model/EagerLoad.swift
index 1ac3f76c..a92021c0 100644
--- a/Sources/FluentKit/Model/EagerLoad.swift
+++ b/Sources/FluentKit/Model/EagerLoad.swift
@@ -11,7 +11,7 @@ extension EagerLoader {
}
}
-public protocol AnyEagerLoader {
+public protocol AnyEagerLoader: Sendable {
func anyRun(models: [any AnyModel], on database: any Database) -> EventLoopFuture
}
diff --git a/Sources/FluentKit/Model/Fields+Codable.swift b/Sources/FluentKit/Model/Fields+Codable.swift
index 2695ec6a..29a3c27d 100644
--- a/Sources/FluentKit/Model/Fields+Codable.swift
+++ b/Sources/FluentKit/Model/Fields+Codable.swift
@@ -1,3 +1,5 @@
+import struct SQLKit.SomeCodingKey
+
extension Fields {
public init(from decoder: any Decoder) throws {
self.init()
diff --git a/Sources/FluentKit/Model/Fields.swift b/Sources/FluentKit/Model/Fields.swift
index d8047dc9..f72afd06 100644
--- a/Sources/FluentKit/Model/Fields.swift
+++ b/Sources/FluentKit/Model/Fields.swift
@@ -1,3 +1,5 @@
+import SQLKit
+
/// A type conforming to ``Fields`` is able to use FluentKit's various property wrappers to declare
/// name, type, and semantic information for individual properties corresponding to fields in a
/// generic database storage system.
@@ -12,16 +14,16 @@
/// custom implementations of any other requirements is **strongly** discouraged; under most
/// circumstances, such implementations will not be invoked in any event. They are only declared on
/// the base protocol rather than solely in extensions because static dispatch improves performance.
-public protocol Fields: AnyObject, Codable {
+public protocol Fields: AnyObject, Codable, Sendable {
/// Returns a fully generic list of every property on the given instance of the type which uses any of
/// the FluentKit property wrapper types (e.g. any wrapper conforming to ``AnyProperty``). This accessor
/// is not static because FluentKit depends upon access to the backing storage of the property wrappers,
/// which is specific to each instance.
///
- /// - Warning: This accessor triggers the use of reflection, which is at the time of this writing the
- /// most severe performance bottleneck in FluentKit by a huge margin. Every access of this property
- /// 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.
+ /// > Warning: This accessor triggers the use of reflection, which is at the time of this writing the
+ /// > most severe performance bottleneck in FluentKit by a huge margin. Every access of this property
+ /// > 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()
@@ -64,9 +66,9 @@ extension Fields {
/// type was either loaded or created, add the key-value pair for said property to the given database
/// input object. This prepares data in memory to be written to the database.
///
- /// - Note: It is trivial to construct ``DatabaseInput`` objects which do not in fact actually transfer
- /// their contents to a database. FluentKit itself does this to implement a save/restore operation for
- /// model state under certain conditions (see ``Model``).
+ /// > Note: It is trivial to construct ``DatabaseInput`` objects which do not in fact actually transfer
+ /// > their contents to a database. FluentKit itself does this to implement a save/restore operation for
+ /// > model state under certain conditions (see ``Model``).
public func input(to input: any DatabaseInput) {
for field in self.databaseProperties {
field.input(to: input)
@@ -77,8 +79,8 @@ extension Fields {
/// output object, attempt to load the corresponding value into the property. This transfers data
/// received from the database into memory.
///
- /// - Note: It is trivial to construct ``DatabaseOutput`` objects which do not in fact actually represent
- /// data from a database. FluentKit itself does this to help keep models up to date (see ``Model``).
+ /// > Note: It is trivial to construct ``DatabaseOutput`` objects which do not in fact actually represent
+ /// > data from a database. FluentKit itself does this to help keep models up to date (see ``Model``).
public func output(from output: any DatabaseOutput) throws {
for field in self.databaseProperties {
try field.output(from: output)
@@ -104,16 +106,16 @@ extension Fields {
/// built-in ``Codable`` machinery (corresponding to the ``AnyCodableProperty`` protocol), indexed by
/// the coding key for each property.
///
- /// - Important: A property's _coding_ key is not the same as a _database_ key. The coding key is derived
- /// directly from the property's Swift name as provided by reflection, while database keys are provided
- /// in the property wrapper initializer declarations.
+ /// > Important: A property's _coding_ key is not the same as a _database_ key. The coding key is derived
+ /// > directly from the property's Swift name as provided by reflection, while database keys are provided
+ /// > in the property wrapper initializer declarations.
///
- /// - Warning: Even if the type has a custom ``CodingKeys`` enum, the property's coding key will _not_
- /// correspond to the definition provided therein; it will always be based solely on the Swift
- /// property name.
+ /// > Warning: Even if the type has a custom ``CodingKeys`` enum, the property's coding key will _not_
+ /// > correspond to the definition provided therein; it will always be based solely on the Swift
+ /// > property name.
///
- /// - Warning: Like ``properties``, this method uses reflection, and incurs all of the accompanying
- /// performance penalties.
+ /// > 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,
@@ -154,9 +156,12 @@ private final class HasChangesInput: DatabaseInput {
// MARK: Collect Input
extension Fields {
+ /// For internal use only.
+ ///
/// Returns a dictionary of field keys and associated values representing all "pending"
/// data - e.g. all fields (if any) which have been changed by something other than Fluent.
- internal func collectInput(withDefaultedValues defaultedValues: Bool = false) -> [FieldKey: DatabaseQuery.Value] {
+ @_spi(FluentSQLSPI)
+ public/*package*/ func collectInput(withDefaultedValues defaultedValues: Bool = false) -> [FieldKey: DatabaseQuery.Value] {
let input = DictionaryInput(wantsUnmodifiedKeys: defaultedValues)
self.input(to: input)
return input.storage
diff --git a/Sources/FluentKit/Model/MirrorBypass.swift b/Sources/FluentKit/Model/MirrorBypass.swift
index 99906be5..6a29da1c 100644
--- a/Sources/FluentKit/Model/MirrorBypass.swift
+++ b/Sources/FluentKit/Model/MirrorBypass.swift
@@ -1,4 +1,4 @@
-#if compiler(<5.10)
+#if compiler(<6)
@_silgen_name("swift_reflectionMirror_normalizedType")
internal func _getNormalizedType(_: T, type: Any.Type) -> Any.Type
@@ -24,40 +24,40 @@ internal struct _FastChildIterator: IteratorProtocol {
deinit { self.freeFunc(self.ptr) }
}
-#if compiler(<5.10)
+ #if compiler(<6)
private let subject: AnyObject
private let type: Any.Type
private let childCount: Int
private var index: Int
-#else
+ #else
private var iterator: Mirror.Children.Iterator
-#endif
+ #endif
private var lastNameBox: _CStringBox?
-#if compiler(<5.10)
+ #if compiler(<6)
fileprivate init(subject: AnyObject, type: Any.Type, childCount: Int) {
self.subject = subject
self.type = type
self.childCount = childCount
self.index = 0
}
-#else
+ #else
fileprivate init(iterator: Mirror.Children.Iterator) {
self.iterator = iterator
}
-#endif
+ #endif
init(subject: AnyObject) {
-#if compiler(<5.10)
+ #if compiler(<6)
let type = _getNormalizedType(subject, type: Swift.type(of: subject))
self.init(
subject: subject,
type: type,
childCount: _getChildCount(subject, type: type)
)
-#else
+ #else
self.init(iterator: Mirror(reflecting: subject).children.makeIterator())
-#endif
+ #endif
}
/// The `name` pointer returned by this iterator has a rather unusual lifetime guarantee - it shall remain valid
@@ -66,10 +66,10 @@ internal struct _FastChildIterator: IteratorProtocol {
/// `Mirror` as much as possible, and copying a name that many callers will never even access to begin with is
/// hardly a means to that end.
///
- /// - Note: Ironically, in the fallback case that uses `Mirror` directly, preserving this semantic actually imposes
- /// an _additional_ performance penalty.
+ /// > Note: Ironically, in the fallback case that uses `Mirror` directly, preserving this semantic actually imposes
+ /// > an _additional_ performance penalty.
mutating func next() -> (name: UnsafePointer?, child: Any)? {
-#if compiler(<5.10)
+ #if compiler(<6)
guard self.index < self.childCount else {
self.lastNameBox = nil // ensure any lingering name gets freed
return nil
@@ -82,7 +82,7 @@ internal struct _FastChildIterator: IteratorProtocol {
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
+ #else
guard let child = self.iterator.next() else {
self.lastNameBox = nil
return nil
@@ -100,34 +100,34 @@ internal struct _FastChildIterator: IteratorProtocol {
self.lastNameBox = nil
return (name: nil, child: child.value)
}
-#endif
+ #endif
}
}
internal struct _FastChildSequence: Sequence {
-#if compiler(<5.10)
+ #if compiler(<6)
private let subject: AnyObject
private let type: Any.Type
private let childCount: Int
-#else
+ #else
private let children: Mirror.Children
-#endif
+ #endif
init(subject: AnyObject) {
-#if compiler(<5.10)
+ #if compiler(<6)
self.subject = subject
self.type = _getNormalizedType(subject, type: Swift.type(of: subject))
self.childCount = _getChildCount(subject, type: self.type)
-#else
+ #else
self.children = Mirror(reflecting: subject).children
-#endif
+ #endif
}
func makeIterator() -> _FastChildIterator {
-#if compiler(<5.10)
- return _FastChildIterator(subject: self.subject, type: self.type, childCount: self.childCount)
-#else
- return _FastChildIterator(iterator: self.children.makeIterator())
-#endif
+ #if compiler(<6)
+ _FastChildIterator(subject: self.subject, type: self.type, childCount: self.childCount)
+ #else
+ _FastChildIterator(iterator: self.children.makeIterator())
+ #endif
}
}
diff --git a/Sources/FluentKit/Model/Model+CRUD.swift b/Sources/FluentKit/Model/Model+CRUD.swift
index e4042209..524a8465 100644
--- a/Sources/FluentKit/Model/Model+CRUD.swift
+++ b/Sources/FluentKit/Model/Model+CRUD.swift
@@ -17,6 +17,7 @@ extension Model {
}
private func _create(on database: any Database) -> EventLoopFuture {
+ let transfer = UnsafeTransfer(wrappedValue: self)
precondition(!self._$idExists)
self.touchTimestamps(.create, .update)
if self.anyID is any AnyQueryableProperty {
@@ -28,12 +29,12 @@ extension Model {
.run { promise.succeed($0) }
.cascadeFailure(to: promise)
return promise.futureResult.flatMapThrowing { output in
- var input = self.collectInput()
- if case .default = self._$id.inputValue {
+ var input = transfer.wrappedValue.collectInput()
+ if case .default = transfer.wrappedValue._$id.inputValue {
let idKey = Self()._$id.key
input[idKey] = try .bind(output.decode(idKey, as: Self.IDValue.self))
}
- try self.output(from: SavedInput(input))
+ try transfer.wrappedValue.output(from: SavedInput(input))
}
} else {
return Self.query(on: database)
@@ -41,7 +42,7 @@ extension Model {
.action(.create)
.run()
.flatMapThrowing {
- try self.output(from: SavedInput(self.collectInput()))
+ try transfer.wrappedValue.output(from: SavedInput(transfer.wrappedValue.collectInput()))
}
}
}
@@ -60,13 +61,14 @@ extension Model {
self.touchTimestamps(.update)
let input = self.collectInput()
guard let id = self.id else { throw FluentError.idRequired }
+ let transfer = UnsafeTransfer(wrappedValue: self)
return Self.query(on: database)
.filter(id: id)
.set(input)
.update()
.flatMapThrowing
{
- try self.output(from: SavedInput(input))
+ try transfer.wrappedValue.output(from: SavedInput(input))
}
}
@@ -85,13 +87,14 @@ extension Model {
private func _delete(force: Bool = false, on database: any Database) throws -> EventLoopFuture {
guard let id = self.id else { throw FluentError.idRequired }
+ let transfer = UnsafeTransfer(wrappedValue: self)
return Self.query(on: database)
.filter(id: id)
.delete(force: force)
.map
{
- if force || self.deletedTimestamp == nil {
- self._$idExists = false
+ if force || transfer.wrappedValue.deletedTimestamp == nil {
+ transfer.wrappedValue._$idExists = false
}
}
}
@@ -109,6 +112,7 @@ extension Model {
timestamp.touch(date: nil)
precondition(self._$idExists)
guard let id = self.id else { throw FluentError.idRequired }
+ let transfer = UnsafeTransfer(wrappedValue: self)
return Self.query(on: database)
.withDeleted()
.filter(id: id)
@@ -117,8 +121,8 @@ extension Model {
.run()
.flatMapThrowing
{
- try self.output(from: SavedInput(self.collectInput()))
- self._$idExists = true
+ try transfer.wrappedValue.output(from: SavedInput(transfer.wrappedValue.collectInput()))
+ transfer.wrappedValue._$idExists = true
}
}
@@ -146,18 +150,20 @@ extension Collection where Element: FluentKit.Model {
precondition(self.allSatisfy { $0._$idExists })
+ let transfer = UnsafeTransfer(wrappedValue: self) // ouch, the retains...
+
return EventLoopFuture.andAllSucceed(self.map { model in
database.configuration.middleware.chainingTo(Element.self) { event, model, db in
- return db.eventLoop.makeSucceededFuture(())
+ db.eventLoop.makeSucceededFuture(())
}.delete(model, force: force, on: database)
}, on: database.eventLoop).flatMap {
Element.query(on: database)
- .filter(ids: self.map { $0.id! })
+ .filter(ids: transfer.wrappedValue.map { $0.id! })
.delete(force: force)
}.map {
guard force else { return }
- for model in self where model.deletedTimestamp == nil {
+ for model in transfer.wrappedValue where model.deletedTimestamp == nil {
model._$idExists = false
}
}
@@ -170,6 +176,8 @@ extension Collection where Element: FluentKit.Model {
precondition(self.allSatisfy { !$0._$idExists })
+ let transfer = UnsafeTransfer(wrappedValue: self) // ouch, the retains...
+
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 {
@@ -180,10 +188,10 @@ extension Collection where Element: FluentKit.Model {
}.create(model, on: database)
}, on: database.eventLoop).flatMap {
Element.query(on: database)
- .set(self.map { $0.collectInput(withDefaultedValues: database is any SQLDatabase) })
+ .set(transfer.wrappedValue.map { $0.collectInput(withDefaultedValues: database is any SQLDatabase) })
.create()
}.map {
- for model in self {
+ for model in transfer.wrappedValue {
model._$idExists = true
}
}
diff --git a/Sources/FluentKit/Model/Model.swift b/Sources/FluentKit/Model/Model.swift
index bcfeae2f..175821bb 100644
--- a/Sources/FluentKit/Model/Model.swift
+++ b/Sources/FluentKit/Model/Model.swift
@@ -1,7 +1,7 @@
import NIOCore
public protocol Model: AnyModel {
- associatedtype IDValue: Codable, Hashable
+ associatedtype IDValue: Codable, Hashable, Sendable
var id: IDValue? { get set }
}
@@ -35,10 +35,10 @@ extension Model {
/// version works for models which use `@CompositeID()`. It would not be necessary if
/// support existed for property wrappers in protocols.
///
- /// - Note: Adding this property to ``Model`` rather than making the ``AnyID`` protocol
- /// and ``anyID`` property public was chosen because implementing a new conformance for
- /// ``AnyID`` can not be done correctly from outside FluentKit; it would be mostly useless
- /// and potentially confusing public API surface.
+ /// > Note: Adding this property to ``Model`` rather than making the ``AnyID`` protocol
+ /// > and ``anyID`` property public was chosen because implementing a new conformance for
+ /// > ``AnyID`` can not be done correctly from outside FluentKit; it would be mostly useless
+ /// > and potentially confusing public API surface.
public var _$idExists: Bool {
get { self.anyID.exists }
set { self.anyID.exists = newValue }
diff --git a/Sources/FluentKit/Operators/FieldOperators.swift b/Sources/FluentKit/Operators/FieldOperators.swift
index 69793d99..e797c2f3 100644
--- a/Sources/FluentKit/Operators/FieldOperators.swift
+++ b/Sources/FluentKit/Operators/FieldOperators.swift
@@ -176,7 +176,7 @@ public func !=~ (
.init(lhs, .contains(inverse: true, .prefix), rhs)
}
-public struct ModelFieldFilter
+public struct ModelFieldFilter: Sendable
where Left: FluentKit.Schema, Right: FluentKit.Schema
{
public init(
diff --git a/Sources/FluentKit/Operators/ValueOperators.swift b/Sources/FluentKit/Operators/ValueOperators.swift
index e7339142..ed356969 100644
--- a/Sources/FluentKit/Operators/ValueOperators.swift
+++ b/Sources/FluentKit/Operators/ValueOperators.swift
@@ -130,7 +130,7 @@ public func <= (lhs: KeyPath, rhs: DatabaseQuery.Val
.init(lhs, .lessThanOrEqual, rhs)
}
-public struct ModelValueFilter where Model: Fields {
+public struct ModelValueFilter: Sendable where Model: Fields {
public init(
_ lhs: KeyPath,
_ method: DatabaseQuery.Filter.Method,
@@ -148,7 +148,7 @@ public struct ModelValueFilter where Model: Fields {
let value: DatabaseQuery.Value
}
-public struct ModelCompositeIDFilter where Model: FluentKit.Model, Model.IDValue: Fields {
+public struct ModelCompositeIDFilter: Sendable where Model: FluentKit.Model, Model.IDValue: Fields {
public init(
_ method: DatabaseQuery.Filter.Method,
_ rhs: Model.IDValue
diff --git a/Sources/FluentKit/Properties/Boolean.swift b/Sources/FluentKit/Properties/Boolean.swift
index 2f99e097..50e5570a 100644
--- a/Sources/FluentKit/Properties/Boolean.swift
+++ b/Sources/FluentKit/Properties/Boolean.swift
@@ -41,7 +41,7 @@ extension Fields {
/// func revert(on database: Database) async throws -> Void { try await database.schema(MyModel.schema).delete() }
/// }
///
-/// - Note: See also ``OptionalBooleanProperty`` and ``BooleanPropertyFormat``.
+/// > Note: See also ``OptionalBooleanProperty`` and ``BooleanPropertyFormat``.
@propertyWrapper
public final class BooleanProperty
where Model: FluentKit.Fields, Format: BooleanPropertyFormat
diff --git a/Sources/FluentKit/Properties/BooleanPropertyFormat.swift b/Sources/FluentKit/Properties/BooleanPropertyFormat.swift
index b2131752..83bee2b7 100644
--- a/Sources/FluentKit/Properties/BooleanPropertyFormat.swift
+++ b/Sources/FluentKit/Properties/BooleanPropertyFormat.swift
@@ -1,6 +1,6 @@
/// A conversion between `Bool` and an arbitrary alternative storage format, usually a string.
-public protocol BooleanPropertyFormat {
- associatedtype Value: Codable
+public protocol BooleanPropertyFormat: Sendable {
+ associatedtype Value: Codable & Sendable
init()
@@ -27,10 +27,10 @@ extension BooleanPropertyFormat where Self == DefaultBooleanPropertyFormat {
/// Represent a `Bool` as any integer type. Any value other than `0` or `1` is considered invalid.
///
-/// - Note: This format is primarily useful when the underlying database's native boolean format is
-/// an integer of different width than the one that was used by the model - for example, a MySQL
-/// model with a `BIGINT` field instead of the default `TINYINT`.
-public struct IntegerBooleanPropertyFormat: BooleanPropertyFormat {
+/// > Note: This format is primarily useful when the underlying database's native boolean format is
+/// > an integer of different width than the one that was used by the model - for example, a MySQL
+/// > model with a `BIGINT` field instead of the default `TINYINT`.
+public struct IntegerBooleanPropertyFormat: BooleanPropertyFormat {
public init() {}
public func parse(_ value: T) -> Bool? {
diff --git a/Sources/FluentKit/Properties/Children.swift b/Sources/FluentKit/Properties/Children.swift
index e7ebac9e..56a7cc43 100644
--- a/Sources/FluentKit/Properties/Children.swift
+++ b/Sources/FluentKit/Properties/Children.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
extension Model {
public typealias Children = ChildrenProperty
@@ -8,14 +9,14 @@ extension Model {
// MARK: Type
@propertyWrapper
-public final class ChildrenProperty
+public final class ChildrenProperty: @unchecked Sendable
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>) {
@@ -102,7 +103,7 @@ extension ChildrenProperty: CustomStringConvertible {
// MARK: Property
-extension ChildrenProperty: AnyProperty { }
+extension ChildrenProperty: AnyProperty {}
extension ChildrenProperty: Property {
public typealias Model = From
@@ -220,8 +221,9 @@ private struct ChildrenEagerLoader: EagerLoader
if (self.withDeleted) {
builder.withDeleted()
}
+ let models = UnsafeTransfer(wrappedValue: models)
return builder.all().map {
- for model in models {
+ for model in models.wrappedValue {
let id = model[keyPath: self.relationKey].idValue!
model[keyPath: self.relationKey].value = $0.filter { child in
switch parentKey {
diff --git a/Sources/FluentKit/Properties/CompositeChildren.swift b/Sources/FluentKit/Properties/CompositeChildren.swift
index 94925c7e..1d62014d 100644
--- a/Sources/FluentKit/Properties/CompositeChildren.swift
+++ b/Sources/FluentKit/Properties/CompositeChildren.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
extension Model {
/// A convenience alias for ``CompositeChildrenProperty``. It is strongly recommended that callers use this
@@ -22,8 +23,8 @@ extension Model {
///
/// Example:
///
-/// - Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
-/// complex relationships.
+/// > Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
+/// > complex relationships.
///
/// ```
/// final class TableMetadata: Model {
@@ -73,7 +74,7 @@ extension Model {
/// }
/// ```
@propertyWrapper
-public final class CompositeChildrenProperty
+public final class CompositeChildrenProperty: @unchecked Sendable
where From: Model, To: Model, From.IDValue: Fields
{
public typealias Key = CompositeRelationParentKey
@@ -200,10 +201,11 @@ private struct CompositeChildrenEagerLoader: EagerLoader
_ = parentKey.queryFilterIds(ids, in: query)
}
+ let models = UnsafeTransfer(wrappedValue: models)
return builder.all().map {
let indexedResults = Dictionary(grouping: $0, by: { parentKey.referencedId(in: $0)! })
- for model in models {
+ for model in models.wrappedValue {
model[keyPath: self.relationKey].value = indexedResults[model[keyPath: self.relationKey].idValue!] ?? []
}
}
diff --git a/Sources/FluentKit/Properties/CompositeID.swift b/Sources/FluentKit/Properties/CompositeID.swift
index cbd42bb6..28db8878 100644
--- a/Sources/FluentKit/Properties/CompositeID.swift
+++ b/Sources/FluentKit/Properties/CompositeID.swift
@@ -1,3 +1,5 @@
+import NIOConcurrencyHelpers
+
extension Model {
public typealias CompositeID = CompositeIDProperty
where Value: Fields
@@ -6,11 +8,11 @@ extension Model {
// MARK: Type
@propertyWrapper @dynamicMemberLookup
-public final class CompositeIDProperty
+public final class CompositeIDProperty: @unchecked Sendable
where Model: FluentKit.Model, Value: FluentKit.Fields
{
- public var value: Value?
- public var exists: Bool
+ public var value: Value? = .init(.init())
+ public var exists: Bool = false
var cachedOutput: (any DatabaseOutput)?
public var projectedValue: CompositeIDProperty { self }
@@ -20,11 +22,7 @@ public final class CompositeIDProperty
set { self.value = newValue }
}
- public init() {
- self.value = .init()
- self.exists = false
- self.cachedOutput = nil
- }
+ public init() {}
public subscript(
dynamicMember keyPath: KeyPath
diff --git a/Sources/FluentKit/Properties/CompositeOptionalChild.swift b/Sources/FluentKit/Properties/CompositeOptionalChild.swift
index bbd320e1..4e8cdebc 100644
--- a/Sources/FluentKit/Properties/CompositeOptionalChild.swift
+++ b/Sources/FluentKit/Properties/CompositeOptionalChild.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
extension Model {
/// A convenience alias for ``CompositeOptionalChildProperty``. It is strongly recommended that callers use this
@@ -22,8 +23,8 @@ extension Model {
///
/// Example:
///
-/// - Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
-/// complex relationships.
+/// > Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
+/// > complex relationships.
///
/// ```
/// final class TableMetadata: Model {
@@ -58,7 +59,7 @@ extension Model {
/// }
/// ```
@propertyWrapper
-public final class CompositeOptionalChildProperty
+public final class CompositeOptionalChildProperty: @unchecked Sendable
where From: Model, To: Model, From.IDValue: Fields
{
public typealias Key = CompositeRelationParentKey
@@ -187,10 +188,11 @@ private struct CompositeOptionalChildEagerLoader: EagerLoader
if (self.withDeleted) {
builder.withDeleted()
}
+ let models = UnsafeTransfer(wrappedValue: models)
return builder.all().map {
let indexedResults = Dictionary(grouping: $0, by: { parentKey.referencedId(in: $0)! })
- for model in models {
+ for model in models.wrappedValue {
model[keyPath: self.relationKey].value = indexedResults[model[keyPath: self.relationKey].idValue!]?.first
}
}
diff --git a/Sources/FluentKit/Properties/CompositeOptionalParent.swift b/Sources/FluentKit/Properties/CompositeOptionalParent.swift
index e0b0f145..a20bf958 100644
--- a/Sources/FluentKit/Properties/CompositeOptionalParent.swift
+++ b/Sources/FluentKit/Properties/CompositeOptionalParent.swift
@@ -1,4 +1,6 @@
import NIOCore
+import NIOConcurrencyHelpers
+import struct SQLKit.SomeCodingKey
extension Model {
/// A convenience alias for ``CompositeOptionalParentProperty``. It is strongly recommended that callers
@@ -20,8 +22,8 @@ extension Model {
///
/// Example:
///
-/// - Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
-/// complex relationships.
+/// > Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
+/// > complex relationships.
///
/// ```
/// final class TableMetadata: Model {
@@ -69,7 +71,7 @@ extension Model {
/// }
/// ```
@propertyWrapper @dynamicMemberLookup
-public final class CompositeOptionalParentProperty
+public final class CompositeOptionalParentProperty: @unchecked Sendable
where From: Model, To: Model, To.IDValue: Fields
{
public let prefix: FieldKey
@@ -217,18 +219,20 @@ private struct CompositeOptionalParentEagerLoader: EagerLoader
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) ?? []
+ var _sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id })
+ let nilParentModels = UnsafeTransfer(wrappedValue: _sets.removeValue(forKey: nil) ?? [])
+ let sets = UnsafeTransfer(wrappedValue: _sets)
let builder = To.query(on: database)
- .group(.or) { _ = sets.keys.reduce($0) { query, id in query.group(.and) { id!.input(to: QueryFilterInput(builder: $0)) } } }
+ .group(.or) { _ = sets.wrappedValue.keys.reduce($0) { query, id in query.group(.and) { id!.input(to: QueryFilterInput(builder: $0)) } } }
if (self.withDeleted) {
builder.withDeleted()
}
+
return builder.all().flatMapThrowing {
let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) })
- for (parentId, models) in sets {
+ for (parentId, models) in sets.wrappedValue {
guard let parent = parents[parentId!] else {
database.logger.debug(
"Missing parent model in eager-load lookup results.",
@@ -238,7 +242,7 @@ private struct CompositeOptionalParentEagerLoader: EagerLoader
}
models.forEach { $0[keyPath: self.relationKey].value = .some(.some(parent)) }
}
- nilParentModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
+ nilParentModels.wrappedValue.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
}
}
}
diff --git a/Sources/FluentKit/Properties/CompositeParent.swift b/Sources/FluentKit/Properties/CompositeParent.swift
index 646a7231..9c9b4d81 100644
--- a/Sources/FluentKit/Properties/CompositeParent.swift
+++ b/Sources/FluentKit/Properties/CompositeParent.swift
@@ -1,4 +1,6 @@
import NIOCore
+import NIOConcurrencyHelpers
+import struct SQLKit.SomeCodingKey
extension Model {
/// A convenience alias for ``CompositeParentProperty``. It is strongly recommended that callers use this
@@ -20,8 +22,8 @@ extension Model {
///
/// Example:
///
-/// - Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
-/// complex relationships.
+/// > Note: This example is somewhat contrived; in reality, this kind of metadata would have much more
+/// > complex relationships.
///
/// ```
/// final class TableMetadata: Model {
@@ -64,12 +66,12 @@ extension Model {
/// }
/// ```
@propertyWrapper @dynamicMemberLookup
-public final class CompositeParentProperty
+public final class CompositeParentProperty: @unchecked Sendable
where From: Model, To: Model, To.IDValue: Fields
{
public let prefix: FieldKey
public let prefixingStrategy: KeyPrefixingStrategy
- public var id: To.IDValue
+ public var id: To.IDValue = .init()
public var value: To?
public var wrappedValue: To {
@@ -91,7 +93,6 @@ public final class CompositeParentProperty
/// - strategy: The strategy to use when applying prefixes to keys. ``KeyPrefixingStrategy/snakeCase`` is
/// the default.
public init(prefix: FieldKey, strategy: KeyPrefixingStrategy = .snakeCase) {
- self.id = .init()
self.prefix = prefix
self.prefixingStrategy = strategy
}
@@ -194,11 +195,11 @@ private struct CompositeParentEagerLoader: EagerLoader
let withDeleted: Bool
func run(models: [From], on database: any Database) -> EventLoopFuture {
- let sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id })
+ let sets = UnsafeTransfer(wrappedValue: Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id }))
let builder = To.query(on: database)
.group(.or) {
- _ = sets.keys.reduce($0) { query, id in query.group(.and) { id.input(to: QueryFilterInput(builder: $0)) } }
+ _ = sets.wrappedValue.keys.reduce($0) { query, id in query.group(.and) { id.input(to: QueryFilterInput(builder: $0)) } }
}
if (self.withDeleted) {
builder.withDeleted()
@@ -207,7 +208,7 @@ private struct CompositeParentEagerLoader: EagerLoader
.flatMapThrowing {
let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) })
- for (parentId, models) in sets {
+ for (parentId, models) in sets.wrappedValue {
guard let parent = parents[parentId] else {
database.logger.debug(
"Missing parent model in eager-load lookup results.",
diff --git a/Sources/FluentKit/Properties/Field.swift b/Sources/FluentKit/Properties/Field.swift
index a81cc6cd..9d523cd5 100644
--- a/Sources/FluentKit/Properties/Field.swift
+++ b/Sources/FluentKit/Properties/Field.swift
@@ -1,13 +1,15 @@
+import NIOConcurrencyHelpers
+
extension Fields {
public typealias Field = FieldProperty
- where Value: Codable
+ where Value: Codable & Sendable
}
// MARK: Type
@propertyWrapper
-public final class FieldProperty
- where Model: FluentKit.Fields, Value: Codable
+public final class FieldProperty: @unchecked Sendable
+ where Model: FluentKit.Fields, Value: Codable & Sendable
{
public let key: FieldKey
var outputValue: Value?
diff --git a/Sources/FluentKit/Properties/FieldKey.swift b/Sources/FluentKit/Properties/FieldKey.swift
index d2549bc5..569c76a5 100644
--- a/Sources/FluentKit/Properties/FieldKey.swift
+++ b/Sources/FluentKit/Properties/FieldKey.swift
@@ -1,4 +1,4 @@
-public indirect enum FieldKey {
+public indirect enum FieldKey: Sendable {
case id
case string(String)
case aggregate
diff --git a/Sources/FluentKit/Properties/Group.swift b/Sources/FluentKit/Properties/Group.swift
index 8183c726..977c16f2 100644
--- a/Sources/FluentKit/Properties/Group.swift
+++ b/Sources/FluentKit/Properties/Group.swift
@@ -1,3 +1,5 @@
+import NIOConcurrencyHelpers
+
extension Fields {
public typealias Group = GroupProperty
where Value: Fields
@@ -6,7 +8,7 @@ extension Fields {
// MARK: Type
@propertyWrapper @dynamicMemberLookup
-public final class GroupProperty
+public final class GroupProperty: @unchecked Sendable
where Model: FluentKit.Fields, Value: FluentKit.Fields
{
public let key: FieldKey
diff --git a/Sources/FluentKit/Properties/ID.swift b/Sources/FluentKit/Properties/ID.swift
index 6f972956..b3e9624f 100644
--- a/Sources/FluentKit/Properties/ID.swift
+++ b/Sources/FluentKit/Properties/ID.swift
@@ -2,14 +2,14 @@ import Foundation
extension Model {
public typealias ID = IDProperty
- where Value: Codable
+ where Value: Codable & Sendable
}
// MARK: Type
@propertyWrapper
-public final class IDProperty
- where Model: FluentKit.Model, Value: Codable
+public final class IDProperty: @unchecked Sendable
+ where Model: FluentKit.Model, Value: Codable & Sendable
{
public enum Generator {
case user
@@ -109,7 +109,7 @@ public final class IDProperty
case .database:
self.inputValue = .default
case .random:
- let generatable = Value.self as! any (RandomGeneratable & Encodable).Type
+ let generatable = Value.self as! any (RandomGeneratable & Encodable & Sendable).Type
self.inputValue = .bind(generatable.generateRandom())
case .user:
// do nothing
diff --git a/Sources/FluentKit/Properties/OptionalBoolean.swift b/Sources/FluentKit/Properties/OptionalBoolean.swift
index 512f8ffa..7c77be61 100644
--- a/Sources/FluentKit/Properties/OptionalBoolean.swift
+++ b/Sources/FluentKit/Properties/OptionalBoolean.swift
@@ -41,7 +41,7 @@ extension Fields {
/// func revert(on database: Database) async throws -> Void { try await database.schema(MyModel.schema).delete() }
/// }
///
-/// - Note: See also ``BooleanProperty`` and ``BooleanPropertyFormat``.
+/// > Note: See also ``BooleanProperty`` and ``BooleanPropertyFormat``.
@propertyWrapper
public final class OptionalBooleanProperty
where Model: FluentKit.Fields, Format: BooleanPropertyFormat
diff --git a/Sources/FluentKit/Properties/OptionalChild.swift b/Sources/FluentKit/Properties/OptionalChild.swift
index 817eb6fc..372f8f05 100644
--- a/Sources/FluentKit/Properties/OptionalChild.swift
+++ b/Sources/FluentKit/Properties/OptionalChild.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
extension Model {
public typealias OptionalChild = OptionalChildProperty
@@ -8,7 +9,7 @@ extension Model {
// MARK: Type
@propertyWrapper
-public final class OptionalChildProperty
+public final class OptionalChildProperty: @unchecked Sendable
where From: Model, To: Model
{
public typealias Key = RelationParentKey
@@ -205,8 +206,9 @@ private struct OptionalChildEagerLoader: EagerLoader
if (self.withDeleted) {
builder.withDeleted()
}
+ let models = UnsafeTransfer(wrappedValue: models)
return builder.all().map {
- for model in models {
+ for model in models.wrappedValue {
let id = model[keyPath: self.relationKey].idValue!
let children = $0.filter { child in
switch parentKey {
diff --git a/Sources/FluentKit/Properties/OptionalField.swift b/Sources/FluentKit/Properties/OptionalField.swift
index 46d9c6ab..4b0c6b6a 100644
--- a/Sources/FluentKit/Properties/OptionalField.swift
+++ b/Sources/FluentKit/Properties/OptionalField.swift
@@ -1,13 +1,15 @@
+import NIOConcurrencyHelpers
+
extension Fields {
public typealias OptionalField = OptionalFieldProperty
- where Value: Codable
+ where Value: Codable & Sendable
}
// MARK: Type
@propertyWrapper
-public final class OptionalFieldProperty
- where Model: FluentKit.Fields, WrappedValue: Codable
+public final class OptionalFieldProperty: @unchecked Sendable
+ where Model: FluentKit.Fields, WrappedValue: Codable & Sendable
{
public let key: FieldKey
var outputValue: WrappedValue??
diff --git a/Sources/FluentKit/Properties/OptionalParent.swift b/Sources/FluentKit/Properties/OptionalParent.swift
index 554a3e1f..4dff02a4 100644
--- a/Sources/FluentKit/Properties/OptionalParent.swift
+++ b/Sources/FluentKit/Properties/OptionalParent.swift
@@ -1,4 +1,6 @@
import NIOCore
+import NIOConcurrencyHelpers
+import struct SQLKit.SomeCodingKey
extension Model {
public typealias OptionalParent = OptionalParentProperty
@@ -8,7 +10,7 @@ extension Model {
// MARK: Type
@propertyWrapper
-public final class OptionalParentProperty
+public final class OptionalParentProperty: @unchecked Sendable
where From: Model, To: Model
{
@OptionalFieldProperty
@@ -170,23 +172,24 @@ private struct OptionalParentEagerLoader: EagerLoader
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) ?? []
+ var _sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id })
+ let nilParentModels = UnsafeTransfer(wrappedValue: _sets.removeValue(forKey: nil) ?? [])
+ let sets = UnsafeTransfer(wrappedValue: _sets)
- if sets.isEmpty {
+ if sets.wrappedValue.isEmpty {
// Fetching "To" objects is unnecessary when no models have an id for "To".
- nilParentModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
+ nilParentModels.wrappedValue.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
return database.eventLoop.makeSucceededVoidFuture()
}
- let builder = To.query(on: database).filter(\._$id ~~ Set(sets.keys.compactMap { $0 }))
+ let builder = To.query(on: database).filter(\._$id ~~ Set(sets.wrappedValue.keys.compactMap { $0 }))
if (self.withDeleted) {
builder.withDeleted()
}
return builder.all().flatMapThrowing {
let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) })
- for (parentId, models) in sets {
+ for (parentId, models) in sets.wrappedValue {
guard let parent = parents[parentId!] else {
database.logger.debug(
"Missing parent model in eager-load lookup results.",
@@ -196,7 +199,7 @@ private struct OptionalParentEagerLoader: EagerLoader
}
models.forEach { $0[keyPath: self.relationKey].value = .some(.some(parent)) }
}
- nilParentModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
+ nilParentModels.wrappedValue.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
}
}
}
diff --git a/Sources/FluentKit/Properties/Parent.swift b/Sources/FluentKit/Properties/Parent.swift
index b2d5b3ec..cb01cc95 100644
--- a/Sources/FluentKit/Properties/Parent.swift
+++ b/Sources/FluentKit/Properties/Parent.swift
@@ -1,4 +1,6 @@
import NIOCore
+import NIOConcurrencyHelpers
+import struct SQLKit.SomeCodingKey
extension Model {
public typealias Parent = ParentProperty
@@ -8,7 +10,7 @@ extension Model {
// MARK: Type
@propertyWrapper
-public final class ParentProperty
+public final class ParentProperty: @unchecked Sendable
where From: Model, To: Model
{
@FieldProperty
@@ -164,15 +166,15 @@ private struct ParentEagerLoader: EagerLoader
let withDeleted: Bool
func run(models: [From], on database: any Database) -> EventLoopFuture {
- let sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id })
- let builder = To.query(on: database).filter(\._$id ~~ Set(sets.keys))
+ let sets = UnsafeTransfer(wrappedValue: Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id }))
+ let builder = To.query(on: database).filter(\._$id ~~ Set(sets.wrappedValue.keys))
if (self.withDeleted) {
builder.withDeleted()
}
return builder.all().flatMapThrowing {
let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) })
- for (parentId, models) in sets {
+ for (parentId, models) in sets.wrappedValue {
guard let parent = parents[parentId] else {
database.logger.debug(
"Missing parent model in eager-load lookup results.",
diff --git a/Sources/FluentKit/Properties/Property.swift b/Sources/FluentKit/Properties/Property.swift
index a87f00ef..41a8c614 100644
--- a/Sources/FluentKit/Properties/Property.swift
+++ b/Sources/FluentKit/Properties/Property.swift
@@ -17,7 +17,7 @@ public protocol AnyProperty: AnyObject {
/// many-to-many relation.
public protocol Property: AnyProperty {
associatedtype Model: Fields
- associatedtype Value: Codable
+ associatedtype Value: Codable & Sendable
var value: Value? { get set }
}
@@ -40,12 +40,12 @@ extension AnyProperty where Self: Property {
/// receives output from the results of read queries, provides input to write queries,
/// and/or represents one or more model fields.
///
-/// - Note: Most "database" properties participate in all three aspects (is/has fields,
-/// provides input, receives output), but certain properties only participate in
-/// receiving output (most notably the non-parent relation property types). Those
-/// properties only behave in this manner because the ability to look up the needed
-/// information on demand was not available in Swift until after the implementation was
-/// effectively complete. They should not be considered actual "database" properties.
+/// > Note: Most "database" properties participate in all three aspects (is/has fields,
+/// > provides input, receives output), but certain properties only participate in
+/// > receiving output (most notably the non-parent relation property types). Those
+/// > properties only behave in this manner because the ability to look up the needed
+/// > information on demand was not available in Swift until after the implementation was
+/// > effectively complete. They should not be considered actual "database" properties.
public protocol AnyDatabaseProperty: AnyProperty {
var keys: [FieldKey] { get }
func input(to input: any DatabaseInput)
@@ -59,12 +59,12 @@ public protocol AnyDatabaseProperty: AnyProperty {
/// which also wish to participate. Just about every property type is codable.
///
/// > Warning: The various relation property types sometimes behave somewhat oddly
-/// when encoded and/or decoded.
+/// > when encoded and/or decoded.
///
/// > TODO: When corresponding parent and child properties on their respective models
-/// refer to each other, such as due to both relations being eager-loaded, both
-/// encoding and decoding will crash due to infinite recursion. At some point, look
-/// into a way to at least error out rather than crashing.
+/// > refer to each other, such as due to both relations being eager-loaded, both
+/// > encoding and decoding will crash due to infinite recursion. At some point, look
+/// > into a way to at least error out rather than crashing.
public protocol AnyCodableProperty: AnyProperty {
/// Encode the property's data to an external representation.
func encode(to encoder: any Encoder) throws
@@ -115,12 +115,12 @@ public protocol QueryableProperty: AnyQueryableProperty, Property {
/// version of ``AnyQueryableProperty/queryableValue()-3uzih``, except that this
/// version will always have an input and thus can not return `nil`.
///
- /// - Warning: The existence of this method implies that any two identically-typed
- /// instances of a property _must_ encode their values into queries in exactly
- /// the same fashion, and Fluent does have code paths which proceed on that
- /// assumption. For example, this requirement is the primary reason that a
- /// ``TimestampProperty``'s format is represented as a generic type parameter
- /// rather than being provided to an initializer.
+ /// > Warning: The existence of this method implies that any two identically-typed
+ /// > instances of a property _must_ encode their values into queries in exactly
+ /// > the same fashion, and Fluent does have code paths which proceed on that
+ /// > assumption. For example, this requirement is the primary reason that a
+ /// > ``TimestampProperty``'s format is represented as a generic type parameter
+ /// > rather than being provided to an initializer.
static func queryValue(_ value: Value) -> DatabaseQuery.Value
}
diff --git a/Sources/FluentKit/Properties/Relation.swift b/Sources/FluentKit/Properties/Relation.swift
index 9a6e6e4e..68fb8362 100644
--- a/Sources/FluentKit/Properties/Relation.swift
+++ b/Sources/FluentKit/Properties/Relation.swift
@@ -3,9 +3,9 @@ import NIOCore
/// A protocol which designates a conforming type as representing a database relation of any kind. Intended
/// for use only by FluentKit property wrappers.
///
-/// - Note: This protocol should probably require conformance to ``Property``, but adding that requirement
-/// wouldn't have enough value to be worth having to hand-wave a technically semver-major change.
-public protocol Relation {
+/// > Note: This protocol should probably require conformance to ``Property``, but adding that requirement
+/// > wouldn't have enough value to be worth having to hand-wave a technically semver-major change.
+public protocol Relation: Sendable {
associatedtype RelatedValue
var name: String { get }
var value: RelatedValue? { get set }
@@ -17,8 +17,8 @@ extension Relation {
///
/// If the value is loaded (including reloading), the value is set in the property before being returned.
///
- /// - Note: This API is strongly preferred over ``Relation/load(on:)``, even when the caller does not need
- /// the returned value, in order to minimize unnecessary database traffic.
+ /// > Note: This API is strongly preferred over ``Relation/load(on:)``, even when the caller does not need
+ /// > the returned value, in order to minimize unnecessary database traffic.
///
/// - Parameters:
/// - reload: If `true`, load the value from the database unconditionally, overwriting any previously
@@ -44,7 +44,7 @@ extension Relation {
///
/// 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
+public enum RelationParentKey: Sendable
where From: FluentKit.Model, To: FluentKit.Model
{
case required(KeyPath>)
@@ -67,9 +67,9 @@ extension RelationParentKey: CustomStringConvertible {
/// purposes of ``CompositeChildrenProperty`` etc. makes it impractical to combine this and ``RelationParentKey``
/// in a single helper type.
///
-/// - 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
+/// > 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
{
case required(KeyPath>)
diff --git a/Sources/FluentKit/Properties/Siblings.swift b/Sources/FluentKit/Properties/Siblings.swift
index 7bf8d8a6..e1c618ca 100644
--- a/Sources/FluentKit/Properties/Siblings.swift
+++ b/Sources/FluentKit/Properties/Siblings.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOConcurrencyHelpers
extension Model {
public typealias Siblings = SiblingsProperty
@@ -8,7 +9,7 @@ extension Model {
// MARK: Type
@propertyWrapper
-public final class SiblingsProperty
+public final class SiblingsProperty: @unchecked Sendable
where From: Model, To: Model, Through: Model
{
public enum AttachMethod {
@@ -148,18 +149,19 @@ public final class SiblingsProperty
_ to: To,
method: AttachMethod,
on database: any Database,
- _ edit: @escaping (Through) -> () = { _ in }
+ _ edit: @escaping @Sendable (Through) -> () = { _ in }
) -> EventLoopFuture {
switch method {
case .always:
return self.attach(to, on: database, edit)
case .ifNotExists:
- return self.isAttached(to: to, on: database).flatMap { alreadyAttached in
+ let to = UnsafeTransfer(wrappedValue: to)
+ return self.isAttached(to: to.wrappedValue, on: database).flatMap { alreadyAttached in
if alreadyAttached {
return database.eventLoop.makeSucceededFuture(())
}
- return self.attach(to, on: database, edit)
+ return self.attach(to.wrappedValue, on: database, edit)
}
}
}
@@ -173,7 +175,7 @@ public final class SiblingsProperty
public func attach(
_ to: To,
on database: any Database,
- _ edit: (Through) -> () = { _ in }
+ _ edit: @Sendable (Through) -> () = { _ in }
) -> EventLoopFuture {
guard let fromID = self.idValue else {
return database.eventLoop.makeFailedFuture(SiblingsPropertyError.owningModelIdRequired(property: self.name))
@@ -388,15 +390,14 @@ private struct SiblingsEagerLoader: EagerLoader
if (self.withDeleted) {
builder.withDeleted()
}
- return builder.all()
- .flatMapThrowing
- {
+ let models = UnsafeTransfer(wrappedValue: models)
+ return builder.all().flatMapThrowing {
var map: [From.IDValue: [To]] = [:]
for to in $0 {
let fromID = try to.joined(Through.self)[keyPath: from].id
map[fromID, default: []].append(to)
}
- for model in models {
+ for model in models.wrappedValue {
guard let id = model.id else { throw FluentError.idRequired }
model[keyPath: self.relationKey].value = map[id] ?? []
}
diff --git a/Sources/FluentKit/Properties/TimestampFormat.swift b/Sources/FluentKit/Properties/TimestampFormat.swift
index 25643280..a32043bd 100644
--- a/Sources/FluentKit/Properties/TimestampFormat.swift
+++ b/Sources/FluentKit/Properties/TimestampFormat.swift
@@ -1,10 +1,11 @@
-import class NIO.ThreadSpecificVariable
+import NIOConcurrencyHelpers
+import class NIOPosix.ThreadSpecificVariable
import Foundation
// MARK: Format
-public protocol TimestampFormat {
- associatedtype Value: Codable
+public protocol TimestampFormat: Sendable {
+ associatedtype Value: Codable & Sendable
func parse(_ value: Value) -> Date?
func serialize(_ date: Date) -> Value?
@@ -52,32 +53,21 @@ extension TimestampFormatFactory {
withMilliseconds: Bool
) -> TimestampFormatFactory {
.init {
- let formatter = ISO8601DateFormatter.threadSpecific
- if withMilliseconds {
- formatter.formatOptions.insert(.withFractionalSeconds)
+ ISO8601DateFormatter.shared.withLockedValue {
+ if withMilliseconds {
+ $0.formatOptions.insert(.withFractionalSeconds)
+ }
+ return ISO8601TimestampFormat(formatter: $0)
}
- return ISO8601TimestampFormat(formatter: formatter)
}
}
}
extension ISO8601DateFormatter {
- private static var cache: ThreadSpecificVariable = .init()
-
- static var threadSpecific: ISO8601DateFormatter {
- let formatter: ISO8601DateFormatter
- if let existing = ISO8601DateFormatter.cache.currentValue {
- formatter = existing
- } else {
- let new = ISO8601DateFormatter()
- self.cache.currentValue = new
- formatter = new
- }
- return formatter
- }
+ fileprivate static let shared: NIOLockedValueBox = .init(.init())
}
-public struct ISO8601TimestampFormat: TimestampFormat {
+public struct ISO8601TimestampFormat: TimestampFormat, @unchecked Sendable {
public typealias Value = String
let formatter: ISO8601DateFormatter
diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift
index a1137ecd..98f11971 100644
--- a/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift
+++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Aggregate.swift
@@ -14,117 +14,117 @@ extension QueryBuilder {
}
public func count(_ key: KeyPath) -> EventLoopFuture
- where Field: QueryableProperty, Field.Model == Model
+ 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
+ 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
+ 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
+ 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, 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, 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