@@ -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+
213279extension TableDefinition where QueryValue: Codable {
214280 /// A JSON representation of a table's columns.
215281 ///
0 commit comments