Skip to content

Commit ccea982

Browse files
authored
Correctly handle @OptionalEnum properties when using ~~ and != in filter() (#580)
* Enable use of the ~~ and !~ "array membership" operators with @OptionalEnum in the same way they can already be used with @enum * Add test and benchmark coverage for using the array operators with optional enum properties
1 parent 116eebb commit ccea982

File tree

4 files changed

+103
-24
lines changed

4 files changed

+103
-24
lines changed

Sources/FluentBenchmark/Tests/EnumTests.swift

+16
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ extension FluentBenchmarker {
9898
XCTAssertEqual(fetched3?.bar, .baz)
9999
XCTAssertEqual(fetched3?.baz, .qux)
100100

101+
let fetched3Opt = try Foo
102+
.query(on: self.database)
103+
.filter(\.$baz ~~ [.baz, .qux])
104+
.first()
105+
.wait()
106+
XCTAssertEqual(fetched3Opt?.bar, .baz)
107+
XCTAssertEqual(fetched3Opt?.baz, .qux)
108+
101109
// not in
102110
let foo4 = Foo(bar: .baz, baz: .qux)
103111
try foo4.save(on: self.database).wait()
@@ -110,6 +118,14 @@ extension FluentBenchmarker {
110118
XCTAssertEqual(fetched4?.bar, .baz)
111119
XCTAssertEqual(fetched4?.baz, .qux)
112120

121+
let fetched4Opt = try Foo
122+
.query(on: self.database)
123+
.filter(\.$baz !~ [.baz])
124+
.first()
125+
.wait()
126+
XCTAssertEqual(fetched4Opt?.bar, .baz)
127+
XCTAssertEqual(fetched4Opt?.baz, .qux)
128+
113129
// is null
114130
let foo5 = Foo(bar: .baz, baz: nil)
115131
try foo5.save(on: self.database).wait()

Sources/FluentKit/Operators/ValueOperators+Array.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public func ~~ <Model, Field, Values>(lhs: KeyPath<Model, Field>, rhs: Values) -
1717
Values: Collection,
1818
Values.Element == Field.Value.Wrapped
1919
{
20-
lhs ~~ .array(rhs.map { .bind($0) })
20+
lhs ~~ .array(rhs.map { Field.queryValue(.init($0)) })
2121
}
2222

2323
public func !~ <Model, Field, Values>(lhs: KeyPath<Model, Field>, rhs: Values) -> ModelValueFilter<Model>
@@ -37,7 +37,7 @@ public func !~ <Model, Field, Values>(lhs: KeyPath<Model, Field>, rhs: Values) -
3737
Values: Collection,
3838
Values.Element == Field.Value.Wrapped
3939
{
40-
lhs !~ .array(rhs.map { .bind($0) })
40+
lhs !~ .array(rhs.map { Field.queryValue(.init($0)) })
4141
}
4242

4343
// MARK: DatabaseQuery.Value

Tests/FluentKitTests/AsyncTests/AsyncFilterQueryTests.swift

+41-8
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,64 @@ final class AsyncFilterQueryTests: XCTestCase {
1515
let db = DummyDatabaseForTestSQLSerializer()
1616
_ = try await Task.query(on: db).filter(\.$status == .done).all()
1717
XCTAssertEqual(db.sqlSerializers.count, 1)
18-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" = 'done'"#)
18+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" = 'done'"#)
1919
db.reset()
2020
}
2121

2222
func test_enumNotEquals() async throws {
2323
let db = DummyDatabaseForTestSQLSerializer()
2424
_ = try await Task.query(on: db).filter(\.$status != .done).all()
2525
XCTAssertEqual(db.sqlSerializers.count, 1)
26-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"#)
26+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"#)
2727
db.reset()
2828
}
2929

3030
func test_enumIn() async throws {
3131
let db = DummyDatabaseForTestSQLSerializer()
3232
_ = try await Task.query(on: db).filter(\.$status ~~ [.done, .notDone]).all()
3333
XCTAssertEqual(db.sqlSerializers.count, 1)
34-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"#)
34+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"#)
3535
db.reset()
3636
}
3737

3838
func test_enumNotIn() async throws {
3939
let db = DummyDatabaseForTestSQLSerializer()
4040
_ = try await Task.query(on: db).filter(\.$status !~ [.done, .notDone]).all()
4141
XCTAssertEqual(db.sqlSerializers.count, 1)
42-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"#)
42+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"#)
43+
db.reset()
44+
}
45+
46+
// MARK: OptionalEnum
47+
func test_optionalEnumEquals() async throws {
48+
let db = DummyDatabaseForTestSQLSerializer()
49+
_ = try await Task.query(on: db).filter(\.$optionalStatus == .done).all()
50+
XCTAssertEqual(db.sqlSerializers.count, 1)
51+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" = 'done'"#)
52+
db.reset()
53+
}
54+
55+
func test_optionalEnumNotEquals() async throws {
56+
let db = DummyDatabaseForTestSQLSerializer()
57+
_ = try await Task.query(on: db).filter(\.$optionalStatus != .done).all()
58+
XCTAssertEqual(db.sqlSerializers.count, 1)
59+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" <> 'done'"#)
60+
db.reset()
61+
}
62+
63+
func test_optionalEnumIn() async throws {
64+
let db = DummyDatabaseForTestSQLSerializer()
65+
_ = try await Task.query(on: db).filter(\.$optionalStatus ~~ [.done, .notDone]).all()
66+
XCTAssertEqual(db.sqlSerializers.count, 1)
67+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" IN ('done','notDone')"#)
68+
db.reset()
69+
}
70+
71+
func test_optionalEnumNotIn() async throws {
72+
let db = DummyDatabaseForTestSQLSerializer()
73+
_ = try await Task.query(on: db).filter(\.$optionalStatus !~ [.done, .notDone]).all()
74+
XCTAssertEqual(db.sqlSerializers.count, 1)
75+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" NOT IN ('done','notDone')"#)
4376
db.reset()
4477
}
4578

@@ -48,31 +81,31 @@ final class AsyncFilterQueryTests: XCTestCase {
4881
let db = DummyDatabaseForTestSQLSerializer()
4982
_ = try await Task.query(on: db).filter(\.$description == "hello").all()
5083
XCTAssertEqual(db.sqlSerializers.count, 1)
51-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" = $1"#)
84+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" = $1"#)
5285
db.reset()
5386
}
5487

5588
func test_stringNotEquals() async throws {
5689
let db = DummyDatabaseForTestSQLSerializer()
5790
_ = try await Task.query(on: db).filter(\.$description != "hello").all()
5891
XCTAssertEqual(db.sqlSerializers.count, 1)
59-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" <> $1"#)
92+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" <> $1"#)
6093
db.reset()
6194
}
6295

6396
func test_stringIn() async throws {
6497
let db = DummyDatabaseForTestSQLSerializer()
6598
_ = try await Task.query(on: db).filter(\.$description ~~ ["hello"]).all()
6699
XCTAssertEqual(db.sqlSerializers.count, 1)
67-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"#)
100+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"#)
68101
db.reset()
69102
}
70103

71104
func test_stringNotIn() async throws {
72105
let db = DummyDatabaseForTestSQLSerializer()
73106
_ = try await Task.query(on: db).filter(\.$description !~ ["hello"]).all()
74107
XCTAssertEqual(db.sqlSerializers.count, 1)
75-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"#)
108+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"#)
76109
db.reset()
77110
}
78111
}

Tests/FluentKitTests/FilterQueryTests.swift

+44-14
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,64 @@ final class FilterQueryTests: XCTestCase {
2222
let db = DummyDatabaseForTestSQLSerializer()
2323
_ = try Task.query(on: db).filter(\.$status == .done).all().wait()
2424
XCTAssertEqual(db.sqlSerializers.count, 1)
25-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" = 'done'"#)
25+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" = 'done'"#)
2626
db.reset()
2727
}
2828

2929
func test_enumNotEquals() throws {
3030
let db = DummyDatabaseForTestSQLSerializer()
3131
_ = try Task.query(on: db).filter(\.$status != .done).all().wait()
3232
XCTAssertEqual(db.sqlSerializers.count, 1)
33-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"#)
33+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" <> 'done'"#)
3434
db.reset()
3535
}
3636

3737
func test_enumIn() throws {
3838
let db = DummyDatabaseForTestSQLSerializer()
3939
_ = try Task.query(on: db).filter(\.$status ~~ [.done, .notDone]).all().wait()
4040
XCTAssertEqual(db.sqlSerializers.count, 1)
41-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"#)
41+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" IN ('done','notDone')"#)
4242
db.reset()
4343
}
4444

4545
func test_enumNotIn() throws {
4646
let db = DummyDatabaseForTestSQLSerializer()
4747
_ = try Task.query(on: db).filter(\.$status !~ [.done, .notDone]).all().wait()
4848
XCTAssertEqual(db.sqlSerializers.count, 1)
49-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"#)
49+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."status" NOT IN ('done','notDone')"#)
50+
db.reset()
51+
}
52+
53+
// MARK: OptionalEnum
54+
func test_optionalEnumEquals() throws {
55+
let db = DummyDatabaseForTestSQLSerializer()
56+
_ = try Task.query(on: db).filter(\.$optionalStatus == .done).all().wait()
57+
XCTAssertEqual(db.sqlSerializers.count, 1)
58+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" = 'done'"#)
59+
db.reset()
60+
}
61+
62+
func test_optionalEnumNotEquals() throws {
63+
let db = DummyDatabaseForTestSQLSerializer()
64+
_ = try Task.query(on: db).filter(\.$optionalStatus != .done).all().wait()
65+
XCTAssertEqual(db.sqlSerializers.count, 1)
66+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" <> 'done'"#)
67+
db.reset()
68+
}
69+
70+
func test_optionalEnumIn() throws {
71+
let db = DummyDatabaseForTestSQLSerializer()
72+
_ = try Task.query(on: db).filter(\.$optionalStatus ~~ [.done, .notDone]).all().wait()
73+
XCTAssertEqual(db.sqlSerializers.count, 1)
74+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" IN ('done','notDone')"#)
75+
db.reset()
76+
}
77+
78+
func test_optionalEnumNotIn() throws {
79+
let db = DummyDatabaseForTestSQLSerializer()
80+
_ = try Task.query(on: db).filter(\.$optionalStatus !~ [.done, .notDone]).all().wait()
81+
XCTAssertEqual(db.sqlSerializers.count, 1)
82+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."optional_status" NOT IN ('done','notDone')"#)
5083
db.reset()
5184
}
5285

@@ -55,31 +88,31 @@ final class FilterQueryTests: XCTestCase {
5588
let db = DummyDatabaseForTestSQLSerializer()
5689
_ = try Task.query(on: db).filter(\.$description == "hello").all().wait()
5790
XCTAssertEqual(db.sqlSerializers.count, 1)
58-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" = $1"#)
91+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" = $1"#)
5992
db.reset()
6093
}
6194

6295
func test_stringNotEquals() throws {
6396
let db = DummyDatabaseForTestSQLSerializer()
6497
_ = try Task.query(on: db).filter(\.$description != "hello").all().wait()
6598
XCTAssertEqual(db.sqlSerializers.count, 1)
66-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" <> $1"#)
99+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" <> $1"#)
67100
db.reset()
68101
}
69102

70103
func test_stringIn() throws {
71104
let db = DummyDatabaseForTestSQLSerializer()
72105
_ = try Task.query(on: db).filter(\.$description ~~ ["hello"]).all().wait()
73106
XCTAssertEqual(db.sqlSerializers.count, 1)
74-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"#)
107+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" IN ($1)"#)
75108
db.reset()
76109
}
77110

78111
func test_stringNotIn() throws {
79112
let db = DummyDatabaseForTestSQLSerializer()
80113
_ = try Task.query(on: db).filter(\.$description !~ ["hello"]).all().wait()
81114
XCTAssertEqual(db.sqlSerializers.count, 1)
82-
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"#)
115+
XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "tasks"."id" AS "tasks_id", "tasks"."description" AS "tasks_description", "tasks"."status" AS "tasks_status", "tasks"."optional_status" AS "tasks_optional_status" FROM "tasks" WHERE "tasks"."description" NOT IN ($1)"#)
83116
db.reset()
84117
}
85118
}
@@ -99,12 +132,9 @@ final class Task: Model {
99132

100133
@Enum(key: "status")
101134
var status: Diggity
135+
136+
@OptionalEnum(key: "optional_status")
137+
var optionalStatus: Diggity?
102138

103139
init() {}
104-
105-
init(id: Int, status: Diggity, description: String) {
106-
self.id = id
107-
self.status = status
108-
self.description = description
109-
}
110140
}

0 commit comments

Comments
 (0)