Skip to main content

spark_connect/
window.rs

1//! Window functions and specifications mirroring PySpark's `pyspark.sql.window`.
2//!
3//! Provides the `Window` static builder and `WindowSpec` for defining window partitions,
4//! ordering, and frame boundaries used with `Column.over()`.
5
6use crate::expression::{Expression, SortOrder};
7
8/// Frame bound value used in window frame definitions.
9#[derive(Debug, Clone, PartialEq)]
10pub enum FrameBound {
11    /// UNBOUNDED PRECEDING
12    UnboundedPreceding,
13    /// Specific number of rows PRECEDING
14    Preceding(i64),
15    /// CURRENT ROW
16    CurrentRow,
17    /// Specific number of rows FOLLOWING
18    Following(i64),
19    /// UNBOUNDED FOLLOWING
20    UnboundedFollowing,
21}
22
23/// Frame type for window specifications.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FrameType {
26    /// ROWS frame type
27    Row,
28    /// RANGE frame type
29    Range,
30}
31
32/// `pyspark.sql.window.WindowSpec`
33///
34/// Represents a complete window specification with optional partition, order, and frame specs.
35/// This is built using the static builder methods on `Window` or by chaining methods on this struct.
36#[derive(Debug, Clone, PartialEq)]
37pub struct WindowSpec {
38    /// Expressions to partition by.
39    pub partition_spec: Vec<Expression>,
40    /// Sort orders for the window.
41    pub order_spec: Vec<SortOrder>,
42    /// Optional window frame specification.
43    pub frame_spec: Option<(FrameType, FrameBound, FrameBound)>,
44}
45
46impl WindowSpec {
47    /// Create a new empty WindowSpec.
48    pub fn new() -> Self {
49        Self {
50            partition_spec: Vec::new(),
51            order_spec: Vec::new(),
52            frame_spec: None,
53        }
54    }
55
56    /// Add partition columns to this window specification.
57    pub fn partition_by(mut self, cols: Vec<Expression>) -> Self {
58        self.partition_spec.extend(cols);
59        self
60    }
61
62    /// Set ordering for this window specification.
63    pub fn order_by(mut self, sorts: Vec<SortOrder>) -> Self {
64        self.order_spec.extend(sorts);
65        self
66    }
67
68    /// Set the frame specification for ROWS.
69    pub fn rows_between(mut self, start: FrameBound, end: FrameBound) -> Self {
70        self.frame_spec = Some((FrameType::Row, start, end));
71        self
72    }
73
74    /// Set the frame specification for RANGE.
75    pub fn range_between(mut self, start: FrameBound, end: FrameBound) -> Self {
76        self.frame_spec = Some((FrameType::Range, start, end));
77        self
78    }
79}
80
81impl Default for WindowSpec {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87/// `pyspark.sql.window.Window`
88///
89/// Static builder for creating window specifications.
90pub struct Window;
91
92impl Window {
93    /// UNBOUNDED PRECEDING frame bound.
94    pub const fn unbounded_preceding() -> FrameBound {
95        FrameBound::UnboundedPreceding
96    }
97
98    /// CURRENT ROW frame bound.
99    pub const fn current_row() -> FrameBound {
100        FrameBound::CurrentRow
101    }
102
103    /// UNBOUNDED FOLLOWING frame bound.
104    pub const fn unbounded_following() -> FrameBound {
105        FrameBound::UnboundedFollowing
106    }
107
108    /// Create a WindowSpec partitioned by the given columns.
109    pub fn partition_by(cols: Vec<Expression>) -> WindowSpec {
110        WindowSpec::new().partition_by(cols)
111    }
112
113    /// Create a WindowSpec ordered by the given sort orders.
114    pub fn order_by(sorts: Vec<SortOrder>) -> WindowSpec {
115        WindowSpec::new().order_by(sorts)
116    }
117
118    /// Create a WindowSpec with a ROWS frame between the given bounds.
119    pub fn rows_between(start: FrameBound, end: FrameBound) -> WindowSpec {
120        WindowSpec::new().rows_between(start, end)
121    }
122
123    /// Create a WindowSpec with a RANGE frame between the given bounds.
124    pub fn range_between(start: FrameBound, end: FrameBound) -> WindowSpec {
125        WindowSpec::new().range_between(start, end)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::expression::{ColumnReference, NullOrdering, SortOrder};
133
134    fn col(name: &str) -> Expression {
135        Expression::ColumnReference(ColumnReference::new(name))
136    }
137
138    fn sort(name: &str) -> SortOrder {
139        SortOrder {
140            child: col(name),
141            ascending: true,
142            null_ordering: NullOrdering::First,
143        }
144    }
145
146    #[test]
147    fn new_and_default_are_empty() {
148        let w = WindowSpec::new();
149        assert!(w.partition_spec.is_empty());
150        assert!(w.order_spec.is_empty());
151        assert!(w.frame_spec.is_none());
152        assert_eq!(WindowSpec::default(), w);
153    }
154
155    #[test]
156    fn spec_builders_chain() {
157        let w = WindowSpec::new()
158            .partition_by(vec![col("a"), col("b")])
159            .order_by(vec![sort("c")]);
160        assert_eq!(w.partition_spec.len(), 2);
161        assert_eq!(w.order_spec.len(), 1);
162    }
163
164    #[test]
165    fn rows_between_sets_row_frame() {
166        let w = WindowSpec::new().rows_between(FrameBound::Preceding(1), FrameBound::Following(2));
167        assert_eq!(
168            w.frame_spec,
169            Some((
170                FrameType::Row,
171                FrameBound::Preceding(1),
172                FrameBound::Following(2)
173            ))
174        );
175    }
176
177    #[test]
178    fn range_between_sets_range_frame() {
179        let w =
180            WindowSpec::new().range_between(FrameBound::UnboundedPreceding, FrameBound::CurrentRow);
181        assert_eq!(
182            w.frame_spec,
183            Some((
184                FrameType::Range,
185                FrameBound::UnboundedPreceding,
186                FrameBound::CurrentRow
187            ))
188        );
189    }
190
191    #[test]
192    fn a_later_frame_replaces_an_earlier_one() {
193        let w = WindowSpec::new()
194            .rows_between(FrameBound::UnboundedPreceding, FrameBound::CurrentRow)
195            .range_between(FrameBound::CurrentRow, FrameBound::UnboundedFollowing);
196        assert_eq!(
197            w.frame_spec,
198            Some((
199                FrameType::Range,
200                FrameBound::CurrentRow,
201                FrameBound::UnboundedFollowing
202            ))
203        );
204    }
205
206    #[test]
207    fn window_static_constructors_match_spec_builders() {
208        assert_eq!(
209            Window::partition_by(vec![col("a")]),
210            WindowSpec::new().partition_by(vec![col("a")])
211        );
212        assert_eq!(
213            Window::order_by(vec![sort("a")]),
214            WindowSpec::new().order_by(vec![sort("a")])
215        );
216        assert_eq!(
217            Window::rows_between(FrameBound::Preceding(1), FrameBound::CurrentRow),
218            WindowSpec::new().rows_between(FrameBound::Preceding(1), FrameBound::CurrentRow)
219        );
220        assert_eq!(
221            Window::range_between(FrameBound::CurrentRow, FrameBound::Following(3)),
222            WindowSpec::new().range_between(FrameBound::CurrentRow, FrameBound::Following(3))
223        );
224    }
225
226    #[test]
227    fn frame_bound_constants() {
228        assert_eq!(
229            Window::unbounded_preceding(),
230            FrameBound::UnboundedPreceding
231        );
232        assert_eq!(Window::current_row(), FrameBound::CurrentRow);
233        assert_eq!(
234            Window::unbounded_following(),
235            FrameBound::UnboundedFollowing
236        );
237    }
238}