Skip to main content

reifydb_sub_flow/operator/window/
operator.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_abi::operator::capabilities::OperatorCapability;
5use reifydb_codec::encoded::shape::RowShape;
6use reifydb_core::{
7	common::{CommitVersion, TimeDomain, WindowKind, WindowSize},
8	error::diagnostic::flow::{flow_window_timestamp_column_not_found, flow_window_timestamp_column_type_mismatch},
9	interface::{catalog::flow::FlowNodeId, change::Change},
10	value::column::columns::Columns,
11	window::engine::{LatePolicy, config::WindowEngineConfig},
12};
13use reifydb_engine::flow::aggregate::AggregateContext;
14use reifydb_routine::routine::registry::Routines;
15use reifydb_rql::expression::Expression;
16use reifydb_runtime::context::RuntimeContext;
17use reifydb_sdk::operator::Tick;
18use reifydb_value::{
19	Result,
20	error::Error,
21	value::{Value, datetime::DateTime, duration::Duration},
22};
23
24use super::{
25	aggregation::Aggregation,
26	rolling::{
27		apply_rolling_engine, apply_rolling_processing_engine, tick_expire_rolling_engine,
28		tick_expire_rolling_processing_engine,
29	},
30	tumbling::{
31		apply_session_engine, apply_sliding_engine, apply_tumbling_engine, tick_expire_engine_windows,
32		tick_expire_session_engine,
33	},
34};
35use crate::{
36	operator::{
37		Operator, OperatorCell,
38		stateful::{raw::RawStatefulOperator, row::RowNumberProvider, window::WindowStateful},
39	},
40	transaction::FlowTransaction,
41};
42
43pub struct WindowConfig {
44	pub parent: OperatorCell,
45	pub node: FlowNodeId,
46	pub kind: WindowKind,
47	pub group_by: Vec<Expression>,
48	pub aggregations: Vec<Expression>,
49	pub ts: Option<String>,
50	pub runtime_context: RuntimeContext,
51	pub routines: Routines,
52	pub late_policy: LatePolicy,
53	pub lateness: Option<Duration>,
54	pub state_cache_size: Option<usize>,
55	pub internal_state_cache_size: Option<usize>,
56}
57
58pub struct WindowOperator {
59	pub core: Aggregation,
60	pub kind: WindowKind,
61	pub ts: Option<String>,
62
63	pub late_policy: LatePolicy,
64	pub lateness: Option<Duration>,
65	pub state_cache_size: Option<usize>,
66	pub internal_state_cache_size: Option<usize>,
67	pub layout: RowShape,
68	pub row_number_provider: RowNumberProvider,
69}
70
71impl WindowOperator {
72	pub fn new(config: WindowConfig) -> Self {
73		let core = Aggregation::new(
74			config.node,
75			config.parent,
76			config.group_by,
77			config.aggregations,
78			config.routines,
79			config.runtime_context,
80			AggregateContext::Windowed,
81		);
82		Self {
83			core,
84			kind: config.kind,
85			ts: config.ts,
86			late_policy: config.late_policy,
87			lateness: config.lateness,
88			state_cache_size: config.state_cache_size,
89			internal_state_cache_size: config.internal_state_cache_size,
90			layout: RowShape::operator_state(),
91			row_number_provider: RowNumberProvider::new(config.node),
92		}
93	}
94
95	pub(crate) fn engine_config(&self) -> WindowEngineConfig {
96		let mut builder = WindowEngineConfig::builder().late_policy(self.late_policy);
97		if let Some(capacity) = self.state_cache_size {
98			builder = builder.state_cache_capacity(capacity);
99		}
100		if let Some(capacity) = self.internal_state_cache_size {
101			builder = builder.internal_state_cache_capacity(capacity);
102		}
103		builder.build()
104	}
105
106	pub fn is_count_based(&self) -> bool {
107		self.kind.size().is_some_and(|m| m.is_count())
108	}
109
110	pub fn sealing_lateness(&self) -> Option<Duration> {
111		match self.kind.time() {
112			TimeDomain::Event if !self.is_count_based() => self.lateness,
113			_ => None,
114		}
115	}
116
117	pub fn is_rolling(&self) -> bool {
118		matches!(self.kind, WindowKind::Rolling { .. })
119	}
120
121	pub fn size_duration(&self) -> Option<Duration> {
122		self.kind.size().and_then(|m| m.as_duration())
123	}
124
125	pub fn size_count(&self) -> Option<u64> {
126		self.kind.size().and_then(|m| m.as_count())
127	}
128
129	pub fn resolve_event_timestamps(&self, columns: &Columns, row_count: usize) -> Result<Vec<u64>> {
130		if row_count == 0 {
131			return Ok(Vec::new());
132		}
133		match (self.kind.time(), &self.ts) {
134			(TimeDomain::Event, Some(ts_col)) => {
135				let col = columns.column(ts_col).ok_or_else(|| {
136					Error(Box::new(flow_window_timestamp_column_not_found(ts_col)))
137				})?;
138				let mut timestamps = Vec::with_capacity(row_count);
139				for i in 0..row_count {
140					match col.data().get_value(i) {
141						Value::DateTime(dt) => timestamps.push(dt.timestamp_millis() as u64),
142						other => {
143							return Err(Error(Box::new(
144								flow_window_timestamp_column_type_mismatch(
145									ts_col,
146									other.get_type(),
147								),
148							)));
149						}
150					}
151				}
152				Ok(timestamps)
153			}
154			_ => {
155				let now = self.core.current_timestamp();
156				Ok(vec![now; row_count])
157			}
158		}
159	}
160}
161
162impl RawStatefulOperator for WindowOperator {}
163
164impl WindowStateful for WindowOperator {
165	fn layout(&self) -> RowShape {
166		self.layout.clone()
167	}
168}
169
170impl Operator for WindowOperator {
171	fn id(&self) -> FlowNodeId {
172		self.core.node
173	}
174
175	fn capabilities(&self) -> &[OperatorCapability] {
176		OperatorCapability::STANDARD_WITH_TICK
177	}
178
179	fn ticks(&self) -> Option<Duration> {
180		match &self.kind {
181			WindowKind::Tumbling {
182				..
183			}
184			| WindowKind::Sliding {
185				..
186			}
187			| WindowKind::Session {
188				..
189			}
190			| WindowKind::Rolling {
191				size: WindowSize::Duration(_),
192				..
193			} => Some(Duration::from_seconds(1).unwrap()),
194			WindowKind::Rolling {
195				size: WindowSize::Count(_),
196				..
197			} => None,
198		}
199	}
200
201	fn apply(&self, txn: &mut FlowTransaction, change: Change) -> Result<Change> {
202		match &self.kind {
203			WindowKind::Tumbling {
204				..
205			} => apply_tumbling_engine(self, txn, change),
206			WindowKind::Sliding {
207				..
208			} => apply_sliding_engine(self, txn, change),
209			WindowKind::Rolling {
210				..
211			} => {
212				if self.is_rolling_processing() {
213					apply_rolling_processing_engine(self, txn, change)
214				} else {
215					apply_rolling_engine(self, txn, change)
216				}
217			}
218			WindowKind::Session {
219				..
220			} => apply_session_engine(self, txn, change),
221		}
222	}
223
224	fn tick(&self, txn: &mut FlowTransaction, tick: Tick) -> Result<Option<Change>> {
225		let current_timestamp = tick.now.to_nanos() / 1_000_000;
226		let diffs = match &self.kind {
227			WindowKind::Tumbling {
228				..
229			}
230			| WindowKind::Sliding {
231				..
232			} => tick_expire_engine_windows(self, txn, current_timestamp)?,
233			WindowKind::Rolling {
234				size: WindowSize::Duration(_),
235				..
236			} if self.is_rolling_processing() => tick_expire_rolling_processing_engine(self, txn, current_timestamp)?,
237			WindowKind::Rolling {
238				size: WindowSize::Duration(_),
239				..
240			} => tick_expire_rolling_engine(self, txn, current_timestamp)?,
241			WindowKind::Session {
242				..
243			} => tick_expire_session_engine(self, txn, current_timestamp)?,
244			_ => vec![],
245		};
246
247		if diffs.is_empty() {
248			Ok(None)
249		} else {
250			Ok(Some(Change::from_flow(
251				self.core.node,
252				CommitVersion(0),
253				diffs,
254				DateTime::from_nanos(self.core.runtime_context.clock.now_nanos()),
255			)))
256		}
257	}
258}