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#[derive(Debug, Clone, Default)]
30pub struct Window {
31 pub based_on: Option<Cow<'static, str>>,
34 pub partition_by: Vec<Expr>,
36 pub order_by: OrderBy,
38 pub frame: Frame,
40}
41
42impl Window {
43 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 pub fn add_partition_by(&mut self, exprs: impl IntoExprList) {
53 self.partition_by.extend(exprs.into_expr_list());
54 }
55
56 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 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
114pub trait HasWindow {
117 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#[derive(Debug, Clone, Default)]
129pub struct NamedWindow {
130 pub name: Cow<'static, str>,
132 pub definition: Window,
134}
135
136impl NamedWindow {
137 pub fn new(name: impl Into<Cow<'static, str>>, definition: Window) -> Self {
139 NamedWindow {
140 name: name.into(),
141 definition,
142 }
143 }
144
145 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 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#[derive(Debug, Clone, Default)]
172pub struct Windows {
173 pub windows: Vec<NamedWindow>,
176}
177
178impl Windows {
179 pub fn append_window(&mut self, window: NamedWindow) {
181 self.windows.push(window);
182 }
183
184 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
196pub trait HasWindows {
198 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 const DEF_FRAME: &str = r#"SELECT count(*) OVER ({}) FROM users"#;
229 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 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 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 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 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 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 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}