laminar_core/operator/window/
mod.rs1use super::Event;
8use smallvec::SmallVec;
9use std::time::Duration;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub enum EmitStrategy {
14 #[default]
16 OnWatermark,
17 Periodic(Duration),
19 OnUpdate,
21 OnWindowClose,
24 Changelog,
27 Final,
30}
31
32impl EmitStrategy {
33 #[must_use]
35 pub fn needs_periodic_timer(&self) -> bool {
36 matches!(self, Self::Periodic(_))
37 }
38
39 #[must_use]
41 pub fn periodic_interval(&self) -> Option<Duration> {
42 match self {
43 Self::Periodic(d) => Some(*d),
44 _ => None,
45 }
46 }
47
48 #[must_use]
50 pub fn emits_on_update(&self) -> bool {
51 matches!(self, Self::OnUpdate)
52 }
53
54 #[must_use]
56 pub fn emits_intermediate(&self) -> bool {
57 matches!(self, Self::OnUpdate | Self::Periodic(_))
58 }
59
60 #[must_use]
62 pub fn requires_changelog(&self) -> bool {
63 matches!(self, Self::Changelog)
64 }
65
66 #[must_use]
68 pub fn is_append_only_compatible(&self) -> bool {
69 matches!(self, Self::OnWindowClose | Self::Final)
70 }
71
72 #[must_use]
74 pub fn generates_retractions(&self) -> bool {
75 matches!(self, Self::OnWatermark | Self::OnUpdate | Self::Changelog)
76 }
77
78 #[must_use]
80 pub fn suppresses_intermediate(&self) -> bool {
81 matches!(self, Self::OnWindowClose | Self::Final)
82 }
83
84 #[must_use]
86 pub fn drops_late_data(&self) -> bool {
87 matches!(self, Self::Final)
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub struct WindowId {
94 pub start: i64,
96 pub end: i64,
98}
99
100impl WindowId {
101 #[must_use]
103 pub fn new(start: i64, end: i64) -> Self {
104 Self { start, end }
105 }
106
107 #[must_use]
109 pub fn duration_ms(&self) -> i64 {
110 self.end - self.start
111 }
112
113 #[inline]
115 #[must_use]
116 pub fn to_key(&self) -> super::TimerKey {
117 super::TimerKey::from(self.to_key_inline())
118 }
119
120 #[inline]
122 #[must_use]
123 pub fn to_key_inline(&self) -> [u8; 16] {
124 let mut key = [0u8; 16];
125 key[..8].copy_from_slice(&self.start.to_be_bytes());
126 key[8..16].copy_from_slice(&self.end.to_be_bytes());
127 key
128 }
129
130 #[must_use]
132 pub fn from_key(key: &[u8]) -> Option<Self> {
133 if key.len() != 16 {
134 return None;
135 }
136 let start = i64::from_be_bytes(key[0..8].try_into().ok()?);
137 let end = i64::from_be_bytes(key[8..16].try_into().ok()?);
138 Some(Self { start, end })
139 }
140}
141
142pub type WindowIdVec = SmallVec<[WindowId; 4]>;
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
147#[error("window boundaries [{start}, {end}) for timestamp {timestamp} do not fit in i64")]
148pub struct WindowAssignmentError {
149 timestamp: i64,
150 start: i128,
151 end: i128,
152}
153
154impl WindowAssignmentError {
155 pub(crate) const fn new(timestamp: i64, start: i128, end: i128) -> Self {
156 Self {
157 timestamp,
158 start,
159 end,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum CdcOperation {
167 Insert,
169 Delete,
171 UpdateBefore,
173 UpdateAfter,
175}
176
177impl CdcOperation {
178 #[must_use]
180 pub fn weight(&self) -> i32 {
181 match self {
182 Self::Insert | Self::UpdateAfter => 1,
183 Self::Delete | Self::UpdateBefore => -1,
184 }
185 }
186
187 #[must_use]
189 pub fn is_insert(&self) -> bool {
190 matches!(self, Self::Insert | Self::UpdateAfter)
191 }
192
193 #[must_use]
195 pub fn is_delete(&self) -> bool {
196 matches!(self, Self::Delete | Self::UpdateBefore)
197 }
198
199 #[inline]
201 #[must_use]
202 pub fn to_u8(self) -> u8 {
203 match self {
204 Self::Insert => 0,
205 Self::Delete => 1,
206 Self::UpdateBefore => 2,
207 Self::UpdateAfter => 3,
208 }
209 }
210
211 #[inline]
213 #[must_use]
214 pub fn from_u8(value: u8) -> Option<Self> {
215 match value {
216 0 => Some(Self::Insert),
217 1 => Some(Self::Delete),
218 2 => Some(Self::UpdateBefore),
219 3 => Some(Self::UpdateAfter),
220 _ => None,
221 }
222 }
223}
224
225#[derive(Debug, Clone)]
227pub struct ChangelogRecord {
228 pub operation: CdcOperation,
230 pub weight: i32,
232 pub emit_timestamp: i64,
234 pub event: Event,
236}
237
238impl ChangelogRecord {
239 #[must_use]
241 pub fn insert(event: Event, emit_timestamp: i64) -> Self {
242 Self {
243 operation: CdcOperation::Insert,
244 weight: 1,
245 emit_timestamp,
246 event,
247 }
248 }
249
250 #[must_use]
252 pub fn delete(event: Event, emit_timestamp: i64) -> Self {
253 Self {
254 operation: CdcOperation::Delete,
255 weight: -1,
256 emit_timestamp,
257 event,
258 }
259 }
260
261 #[must_use]
263 pub fn update(old_event: Event, new_event: Event, emit_timestamp: i64) -> (Self, Self) {
264 let before = Self {
265 operation: CdcOperation::UpdateBefore,
266 weight: -1,
267 emit_timestamp,
268 event: old_event,
269 };
270 let after = Self {
271 operation: CdcOperation::UpdateAfter,
272 weight: 1,
273 emit_timestamp,
274 event: new_event,
275 };
276 (before, after)
277 }
278
279 #[must_use]
281 pub fn new(operation: CdcOperation, event: Event, emit_timestamp: i64) -> Self {
282 Self {
283 operation,
284 weight: operation.weight(),
285 emit_timestamp,
286 event,
287 }
288 }
289
290 #[must_use]
292 pub fn is_insert(&self) -> bool {
293 self.operation.is_insert()
294 }
295
296 #[must_use]
298 pub fn is_delete(&self) -> bool {
299 self.operation.is_delete()
300 }
301}
302
303pub trait WindowAssigner: Send {
305 fn assign_windows(&self, timestamp: i64) -> WindowIdVec;
307
308 fn max_timestamp(&self, window_end: i64) -> i64 {
310 window_end - 1
311 }
312}
313
314#[derive(Debug, Clone)]
316pub struct TumblingWindowAssigner {
317 size_ms: i64,
318 offset_ms: i64,
319}
320
321impl TumblingWindowAssigner {
322 #[must_use]
325 pub fn new(size: Duration) -> Self {
326 let size_ms = i64::try_from(size.as_millis()).expect("Window size must fit in i64");
327 assert!(size_ms > 0, "Window size must be positive");
328 Self {
329 size_ms,
330 offset_ms: 0,
331 }
332 }
333
334 #[must_use]
337 pub fn from_millis(size_ms: i64) -> Self {
338 assert!(size_ms > 0, "Window size must be positive");
339 Self {
340 size_ms,
341 offset_ms: 0,
342 }
343 }
344
345 #[must_use]
347 pub fn with_offset_ms(mut self, offset_ms: i64) -> Self {
348 self.offset_ms = offset_ms;
349 self
350 }
351
352 #[must_use]
354 pub fn size_ms(&self) -> i64 {
355 self.size_ms
356 }
357
358 #[must_use]
360 pub fn offset_ms(&self) -> i64 {
361 self.offset_ms
362 }
363
364 #[inline]
369 #[must_use]
370 pub fn assign(&self, timestamp: i64) -> WindowId {
371 self.try_assign(timestamp)
372 .expect("tumbling window boundaries must fit in i64")
373 }
374
375 #[inline]
381 pub fn try_assign(&self, timestamp: i64) -> Result<WindowId, WindowAssignmentError> {
382 if let Some(adjusted) = timestamp.checked_sub(self.offset_ms) {
383 let quotient = adjusted.div_euclid(self.size_ms);
384 if let Some((start, end)) = quotient
385 .checked_mul(self.size_ms)
386 .and_then(|start| start.checked_add(self.offset_ms))
387 .and_then(|start| start.checked_add(self.size_ms).map(|end| (start, end)))
388 {
389 return Ok(WindowId::new(start, end));
390 }
391 }
392
393 self.assign_wide(timestamp)
394 }
395
396 #[cold]
397 fn assign_wide(&self, timestamp: i64) -> Result<WindowId, WindowAssignmentError> {
398 let size_ms = i128::from(self.size_ms);
399 let offset_ms = i128::from(self.offset_ms);
400 let adjusted = i128::from(timestamp) - offset_ms;
401 let window_start = adjusted.div_euclid(size_ms) * size_ms + offset_ms;
402 let window_end = window_start + size_ms;
403
404 match (i64::try_from(window_start), i64::try_from(window_end)) {
405 (Ok(start), Ok(end)) => Ok(WindowId::new(start, end)),
406 _ => Err(WindowAssignmentError::new(
407 timestamp,
408 window_start,
409 window_end,
410 )),
411 }
412 }
413}
414
415impl WindowAssigner for TumblingWindowAssigner {
416 #[inline]
417 fn assign_windows(&self, timestamp: i64) -> WindowIdVec {
418 let mut windows = WindowIdVec::new();
419 windows.push(self.assign(timestamp));
420 windows
421 }
422}
423
424#[cfg(test)]
425mod tests;