laminar_sql/translator/window_translator/
mod.rs1use std::time::Duration;
7
8use crate::parser::{
9 EmitClause, EmitStrategy, LateDataClause, ParseError, WindowFunction, WindowRewriter,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum WindowType {
15 Tumbling,
17 Sliding,
19 Session,
21 Cumulate,
23}
24
25#[derive(Debug, Clone)]
37pub struct WindowOperatorConfig {
38 pub window_type: WindowType,
40 pub time_column: String,
42 pub size: Duration,
44 pub slide: Option<Duration>,
46 pub gap: Option<Duration>,
48 pub offset_ms: i64,
50 pub allowed_lateness: Duration,
52 pub emit_strategy: EmitStrategy,
54 pub late_data_side_output: Option<String>,
56}
57
58fn format_duration(d: Duration) -> String {
60 let secs = d.as_secs();
61 if secs == 0 {
62 return format!("{}ms", d.as_millis());
63 }
64 if secs.is_multiple_of(3600) {
65 format!("{}h", secs / 3600)
66 } else if secs.is_multiple_of(60) {
67 format!("{}m", secs / 60)
68 } else {
69 format!("{secs}s")
70 }
71}
72
73impl std::fmt::Display for WindowType {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 WindowType::Tumbling => write!(f, "TUMBLE"),
77 WindowType::Sliding => write!(f, "HOP"),
78 WindowType::Session => write!(f, "SESSION"),
79 WindowType::Cumulate => write!(f, "CUMULATE"),
80 }
81 }
82}
83
84impl std::fmt::Display for WindowOperatorConfig {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self.window_type {
87 WindowType::Tumbling => {
88 write!(
89 f,
90 "TUMBLE({}, {})",
91 self.time_column,
92 format_duration(self.size)
93 )
94 }
95 WindowType::Sliding => {
96 let slide = self.slide.unwrap_or(self.size);
97 write!(
98 f,
99 "HOP({}, {} SLIDE {})",
100 self.time_column,
101 format_duration(self.size),
102 format_duration(slide)
103 )
104 }
105 WindowType::Session => {
106 let gap = self.gap.unwrap_or(Duration::ZERO);
107 write!(
108 f,
109 "SESSION({}, GAP {})",
110 self.time_column,
111 format_duration(gap)
112 )
113 }
114 WindowType::Cumulate => {
115 let step = self.slide.unwrap_or(self.size);
116 write!(
117 f,
118 "CUMULATE({}, STEP {} SIZE {})",
119 self.time_column,
120 format_duration(step),
121 format_duration(self.size)
122 )
123 }
124 }
125 }
126}
127
128impl WindowOperatorConfig {
129 #[must_use]
131 pub fn tumbling(time_column: String, size: Duration) -> Self {
132 Self {
133 window_type: WindowType::Tumbling,
134 time_column,
135 size,
136 slide: None,
137 gap: None,
138 offset_ms: 0,
139 allowed_lateness: Duration::ZERO,
140 emit_strategy: EmitStrategy::OnWatermark,
141 late_data_side_output: None,
142 }
143 }
144
145 #[must_use]
147 pub fn sliding(time_column: String, size: Duration, slide: Duration) -> Self {
148 Self {
149 window_type: WindowType::Sliding,
150 time_column,
151 size,
152 slide: Some(slide),
153 gap: None,
154 offset_ms: 0,
155 allowed_lateness: Duration::ZERO,
156 emit_strategy: EmitStrategy::OnWatermark,
157 late_data_side_output: None,
158 }
159 }
160
161 #[must_use]
163 pub fn session(time_column: String, gap: Duration) -> Self {
164 Self {
165 window_type: WindowType::Session,
166 time_column,
167 size: Duration::ZERO, slide: None,
169 gap: Some(gap),
170 offset_ms: 0,
171 allowed_lateness: Duration::ZERO,
172 emit_strategy: EmitStrategy::OnWatermark,
173 late_data_side_output: None,
174 }
175 }
176
177 #[must_use]
182 pub fn cumulate(time_column: String, step: Duration, max_size: Duration) -> Self {
183 Self {
184 window_type: WindowType::Cumulate,
185 time_column,
186 size: max_size,
187 slide: Some(step),
188 gap: None,
189 offset_ms: 0,
190 allowed_lateness: Duration::ZERO,
191 emit_strategy: EmitStrategy::OnWatermark,
192 late_data_side_output: None,
193 }
194 }
195
196 #[must_use]
198 pub fn with_offset_ms(mut self, offset_ms: i64) -> Self {
199 self.offset_ms = offset_ms;
200 self
201 }
202
203 pub fn from_window_function(window: &WindowFunction) -> Result<Self, ParseError> {
211 let time_column = WindowRewriter::get_time_column_name(window).ok_or_else(|| {
212 ParseError::WindowError("Cannot extract time column name".to_string())
213 })?;
214
215 match window {
216 WindowFunction::Tumble {
217 interval, offset, ..
218 } => {
219 let size = WindowRewriter::parse_interval_to_duration(interval)?;
220 let mut config = Self::tumbling(time_column, size);
221 if let Some(ref off) = offset {
222 let off_dur = WindowRewriter::parse_interval_to_duration(off)?;
223 config.offset_ms = i64::try_from(off_dur.as_millis()).unwrap_or(0);
224 }
225 Ok(config)
226 }
227 WindowFunction::Hop {
228 slide_interval,
229 window_interval,
230 offset,
231 ..
232 } => {
233 let size = WindowRewriter::parse_interval_to_duration(window_interval)?;
234 let slide = WindowRewriter::parse_interval_to_duration(slide_interval)?;
235 let mut config = Self::sliding(time_column, size, slide);
236 if let Some(ref off) = offset {
237 let off_dur = WindowRewriter::parse_interval_to_duration(off)?;
238 config.offset_ms = i64::try_from(off_dur.as_millis()).unwrap_or(0);
239 }
240 Ok(config)
241 }
242 WindowFunction::Session { gap_interval, .. } => {
243 let gap = WindowRewriter::parse_interval_to_duration(gap_interval)?;
244 Ok(Self::session(time_column, gap))
245 }
246 WindowFunction::Cumulate {
247 step_interval,
248 max_size_interval,
249 ..
250 } => {
251 let step = WindowRewriter::parse_interval_to_duration(step_interval)?;
252 let max_size = WindowRewriter::parse_interval_to_duration(max_size_interval)?;
253 Ok(Self::cumulate(time_column, step, max_size))
254 }
255 }
256 }
257
258 pub fn with_emit_clause(mut self, emit_clause: &EmitClause) -> Result<Self, ParseError> {
264 self.emit_strategy = emit_clause.to_emit_strategy()?;
265 Ok(self)
266 }
267
268 pub fn with_late_data_clause(
274 mut self,
275 late_data_clause: &LateDataClause,
276 ) -> Result<Self, ParseError> {
277 if late_data_clause.side_output.is_some() {
278 return Err(ParseError::WindowError(
279 "LATE DATA SIDE OUTPUT is not yet supported in pipeline mode; \
280 use ALLOWED LATENESS without a side output, or omit the clause"
281 .to_string(),
282 ));
283 }
284 self.allowed_lateness = late_data_clause.to_allowed_lateness()?;
285 self.late_data_side_output
286 .clone_from(&late_data_clause.side_output);
287 Ok(self)
288 }
289
290 #[must_use]
292 pub fn with_allowed_lateness(mut self, lateness: Duration) -> Self {
293 self.allowed_lateness = lateness;
294 self
295 }
296
297 #[must_use]
299 pub fn with_emit_strategy(mut self, strategy: EmitStrategy) -> Self {
300 self.emit_strategy = strategy;
301 self
302 }
303
304 #[must_use]
306 pub fn with_late_data_side_output(mut self, name: String) -> Self {
307 self.late_data_side_output = Some(name);
308 self
309 }
310
311 pub fn validate(&self, has_watermark: bool, has_window: bool) -> Result<(), ParseError> {
319 if matches!(
320 self.emit_strategy,
321 EmitStrategy::OnWindowClose | EmitStrategy::FinalOnly
322 ) {
323 if !has_watermark {
324 return Err(ParseError::WindowError(
325 "EMIT ON WINDOW CLOSE requires a watermark definition \
326 on the source. Add WATERMARK FOR <column> AS <expr> \
327 to the CREATE SOURCE statement."
328 .to_string(),
329 ));
330 }
331 if !has_window {
332 return Err(ParseError::WindowError(
333 "EMIT ON WINDOW CLOSE is only valid with windowed \
334 aggregation queries. Use EMIT ON UPDATE for \
335 non-windowed queries."
336 .to_string(),
337 ));
338 }
339 }
340 Ok(())
341 }
342
343 #[must_use]
348 pub fn is_append_only_compatible(&self) -> bool {
349 matches!(
350 self.emit_strategy,
351 EmitStrategy::OnWatermark | EmitStrategy::OnWindowClose | EmitStrategy::FinalOnly
352 )
353 }
354
355 #[must_use]
357 pub fn has_late_data_handling(&self) -> bool {
358 self.allowed_lateness > Duration::ZERO || self.late_data_side_output.is_some()
359 }
360}
361
362#[cfg(test)]
363mod tests;