Skip to main content

keelson_core/clause/
window.rs

1use std::borrow::Cow;
2
3use crate::expr::{Expr, IntoExprList};
4use crate::writer::{Expression, SqlWriter};
5
6use super::frame::{Frame, HasFrame};
7use super::order_by::{HasOrderBy, OrderBy};
8use super::{MaybeAbsent, write_present};
9
10/// A window definition: what goes inside `OVER (…)`, or after `WINDOW name AS`.
11///
12/// From PostgreSQL 17,
13/// <https://www.postgresql.org/docs/17/sql-select.html#SQL-WINDOW>:
14///
15/// ```text
16/// [ existing_window_name ] [ PARTITION BY expression [, ...] ]
17/// [ ORDER BY expression [ASC | DESC | USING operator] [NULLS {FIRST|LAST}] [, ...] ]
18/// [ frame_clause ]
19/// ```
20///
21/// Every part is optional, including all of them at once: `OVER ()` and
22/// `WINDOW w AS ()` are both legal and mean "the whole partition". So an empty
23/// `Window` renders the empty string and the parentheses come from whatever
24/// contains it.
25///
26/// Because it holds an [`OrderBy`] and a [`Frame`] and implements [`HasOrderBy`]
27/// and [`HasFrame`], the ordinary order-by and frame mods apply to a window
28/// unchanged.
29#[derive(Debug, Clone, Default)]
30pub struct Window {
31    /// A window this one extends, copying its `PARTITION BY` and — unless this one
32    /// has its own — its `ORDER BY`. Quoted on output.
33    pub based_on: Option<Cow<'static, str>>,
34    /// `PARTITION BY`.
35    pub partition_by: Vec<Expr>,
36    /// `ORDER BY`, which is what gives `RANGE` frames and `rank()` a meaning.
37    pub order_by: OrderBy,
38    /// The frame.
39    pub frame: Frame,
40}
41
42impl Window {
43    /// A window extending an existing named one.
44    pub fn based_on(name: impl Into<Cow<'static, str>>) -> Self {
45        Window {
46            based_on: Some(name.into()),
47            ..Window::default()
48        }
49    }
50
51    /// Append partition expressions.
52    pub fn add_partition_by(&mut self, exprs: impl IntoExprList) {
53        self.partition_by.extend(exprs.into_expr_list());
54    }
55
56    /// Whether nothing was set, so that `OVER ()` is what will be written.
57    pub fn is_empty(&self) -> bool {
58        self.based_on.is_none()
59            && self.partition_by.is_empty()
60            && self.order_by.is_empty()
61            && self.frame.is_empty()
62    }
63}
64
65impl Expression for Window {
66    fn write_sql(&self, w: &mut SqlWriter<'_>) {
67        // Each part writes its own separator only when something precedes it, so
68        // there is never a leading or doubled space. bob pads unconditionally and
69        // emits `PARTITION BY a  ORDER BY b`.
70        let mut written = false;
71
72        if let Some(based_on) = &self.based_on {
73            w.push_quoted(&[based_on]);
74            written = true;
75        }
76
77        if !self.partition_by.is_empty() {
78            if written {
79                w.push_str(" ");
80            }
81            w.write_slice(&self.partition_by, "PARTITION BY ", ", ", "");
82            written = true;
83        }
84
85        if !self.order_by.is_empty() {
86            if written {
87                w.push_str(" ");
88            }
89            w.write_expr(&self.order_by);
90            written = true;
91        }
92
93        if !self.frame.is_empty() {
94            if written {
95                w.push_str(" ");
96            }
97            w.write_expr(&self.frame);
98        }
99    }
100}
101
102impl HasOrderBy for Window {
103    fn order_by_mut(&mut self) -> &mut OrderBy {
104        &mut self.order_by
105    }
106}
107
108impl HasFrame for Window {
109    fn frame_mut(&mut self) -> &mut Frame {
110        &mut self.frame
111    }
112}
113
114/// Anything with a window definition: a [`NamedWindow`], or a dialect's function
115/// builder holding the window of its `OVER`.
116pub trait HasWindow {
117    /// The window definition to modify.
118    fn window_mut(&mut self) -> &mut Window;
119}
120
121impl HasWindow for Window {
122    fn window_mut(&mut self) -> &mut Window {
123        self
124    }
125}
126
127/// `name AS (<definition>)` — one entry of a statement's `WINDOW` clause.
128#[derive(Debug, Clone, Default)]
129pub struct NamedWindow {
130    /// The name, quoted on output.
131    pub name: Cow<'static, str>,
132    /// What the name means.
133    pub definition: Window,
134}
135
136impl NamedWindow {
137    /// Name a window definition.
138    pub fn new(name: impl Into<Cow<'static, str>>, definition: Window) -> Self {
139        NamedWindow {
140            name: name.into(),
141            definition,
142        }
143    }
144
145    /// Whether there is no name, so that nothing will be written.
146    pub fn is_empty(&self) -> bool {
147        self.name.is_empty()
148    }
149}
150
151impl Expression for NamedWindow {
152    fn write_sql(&self, w: &mut SqlWriter<'_>) {
153        if self.name.is_empty() {
154            // An unnamed entry cannot be referred to, so it is not an entry.
155            return;
156        }
157        w.push_quoted(&[&self.name]);
158        w.push_str(" AS (");
159        w.write_expr(&self.definition);
160        w.push_str(")");
161    }
162}
163
164impl HasWindow for NamedWindow {
165    fn window_mut(&mut self) -> &mut Window {
166        &mut self.definition
167    }
168}
169
170/// `WINDOW w AS (…), v AS (…)`
171#[derive(Debug, Clone, Default)]
172pub struct Windows {
173    /// The named windows, in order. A later one may be
174    /// [`based_on`](Window::based_on) an earlier one.
175    pub windows: Vec<NamedWindow>,
176}
177
178impl Windows {
179    /// Append a named window.
180    pub fn append_window(&mut self, window: NamedWindow) {
181        self.windows.push(window);
182    }
183
184    /// Whether the clause is absent.
185    pub fn is_empty(&self) -> bool {
186        self.windows.is_empty()
187    }
188}
189
190impl Expression for Windows {
191    fn write_sql(&self, w: &mut SqlWriter<'_>) {
192        write_present(w, &self.windows, "WINDOW ", ", ", "");
193    }
194}
195
196/// A statement with a `WINDOW` clause.
197pub trait HasWindows {
198    /// The `WINDOW` clause to modify.
199    fn windows_mut(&mut self) -> &mut Windows;
200}
201
202impl HasWindows for Windows {
203    fn windows_mut(&mut self) -> &mut Windows {
204        self
205    }
206}
207
208impl MaybeAbsent for NamedWindow {
209    fn is_absent(&self) -> bool {
210        self.is_empty()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use keelson_sqlcheck::testing::assert_frag_sql;
217
218    use super::*;
219    use crate::clause::frame::{FrameExclusion, FrameMode};
220    use crate::clause::order_by::{OrderDef, OrderDirection};
221    use crate::dialect::testing::Numbered;
222    use crate::expr::{arg, quote};
223    use crate::value::Value;
224    use crate::writer::build;
225
226    /// A window definition is what goes inside an `OVER (…)`; a [`Windows`] is the
227    /// `WINDOW` clause that names them. Hence two frames.
228    const DEF_FRAME: &str = r#"SELECT count(*) OVER ({}) FROM users"#;
229    /// `OVER "w"` and not `OVER ("w")`: the parenthesised form *copies* the named
230    /// window, and PostgreSQL refuses to copy one that carries a frame clause
231    /// ("Omit the parentheses in this OVER clause"). Referring to it by name is
232    /// what works whatever the definition holds.
233    const CLAUSE_FRAME: &str = r#"SELECT count(*) OVER "w" FROM users {}"#;
234
235    fn sql(e: &impl Expression) -> String {
236        build(&Numbered, e).expect("render").0
237    }
238
239    #[test]
240    fn an_empty_window_writes_nothing_which_is_what_over_wants() {
241        // `OVER ()` is the whole partition, so an empty definition is legal SQL
242        // rather than an omission.
243        assert_frag_sql(DEF_FRAME, &sql(&Window::default()), "");
244        assert!(Window::default().is_empty());
245        assert_frag_sql(
246            r#"SELECT count(*) FROM users {}"#,
247            &sql(&Windows::default()),
248            "",
249        );
250    }
251
252    #[test]
253    fn a_window_based_on_a_name_is_just_that_name() {
254        // PostgreSQL 17: `[ existing_window_name ]` is the first thing in a window
255        // definition, and it may be the only thing.
256        assert_frag_sql(
257            r#"SELECT count(*) OVER ({}) FROM users WINDOW "w" AS ()"#,
258            &sql(&Window::based_on("w")),
259            r#""w""#,
260        );
261    }
262
263    #[test]
264    fn the_parts_render_in_grammar_order_with_single_spaces() {
265        let mut win = Window::based_on("w");
266        win.add_partition_by((quote("age"), quote("is_active")));
267        win.order_by_mut()
268            .append_order(Expr::custom(OrderDef::new(quote("created_at"))));
269        win.frame_mut().set_mode(FrameMode::Rows);
270        win.frame_mut().set_end("CURRENT ROW");
271
272        // Not framed. PostgreSQL's analyser refuses `PARTITION BY` on a definition
273        // that copies another window ("cannot override PARTITION BY clause of
274        // window"), so no frame makes this engine-checkable — while the ordering it
275        // pins, from PostgreSQL 17's window_definition, is
276        //   [ existing_window_name ] [ PARTITION BY … ] [ ORDER BY … ] [ frame ]
277        // and is what a query type has to get right before any of the legal
278        // combinations can be.
279        assert_eq!(
280            build(&Numbered, &win).unwrap().0,
281            r#""w" PARTITION BY "age", "is_active" ORDER BY "created_at" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"#
282        );
283    }
284
285    #[test]
286    fn each_part_can_appear_alone_without_stray_spaces() {
287        let mut partition_only = Window::default();
288        partition_only.add_partition_by(quote("age"));
289        assert_frag_sql(DEF_FRAME, &sql(&partition_only), r#"PARTITION BY "age""#);
290
291        let mut order_only = Window::default();
292        order_only.order_by_mut().append_order(quote("age"));
293        assert_frag_sql(DEF_FRAME, &sql(&order_only), r#"ORDER BY "age""#);
294
295        let mut frame_only = Window::default();
296        frame_only.frame_mut().set_exclusion(FrameExclusion::Group);
297        assert_frag_sql(
298            DEF_FRAME,
299            &sql(&frame_only),
300            "RANGE UNBOUNDED PRECEDING EXCLUDE GROUP",
301        );
302    }
303
304    #[test]
305    fn a_partition_expression_may_bind_an_argument() {
306        let mut win = Window::default();
307        win.add_partition_by(Expr::func("coalesce", (quote("age"), arg(0i32))));
308        let (rendered, args) = build(&Numbered, &win).unwrap();
309        assert_frag_sql(DEF_FRAME, &rendered, r#"PARTITION BY coalesce("age", $1)"#);
310        assert_eq!(args, vec![Value::I32(0)]);
311    }
312
313    #[test]
314    fn named_windows_are_comma_separated_under_one_keyword() {
315        let mut w1 = Window::default();
316        w1.add_partition_by(quote("is_active"));
317        w1.order_by_mut().append_order(Expr::custom(OrderDef {
318            direction: Some(OrderDirection::Desc),
319            ..OrderDef::new(quote("age"))
320        }));
321
322        let mut ws = Windows::default();
323        ws.append_window(NamedWindow::new("w", w1));
324        // The empty definition is legal: `v AS ()` is the whole partition.
325        ws.append_window(NamedWindow::new("v", Window::default()));
326
327        assert_frag_sql(
328            CLAUSE_FRAME,
329            &sql(&ws),
330            r#"WINDOW "w" AS (PARTITION BY "is_active" ORDER BY "age" DESC), "v" AS ()"#,
331        );
332    }
333
334    #[test]
335    fn an_unnamed_entry_takes_the_keyword_and_its_comma_with_it() {
336        // A `WINDOW` with nothing after it, or a dangling comma, is a syntax error
337        // rather than untidiness, so an absent entry is skipped separator and all.
338        let mut ws = Windows::default();
339        ws.append_window(NamedWindow::default());
340        assert!(NamedWindow::default().is_empty());
341        assert_frag_sql(r#"SELECT count(*) FROM users {}"#, &sql(&ws), "");
342
343        ws.append_window(NamedWindow::new("w", Window::default()));
344        ws.append_window(NamedWindow::default());
345        assert_frag_sql(CLAUSE_FRAME, &sql(&ws), r#"WINDOW "w" AS ()"#);
346    }
347
348    #[test]
349    fn the_window_and_frame_traits_reach_a_named_window() {
350        // The nesting that matters: a mod written for a Window applies to the
351        // definition inside a NamedWindow, and a frame mod applies through both.
352        // The ORDER BY is there because GROUPS mode is defined in terms of peer
353        // groups, which need one.
354        let mut named = NamedWindow::new("w", Window::default());
355        named.window_mut().add_partition_by(quote("is_active"));
356        named.window_mut().order_by_mut().append_order(quote("age"));
357        named.window_mut().frame_mut().set_mode(FrameMode::Groups);
358        assert_frag_sql(
359            r#"SELECT count(*) OVER "w" FROM users WINDOW {}"#,
360            &sql(&named),
361            r#""w" AS (PARTITION BY "is_active" ORDER BY "age" GROUPS UNBOUNDED PRECEDING)"#,
362        );
363    }
364}