keelson_core/clause/
frame.rs1use crate::expr::{Expr, IntoExpr};
2use crate::writer::{Expression, SqlWriter};
3
4#[derive(Debug, Clone, Default)]
36pub struct Frame {
37 pub mode: Option<FrameMode>,
39 pub start: Option<Expr>,
41 pub end: Option<Expr>,
43 pub exclusion: Option<FrameExclusion>,
45}
46
47impl Frame {
48 pub fn new(mode: FrameMode) -> Self {
50 Frame {
51 mode: Some(mode),
52 ..Frame::default()
53 }
54 }
55
56 pub fn set_mode(&mut self, mode: FrameMode) {
58 self.mode = Some(mode);
59 }
60
61 pub fn set_start(&mut self, start: impl IntoExpr) {
63 self.start = Some(start.into_expr());
64 }
65
66 pub fn set_end(&mut self, end: impl IntoExpr) {
68 self.end = Some(end.into_expr());
69 }
70
71 pub fn set_exclusion(&mut self, exclusion: FrameExclusion) {
73 self.exclusion = Some(exclusion);
74 }
75
76 pub fn is_empty(&self) -> bool {
78 self.mode.is_none()
79 && self.start.is_none()
80 && self.end.is_none()
81 && self.exclusion.is_none()
82 }
83}
84
85impl Expression for Frame {
86 fn write_sql(&self, w: &mut SqlWriter<'_>) {
87 if self.is_empty() {
88 return;
89 }
90
91 w.push_str(self.mode.unwrap_or(FrameMode::Range).as_str());
92 w.push_str(" ");
93
94 if self.end.is_some() {
95 w.push_str("BETWEEN ");
96 }
97
98 match &self.start {
99 Some(start) => w.write_expr(start),
100 None => w.push_str("UNBOUNDED PRECEDING"),
101 }
102
103 if let Some(end) = &self.end {
104 w.push_str(" AND ");
105 w.write_expr(end);
106 }
107
108 if let Some(exclusion) = &self.exclusion {
109 w.push_str(" EXCLUDE ");
110 w.push_str(exclusion.as_str());
111 }
112 }
113}
114
115pub trait HasFrame {
117 fn frame_mut(&mut self) -> &mut Frame;
119}
120
121impl HasFrame for Frame {
122 fn frame_mut(&mut self) -> &mut Frame {
123 self
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum FrameMode {
130 Range,
133 Rows,
135 Groups,
137}
138
139impl FrameMode {
140 pub fn as_str(self) -> &'static str {
142 match self {
143 FrameMode::Range => "RANGE",
144 FrameMode::Rows => "ROWS",
145 FrameMode::Groups => "GROUPS",
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum FrameExclusion {
153 NoOthers,
155 CurrentRow,
157 Group,
159 Ties,
161}
162
163impl FrameExclusion {
164 pub fn as_str(self) -> &'static str {
166 match self {
167 FrameExclusion::NoOthers => "NO OTHERS",
168 FrameExclusion::CurrentRow => "CURRENT ROW",
169 FrameExclusion::Group => "GROUP",
170 FrameExclusion::Ties => "TIES",
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use keelson_sqlcheck::testing::assert_frag_sql;
178
179 use super::*;
180 use crate::dialect::testing::Numbered;
181 use crate::expr::arg;
182 use crate::value::Value;
183 use crate::writer::build;
184
185 const FRAME: &str = r#"SELECT count(*) OVER (ORDER BY "id" {}) FROM users"#;
188
189 fn sql(f: &Frame) -> String {
190 build(&Numbered, f).expect("render").0
191 }
192
193 #[test]
194 fn an_untouched_frame_writes_nothing() {
195 assert_frag_sql(FRAME, &sql(&Frame::default()), "");
196 assert!(Frame::default().is_empty());
197 }
198
199 #[test]
200 fn a_mode_alone_gets_the_grammars_default_start() {
201 assert_frag_sql(
204 FRAME,
205 &sql(&Frame::new(FrameMode::Rows)),
206 "ROWS UNBOUNDED PRECEDING",
207 );
208 }
209
210 #[test]
211 fn an_exclusion_alone_still_produces_a_complete_frame() {
212 let mut f = Frame::default();
214 f.set_exclusion(FrameExclusion::Ties);
215 assert!(!f.is_empty());
216 assert_frag_sql(FRAME, &sql(&f), "RANGE UNBOUNDED PRECEDING EXCLUDE TIES");
217 }
218
219 #[test]
220 fn a_start_alone_is_the_single_bound_form_with_no_between() {
221 let mut f = Frame::new(FrameMode::Rows);
223 f.set_start("CURRENT ROW");
224 assert_frag_sql(FRAME, &sql(&f), "ROWS CURRENT ROW");
225 }
226
227 #[test]
228 fn an_end_bound_is_what_introduces_between() {
229 let mut f = Frame::new(FrameMode::Rows);
232 f.set_end("CURRENT ROW");
233 assert_frag_sql(
234 FRAME,
235 &sql(&f),
236 "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
237 );
238
239 f.set_start("UNBOUNDED PRECEDING");
240 assert_frag_sql(
241 FRAME,
242 &sql(&f),
243 "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
244 );
245 }
246
247 #[test]
248 fn bounds_may_bind_arguments_and_are_numbered_left_to_right() {
249 let mut f = Frame::new(FrameMode::Groups);
253 f.set_start(Expr::join((arg(1i32), Expr::raw("PRECEDING"))));
254 f.set_end(Expr::join((arg(2i32), Expr::raw("FOLLOWING"))));
255 f.set_exclusion(FrameExclusion::CurrentRow);
256
257 let (rendered, args) = build(&Numbered, &f).unwrap();
258 assert_frag_sql(
259 FRAME,
260 &rendered,
261 "GROUPS BETWEEN $1 PRECEDING AND $2 FOLLOWING EXCLUDE CURRENT ROW",
262 );
263 assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
264 }
265
266 #[test]
267 fn every_mode_and_exclusion_has_its_spelling() {
268 for (mode, keyword) in [
269 (FrameMode::Range, "RANGE"),
270 (FrameMode::Rows, "ROWS"),
271 (FrameMode::Groups, "GROUPS"),
272 ] {
273 assert_frag_sql(
274 FRAME,
275 &sql(&Frame::new(mode)),
276 &format!("{keyword} UNBOUNDED PRECEDING"),
277 );
278 }
279
280 for (exclusion, keyword) in [
281 (FrameExclusion::NoOthers, "NO OTHERS"),
282 (FrameExclusion::CurrentRow, "CURRENT ROW"),
283 (FrameExclusion::Group, "GROUP"),
284 (FrameExclusion::Ties, "TIES"),
285 ] {
286 let mut f = Frame::new(FrameMode::Rows);
287 f.set_exclusion(exclusion);
288 assert_frag_sql(
289 FRAME,
290 &sql(&f),
291 &format!("ROWS UNBOUNDED PRECEDING EXCLUDE {keyword}"),
292 );
293 }
294 }
295}