use std::borrow::Cow;
use crate::expr::{Expr, IntoExprList};
use crate::writer::{Expression, SqlWriter};
use super::frame::{Frame, HasFrame};
use super::order_by::{HasOrderBy, OrderBy};
use super::{MaybeAbsent, write_present};
#[derive(Debug, Clone, Default)]
pub struct Window {
pub based_on: Option<Cow<'static, str>>,
pub partition_by: Vec<Expr>,
pub order_by: OrderBy,
pub frame: Frame,
}
impl Window {
pub fn based_on(name: impl Into<Cow<'static, str>>) -> Self {
Window {
based_on: Some(name.into()),
..Window::default()
}
}
pub fn add_partition_by(&mut self, exprs: impl IntoExprList) {
self.partition_by.extend(exprs.into_expr_list());
}
pub fn is_empty(&self) -> bool {
self.based_on.is_none()
&& self.partition_by.is_empty()
&& self.order_by.is_empty()
&& self.frame.is_empty()
}
}
impl Expression for Window {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let mut written = false;
if let Some(based_on) = &self.based_on {
w.push_quoted(&[based_on]);
written = true;
}
if !self.partition_by.is_empty() {
if written {
w.push_str(" ");
}
w.write_slice(&self.partition_by, "PARTITION BY ", ", ", "");
written = true;
}
if !self.order_by.is_empty() {
if written {
w.push_str(" ");
}
w.write_expr(&self.order_by);
written = true;
}
if !self.frame.is_empty() {
if written {
w.push_str(" ");
}
w.write_expr(&self.frame);
}
}
}
impl HasOrderBy for Window {
fn order_by_mut(&mut self) -> &mut OrderBy {
&mut self.order_by
}
}
impl HasFrame for Window {
fn frame_mut(&mut self) -> &mut Frame {
&mut self.frame
}
}
pub trait HasWindow {
fn window_mut(&mut self) -> &mut Window;
}
impl HasWindow for Window {
fn window_mut(&mut self) -> &mut Window {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct NamedWindow {
pub name: Cow<'static, str>,
pub definition: Window,
}
impl NamedWindow {
pub fn new(name: impl Into<Cow<'static, str>>, definition: Window) -> Self {
NamedWindow {
name: name.into(),
definition,
}
}
pub fn is_empty(&self) -> bool {
self.name.is_empty()
}
}
impl Expression for NamedWindow {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.name.is_empty() {
return;
}
w.push_quoted(&[&self.name]);
w.push_str(" AS (");
w.write_expr(&self.definition);
w.push_str(")");
}
}
impl HasWindow for NamedWindow {
fn window_mut(&mut self) -> &mut Window {
&mut self.definition
}
}
#[derive(Debug, Clone, Default)]
pub struct Windows {
pub windows: Vec<NamedWindow>,
}
impl Windows {
pub fn append_window(&mut self, window: NamedWindow) {
self.windows.push(window);
}
pub fn is_empty(&self) -> bool {
self.windows.is_empty()
}
}
impl Expression for Windows {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
write_present(w, &self.windows, "WINDOW ", ", ", "");
}
}
pub trait HasWindows {
fn windows_mut(&mut self) -> &mut Windows;
}
impl HasWindows for Windows {
fn windows_mut(&mut self) -> &mut Windows {
self
}
}
impl MaybeAbsent for NamedWindow {
fn is_absent(&self) -> bool {
self.is_empty()
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::clause::frame::{FrameExclusion, FrameMode};
use crate::clause::order_by::{OrderDef, OrderDirection};
use crate::dialect::testing::Numbered;
use crate::expr::{arg, quote};
use crate::value::Value;
use crate::writer::build;
const DEF_FRAME: &str = r#"SELECT count(*) OVER ({}) FROM users"#;
const CLAUSE_FRAME: &str = r#"SELECT count(*) OVER "w" FROM users {}"#;
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn an_empty_window_writes_nothing_which_is_what_over_wants() {
assert_frag_sql(DEF_FRAME, &sql(&Window::default()), "");
assert!(Window::default().is_empty());
assert_frag_sql(
r#"SELECT count(*) FROM users {}"#,
&sql(&Windows::default()),
"",
);
}
#[test]
fn a_window_based_on_a_name_is_just_that_name() {
assert_frag_sql(
r#"SELECT count(*) OVER ({}) FROM users WINDOW "w" AS ()"#,
&sql(&Window::based_on("w")),
r#""w""#,
);
}
#[test]
fn the_parts_render_in_grammar_order_with_single_spaces() {
let mut win = Window::based_on("w");
win.add_partition_by((quote("age"), quote("is_active")));
win.order_by_mut()
.append_order(Expr::custom(OrderDef::new(quote("created_at"))));
win.frame_mut().set_mode(FrameMode::Rows);
win.frame_mut().set_end("CURRENT ROW");
assert_eq!(
build(&Numbered, &win).unwrap().0,
r#""w" PARTITION BY "age", "is_active" ORDER BY "created_at" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"#
);
}
#[test]
fn each_part_can_appear_alone_without_stray_spaces() {
let mut partition_only = Window::default();
partition_only.add_partition_by(quote("age"));
assert_frag_sql(DEF_FRAME, &sql(&partition_only), r#"PARTITION BY "age""#);
let mut order_only = Window::default();
order_only.order_by_mut().append_order(quote("age"));
assert_frag_sql(DEF_FRAME, &sql(&order_only), r#"ORDER BY "age""#);
let mut frame_only = Window::default();
frame_only.frame_mut().set_exclusion(FrameExclusion::Group);
assert_frag_sql(
DEF_FRAME,
&sql(&frame_only),
"RANGE UNBOUNDED PRECEDING EXCLUDE GROUP",
);
}
#[test]
fn a_partition_expression_may_bind_an_argument() {
let mut win = Window::default();
win.add_partition_by(Expr::func("coalesce", (quote("age"), arg(0i32))));
let (rendered, args) = build(&Numbered, &win).unwrap();
assert_frag_sql(DEF_FRAME, &rendered, r#"PARTITION BY coalesce("age", $1)"#);
assert_eq!(args, vec![Value::I32(0)]);
}
#[test]
fn named_windows_are_comma_separated_under_one_keyword() {
let mut w1 = Window::default();
w1.add_partition_by(quote("is_active"));
w1.order_by_mut().append_order(Expr::custom(OrderDef {
direction: Some(OrderDirection::Desc),
..OrderDef::new(quote("age"))
}));
let mut ws = Windows::default();
ws.append_window(NamedWindow::new("w", w1));
ws.append_window(NamedWindow::new("v", Window::default()));
assert_frag_sql(
CLAUSE_FRAME,
&sql(&ws),
r#"WINDOW "w" AS (PARTITION BY "is_active" ORDER BY "age" DESC), "v" AS ()"#,
);
}
#[test]
fn an_unnamed_entry_takes_the_keyword_and_its_comma_with_it() {
let mut ws = Windows::default();
ws.append_window(NamedWindow::default());
assert!(NamedWindow::default().is_empty());
assert_frag_sql(r#"SELECT count(*) FROM users {}"#, &sql(&ws), "");
ws.append_window(NamedWindow::new("w", Window::default()));
ws.append_window(NamedWindow::default());
assert_frag_sql(CLAUSE_FRAME, &sql(&ws), r#"WINDOW "w" AS ()"#);
}
#[test]
fn the_window_and_frame_traits_reach_a_named_window() {
let mut named = NamedWindow::new("w", Window::default());
named.window_mut().add_partition_by(quote("is_active"));
named.window_mut().order_by_mut().append_order(quote("age"));
named.window_mut().frame_mut().set_mode(FrameMode::Groups);
assert_frag_sql(
r#"SELECT count(*) OVER "w" FROM users WINDOW {}"#,
&sql(&named),
r#""w" AS (PARTITION BY "is_active" ORDER BY "age" GROUPS UNBOUNDED PRECEDING)"#,
);
}
}