Skip to content

Commit 74dc0db

Browse files
committed
Add optional jsonGroupArray overload
1 parent bff00b5 commit 74dc0db

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

Sources/StructuredQueriesSQLiteCore/JSONFunctions.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,72 @@ where
210210
}
211211
}
212212

213+
extension TableColumnExpression
214+
where
215+
Root: PrimaryKeyedTable & _OptionalProtocol,
216+
QueryValue: _OptionalProtocol & Codable & QueryBindable,
217+
QueryValue.Wrapped: Codable
218+
{
219+
/// A JSON array aggregate of an optional column's wrapped value.
220+
///
221+
/// When aggregating from a joined table that may be `NULL` (for instance, a `LEFT JOIN` that
222+
/// yields no matching rows), this overload automatically filters out the rows where the joined
223+
/// table's primary key is `NULL`, ensuring the resulting JSON array only contains fully realized
224+
/// rows.
225+
///
226+
/// ```swift
227+
/// RemindersList
228+
/// .group(by: \.id)
229+
/// .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) }
230+
/// .select { list, reminder in
231+
/// (
232+
/// list.title,
233+
/// reminder.title.jsonGroupArray()
234+
/// )
235+
/// }
236+
/// ```
237+
///
238+
/// - Parameters:
239+
/// - isDistinct: A boolean that enables the `DISTINCT` clause on the aggregation.
240+
/// - order: An `ORDER BY` clause to apply to the aggregation.
241+
/// - filter: A `FILTER` clause to apply to the aggregation.
242+
/// - Returns: A JSON array aggregate of this column's wrapped value.
243+
public func jsonGroupArray(
244+
distinct isDistinct: Bool = false,
245+
order: (some QueryExpression)? = Bool?.none,
246+
filter: (some QueryExpression<Bool>)? = Bool?.none
247+
) -> some QueryExpression<[QueryValue.Wrapped].JSONRepresentation> {
248+
let primaryKeyNames = Root.columns.primaryKey._names
249+
let primaryKeyColumns: QueryFragment = primaryKeyNames
250+
.map { "\(Root.self).\(quote: $0)" }
251+
.joined(separator: ", ")
252+
let primaryKeyNulls: QueryFragment = Array(
253+
repeating: QueryFragment("NULL"),
254+
count: primaryKeyNames.count
255+
)
256+
.joined(separator: ", ")
257+
let primaryKeyFilter = SQLQueryExpression(
258+
"(\(primaryKeyColumns)) IS NOT (\(primaryKeyNulls))",
259+
as: Bool.self
260+
261+
)
262+
let combinedFilter: SQLQueryExpression<Bool>
263+
if let filter {
264+
combinedFilter = SQLQueryExpression(primaryKeyFilter.and(filter))
265+
} else {
266+
combinedFilter = primaryKeyFilter
267+
}
268+
269+
return AggregateFunctionExpression<[QueryValue.Wrapped].JSONRepresentation>(
270+
"json_group_array",
271+
distinct: isDistinct,
272+
self,
273+
order: order,
274+
filter: combinedFilter
275+
)
276+
}
277+
}
278+
213279
extension TableDefinition where QueryValue: Codable {
214280
/// A JSON representation of a table's columns.
215281
///

Tests/StructuredQueriesTests/JSONFunctionsTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,38 @@ extension SnapshotTests {
6464
}
6565
}
6666

67+
@Test func jsonGroupArrayOptionalColumn() {
68+
assertQuery(
69+
RemindersList
70+
.group(by: \.id)
71+
.leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) }
72+
.select { list, reminder in
73+
(list.title, reminder.title.jsonGroupArray())
74+
}
75+
.limit(1)
76+
) {
77+
"""
78+
SELECT "remindersLists"."title", "json_group_array"("reminders"."title") FILTER (WHERE ("reminders"."id") IS NOT (NULL))
79+
FROM "remindersLists"
80+
LEFT JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID")
81+
GROUP BY "remindersLists"."id"
82+
LIMIT 1
83+
"""
84+
} results: {
85+
"""
86+
┌────────────┬──────────────────────────────┐
87+
"Personal" │ [ │
88+
│ │ [0]: "Groceries", │
89+
│ │ [1]: "Haircut", │
90+
│ │ [2]: "Doctor appointment", │
91+
│ │ [3]: "Take a walk", │
92+
│ │ [4]: "Buy concert tickets"
93+
│ │ ] │
94+
└────────────┴──────────────────────────────┘
95+
"""
96+
}
97+
}
98+
6799
@Test func jsonArrayLength() {
68100
assertQuery(
69101
Reminder.select {

0 commit comments

Comments
 (0)