1use std::{collections::HashMap, sync::Arc};
5
6use reifydb_codec::value::{decode_params, encode_params};
7use reifydb_value::{
8 Result,
9 params::Params,
10 value::{Value, duration::Duration},
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{
15 common::{WindowKind, WindowSize},
16 error::CoreError,
17 internal_error,
18 row::{JoinPick, JoinRetention, OperatorRetention},
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum WithSpan {
23 Duration(Duration),
24 Count(u64),
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct WindowWith {
29 pub kind: WindowKind,
30 pub lateness: Option<Duration>,
31 pub immutable: Option<Duration>,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
35pub struct JoinWith {
36 pub retention: Option<JoinRetention>,
37 pub snapshot: bool,
38 pub pick: Option<JoinPick>,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42pub struct LookupWith {
43 pub retention: Option<OperatorRetention>,
44}
45
46#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
47pub struct ApplyWith {
48 pub window: Option<WindowKind>,
49 pub lateness: Option<WithSpan>,
50 pub immutable: Option<WithSpan>,
51 pub retention: Option<Duration>,
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
55pub struct AggregateWith {}
56
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct DistinctWith {}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct WindowSealing {
62 pub lateness: Option<Duration>,
63 pub immutable: Option<Duration>,
64}
65
66impl WindowSealing {
67 pub fn from_operator_with(with: &ApplyWith) -> Result<Self> {
68 let sealing = Self {
69 lateness: duration_span("lateness", with.lateness)?,
70 immutable: duration_span("immutable", with.immutable)?,
71 };
72 if let (Some(lateness), Some(immutable)) = (sealing.lateness, sealing.immutable)
73 && immutable >= lateness
74 {
75 return Err(CoreError::OperatorWithImmutableNotBelowLateness {
76 immutable,
77 lateness,
78 }
79 .into());
80 }
81 Ok(sealing)
82 }
83}
84
85impl ApplyWith {
86 pub fn window_duration(&self) -> Result<Duration> {
87 let Some(kind) = &self.window else {
88 return Err(CoreError::OperatorWithWindowMissing.into());
89 };
90 match kind.size() {
91 None => Err(CoreError::OperatorWithWindowKindUnsupported {
92 kind: kind.name(),
93 supported: "tumbling, sliding or rolling".to_string(),
94 }
95 .into()),
96 Some(WindowSize::Duration(d)) => Ok(*d),
97 Some(WindowSize::Count(count)) => Err(CoreError::OperatorWithWindowSizeCount {
98 count: *count,
99 }
100 .into()),
101 }
102 }
103
104 pub fn window_slots(&self) -> Result<u64> {
105 let Some(kind) = &self.window else {
106 return Err(CoreError::OperatorWithWindowMissing.into());
107 };
108 match kind.size() {
109 None => Err(CoreError::OperatorWithWindowKindUnsupported {
110 kind: kind.name(),
111 supported: "tumbling, sliding or rolling".to_string(),
112 }
113 .into()),
114 Some(WindowSize::Count(n)) => Ok(*n),
115 Some(WindowSize::Duration(d)) => Err(CoreError::OperatorWithWindowSizeDuration {
116 size: *d,
117 }
118 .into()),
119 }
120 }
121
122 pub fn window_slide_duration(&self) -> Result<Option<Duration>> {
123 match &self.window {
124 Some(WindowKind::Sliding {
125 slide,
126 ..
127 }) => match slide {
128 WindowSize::Duration(d) => Ok(Some(*d)),
129 WindowSize::Count(count) => Err(CoreError::OperatorWithWindowSlideCount {
130 count: *count,
131 }
132 .into()),
133 },
134 _ => Ok(None),
135 }
136 }
137
138 pub fn window_slide_slots(&self) -> Result<Option<u64>> {
139 match &self.window {
140 Some(WindowKind::Sliding {
141 slide,
142 ..
143 }) => match slide {
144 WindowSize::Count(n) => Ok(Some(*n)),
145 WindowSize::Duration(d) => Err(CoreError::OperatorWithWindowSlideDuration {
146 slide: *d,
147 }
148 .into()),
149 },
150 _ => Ok(None),
151 }
152 }
153
154 pub fn window_session_gap(&self) -> Option<Duration> {
155 match &self.window {
156 Some(WindowKind::Session {
157 gap,
158 }) => Some(*gap),
159 _ => None,
160 }
161 }
162
163 pub fn check_session_window(&self) -> Result<()> {
164 let Some(gap) = self.window_session_gap() else {
165 return Ok(());
166 };
167 if !gap.is_positive() {
168 return Err(CoreError::OperatorWithSessionZeroGap.into());
169 }
170 match self.lateness_duration()? {
171 Some(lateness) if lateness.is_positive() => Err(CoreError::OperatorWithSessionLateness {
172 lateness,
173 }
174 .into()),
175 _ => Ok(()),
176 }
177 }
178
179 pub fn effective_retention(&self) -> Result<Option<Duration>> {
180 match self.retention {
181 Some(retention) => Ok(Some(retention)),
182 None => self.lateness_duration(),
183 }
184 }
185
186 pub fn check_retention(&self) -> Result<()> {
187 let (Some(retention), Some(lateness)) = (self.retention, self.lateness_duration()?) else {
188 return Ok(());
189 };
190 if retention < lateness {
191 return Err(CoreError::OperatorWithRetentionBelowLateness {
192 retention,
193 lateness,
194 }
195 .into());
196 }
197 Ok(())
198 }
199
200 pub fn reject_retention(&self) -> Result<()> {
201 match self.retention {
202 None => Ok(()),
203 Some(_) => Err(CoreError::OperatorWithRetentionNotSupported.into()),
204 }
205 }
206 pub fn reject_window(&self) -> Result<()> {
207 match &self.window {
208 None => Ok(()),
209 Some(kind) => Err(CoreError::OperatorWithWindowNotSupported {
210 kind: kind.name(),
211 }
212 .into()),
213 }
214 }
215
216 pub fn require_window(&self, supported: &'static str) -> Result<&WindowKind> {
217 let Some(kind) = &self.window else {
218 return Err(CoreError::OperatorWithWindowMissing.into());
219 };
220 if kind.name() != supported {
221 return Err(CoreError::OperatorWithWindowKindUnsupported {
222 kind: kind.name(),
223 supported: supported.to_string(),
224 }
225 .into());
226 }
227 Ok(kind)
228 }
229
230 pub fn lateness_duration(&self) -> Result<Option<Duration>> {
231 match self.lateness {
232 None => Ok(None),
233 Some(WithSpan::Duration(d)) => Ok(Some(d)),
234 Some(WithSpan::Count(count)) => Err(CoreError::OperatorWithCountSpan {
235 key: "lateness",
236 count,
237 }
238 .into()),
239 }
240 }
241
242 pub fn lateness_count(&self) -> Result<Option<u64>> {
243 match self.lateness {
244 None => Ok(None),
245 Some(WithSpan::Count(n)) => Ok(Some(n)),
246 Some(WithSpan::Duration(duration)) => Err(CoreError::OperatorWithDurationSpan {
247 key: "lateness",
248 duration,
249 }
250 .into()),
251 }
252 }
253
254 pub fn immutable_count(&self) -> Result<Option<u64>> {
255 match self.immutable {
256 None => Ok(None),
257 Some(WithSpan::Count(n)) => Ok(Some(n)),
258 Some(WithSpan::Duration(duration)) => Err(CoreError::OperatorWithDurationSpan {
259 key: "immutable",
260 duration,
261 }
262 .into()),
263 }
264 }
265}
266
267pub fn encode_apply_with(with: &ApplyWith) -> Result<Vec<u8>> {
268 let mut values = HashMap::new();
269 if let Some(window) = &with.window {
270 values.insert("window".to_string(), Value::Utf8(window.name().to_string()));
271 match window {
272 WindowKind::Tumbling {
273 size,
274 } => insert_window_size(&mut values, size),
275 WindowKind::Sliding {
276 size,
277 slide,
278 } => {
279 insert_window_size(&mut values, size);
280 values.insert("slide".to_string(), window_size_value(slide));
281 }
282 WindowKind::Rolling {
283 size,
284 lag,
285 pane,
286 } => {
287 insert_window_size(&mut values, size);
288 if let Some(lag) = lag {
289 values.insert("lag".to_string(), Value::Duration(*lag));
290 }
291 if let Some(pane) = pane {
292 values.insert("pane".to_string(), Value::Duration(*pane));
293 }
294 }
295 WindowKind::Session {
296 gap,
297 } => {
298 values.insert("gap".to_string(), Value::Duration(*gap));
299 }
300 }
301 }
302 if let Some(lateness) = with.lateness {
303 values.insert("lateness".to_string(), span_value(lateness));
304 }
305 if let Some(immutable) = with.immutable {
306 values.insert("immutable".to_string(), span_value(immutable));
307 }
308 if let Some(retention) = with.retention {
309 values.insert("retention".to_string(), Value::Duration(retention));
310 }
311 encode_params(&Params::Named(Arc::new(values)))
312 .map_err(|e| internal_error!("failed to encode apply with: {}", e))
313}
314
315pub fn decode_apply_with(bytes: &[u8]) -> Result<ApplyWith> {
316 let values = match decode_params(bytes).map_err(|e| internal_error!("failed to decode apply with: {}", e))? {
317 Params::None => return Ok(ApplyWith::default()),
318 Params::Named(values) => values,
319 Params::Positional(_) => return Err(internal_error!("apply with must be named params")),
320 };
321 let mut with = ApplyWith::default();
322 let mut window_name: Option<String> = None;
323 let mut duration: Option<Duration> = None;
324 let mut slots: Option<u64> = None;
325 let mut slide: Option<WindowSize> = None;
326 let mut gap: Option<Duration> = None;
327 let mut lag: Option<Duration> = None;
328 let mut pane: Option<Duration> = None;
329 for (key, value) in values.iter() {
330 match (key.as_str(), value) {
331 ("window", Value::Utf8(name)) => window_name = Some(name.clone()),
332 ("duration", Value::Duration(d)) => duration = Some(*d),
333 ("slots", Value::Uint8(n)) => slots = Some(*n),
334 ("slide", Value::Duration(d)) => slide = Some(WindowSize::Duration(*d)),
335 ("slide", Value::Uint8(n)) => slide = Some(WindowSize::Count(*n)),
336 ("gap", Value::Duration(d)) => gap = Some(*d),
337 ("lag", Value::Duration(d)) => lag = Some(*d),
338 ("pane", Value::Duration(d)) => pane = Some(*d),
339 ("lateness", value) => with.lateness = Some(value_span(key, value)?),
340 ("immutable", value) => with.immutable = Some(value_span(key, value)?),
341 ("retention", Value::Duration(retention)) => with.retention = Some(*retention),
342 _ => return Err(internal_error!("unexpected apply with entry {}: {:?}", key, value)),
343 }
344 }
345 with.window = decode_window_kind(window_name, duration, slots, slide, gap, lag, pane)?;
346 Ok(with)
347}
348
349fn decode_window_kind(
350 window_name: Option<String>,
351 duration: Option<Duration>,
352 slots: Option<u64>,
353 slide: Option<WindowSize>,
354 gap: Option<Duration>,
355 lag: Option<Duration>,
356 pane: Option<Duration>,
357) -> Result<Option<WindowKind>> {
358 let Some(window_name) = window_name else {
359 if duration.is_some()
360 || slots.is_some() || slide.is_some()
361 || gap.is_some() || lag.is_some()
362 || pane.is_some()
363 {
364 return Err(internal_error!("apply with has a window size key without 'window'"));
365 }
366 return Ok(None);
367 };
368 match window_name.as_str() {
369 "tumbling" => {
370 if slide.is_some() || gap.is_some() || lag.is_some() || pane.is_some() {
371 return Err(internal_error!("apply with has a key the tumbling window does not use"));
372 }
373 Ok(Some(WindowKind::Tumbling {
374 size: decode_window_size(duration, slots)?,
375 }))
376 }
377 "sliding" => {
378 let size = decode_window_size(duration, slots)?;
379 let Some(slide) = slide else {
380 return Err(internal_error!("apply with is missing 'slide' for a sliding window"));
381 };
382 if gap.is_some() || lag.is_some() || pane.is_some() {
383 return Err(internal_error!("apply with has a key the sliding window does not use"));
384 }
385 Ok(Some(WindowKind::Sliding {
386 size,
387 slide,
388 }))
389 }
390 "rolling" => {
391 let size = decode_window_size(duration, slots)?;
392 if slide.is_some() || gap.is_some() {
393 return Err(internal_error!("apply with has a key the rolling window does not use"));
394 }
395 Ok(Some(WindowKind::Rolling {
396 size,
397 lag,
398 pane,
399 }))
400 }
401 "session" => {
402 let Some(gap) = gap else {
403 return Err(internal_error!("apply with is missing 'gap' for a session window"));
404 };
405 if duration.is_some() || slots.is_some() || slide.is_some() || lag.is_some() || pane.is_some() {
406 return Err(internal_error!("apply with has a key the session window does not use"));
407 }
408 Ok(Some(WindowKind::Session {
409 gap,
410 }))
411 }
412 other => Err(internal_error!("apply with has an unknown window kind {}", other)),
413 }
414}
415
416fn decode_window_size(duration: Option<Duration>, slots: Option<u64>) -> Result<WindowSize> {
417 match (duration, slots) {
418 (Some(d), None) => Ok(WindowSize::Duration(d)),
419 (None, Some(n)) => Ok(WindowSize::Count(n)),
420 (None, None) => Err(internal_error!("apply with is missing 'duration' or 'slots' for its window")),
421 (Some(_), Some(_)) => Err(internal_error!("apply with has both 'duration' and 'slots'")),
422 }
423}
424
425fn insert_window_size(values: &mut HashMap<String, Value>, size: &WindowSize) {
426 match size {
427 WindowSize::Duration(d) => {
428 values.insert("duration".to_string(), Value::Duration(*d));
429 }
430 WindowSize::Count(n) => {
431 values.insert("slots".to_string(), Value::Uint8(*n));
432 }
433 }
434}
435
436fn window_size_value(size: &WindowSize) -> Value {
437 match size {
438 WindowSize::Duration(d) => Value::Duration(*d),
439 WindowSize::Count(n) => Value::Uint8(*n),
440 }
441}
442
443fn span_value(span: WithSpan) -> Value {
444 match span {
445 WithSpan::Duration(duration) => Value::Duration(duration),
446 WithSpan::Count(count) => Value::Uint8(count),
447 }
448}
449
450fn value_span(key: &str, value: &Value) -> Result<WithSpan> {
451 match value {
452 Value::Duration(duration) => Ok(WithSpan::Duration(*duration)),
453 Value::Uint8(count) => Ok(WithSpan::Count(*count)),
454 _ => Err(internal_error!("unexpected apply with entry {}: {:?}", key, value)),
455 }
456}
457
458fn duration_span(key: &'static str, span: Option<WithSpan>) -> Result<Option<Duration>> {
459 match span {
460 None => Ok(None),
461 Some(WithSpan::Duration(duration)) => Ok(Some(duration)),
462 Some(WithSpan::Count(count)) => Err(CoreError::OperatorWithCountSpan {
463 key,
464 count,
465 }
466 .into()),
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use std::{collections::HashMap, sync::Arc};
473
474 use reifydb_codec::value::encode_params;
475 use reifydb_value::{
476 params::Params,
477 value::{Value, duration::Duration},
478 };
479
480 use super::{ApplyWith, WindowKind, WindowSealing, WindowSize, WithSpan, decode_apply_with, encode_apply_with};
481
482 fn secs(n: i64) -> Duration {
483 Duration::from_seconds(n).unwrap()
484 }
485
486 fn with(lateness: Option<i64>, immutable: Option<i64>) -> ApplyWith {
487 ApplyWith {
488 window: None,
489 lateness: lateness.map(|n| WithSpan::Duration(secs(n))),
490 immutable: immutable.map(|n| WithSpan::Duration(secs(n))),
491 retention: None,
492 }
493 }
494
495 #[test]
496 fn declared_immutable_below_lateness_is_kept_verbatim() {
497 assert_eq!(
498 WindowSealing::from_operator_with(&with(Some(20), Some(15))).unwrap(),
499 WindowSealing {
500 lateness: Some(secs(20)),
501 immutable: Some(secs(15)),
502 }
503 );
504 }
505
506 #[test]
507 fn lateness_without_immutable_resolves_with_no_immutable() {
508 assert_eq!(
510 WindowSealing::from_operator_with(&with(Some(20), None)).unwrap(),
511 WindowSealing {
512 lateness: Some(secs(20)),
513 immutable: None,
514 }
515 );
516 }
517
518 #[test]
519 fn declared_immutable_equal_to_lateness_is_rejected() {
520 assert!(WindowSealing::from_operator_with(&with(Some(20), Some(20))).is_err());
522 }
523
524 #[test]
525 fn declared_immutable_above_lateness_is_rejected() {
526 assert!(WindowSealing::from_operator_with(&with(Some(20), Some(30))).is_err());
527 }
528
529 #[test]
530 fn immutable_without_lateness_is_accepted() {
531 assert_eq!(
533 WindowSealing::from_operator_with(&with(None, Some(15))).unwrap(),
534 WindowSealing {
535 lateness: None,
536 immutable: Some(secs(15)),
537 }
538 );
539 }
540
541 #[test]
542 fn neither_knob_declared_leaves_both_absent() {
543 assert_eq!(
545 WindowSealing::from_operator_with(&with(None, None)).unwrap(),
546 WindowSealing {
547 lateness: None,
548 immutable: None,
549 }
550 );
551 }
552
553 #[test]
554 fn validated_returns_both_when_declared() {
555 assert_eq!(
556 WindowSealing::from_operator_with(&with(Some(20), Some(15))).unwrap(),
557 WindowSealing {
558 lateness: Some(secs(20)),
559 immutable: Some(secs(15)),
560 }
561 );
562 }
563
564 #[test]
565 fn validated_returns_no_immutable_when_only_the_lateness_is_declared() {
566 assert_eq!(
568 WindowSealing::from_operator_with(&with(Some(20), None)).unwrap(),
569 WindowSealing {
570 lateness: Some(secs(20)),
571 immutable: None,
572 }
573 );
574 }
575
576 #[test]
577 fn validated_names_the_ordering_violation() {
578 let err = WindowSealing::from_operator_with(&with(Some(20), Some(20))).unwrap_err();
579 assert!(err.to_string().contains("must be strictly less than lateness"), "{err}");
580 }
581
582 #[test]
583 fn validated_accepts_an_immutable_without_a_lateness() {
584 assert_eq!(
586 WindowSealing::from_operator_with(&with(None, Some(15))).unwrap(),
587 WindowSealing {
588 lateness: None,
589 immutable: Some(secs(15)),
590 }
591 );
592 }
593
594 #[test]
595 fn validated_accepts_a_window_that_declares_no_knob_at_all() {
596 assert_eq!(
597 WindowSealing::from_operator_with(&with(None, None)).unwrap(),
598 WindowSealing {
599 lateness: None,
600 immutable: None,
601 }
602 );
603 }
604
605 #[test]
606 fn a_count_span_is_rejected_for_a_time_sealed_operator() {
607 let with = ApplyWith {
609 window: None,
610 lateness: Some(WithSpan::Count(150)),
611 immutable: None,
612 retention: None,
613 };
614 let err = WindowSealing::from_operator_with(&with).unwrap_err();
615 assert!(err.to_string().contains("lateness"), "{err}");
616 }
617
618 #[test]
619 fn apply_with_round_trips_through_the_create_buffer() {
620 for with in [
622 ApplyWith::default(),
623 with(Some(30), Some(10)),
624 ApplyWith {
625 window: None,
626 lateness: Some(WithSpan::Count(150)),
627 immutable: Some(WithSpan::Count(0)),
628 retention: None,
629 },
630 ] {
631 assert_eq!(decode_apply_with(&encode_apply_with(&with).unwrap()).unwrap(), with);
632 }
633 }
634
635 #[test]
636 fn an_unknown_apply_with_key_fails_to_decode() {
637 let values = HashMap::from([("ttl".to_string(), Value::Duration(secs(1)))]);
639 let bytes = encode_params(&Params::Named(Arc::new(values))).unwrap();
640 assert!(decode_apply_with(&bytes).is_err());
641 }
642
643 #[test]
644 fn a_span_of_the_wrong_type_fails_to_decode() {
645 let values = HashMap::from([("lateness".to_string(), Value::Int8(5))]);
646 let bytes = encode_params(&Params::Named(Arc::new(values))).unwrap();
647 assert!(decode_apply_with(&bytes).is_err());
648 }
649
650 #[test]
651 fn positional_params_fail_to_decode() {
652 let bytes = encode_params(&Params::Positional(Arc::new(vec![Value::Duration(secs(1))]))).unwrap();
653 assert!(decode_apply_with(&bytes).is_err());
654 }
655
656 #[test]
657 fn every_window_kind_round_trips_through_the_create_buffer() {
658 let lateness = Some(WithSpan::Duration(secs(30)));
660 let kinds = [
661 WindowKind::Tumbling {
662 size: WindowSize::Duration(secs(60)),
663 },
664 WindowKind::Tumbling {
665 size: WindowSize::Count(10),
666 },
667 WindowKind::Sliding {
668 size: WindowSize::Duration(secs(60)),
669 slide: WindowSize::Duration(secs(30)),
670 },
671 WindowKind::Sliding {
672 size: WindowSize::Count(10),
673 slide: WindowSize::Count(5),
674 },
675 WindowKind::Rolling {
676 size: WindowSize::Duration(secs(60)),
677 lag: Some(secs(5)),
678 pane: None,
679 },
680 WindowKind::Rolling {
681 size: WindowSize::Duration(secs(60)),
682 lag: None,
683 pane: None,
684 },
685 WindowKind::Session {
686 gap: secs(30),
687 },
688 ];
689 for kind in kinds {
690 let with = ApplyWith {
691 window: Some(kind),
692 lateness,
693 immutable: None,
694 retention: None,
695 };
696 assert_eq!(decode_apply_with(&encode_apply_with(&with).unwrap()).unwrap(), with);
697 }
698 }
699
700 #[test]
701 fn a_size_key_without_a_window_fails_to_decode() {
702 let values = HashMap::from([("duration".to_string(), Value::Duration(secs(60)))]);
704 let bytes = encode_params(&Params::Named(Arc::new(values))).unwrap();
705 assert!(decode_apply_with(&bytes).is_err());
706 }
707
708 #[test]
709 fn window_duration_rejects_a_missing_window_a_count_size_and_a_session() {
710 assert!(ApplyWith::default().window_duration().is_err());
711
712 let count = ApplyWith {
713 window: Some(WindowKind::Tumbling {
714 size: WindowSize::Count(10),
715 }),
716 lateness: None,
717 immutable: None,
718 retention: None,
719 };
720 assert!(count.window_duration().is_err());
721
722 let session = ApplyWith {
723 window: Some(WindowKind::Session {
724 gap: secs(30),
725 }),
726 lateness: None,
727 immutable: None,
728 retention: None,
729 };
730 assert!(session.window_duration().is_err());
731 }
732
733 #[test]
734 fn window_slots_rejects_a_missing_window_and_a_duration_size() {
735 assert!(ApplyWith::default().window_slots().is_err());
736
737 let duration = ApplyWith {
738 window: Some(WindowKind::Tumbling {
739 size: WindowSize::Duration(secs(60)),
740 }),
741 lateness: None,
742 immutable: None,
743 retention: None,
744 };
745 assert!(duration.window_slots().is_err());
746 }
747
748 #[test]
749 fn window_slide_reads_only_a_sliding_window_and_only_in_its_unit() {
750 let by_time = ApplyWith {
753 window: Some(WindowKind::Sliding {
754 size: WindowSize::Duration(secs(60)),
755 slide: WindowSize::Duration(secs(15)),
756 }),
757 lateness: None,
758 immutable: None,
759 retention: None,
760 };
761 let by_slots = ApplyWith {
762 window: Some(WindowKind::Sliding {
763 size: WindowSize::Count(10),
764 slide: WindowSize::Count(4),
765 }),
766 lateness: None,
767 immutable: None,
768 retention: None,
769 };
770 let tumbling = ApplyWith {
771 window: Some(WindowKind::Tumbling {
772 size: WindowSize::Duration(secs(60)),
773 }),
774 lateness: None,
775 immutable: None,
776 retention: None,
777 };
778
779 assert_eq!(by_time.window_slide_duration().unwrap(), Some(secs(15)));
780 assert_eq!(by_slots.window_slide_slots().unwrap(), Some(4));
781 assert!(by_time.window_slide_slots().is_err());
782 assert!(by_slots.window_slide_duration().is_err());
783 assert_eq!(tumbling.window_slide_duration().unwrap(), None);
784 assert_eq!(tumbling.window_slide_slots().unwrap(), None);
785 }
786
787 #[test]
788 fn window_session_gap_reads_only_a_session_window() {
789 let session = ApplyWith {
792 window: Some(WindowKind::Session {
793 gap: secs(30),
794 }),
795 lateness: None,
796 immutable: None,
797 retention: None,
798 };
799 let others = [
800 WindowKind::Tumbling {
801 size: WindowSize::Duration(secs(30)),
802 },
803 WindowKind::Sliding {
804 size: WindowSize::Duration(secs(30)),
805 slide: WindowSize::Duration(secs(10)),
806 },
807 WindowKind::Rolling {
808 size: WindowSize::Duration(secs(30)),
809 lag: None,
810 pane: None,
811 },
812 ];
813
814 assert_eq!(session.window_session_gap(), Some(secs(30)));
815 assert_eq!(ApplyWith::default().window_session_gap(), None);
816 for kind in others {
817 let name = kind.name();
818 let with = ApplyWith {
819 window: Some(kind),
820 lateness: None,
821 immutable: None,
822 retention: None,
823 };
824 assert_eq!(with.window_session_gap(), None, "a {name} window has no session gap");
825 }
826 }
827
828 #[test]
829 fn check_session_window_refuses_a_zero_gap_and_a_positive_lateness_only_on_a_session() {
830 let session = |gap: Duration, lateness: Option<WithSpan>| ApplyWith {
832 window: Some(WindowKind::Session {
833 gap,
834 }),
835 lateness,
836 immutable: None,
837 retention: None,
838 };
839 let code = |with: ApplyWith| with.check_session_window().unwrap_err().0.code;
840
841 assert!(session(secs(30), None).check_session_window().is_ok());
842 assert!(session(secs(30), Some(WithSpan::Duration(secs(0)))).check_session_window().is_ok());
843 assert_eq!(code(session(secs(30), Some(WithSpan::Duration(secs(1))))), "FLOW_078");
844 assert_eq!(code(session(secs(0), None)), "FLOW_079");
845 assert_eq!(
846 code(session(secs(0), Some(WithSpan::Duration(secs(1))))),
847 "FLOW_079",
848 "the gap is checked first"
849 );
850 assert_eq!(
851 code(session(secs(30), Some(WithSpan::Count(5)))),
852 "FLOW_063",
853 "a session seals by time, so a count lateness is the wrong unit"
854 );
855
856 let tumbling = ApplyWith {
857 window: Some(WindowKind::Tumbling {
858 size: WindowSize::Duration(secs(60)),
859 }),
860 lateness: Some(WithSpan::Duration(secs(1))),
861 immutable: None,
862 retention: None,
863 };
864 assert!(tumbling.check_session_window().is_ok());
865 assert!(ApplyWith::default().check_session_window().is_ok());
866 }
867
868 #[test]
869 fn reject_window_refuses_any_window() {
870 assert!(ApplyWith::default().reject_window().is_ok());
871
872 let windowed = ApplyWith {
873 window: Some(WindowKind::Tumbling {
874 size: WindowSize::Duration(secs(60)),
875 }),
876 lateness: None,
877 immutable: None,
878 retention: None,
879 };
880 assert!(windowed.reject_window().is_err());
881 }
882
883 #[test]
884 fn require_window_refuses_a_missing_window_and_another_kind() {
885 assert!(ApplyWith::default().require_window("tumbling").is_err());
886
887 let rolling = ApplyWith {
888 window: Some(WindowKind::Rolling {
889 size: WindowSize::Duration(secs(60)),
890 lag: None,
891 pane: None,
892 }),
893 lateness: None,
894 immutable: None,
895 retention: None,
896 };
897 assert!(rolling.require_window("tumbling").is_err());
898 assert!(rolling.require_window("rolling").is_ok());
899 }
900
901 #[test]
902 fn lateness_readers_refuse_the_other_unit() {
903 let none = ApplyWith::default();
904 assert_eq!(none.lateness_duration().unwrap(), None);
905 assert_eq!(none.lateness_count().unwrap(), None);
906
907 let duration = ApplyWith {
908 window: None,
909 lateness: Some(WithSpan::Duration(secs(30))),
910 immutable: None,
911 retention: None,
912 };
913 assert_eq!(duration.lateness_duration().unwrap(), Some(secs(30)));
914 assert!(duration.lateness_count().is_err());
915
916 let count = ApplyWith {
917 window: None,
918 lateness: Some(WithSpan::Count(5)),
919 immutable: None,
920 retention: None,
921 };
922 assert_eq!(count.lateness_count().unwrap(), Some(5));
923 assert!(count.lateness_duration().is_err());
924 }
925
926 #[test]
927 fn a_rolling_pane_round_trips_through_the_create_buffer() {
928 let with = ApplyWith {
930 window: Some(WindowKind::Rolling {
931 size: WindowSize::Duration(secs(3600)),
932 lag: Some(secs(5)),
933 pane: Some(secs(1)),
934 }),
935 lateness: None,
936 immutable: None,
937 retention: None,
938 };
939 assert_eq!(decode_apply_with(&encode_apply_with(&with).unwrap()).unwrap(), with);
940 }
941
942 #[test]
943 fn a_pane_on_a_window_that_is_not_rolling_fails_to_decode() {
944 for (window, extra) in [
946 ("tumbling", ("duration", Value::Duration(secs(60)))),
947 ("sliding", ("duration", Value::Duration(secs(60)))),
948 ("session", ("gap", Value::Duration(secs(30)))),
949 ] {
950 let mut values = HashMap::from([
951 ("window".to_string(), Value::Utf8(window.to_string())),
952 ("pane".to_string(), Value::Duration(secs(1))),
953 (extra.0.to_string(), extra.1),
954 ]);
955 if window == "sliding" {
956 values.insert("slide".to_string(), Value::Duration(secs(30)));
957 }
958 let bytes = encode_params(&Params::Named(Arc::new(values))).unwrap();
959 assert!(decode_apply_with(&bytes).is_err(), "pane must be rejected on {window}");
960 }
961 }
962
963 #[test]
964 fn a_pane_without_a_window_fails_to_decode() {
965 let values = HashMap::from([("pane".to_string(), Value::Duration(secs(1)))]);
966 let bytes = encode_params(&Params::Named(Arc::new(values))).unwrap();
967 assert!(decode_apply_with(&bytes).is_err());
968 }
969
970 #[test]
971 fn immutable_count_refuses_a_duration_and_reads_a_count() {
972 let none = ApplyWith::default();
974 assert_eq!(none.immutable_count().unwrap(), None);
975
976 let count = ApplyWith {
977 window: None,
978 lateness: None,
979 immutable: Some(WithSpan::Count(4)),
980 retention: None,
981 };
982 assert_eq!(count.immutable_count().unwrap(), Some(4));
983
984 let duration = ApplyWith {
985 window: None,
986 lateness: None,
987 immutable: Some(WithSpan::Duration(secs(4))),
988 retention: None,
989 };
990 assert!(duration.immutable_count().is_err());
991 }
992
993 #[test]
994 fn retention_round_trips_through_the_create_buffer() {
995 let declared = ApplyWith {
997 retention: Some(secs(3600)),
998 ..with(Some(20), None)
999 };
1000 let bytes = encode_apply_with(&declared).unwrap();
1001 assert_eq!(decode_apply_with(&bytes).unwrap(), declared);
1002 }
1003
1004 #[test]
1005 fn effective_retention_is_the_retention_or_else_the_lateness() {
1006 assert_eq!(with(Some(20), None).effective_retention().unwrap(), Some(secs(20)));
1008 assert_eq!(with(None, None).effective_retention().unwrap(), None);
1009 let declared = ApplyWith {
1010 retention: Some(secs(60)),
1011 ..with(Some(20), None)
1012 };
1013 assert_eq!(declared.effective_retention().unwrap(), Some(secs(60)));
1014 }
1015
1016 #[test]
1017 fn check_retention_refuses_below_the_lateness_and_accepts_equal() {
1018 let below = ApplyWith {
1020 retention: Some(secs(19)),
1021 ..with(Some(20), None)
1022 };
1023 let err = below.check_retention().unwrap_err();
1024 assert!(err.to_string().contains("must not be below lateness"), "{err}");
1025
1026 let equal = ApplyWith {
1027 retention: Some(secs(20)),
1028 ..with(Some(20), None)
1029 };
1030 assert!(equal.check_retention().is_ok());
1031 assert!(with(Some(20), None).check_retention().is_ok());
1032 }
1033
1034 #[test]
1035 fn reject_retention_refuses_only_a_declared_retention() {
1036 assert!(with(Some(20), None).reject_retention().is_ok());
1039 let declared = ApplyWith {
1040 retention: Some(secs(60)),
1041 ..with(None, None)
1042 };
1043 let err = declared.reject_retention().unwrap_err();
1044 assert!(err.to_string().contains("takes no 'retention'"), "{err}");
1045 }
1046}