Skip to content

Commit f669487

Browse files
committed
refactor: organize Window Functions under Window namespace pattern
- Split monolithic WindowFunctions.swift (410 lines) into focused files: - Window.swift: Namespace declaration with documentation - Window.Function.swift: Window.Function<Value> builder type - Window.Base.swift: Internal Window.Base<Value> implementation - Window.Named.swift: Internal Window.Named<Value> for named windows - Window+Ranking.swift: Global ranking functions (rowNumber, rank, etc.) - Window+ValueAccess.swift: QueryExpression extensions (lag, lead, etc.) - Maintain ergonomic global functions (no naming conflicts): - rowNumber(), rank(), denseRank(), percentRank(), cumeDist(), ntile() - Extensions: .lag(), .lead(), .firstValue(), .lastValue(), .nthValue() - Add Window namespace static methods for explicit usage Pattern: Domain namespace (noun) with ergonomic global constructors
1 parent 5309599 commit f669487

File tree

7 files changed

+529
-409
lines changed

7 files changed

+529
-409
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import Foundation
2+
import StructuredQueriesCore
3+
4+
// MARK: - Ranking Window Functions
5+
6+
/// PostgreSQL `ROW_NUMBER()` window function
7+
///
8+
/// Assigns a unique sequential number to each row within the window partition.
9+
///
10+
/// ```swift
11+
/// User.select {
12+
/// let createdAt = $0.createdAt
13+
/// return ($0, rowNumber().over { $0.order(by: createdAt) })
14+
/// }
15+
/// // SELECT *, ROW_NUMBER() OVER (ORDER BY "created_at")
16+
/// ```
17+
///
18+
/// - Returns: An integer expression with the row number
19+
public func rowNumber() -> Window.Function<Int> {
20+
Window.Function(functionName: "ROW_NUMBER", arguments: [])
21+
}
22+
23+
/// PostgreSQL `RANK()` window function
24+
///
25+
/// Assigns a rank to each row within the partition, with gaps for tied values.
26+
/// Tied rows receive the same rank, and the next rank skips numbers.
27+
///
28+
/// ```swift
29+
/// // Leaderboard with gaps for ties
30+
/// Score.select {
31+
/// let points = $0.points
32+
/// return ($0, rank().over { $0.order(by: points.desc()) })
33+
/// }
34+
/// // Ranks: 1, 2, 2, 4, 5 (gap at 3)
35+
/// ```
36+
///
37+
/// - Returns: A rank expression (bigint)
38+
public func rank() -> Window.Function<Int> {
39+
Window.Function(functionName: "RANK", arguments: [])
40+
}
41+
42+
/// PostgreSQL `DENSE_RANK()` window function
43+
///
44+
/// Assigns a rank to each row within the partition, without gaps for tied values.
45+
/// Tied rows receive the same rank, and the next rank continues sequentially.
46+
///
47+
/// ```swift
48+
/// // Leaderboard without gaps
49+
/// Score.select {
50+
/// let points = $0.points
51+
/// return ($0, denseRank().over { $0.order(by: points.desc()) })
52+
/// }
53+
/// // Ranks: 1, 2, 2, 3, 4 (no gap)
54+
/// ```
55+
///
56+
/// - Returns: A dense rank expression (bigint)
57+
public func denseRank() -> Window.Function<Int> {
58+
Window.Function(functionName: "DENSE_RANK", arguments: [])
59+
}
60+
61+
/// PostgreSQL `PERCENT_RANK()` window function
62+
///
63+
/// Calculates the relative rank of the current row: `(rank - 1) / (total rows - 1)`.
64+
/// Returns a value between 0 and 1.
65+
///
66+
/// ```swift
67+
/// Score.select {
68+
/// let points = $0.points
69+
/// return ($0, percentRank().over { $0.order(by: points.desc()) })
70+
/// }
71+
/// // Returns: 0.0, 0.25, 0.5, 0.75, 1.0
72+
/// ```
73+
///
74+
/// - Returns: A double precision expression
75+
public func percentRank() -> Window.Function<Double> {
76+
Window.Function(functionName: "PERCENT_RANK", arguments: [])
77+
}
78+
79+
/// PostgreSQL `CUME_DIST()` window function
80+
///
81+
/// Calculates the cumulative distribution: (number of partition rows ≤ current row) / (total partition rows).
82+
/// Returns a value between 0 and 1.
83+
///
84+
/// ```swift
85+
/// Score.select {
86+
/// let points = $0.points
87+
/// return ($0, cumeDist().over { $0.order(by: points.desc()) })
88+
/// }
89+
/// ```
90+
///
91+
/// - Returns: A double precision expression
92+
public func cumeDist() -> Window.Function<Double> {
93+
Window.Function(functionName: "CUME_DIST", arguments: [])
94+
}
95+
96+
/// PostgreSQL `NTILE(n)` window function
97+
///
98+
/// Divides the partition into `n` buckets and assigns each row a bucket number (1 to n).
99+
/// Useful for creating percentiles or quartiles.
100+
///
101+
/// ```swift
102+
/// // Divide into quartiles
103+
/// User.select {
104+
/// let age = $0.age
105+
/// return ($0, ntile(4).over { $0.order(by: age) })
106+
/// }
107+
/// // Returns: 1, 1, 2, 2, 3, 3, 4, 4
108+
/// ```
109+
///
110+
/// - Parameter buckets: Number of buckets (must be positive)
111+
/// - Returns: An integer expression (1 to n)
112+
public func ntile(_ buckets: Int) -> Window.Function<Int> {
113+
precondition(buckets > 0, "ntile buckets must be positive")
114+
return Window.Function(
115+
functionName: "NTILE",
116+
arguments: [QueryFragment(stringLiteral: "\(buckets)")]
117+
)
118+
}
119+
120+
// MARK: - Namespace Convenience
121+
122+
extension Window {
123+
/// PostgreSQL `ROW_NUMBER()` window function
124+
///
125+
/// Assigns a unique sequential number to each row within the window partition.
126+
///
127+
/// - Returns: An integer expression with the row number
128+
/// - SeeAlso: `rowNumber()` global function
129+
public static func rowNumber() -> Function<Int> {
130+
StructuredQueriesPostgres.rowNumber()
131+
}
132+
133+
/// PostgreSQL `RANK()` window function
134+
///
135+
/// Assigns a rank to each row within the partition, with gaps for tied values.
136+
///
137+
/// - Returns: A rank expression (bigint)
138+
/// - SeeAlso: `rank()` global function
139+
public static func rank() -> Function<Int> {
140+
StructuredQueriesPostgres.rank()
141+
}
142+
143+
/// PostgreSQL `DENSE_RANK()` window function
144+
///
145+
/// Assigns a rank to each row within the partition, without gaps for tied values.
146+
///
147+
/// - Returns: A dense rank expression (bigint)
148+
/// - SeeAlso: `denseRank()` global function
149+
public static func denseRank() -> Function<Int> {
150+
StructuredQueriesPostgres.denseRank()
151+
}
152+
153+
/// PostgreSQL `PERCENT_RANK()` window function
154+
///
155+
/// Calculates the relative rank of the current row: `(rank - 1) / (total rows - 1)`.
156+
///
157+
/// - Returns: A double precision expression
158+
/// - SeeAlso: `percentRank()` global function
159+
public static func percentRank() -> Function<Double> {
160+
StructuredQueriesPostgres.percentRank()
161+
}
162+
163+
/// PostgreSQL `CUME_DIST()` window function
164+
///
165+
/// Calculates the cumulative distribution.
166+
///
167+
/// - Returns: A double precision expression
168+
/// - SeeAlso: `cumeDist()` global function
169+
public static func cumeDist() -> Function<Double> {
170+
StructuredQueriesPostgres.cumeDist()
171+
}
172+
173+
/// PostgreSQL `NTILE(n)` window function
174+
///
175+
/// Divides the partition into `n` buckets.
176+
///
177+
/// - Parameter buckets: Number of buckets (must be positive)
178+
/// - Returns: An integer expression (1 to n)
179+
/// - SeeAlso: `ntile(_:)` global function
180+
public static func ntile(_ buckets: Int) -> Function<Int> {
181+
StructuredQueriesPostgres.ntile(buckets)
182+
}
183+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Foundation
2+
import StructuredQueriesCore
3+
4+
// MARK: - Value Access Window Functions
5+
6+
extension QueryExpression {
7+
/// PostgreSQL `LAG()` window function
8+
///
9+
/// Accesses the value from a row that is `offset` rows before the current row.
10+
/// Returns the default value if the offset points to a row outside the partition.
11+
///
12+
/// ```swift
13+
/// // Compare with previous day's price
14+
/// StockPrice.select {
15+
/// let price = $0.price
16+
/// let date = $0.date
17+
/// return ($0, price.lag(offset: 1, default: 0).over { $0.order(by: date) })
18+
/// }
19+
/// ```
20+
///
21+
/// - Parameters:
22+
/// - offset: Number of rows to look back (default: 1)
23+
/// - default: Default value when offset goes out of bounds
24+
/// - Returns: The lagged value expression
25+
public func lag(
26+
offset: Int = 1,
27+
default defaultValue: QueryValue? = nil
28+
) -> Window.Function<QueryValue?> where QueryValue: QueryBindable {
29+
var args: [QueryFragment] = [self.queryFragment, QueryFragment(stringLiteral: "\(offset)")]
30+
if let defaultValue {
31+
args.append("\(bind: defaultValue)")
32+
}
33+
return Window.Function(functionName: "LAG", arguments: args)
34+
}
35+
36+
/// PostgreSQL `LEAD()` window function
37+
///
38+
/// Accesses the value from a row that is `offset` rows after the current row.
39+
/// Returns the default value if the offset points to a row outside the partition.
40+
///
41+
/// ```swift
42+
/// // Compare with next day's price
43+
/// StockPrice.select {
44+
/// let price = $0.price
45+
/// let date = $0.date
46+
/// return ($0, price.lead(offset: 1, default: 0).over { $0.order(by: date) })
47+
/// }
48+
/// ```
49+
///
50+
/// - Parameters:
51+
/// - offset: Number of rows to look ahead (default: 1)
52+
/// - default: Default value when offset goes out of bounds
53+
/// - Returns: The lead value expression
54+
public func lead(
55+
offset: Int = 1,
56+
default defaultValue: QueryValue? = nil
57+
) -> Window.Function<QueryValue?> where QueryValue: QueryBindable {
58+
var args: [QueryFragment] = [self.queryFragment, QueryFragment(stringLiteral: "\(offset)")]
59+
if let defaultValue {
60+
args.append("\(bind: defaultValue)")
61+
}
62+
return Window.Function(functionName: "LEAD", arguments: args)
63+
}
64+
65+
/// PostgreSQL `FIRST_VALUE()` window function
66+
///
67+
/// Returns the value from the first row of the window frame.
68+
///
69+
/// ```swift
70+
/// // Show highest price in each category
71+
/// Product.select {
72+
/// let category = $0.category
73+
/// let price = $0.price
74+
/// return ($0, price.firstValue().over {
75+
/// $0.partition(by: category)
76+
/// .order(by: price.desc())
77+
/// })
78+
/// }
79+
/// ```
80+
///
81+
/// - Returns: The first value in the frame
82+
public func firstValue() -> Window.Function<QueryValue> where QueryValue: QueryBindable {
83+
Window.Function(functionName: "FIRST_VALUE", arguments: [self.queryFragment])
84+
}
85+
86+
/// PostgreSQL `LAST_VALUE()` window function
87+
///
88+
/// Returns the value from the last row of the window frame.
89+
///
90+
/// **Note:** Default frame is `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`,
91+
/// so you often need to specify a frame to get the actual last value in the partition.
92+
///
93+
/// ```swift
94+
/// // Get last value in partition
95+
/// Product.select {
96+
/// let category = $0.category
97+
/// let price = $0.price
98+
/// return ($0, price.lastValue().over {
99+
/// $0.partition(by: category)
100+
/// .order(by: price.desc())
101+
/// })
102+
/// }
103+
/// ```
104+
///
105+
/// - Returns: The last value in the frame
106+
public func lastValue() -> Window.Function<QueryValue> where QueryValue: QueryBindable {
107+
Window.Function(functionName: "LAST_VALUE", arguments: [self.queryFragment])
108+
}
109+
110+
/// PostgreSQL `NTH_VALUE()` window function
111+
///
112+
/// Returns the value from the nth row of the window frame (1-indexed).
113+
///
114+
/// ```swift
115+
/// // Get second-highest price in each category
116+
/// Product.select {
117+
/// let category = $0.category
118+
/// let price = $0.price
119+
/// return ($0, price.nthValue(2).over {
120+
/// $0.partition(by: category)
121+
/// .order(by: price.desc())
122+
/// })
123+
/// }
124+
/// ```
125+
///
126+
/// - Parameter n: Row number (1-indexed, must be positive)
127+
/// - Returns: The nth value in the frame
128+
public func nthValue(_ n: Int) -> Window.Function<QueryValue?>
129+
where QueryValue: QueryBindable {
130+
precondition(n > 0, "nth value position must be positive (1-indexed)")
131+
return Window.Function(
132+
functionName: "NTH_VALUE",
133+
arguments: [self.queryFragment, QueryFragment(stringLiteral: "\(n)")]
134+
)
135+
}
136+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import StructuredQueriesCore
3+
4+
// MARK: - Window Function Base Implementation
5+
6+
extension Window {
7+
/// Base implementation for all window functions
8+
///
9+
/// This internal type generates the actual SQL for window function calls with OVER clauses.
10+
struct Base<Value: QueryBindable>: QueryExpression {
11+
typealias QueryValue = Value
12+
13+
let functionName: String
14+
let arguments: [QueryFragment]
15+
let windowSpec: WindowSpec?
16+
17+
var queryFragment: QueryFragment {
18+
var fragment: QueryFragment = "\(raw: functionName)("
19+
if !arguments.isEmpty {
20+
fragment.append(arguments.joined(separator: ", "))
21+
}
22+
fragment.append(")")
23+
24+
if let windowSpec {
25+
fragment.append(" ")
26+
fragment.append(windowSpec.generateOverClause())
27+
} else {
28+
fragment.append(" OVER ()")
29+
}
30+
31+
return fragment
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)