Skip to content

Commit eb7bc1a

Browse files
committed
feat: complete lifted aggregate support with high-level APIs
Implements comprehensive aggregate function support including: - Lifted aggregates: sum(), avg(), min(), max() with optional filtering - High-level Table and Where APIs for intuitive aggregate queries - Closure-based filter parameters for conditional aggregation - HAVING clause support for post-aggregation filtering - Reduced duplication via generic aggregate helper infrastructure - Organized by function type (Count, Sum, Avg, Min, Max, ArrayAgg, JsonbAgg, StringAgg) Improves ergonomics with Self.all pattern for cleaner Table.count() semantics.
1 parent e96bd4e commit eb7bc1a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4723
-440
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
import StructuredQueriesCore
3+
4+
extension QueryExpression {
5+
/// PostgreSQL ARRAY_AGG function - aggregates values into an array
6+
///
7+
/// ```swift
8+
/// User.select { $0.id.arrayAgg() }
9+
/// // SELECT ARRAY_AGG("users"."id") FROM "users"
10+
///
11+
/// User.select { $0.id.arrayAgg(distinct: true) }
12+
/// // SELECT ARRAY_AGG(DISTINCT "users"."id") FROM "users"
13+
///
14+
/// User.select { $0.id.arrayAgg(filter: $0.isActive) }
15+
/// // SELECT ARRAY_AGG("users"."id") FILTER (WHERE "users"."is_active") FROM "users"
16+
/// ```
17+
///
18+
/// - Parameters:
19+
/// - isDistinct: Whether to include only distinct values
20+
/// - order: Optional ordering expression for the aggregated values
21+
/// - filter: A FILTER clause to apply to the aggregation
22+
/// - Returns: An array aggregate of this expression
23+
public func arrayAgg(
24+
distinct isDistinct: Bool = false,
25+
order: (any QueryExpression)? = nil,
26+
filter: (some QueryExpression<Bool>)? = Bool?.none
27+
) -> some QueryExpression<String?> {
28+
AggregateFunction<String?>(
29+
"array_agg",
30+
isDistinct: isDistinct,
31+
[queryFragment],
32+
order: order?.queryFragment,
33+
filter: filter?.queryFragment
34+
)
35+
}
36+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import StructuredQueriesCore
2+
3+
extension Table {
4+
/// Aggregates values into an array for the entire table.
5+
///
6+
/// ```swift
7+
/// User.arrayAgg { $0.name }
8+
/// // SELECT ARRAY_AGG("users"."name") FROM "users"
9+
/// ```
10+
///
11+
/// - Parameter expression: A closure that returns the column to aggregate into an array.
12+
/// - Returns: A select statement that returns an array of values.
13+
@inlinable
14+
public static func arrayAgg(
15+
of expression: (TableColumns) -> some QueryExpression
16+
) -> Select<String?, Self, ()> {
17+
_aggregateSelect(of: expression) { $0.arrayAgg() }
18+
}
19+
20+
/// Aggregates values into an array with a filter for the entire table.
21+
///
22+
/// ```swift
23+
/// User.arrayAgg(of: { $0.name }, filter: { $0.isActive })
24+
/// // SELECT ARRAY_AGG("users"."name") FILTER (WHERE "users"."is_active") FROM "users"
25+
/// ```
26+
///
27+
/// - Parameters:
28+
/// - expression: A closure that returns the column to aggregate into an array.
29+
/// - filter: A FILTER clause to apply to the aggregation.
30+
/// - Returns: A select statement that returns an array of values.
31+
@inlinable
32+
public static func arrayAgg<Expr: QueryExpression, Filter: QueryExpression<Bool>>(
33+
of expression: (TableColumns) -> Expr,
34+
filter: @escaping (TableColumns) -> Filter
35+
) -> Select<String?, Self, ()> {
36+
Self.all
37+
.asSelect()
38+
.select { _ in
39+
expression(columns).arrayAgg(filter: filter(columns))
40+
}
41+
}
42+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
import StructuredQueriesCore
3+
4+
extension TableColumn {
5+
/// PostgreSQL ARRAY_AGG function - aggregates values into an array
6+
///
7+
/// ```swift
8+
/// User.select { $0.id.arrayAgg() }
9+
/// // SELECT array_agg("users"."id") FROM "users"
10+
///
11+
/// User.select { $0.id.arrayAgg(distinct: true) }
12+
/// // SELECT array_agg(DISTINCT "users"."id") FROM "users"
13+
///
14+
/// User.select { $0.name.arrayAgg(order: $0.name.desc()) }
15+
/// // SELECT array_agg("users"."name" ORDER BY "users"."name" DESC) FROM "users"
16+
///
17+
/// User.select { $0.id.arrayAgg(filter: $0.isActive) }
18+
/// // SELECT array_agg("users"."id") FILTER (WHERE "users"."is_active") FROM "users"
19+
/// ```
20+
///
21+
/// - Parameters:
22+
/// - isDistinct: Whether to include only distinct values
23+
/// - order: Optional ordering expression for the aggregated values
24+
/// - filter: A FILTER clause to apply to the aggregation
25+
/// - Returns: An array aggregate of this expression
26+
public func arrayAgg(
27+
distinct isDistinct: Bool = false,
28+
order: (any QueryExpression)? = nil,
29+
filter: (some QueryExpression<Bool>)? = Bool?.none
30+
) -> some QueryExpression<String?> {
31+
AggregateFunction<String?>(
32+
"array_agg",
33+
isDistinct: isDistinct,
34+
[queryFragment],
35+
order: order?.queryFragment,
36+
filter: filter?.queryFragment
37+
)
38+
}
39+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import StructuredQueriesCore
2+
3+
extension Where {
4+
/// Aggregates values into an array for rows matching the WHERE clause.
5+
///
6+
/// ```swift
7+
/// User.where { $0.isActive }.arrayAgg { $0.name }
8+
/// // SELECT ARRAY_AGG("users"."name") FROM "users" WHERE "users"."is_active"
9+
/// ```
10+
///
11+
/// - Parameter expression: A closure that returns the column to aggregate into an array.
12+
/// - Returns: A select statement that returns an array of values.
13+
@inlinable
14+
public func arrayAgg(
15+
of expression: (From.TableColumns) -> some QueryExpression
16+
) -> Select<String?, From, ()> {
17+
_aggregateSelect(of: expression) { $0.arrayAgg() }
18+
}
19+
20+
/// Aggregates values into an array with a filter for rows matching the WHERE clause.
21+
///
22+
/// ```swift
23+
/// User.where { $0.createdAt > date }.arrayAgg(of: { $0.name }, filter: { $0.isActive })
24+
/// // SELECT ARRAY_AGG("users"."name") FILTER (WHERE "users"."is_active") FROM "users" WHERE ...
25+
/// ```
26+
///
27+
/// - Parameters:
28+
/// - expression: A closure that returns the column to aggregate into an array.
29+
/// - filter: A FILTER clause to apply to the aggregation.
30+
/// - Returns: A select statement that returns an array of values.
31+
@inlinable
32+
public func arrayAgg<Expr: QueryExpression, Filter: QueryExpression<Bool>>(
33+
of expression: (From.TableColumns) -> Expr,
34+
filter: @escaping (From.TableColumns) -> Filter
35+
) -> Select<String?, From, ()> {
36+
asSelect()
37+
.select { _ in
38+
expression(From.columns).arrayAgg(filter: filter(From.columns))
39+
}
40+
}
41+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import StructuredQueriesCore
2+
3+
extension QueryExpression
4+
where QueryValue: _OptionalPromotable, QueryValue._Optionalized.Wrapped: Numeric {
5+
/// An average aggregate of this expression.
6+
///
7+
/// ```swift
8+
/// Item.select { $0.price.avg() }
9+
/// // SELECT avg("items"."price") FROM "items"
10+
/// ```
11+
///
12+
/// - Parameters:
13+
/// - isDistinct: Whether or not to include a `DISTINCT` clause, which filters duplicates from
14+
/// the aggregation.
15+
/// - filter: A `FILTER` clause to apply to the aggregation.
16+
/// - Returns: An average aggregate of this expression.
17+
public func avg(
18+
distinct isDistinct: Bool = false,
19+
filter: (some QueryExpression<Bool>)? = Bool?.none
20+
) -> some QueryExpression<Double?> {
21+
AggregateFunction(
22+
"avg",
23+
isDistinct: isDistinct,
24+
[queryFragment],
25+
filter: filter?.queryFragment
26+
)
27+
}
28+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import StructuredQueriesCore
2+
3+
extension Select {
4+
/// Creates a new select statement from this one by appending an average aggregate to its selection.
5+
///
6+
/// ```swift
7+
/// Order.select().avg { $0.amount }
8+
/// // SELECT AVG("orders"."amount") FROM "orders"
9+
/// ```
10+
///
11+
/// - Parameter expression: A closure that takes table columns and returns an expression to average.
12+
/// - Returns: A new select statement that includes the average of the expression.
13+
public func avg<Value>(
14+
of expression: (From.TableColumns) -> some QueryExpression<Value>
15+
) -> Select<Double?, From, ()>
16+
where
17+
Columns == (), Joins == (),
18+
Value: _OptionalPromotable,
19+
Value._Optionalized.Wrapped: Numeric,
20+
Value._Optionalized.Wrapped: QueryRepresentable
21+
{
22+
let expr = expression(From.columns)
23+
return select { _ in expr.avg() }
24+
}
25+
26+
/// Creates a new select statement from this one by appending an average aggregate to its selection (with joins).
27+
///
28+
/// - Parameter expression: A closure that takes table columns and returns an expression to average.
29+
/// - Returns: A new select statement that includes the average of the expression.
30+
public func avg<Value, each J: Table>(
31+
of expression: (From.TableColumns, repeat (each J).TableColumns) -> some QueryExpression<Value>
32+
) -> Select<Double?, From, (repeat each J)>
33+
where
34+
Columns == (), Joins == (repeat each J),
35+
Value: _OptionalPromotable,
36+
Value._Optionalized.Wrapped: Numeric,
37+
Value._Optionalized.Wrapped: QueryRepresentable
38+
{
39+
let expr = expression(From.columns, repeat (each J).columns)
40+
return select { _ in expr.avg() }
41+
}
42+
43+
/// Creates a new select statement from this one by appending an average aggregate to its selection (with existing columns).
44+
///
45+
/// - Parameter expression: A closure that takes table columns and returns an expression to average.
46+
/// - Returns: A new select statement that includes the average of the expression.
47+
public func avg<Value, each C: QueryRepresentable, each J: Table>(
48+
of expression: (From.TableColumns, repeat (each J).TableColumns) -> some QueryExpression<Value>
49+
) -> Select<(repeat each C, Double?), From, (repeat each J)>
50+
where
51+
Columns == (repeat each C), Joins == (repeat each J),
52+
Value: _OptionalPromotable,
53+
Value._Optionalized.Wrapped: Numeric,
54+
Value._Optionalized.Wrapped: QueryRepresentable
55+
{
56+
let expr = expression(From.columns, repeat (each J).columns)
57+
return select { _ in expr.avg() }
58+
}
59+
60+
/// Creates a new select statement from this one by appending an average aggregate to its selection (with single join).
61+
///
62+
/// - Parameter expression: A closure that takes table columns and returns an expression to average.
63+
/// - Returns: A new select statement that includes the average of the expression.
64+
public func avg<Value>(
65+
of expression: (From.TableColumns, Joins.TableColumns) -> some QueryExpression<Value>
66+
) -> Select<Double?, From, Joins>
67+
where
68+
Columns == (), Joins: Table,
69+
Value: _OptionalPromotable,
70+
Value._Optionalized.Wrapped: Numeric,
71+
Value._Optionalized.Wrapped: QueryRepresentable
72+
{
73+
let expr = expression(From.columns, Joins.columns)
74+
return select { _, _ in expr.avg() }
75+
}
76+
77+
/// Creates a new select statement from this one by appending an average aggregate to its selection (with single join and existing columns).
78+
///
79+
/// - Parameter expression: A closure that takes table columns and returns an expression to average.
80+
/// - Returns: A new select statement that includes the average of the expression.
81+
public func avg<Value, each C: QueryRepresentable>(
82+
of expression: (From.TableColumns, Joins.TableColumns) -> some QueryExpression<Value>
83+
) -> Select<(repeat each C, Double?), From, Joins>
84+
where
85+
Columns == (repeat each C), Joins: Table,
86+
Value: _OptionalPromotable,
87+
Value._Optionalized.Wrapped: Numeric,
88+
Value._Optionalized.Wrapped: QueryRepresentable
89+
{
90+
let expr = expression(From.columns, Joins.columns)
91+
return select { _, _ in expr.avg() }
92+
}
93+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import StructuredQueriesCore
2+
3+
extension Table {
4+
/// A select statement for the average of an expression from this table.
5+
///
6+
/// ```swift
7+
/// Order.avg { $0.amount }
8+
/// // SELECT AVG("orders"."amount") FROM "orders"
9+
///
10+
/// // KeyPath syntax also works (Swift 5.2+):
11+
/// Order.avg(of: \.amount)
12+
/// ```
13+
///
14+
/// - Parameter expression: A closure that takes table columns and returns an expression to average.
15+
/// - Returns: A select statement that selects the average of the expression.
16+
@inlinable
17+
public static func avg<Value>(
18+
of expression: (TableColumns) -> some QueryExpression<Value>
19+
) -> Select<Double?, Self, ()>
20+
where Value: _OptionalPromotable, Value._Optionalized.Wrapped: Numeric {
21+
_aggregateSelect(of: expression) { $0.avg() }
22+
}
23+
24+
/// A select statement for the average of an expression from this table with a filter clause.
25+
///
26+
/// ```swift
27+
/// Order.avg(of: { $0.amount }, filter: { $0.isPaid })
28+
/// // SELECT AVG("orders"."amount") FILTER (WHERE "orders"."isPaid") FROM "orders"
29+
///
30+
/// // KeyPath syntax also works (Swift 5.2+):
31+
/// Order.avg(of: \.amount, filter: { $0.isPaid })
32+
/// ```
33+
///
34+
/// - Parameters:
35+
/// - expression: A closure that takes table columns and returns an expression to average.
36+
/// - filter: A `FILTER` clause to apply to the aggregation.
37+
/// - Returns: A select statement that selects the average of the expression.
38+
@inlinable
39+
public static func avg<Value, Filter: QueryExpression<Bool>>(
40+
of expression: (TableColumns) -> some QueryExpression<Value>,
41+
filter: @escaping (TableColumns) -> Filter
42+
) -> Select<Double?, Self, ()>
43+
where Value: _OptionalPromotable, Value._Optionalized.Wrapped: Numeric {
44+
_aggregateSelect(of: expression, filter: filter) { $0.avg(filter: $1) }
45+
}
46+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import StructuredQueriesCore
2+
3+
extension Where {
4+
/// Computes the average of a numeric column for rows matching the WHERE clause.
5+
///
6+
/// ```swift
7+
/// Order.where { $0.isPaid }.avg { $0.amount }
8+
/// // SELECT AVG("orders"."amount") FROM "orders" WHERE "orders"."isPaid"
9+
/// ```
10+
///
11+
/// - Parameters:
12+
/// - expression: A closure that returns the column to average.
13+
/// - filter: An optional additional filter clause (FILTER WHERE) to apply to the aggregation.
14+
/// - Returns: A select statement that returns the average as `Double?`.
15+
@inlinable
16+
public func avg<Value>(
17+
of expression: (From.TableColumns) -> some QueryExpression<Value>
18+
) -> Select<Double?, From, ()>
19+
where Value: _OptionalPromotable, Value._Optionalized.Wrapped: Numeric {
20+
_aggregateSelect(of: expression) { $0.avg() }
21+
}
22+
23+
/// Computes the average of a numeric column for rows matching the WHERE clause with a filter.
24+
///
25+
/// ```swift
26+
/// Order.where { $0.createdAt > date }.avg(of: { $0.amount }, filter: { $0.isPaid })
27+
/// // SELECT AVG("orders"."amount") FILTER (WHERE "orders"."isPaid") FROM "orders" WHERE ...
28+
/// ```
29+
///
30+
/// - Parameters:
31+
/// - expression: A closure that returns the column to average.
32+
/// - filter: A FILTER clause to apply to the aggregation.
33+
/// - Returns: A select statement that returns the average as `Double?`.
34+
@inlinable
35+
public func avg<Value, Filter: QueryExpression<Bool>>(
36+
of expression: (From.TableColumns) -> some QueryExpression<Value>,
37+
filter: @escaping (From.TableColumns) -> Filter
38+
) -> Select<Double?, From, ()>
39+
where Value: _OptionalPromotable, Value._Optionalized.Wrapped: Numeric {
40+
_aggregateSelect(of: expression, filter: filter) { $0.avg(filter: $1) }
41+
}
42+
}

0 commit comments

Comments
 (0)