use crate::expr::{Expr, IntoExpr};
use crate::writer::{Expression, SqlWriter};
#[derive(Debug, Clone, Default)]
pub struct Frame {
pub mode: Option<FrameMode>,
pub start: Option<Expr>,
pub end: Option<Expr>,
pub exclusion: Option<FrameExclusion>,
}
impl Frame {
pub fn new(mode: FrameMode) -> Self {
Frame {
mode: Some(mode),
..Frame::default()
}
}
pub fn set_mode(&mut self, mode: FrameMode) {
self.mode = Some(mode);
}
pub fn set_start(&mut self, start: impl IntoExpr) {
self.start = Some(start.into_expr());
}
pub fn set_end(&mut self, end: impl IntoExpr) {
self.end = Some(end.into_expr());
}
pub fn set_exclusion(&mut self, exclusion: FrameExclusion) {
self.exclusion = Some(exclusion);
}
pub fn is_empty(&self) -> bool {
self.mode.is_none()
&& self.start.is_none()
&& self.end.is_none()
&& self.exclusion.is_none()
}
}
impl Expression for Frame {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.is_empty() {
return;
}
w.push_str(self.mode.unwrap_or(FrameMode::Range).as_str());
w.push_str(" ");
if self.end.is_some() {
w.push_str("BETWEEN ");
}
match &self.start {
Some(start) => w.write_expr(start),
None => w.push_str("UNBOUNDED PRECEDING"),
}
if let Some(end) = &self.end {
w.push_str(" AND ");
w.write_expr(end);
}
if let Some(exclusion) = &self.exclusion {
w.push_str(" EXCLUDE ");
w.push_str(exclusion.as_str());
}
}
}
pub trait HasFrame {
fn frame_mut(&mut self) -> &mut Frame;
}
impl HasFrame for Frame {
fn frame_mut(&mut self) -> &mut Frame {
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameMode {
Range,
Rows,
Groups,
}
impl FrameMode {
pub fn as_str(self) -> &'static str {
match self {
FrameMode::Range => "RANGE",
FrameMode::Rows => "ROWS",
FrameMode::Groups => "GROUPS",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameExclusion {
NoOthers,
CurrentRow,
Group,
Ties,
}
impl FrameExclusion {
pub fn as_str(self) -> &'static str {
match self {
FrameExclusion::NoOthers => "NO OTHERS",
FrameExclusion::CurrentRow => "CURRENT ROW",
FrameExclusion::Group => "GROUP",
FrameExclusion::Ties => "TIES",
}
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::Numbered;
use crate::expr::arg;
use crate::value::Value;
use crate::writer::build;
const FRAME: &str = r#"SELECT count(*) OVER (ORDER BY "id" {}) FROM users"#;
fn sql(f: &Frame) -> String {
build(&Numbered, f).expect("render").0
}
#[test]
fn an_untouched_frame_writes_nothing() {
assert_frag_sql(FRAME, &sql(&Frame::default()), "");
assert!(Frame::default().is_empty());
}
#[test]
fn a_mode_alone_gets_the_grammars_default_start() {
assert_frag_sql(
FRAME,
&sql(&Frame::new(FrameMode::Rows)),
"ROWS UNBOUNDED PRECEDING",
);
}
#[test]
fn an_exclusion_alone_still_produces_a_complete_frame() {
let mut f = Frame::default();
f.set_exclusion(FrameExclusion::Ties);
assert!(!f.is_empty());
assert_frag_sql(FRAME, &sql(&f), "RANGE UNBOUNDED PRECEDING EXCLUDE TIES");
}
#[test]
fn a_start_alone_is_the_single_bound_form_with_no_between() {
let mut f = Frame::new(FrameMode::Rows);
f.set_start("CURRENT ROW");
assert_frag_sql(FRAME, &sql(&f), "ROWS CURRENT ROW");
}
#[test]
fn an_end_bound_is_what_introduces_between() {
let mut f = Frame::new(FrameMode::Rows);
f.set_end("CURRENT ROW");
assert_frag_sql(
FRAME,
&sql(&f),
"ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
);
f.set_start("UNBOUNDED PRECEDING");
assert_frag_sql(
FRAME,
&sql(&f),
"ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
);
}
#[test]
fn bounds_may_bind_arguments_and_are_numbered_left_to_right() {
let mut f = Frame::new(FrameMode::Groups);
f.set_start(Expr::join((arg(1i32), Expr::raw("PRECEDING"))));
f.set_end(Expr::join((arg(2i32), Expr::raw("FOLLOWING"))));
f.set_exclusion(FrameExclusion::CurrentRow);
let (rendered, args) = build(&Numbered, &f).unwrap();
assert_frag_sql(
FRAME,
&rendered,
"GROUPS BETWEEN $1 PRECEDING AND $2 FOLLOWING EXCLUDE CURRENT ROW",
);
assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
}
#[test]
fn every_mode_and_exclusion_has_its_spelling() {
for (mode, keyword) in [
(FrameMode::Range, "RANGE"),
(FrameMode::Rows, "ROWS"),
(FrameMode::Groups, "GROUPS"),
] {
assert_frag_sql(
FRAME,
&sql(&Frame::new(mode)),
&format!("{keyword} UNBOUNDED PRECEDING"),
);
}
for (exclusion, keyword) in [
(FrameExclusion::NoOthers, "NO OTHERS"),
(FrameExclusion::CurrentRow, "CURRENT ROW"),
(FrameExclusion::Group, "GROUP"),
(FrameExclusion::Ties, "TIES"),
] {
let mut f = Frame::new(FrameMode::Rows);
f.set_exclusion(exclusion);
assert_frag_sql(
FRAME,
&sql(&f),
&format!("ROWS UNBOUNDED PRECEDING EXCLUDE {keyword}"),
);
}
}
}