Skip to content

Commit 260eff9

Browse files
authored
Fix Fluent model encoding for SQLKit and several Sendable warnings (#624)
* Require Swift 5.9, remove obsolete workaround * Resolve new Sendable warnings produced by Swift 6 compiler * Make Fluent model encoding for SQLKit stable * Fix misuse of EmbeddedEventLoop in tests and disable unfixable test
1 parent 2cceea2 commit 260eff9

File tree

10 files changed

+30
-130
lines changed

10 files changed

+30
-130
lines changed

Package.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.8
1+
// swift-tools-version:5.9
22
import PackageDescription
33

44
let package = Package(
@@ -16,9 +16,9 @@ let package = Package(
1616
.library(name: "XCTFluent", targets: ["XCTFluent"]),
1717
],
1818
dependencies: [
19-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
20-
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
21-
.package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"),
19+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.79.0"),
20+
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
21+
.package(url: "https://github.com/vapor/sql-kit.git", from: "3.32.0"),
2222
.package(url: "https://github.com/vapor/async-kit.git", from: "1.20.0"),
2323
],
2424
targets: [
@@ -72,6 +72,9 @@ let package = Package(
7272
)
7373

7474
var swiftSettings: [SwiftSetting] { [
75+
.enableUpcomingFeature("ExistentialAny"),
7576
.enableUpcomingFeature("ConciseMagicFile"),
7677
.enableUpcomingFeature("ForwardTrailingClosures"),
78+
.enableUpcomingFeature("DisableOutwardActorInference"),
79+
.enableExperimentalFeature("StrictConcurrency=complete"),
7780
] }

[email protected]

Lines changed: 0 additions & 80 deletions
This file was deleted.

Sources/FluentKit/Concurrency/Database+Concurrency.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ public extension Database {
2020
try await self.execute(enum: `enum`).get()
2121
}
2222

23-
func transaction<T>(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
23+
func transaction<T: Sendable>(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
2424
try await self.transaction { db in
2525
self.eventLoop.makeFutureWithTask {
2626
try await closure(db)
2727
}
2828
}.get()
2929
}
3030

31-
func withConnection<T>(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
31+
func withConnection<T: Sendable>(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
3232
try await self.withConnection { db in
3333
self.eventLoop.makeFutureWithTask {
3434
try await closure(db)

Sources/FluentKit/Model/Fields+Codable.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ extension Fields {
77
let container = try decoder.container(keyedBy: SomeCodingKey.self)
88

99
for (key, property) in self.codableProperties {
10-
#if swift(<5.7.1)
11-
let propDecoder = WorkaroundSuperDecoder(container: container, key: key)
12-
#else
1310
let propDecoder = try container.superDecoder(forKey: key)
14-
#endif
1511
do {
1612
try property.decode(from: propDecoder)
1713
} catch {
@@ -40,29 +36,3 @@ extension Fields {
4036
}
4137
}
4238
}
43-
44-
#if swift(<5.7.1)
45-
/// This ``Decoder`` compensates for a bug in `KeyedDecodingContainerProtocol.superDecoder(forKey:)` on Linux
46-
/// which first appeared in Swift 5.5 and was fixed in Swift 5.7.1.
47-
///
48-
/// When a given key is not present in the input JSON, `.superDecoder(forKey:)` is expected to return a valid
49-
/// ``Decoder`` that will only decode a nil value. However, in affected versions of Swift, the method instead
50-
/// throws a ``DecodingError/keyNotFound``.
51-
///
52-
/// As a workaround, instead of calling `.superDecoder(forKey:)`, an instance of this type is created and
53-
/// provided with the decoding container; the apporiate decoding methods are intercepted to provide the
54-
/// desired semantics, with everything else being forwarded directly to the container. This has a minor but
55-
/// nonzero impact on performance, but was determined to be the best and cleanest option.
56-
private struct WorkaroundSuperDecoder<K: CodingKey>: Decoder, SingleValueDecodingContainer {
57-
var codingPath: [CodingKey] { self.container.codingPath }
58-
var userInfo: [CodingUserInfoKey: Any] { [:] }
59-
let container: KeyedDecodingContainer<K>
60-
let key: K
61-
62-
func container<NK: CodingKey>(keyedBy: NK.Type) throws -> KeyedDecodingContainer<NK> { try self.container.nestedContainer(keyedBy: NK.self, forKey: self.key) }
63-
func unkeyedContainer() throws -> UnkeyedDecodingContainer { try self.container.nestedUnkeyedContainer(forKey: self.key) }
64-
func singleValueContainer() throws -> SingleValueDecodingContainer { self }
65-
func decode<T: Decodable>(_: T.Type) throws -> T { try self.container.decode(T.self, forKey: self.key) }
66-
func decodeNil() -> Bool { self.container.contains(self.key) ? try! self.container.decodeNil(forKey: self.key) : true }
67-
}
68-
#endif

Sources/FluentKit/Properties/Relation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import NIOCore
66
/// > Note: This protocol should probably require conformance to ``Property``, but adding that requirement
77
/// > wouldn't have enough value to be worth having to hand-wave a technically semver-major change.
88
public protocol Relation: Sendable {
9-
associatedtype RelatedValue
9+
associatedtype RelatedValue: Sendable
1010
var name: String { get }
1111
var value: RelatedValue? { get set }
1212
func load(on database: any Database) -> EventLoopFuture<Void>

Sources/FluentKit/Query/Builder/QueryBuilder+Paginate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ extension QueryBuilder {
5050
}
5151

5252
/// A single section of a larger, traversable result set.
53-
public struct Page<T> {
53+
public struct Page<T: Sendable>: Sendable {
5454
/// The page's items. Usually models.
5555
public let items: [T]
5656

@@ -76,7 +76,7 @@ extension Page: Encodable where T: Encodable {}
7676
extension Page: Decodable where T: Decodable {}
7777

7878
/// Metadata for a given `Page`.
79-
public struct PageMetadata: Codable {
79+
public struct PageMetadata: Codable, Sendable {
8080
/// Current page number. Starts at `1`.
8181
public let page: Int
8282

Sources/FluentSQL/SQLDatabase+Model.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ extension DatabaseQuery.Value {
8383

8484
extension Model {
8585
fileprivate func encodeForSQL(withDefaultedValues: Bool) -> [(String, any SQLExpression)] {
86-
self.collectInput(withDefaultedValues: withDefaultedValues).map { ($0.description, $1.asSQLExpression) }
86+
self.collectInput(withDefaultedValues: withDefaultedValues).map { ($0.description, $1.asSQLExpression) }.sorted(by: { $0.0 < $1.0 })
8787
}
8888
}
8989

Tests/FluentKitTests/DummyDatabaseForTestSQLSerializer.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import FluentKit
22
import FluentSQL
3-
import NIOEmbedded
3+
import NIOCore
44
import SQLKit
55
import XCTFluent
66
import NIOConcurrencyHelpers
@@ -60,7 +60,7 @@ final class DummyDatabaseForTestSQLSerializer: Database, SQLDatabase {
6060
self.context = .init(
6161
configuration: Configuration(),
6262
logger: .init(label: "test"),
63-
eventLoop: EmbeddedEventLoop()
63+
eventLoop: NIOSingletons.posixEventLoopGroup.any()
6464
)
6565
}
6666

@@ -81,12 +81,14 @@ final class DummyDatabaseForTestSQLSerializer: Database, SQLDatabase {
8181
var sqlSerializer = SQLSerializer(database: self)
8282
query.serialize(to: &sqlSerializer)
8383
self._sqlSerializers.withLockedValue { $0.append(sqlSerializer) }
84-
if !self.fakedRows.isEmpty {
85-
for row in self._fakedRows.withLockedValue({ $0.removeFirst() }) {
84+
return self.eventLoop.submit {
85+
let rows = self._fakedRows.withLockedValue {
86+
$0.isEmpty ? [] : $0.removeFirst()
87+
}
88+
for row in rows {
8689
onRow(row)
8790
}
8891
}
89-
return self.eventLoop.makeSucceededVoidFuture()
9092
}
9193

9294
func transaction<T>(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> {

Tests/FluentKitTests/FluentKitTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ final class FluentKitTests: XCTestCase {
2323
/// Since no part of Fluent or any of its drivers currently relies, or ever will rely, on
2424
/// the format in question, it is desirable to enforce that it should never change, just in
2525
/// case someone actually is relying on it for some hopefully very good reason.
26+
///
27+
/// Update: Ignore all of the above. This test is not reliable due to the instability of serializing
28+
/// dictionaries as strings, and adding sorting changes the output, so the whole point is mooted.
29+
/*
2630
func testAnyModelDescriptionFormatHasNotChanged() throws {
2731
final class Foo: Model, @unchecked Sendable {
2832
static let schema = "foos"
@@ -47,7 +51,8 @@ final class FluentKitTests: XCTestCase {
4751
XCTAssertEqual(modelOutputDesc, "Foo(output: [num: 42, name: \"Test\", id: \(model.id!)])")
4852
XCTAssertEqual(modelBothDesc, "Foo(output: [num: 42, name: \"Test\", id: \(model.id!)], input: [num: 43])")
4953
}
50-
54+
*/
55+
5156
func testMigrationLogNames() throws {
5257
XCTAssertEqual(MigrationLog.path(for: \.$id), [.id])
5358
XCTAssertEqual(MigrationLog.path(for: \.$name), ["name"])

Tests/FluentKitTests/SQLTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,13 @@ final class SQLTests: DbQueryTestCase {
158158
try await self.db.insert(into: FromPivot.schema).fluentModels([fromPivot1, fromPivot2]).run()
159159

160160
XCTAssertEqual(self.db.sqlSerializers.count, 4)
161-
XCTAssertEqual(self.db.sqlSerializers.dropFirst(0).first?.sql, #"INSERT INTO "model1s" ("optfield", "id", "bool", "optbool", "group_groupfield2", "group_groupfield1", "created_at", "enum", "field", "optenum") VALUES ($1, DEFAULT, $2, $3, $4, $5, $6, 'foo', $7, 'bar'), (NULL, $8, $9, NULL, $10, $11, $12, 'foo', $13, NULL)"#)
161+
XCTAssertEqual(self.db.sqlSerializers.dropFirst(0).first?.sql, #"INSERT INTO "model1s" ("bool", "created_at", "enum", "field", "group_groupfield1", "group_groupfield2", "id", "optbool", "optenum", "optfield") VALUES ($1, $2, 'foo', $3, $4, $5, DEFAULT, $6, 'bar', $7), ($8, $9, 'foo', $10, $11, $12, $13, NULL, NULL, NULL)"#)
162162
XCTAssertEqual(self.db.sqlSerializers.dropFirst(0).first?.binds.count, 13)
163-
XCTAssertEqual(self.db.sqlSerializers.dropFirst(1).first?.sql, #"INSERT INTO "model2s" ("id", "model1_id", "field", "othermodel1_id") VALUES (DEFAULT, $1, $2, $3), ($4, $5, $6, NULL)"#)
163+
XCTAssertEqual(self.db.sqlSerializers.dropFirst(1).first?.sql, #"INSERT INTO "model2s" ("field", "id", "model1_id", "othermodel1_id") VALUES ($1, DEFAULT, $2, $3), ($4, $5, $6, NULL)"#)
164164
XCTAssertEqual(self.db.sqlSerializers.dropFirst(1).first?.binds.count, 6)
165-
XCTAssertEqual(self.db.sqlSerializers.dropFirst(2).first?.sql, #"INSERT INTO "pivots" ("model2_id", "model1_id") VALUES ($1, $2)"#)
165+
XCTAssertEqual(self.db.sqlSerializers.dropFirst(2).first?.sql, #"INSERT INTO "pivots" ("model1_id", "model2_id") VALUES ($1, $2)"#)
166166
XCTAssertEqual(self.db.sqlSerializers.dropFirst(2).first?.binds.count, 2)
167-
XCTAssertEqual(self.db.sqlSerializers.dropFirst(3).first?.sql, #"INSERT INTO "from_pivots" ("pivot_model2_id", "optpivot_model1_id", "pivot_model1_id", "field2", "optpivot_model2_id", "field1") VALUES ($1, $2, $3, $4, $5, $6), ($7, NULL, $8, $9, NULL, $10)"#)
167+
XCTAssertEqual(self.db.sqlSerializers.dropFirst(3).first?.sql, #"INSERT INTO "from_pivots" ("field1", "field2", "optpivot_model1_id", "optpivot_model2_id", "pivot_model1_id", "pivot_model2_id") VALUES ($1, $2, $3, $4, $5, $6), ($7, $8, NULL, NULL, $9, $10)"#)
168168
XCTAssertEqual(self.db.sqlSerializers.dropFirst(3).first?.binds.count, 10)
169169
}
170170
}

0 commit comments

Comments
 (0)