Skip to content

Commit 25681af

Browse files
jsonGroupArray filter fixes #103 (#104)
* Fix jsonGroupArray() automatic application of filter when QueryValue is Optional. * Comment corrections for jsonGroupArray() * refine fix to more closely match orginal. add assertion in SelectTests.swift when joined table has no results --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent ce44aee commit 25681af

File tree

3 files changed

+52
-14
lines changed

3 files changed

+52
-14
lines changed

Sources/StructuredQueriesCore/SQLite/JSONFunctions.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension PrimaryKeyedTableDefinition where QueryValue: Codable & Sendable {
7070
/// ```swift
7171
/// @Selection struct Row {
7272
/// let remindersList: RemindersList
73-
/// @Column(as: JSONRepresentation<[Reminder]>.self)
73+
/// @Column(as: [Reminder].JSONRepresentation.self)
7474
/// let reminders: [Reminder]
7575
/// }
7676
/// RemindersList
@@ -87,15 +87,15 @@ extension PrimaryKeyedTableDefinition where QueryValue: Codable & Sendable {
8787
/// ```sql
8888
/// SELECT
8989
/// "remindersLists".…,
90-
/// iif(
91-
/// "reminders"."id" IS NULL,
92-
/// NULL,
90+
/// CASE WHEN
91+
/// ("reminders"."id" IS NOT NULL)
92+
/// THEN
9393
/// json_object(
9494
/// 'id', json_quote("id"),
9595
/// 'title', json_quote("title"),
9696
/// 'priority', json_quote("priority")
9797
/// )
98-
/// )
98+
/// END AS "reminders"
9999
/// FROM "remindersLists"
100100
/// JOIN "reminders"
101101
/// ON ("remindersLists"."id" = "reminders"."remindersListID")
@@ -123,7 +123,7 @@ extension PrimaryKeyedTableDefinition where QueryValue: Codable & Sendable {
123123
}
124124
}
125125

126-
extension PrimaryKeyedTableDefinition {
126+
extension PrimaryKeyedTableDefinition where QueryValue: _OptionalProtocol & Codable & Sendable {
127127
/// A JSON array representation of the aggregation of a table's columns.
128128
///
129129
/// Constructs a JSON array of JSON objects with a field for each column of the table. This can be
@@ -136,7 +136,7 @@ extension PrimaryKeyedTableDefinition {
136136
/// ```swift
137137
/// @Selection struct Row {
138138
/// let remindersList: RemindersList
139-
/// @Column(as: JSONRepresentation<[Reminder]>.self)
139+
/// @Column(as: [Reminder].JSONRepresentation.self)
140140
/// let reminders: [Reminder]
141141
/// }
142142
/// RemindersList
@@ -153,15 +153,15 @@ extension PrimaryKeyedTableDefinition {
153153
/// ```sql
154154
/// SELECT
155155
/// "remindersLists".…,
156-
/// iif(
157-
/// "reminders"."id" IS NULL,
158-
/// NULL,
156+
/// CASE WHEN
157+
/// ("reminders"."id" IS NOT NULL)
158+
/// THEN
159159
/// json_object(
160160
/// 'id', json_quote("id"),
161161
/// 'title', json_quote("title"),
162162
/// 'priority', json_quote("priority")
163163
/// )
164-
/// )
164+
/// END AS "reminders"
165165
/// FROM "remindersLists"
166166
/// JOIN "reminders"
167167
/// ON ("remindersLists"."id" = "reminders"."remindersListID")
@@ -179,7 +179,8 @@ extension PrimaryKeyedTableDefinition {
179179
order: (some QueryExpression)? = Bool?.none,
180180
filter: (some QueryExpression<Bool>)? = Bool?.none
181181
) -> some QueryExpression<[Wrapped].JSONRepresentation>
182-
where QueryValue == Wrapped? {
182+
where QueryValue == Wrapped?
183+
{
183184
let filterQueryFragment =
184185
if let filter {
185186
self.primaryKey.isNot(nil).and(filter).queryFragment
@@ -194,6 +195,9 @@ extension PrimaryKeyedTableDefinition {
194195
filter: filterQueryFragment
195196
)
196197
}
198+
}
199+
200+
extension PrimaryKeyedTableDefinition {
197201

198202
fileprivate var jsonObject: some QueryExpression<QueryValue> {
199203
func open<TableColumn: TableColumnExpression>(_ column: TableColumn) -> QueryFragment {

Tests/StructuredQueriesTests/KitchenSinkTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ extension SnapshotTests {
211211
.select { ($0, $1.jsonGroupArray()) }
212212
) {
213213
"""
214-
SELECT "kitchens"."id", json_group_array(CASE WHEN ("kitchenSinks"."id" IS NOT NULL) THEN json_object('id', json_quote("kitchenSinks"."id"), 'kitchenID', json_quote("kitchenSinks"."kitchenID"), 'bool', json(CASE "kitchenSinks"."bool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'optionalBool', json(CASE "kitchenSinks"."optionalBool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'string', json_quote("kitchenSinks"."string"), 'optionalString', json_quote("kitchenSinks"."optionalString"), 'int', json_quote("kitchenSinks"."int"), 'optionalInt', json_quote("kitchenSinks"."optionalInt"), 'double', json_quote("kitchenSinks"."double"), 'optionalDouble', json_quote("kitchenSinks"."optionalDouble"), 'rawRepresentable', json_quote("kitchenSinks"."rawRepresentable"), 'optionalRawRepresentable', json_quote("kitchenSinks"."optionalRawRepresentable"), 'iso8601Date', json_quote("kitchenSinks"."iso8601Date"), 'optionalISO8601Date', json_quote("kitchenSinks"."optionalISO8601Date"), 'unixTimeDate', datetime("kitchenSinks"."unixTimeDate", 'unixepoch'), 'optionalUnixTimeDate', datetime("kitchenSinks"."optionalUnixTimeDate", 'unixepoch'), 'julianDayDate', datetime("kitchenSinks"."julianDayDate", 'julianday'), 'optionalJulianDayDate', datetime("kitchenSinks"."optionalJulianDayDate", 'julianday'), 'jsonArray', json("kitchenSinks"."jsonArray"), 'optionalJSONArray', json("kitchenSinks"."optionalJSONArray"), 'jsonArrayOfDates', json("kitchenSinks"."jsonArrayOfDates")) END)
214+
SELECT "kitchens"."id", json_group_array(CASE WHEN ("kitchenSinks"."id" IS NOT NULL) THEN json_object('id', json_quote("kitchenSinks"."id"), 'kitchenID', json_quote("kitchenSinks"."kitchenID"), 'bool', json(CASE "kitchenSinks"."bool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'optionalBool', json(CASE "kitchenSinks"."optionalBool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'string', json_quote("kitchenSinks"."string"), 'optionalString', json_quote("kitchenSinks"."optionalString"), 'int', json_quote("kitchenSinks"."int"), 'optionalInt', json_quote("kitchenSinks"."optionalInt"), 'double', json_quote("kitchenSinks"."double"), 'optionalDouble', json_quote("kitchenSinks"."optionalDouble"), 'rawRepresentable', json_quote("kitchenSinks"."rawRepresentable"), 'optionalRawRepresentable', json_quote("kitchenSinks"."optionalRawRepresentable"), 'iso8601Date', json_quote("kitchenSinks"."iso8601Date"), 'optionalISO8601Date', json_quote("kitchenSinks"."optionalISO8601Date"), 'unixTimeDate', datetime("kitchenSinks"."unixTimeDate", 'unixepoch'), 'optionalUnixTimeDate', datetime("kitchenSinks"."optionalUnixTimeDate", 'unixepoch'), 'julianDayDate', datetime("kitchenSinks"."julianDayDate", 'julianday'), 'optionalJulianDayDate', datetime("kitchenSinks"."optionalJulianDayDate", 'julianday'), 'jsonArray', json("kitchenSinks"."jsonArray"), 'optionalJSONArray', json("kitchenSinks"."optionalJSONArray"), 'jsonArrayOfDates', json("kitchenSinks"."jsonArrayOfDates")) END) FILTER (WHERE ("kitchenSinks"."id" IS NOT NULL))
215215
FROM "kitchens"
216216
FULL JOIN "kitchenSinks" ON ("kitchens"."id" IS "kitchenSinks"."kitchenID")
217217
"""

Tests/StructuredQueriesTests/SelectTests.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,7 @@ extension SnapshotTests {
10491049
.select { ($0, $1.jsonGroupArray()) }
10501050
) {
10511051
"""
1052-
SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."id" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END)
1052+
SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."id" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FILTER (WHERE ("r2s"."id" IS NOT NULL))
10531053
FROM "reminders" AS "r1s"
10541054
LEFT JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id")
10551055
GROUP BY "r1s"."id"
@@ -1077,6 +1077,40 @@ extension SnapshotTests {
10771077
└─────────────────────────────────────────────┴─────────────────────────────────────────────────┘
10781078
"""
10791079
}
1080+
1081+
// force empty join
1082+
assertQuery(
1083+
Reminder.as(R1.self)
1084+
.group(by: \.id)
1085+
.leftJoin(Reminder.as(R2.self).all) { $0.id.eq($1.id) && $0.id.eq(42) }
1086+
.limit(1)
1087+
.select { ($0, $1.jsonGroupArray()) }
1088+
) {
1089+
"""
1090+
SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."id" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FILTER (WHERE ("r2s"."id" IS NOT NULL))
1091+
FROM "reminders" AS "r1s"
1092+
LEFT JOIN "reminders" AS "r2s" ON (("r1s"."id" = "r2s"."id") AND ("r1s"."id" = 42))
1093+
GROUP BY "r1s"."id"
1094+
LIMIT 1
1095+
"""
1096+
} results: {
1097+
"""
1098+
┌─────────────────────────────────────────────┬────┐
1099+
│ Reminder( │ [] │
1100+
│ id: 1, │ │
1101+
│ assignedUserID: 1, │ │
1102+
│ dueDate: Date(2001-01-01T00:00:00.000Z), │ │
1103+
│ isCompleted: false, │ │
1104+
│ isFlagged: false, │ │
1105+
│ notes: "Milk, Eggs, Apples", │ │
1106+
│ priority: nil, │ │
1107+
│ remindersListID: 1, │ │
1108+
│ title: "Groceries", │ │
1109+
│ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │
1110+
│ ) │ │
1111+
└─────────────────────────────────────────────┴────┘
1112+
"""
1113+
}
10801114
}
10811115

10821116
@Test func `case`() {

0 commit comments

Comments
 (0)