Skip to main content

reifydb_flow/operator/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_core::interface::change::Change;
5#[cfg(feature = "runtime")]
6use reifydb_core::{
7	interface::{catalog::flow::OperatorId, flow::OperatorCapability},
8	metrics::heap::OperatorSample,
9	value::column::columns::Columns,
10};
11#[cfg(feature = "runtime")]
12use reifydb_value::Result;
13use reifydb_value::value::datetime::DateTime;
14#[cfg(any(feature = "runtime", all(reifydb_target = "host", not(reifydb_dst))))]
15use reifydb_value::value::duration::Duration;
16
17#[cfg(feature = "runtime")]
18use crate::{operator::host::HostContext, timer::Timer};
19
20#[cfg(all(reifydb_target = "host", not(reifydb_dst)))]
21pub fn scale_from_millis(span: Option<u64>) -> Option<Duration> {
22	span.filter(|millis| *millis > 0)
23		.and_then(|millis| i64::try_from(millis).ok())
24		.and_then(|millis| Duration::from_milliseconds(millis).ok())
25}
26
27#[cfg(feature = "runtime")]
28pub mod aggregation;
29#[cfg(feature = "runtime")]
30pub mod append;
31#[cfg(feature = "runtime")]
32pub mod apply;
33#[cfg(feature = "runtime")]
34pub mod distinct;
35#[cfg(feature = "runtime")]
36pub mod drops;
37#[cfg(feature = "runtime")]
38pub mod extend;
39#[cfg(feature = "runtime")]
40pub mod filter;
41#[cfg(feature = "runtime")]
42pub mod gate;
43#[cfg(feature = "runtime")]
44pub mod guard;
45#[cfg(feature = "runtime")]
46pub mod host;
47#[cfg(feature = "runtime")]
48pub mod join;
49#[cfg(feature = "runtime")]
50pub mod map;
51#[cfg(feature = "runtime")]
52pub mod metrics;
53#[cfg(feature = "runtime")]
54pub mod provider;
55#[cfg(feature = "runtime")]
56pub mod scan;
57#[cfg(feature = "runtime")]
58pub mod sink;
59#[cfg(feature = "runtime")]
60pub mod sort;
61pub mod state;
62pub mod state_access;
63#[cfg(feature = "runtime")]
64pub mod take;
65#[cfg(feature = "runtime")]
66pub mod window;
67
68#[cfg(feature = "runtime")]
69pub trait HostOperator: Send {
70	fn id(&self) -> OperatorId;
71
72	fn capabilities(&self) -> &[OperatorCapability];
73
74	fn apply(&mut self, host: &mut dyn HostContext, change: Change) -> Result<Change>;
75
76	fn on_timer(&mut self, _host: &mut dyn HostContext, _timer: Timer) -> Result<Option<Change>> {
77		Ok(None)
78	}
79
80	fn seal_span(&self) -> Option<Duration> {
81		None
82	}
83
84	fn sample(&self) -> Option<OperatorSample> {
85		None
86	}
87
88	fn output_schema(&self) -> Option<Columns> {
89		None
90	}
91}
92
93#[cfg(feature = "runtime")]
94pub type BoxedHostOperator = Box<dyn HostOperator>;
95
96pub fn max_input_time(change: &Change) -> Option<DateTime> {
97	change.diffs
98		.iter()
99		.filter_map(|diff| diff.post().or_else(|| diff.pre()))
100		.flat_map(|columns| columns.time().iter().copied())
101		.max()
102}
103
104#[cfg_attr(not(feature = "runtime"), allow(dead_code))]
105pub(crate) fn stamp_output_time(change: &mut Change, inherited: Option<DateTime>) {
106	let Some(inherited) = inherited else {
107		return;
108	};
109	for diff in change.diffs.iter_mut() {
110		for columns in diff.columns_mut() {
111			let stamped: Vec<DateTime> = columns.time().iter().map(|own| (*own).min(inherited)).collect();
112			columns.system.set_time(stamped);
113		}
114	}
115}
116
117#[cfg(test)]
118mod substrate_stamping_tests {
119	use reifydb_core::{
120		common::CommitVersion,
121		interface::{
122			catalog::flow::OperatorId,
123			change::{Diff, Diffs},
124		},
125		value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns},
126	};
127	use reifydb_value::{
128		factory::time::at_millis,
129		fragment::Fragment,
130		value::{row_number::RowNumber, system_columns::SystemColumns},
131	};
132
133	use super::*;
134
135	fn columns(times: &[DateTime]) -> Columns {
136		let n = times.len();
137		Columns::with_system(
138			vec![ColumnWithName::new(
139				Fragment::internal("v"),
140				ColumnBuffer::int4((0..n as i32).collect::<Vec<_>>()),
141			)],
142			SystemColumns::new(
143				(1..=n as u64).map(RowNumber).collect(),
144				Vec::new(),
145				vec![at_millis(0); n],
146				vec![at_millis(0); n],
147				times.to_vec(),
148			),
149		)
150	}
151
152	fn untimed_columns(n: usize) -> Columns {
153		Columns::with_system(
154			vec![ColumnWithName::new(
155				Fragment::internal("v"),
156				ColumnBuffer::int4((0..n as i32).collect::<Vec<_>>()),
157			)],
158			SystemColumns::new(
159				(1..=n as u64).map(RowNumber).collect(),
160				Vec::new(),
161				vec![at_millis(0); n],
162				vec![at_millis(0); n],
163				Vec::new(),
164			),
165		)
166	}
167
168	fn change(diffs: Diffs) -> Change {
169		Change::from_flow(OperatorId(1), CommitVersion(1), diffs, at_millis(0))
170	}
171
172	#[test]
173	fn the_substrate_stamps_output_with_the_max_input_time() {
174		// The substrate derives the stamp from the input, never from the operator, which is what
175		// lets a guest operator stay oblivious to #time without breaking the clock.
176		let mut diffs = Diffs::new();
177		diffs.push(Diff::insert(columns(&[at_millis(1_000), at_millis(9_000), at_millis(5_000)])));
178
179		assert_eq!(max_input_time(&change(diffs)), Some(at_millis(9_000)));
180	}
181
182	#[test]
183	fn an_operator_cannot_influence_its_own_output_time() {
184		// Stamping above the inputs would advance the flow watermark and seal another operator's
185		// state early, so the clamp is one-directional: it pulls a row down to the inherited instant
186		// and leaves a genuinely earlier row where it is. Clamping both directions would drag a
187		// backfilled row forward into a window it does not belong to.
188		let mut produced = Diffs::new();
189		produced.push(Diff::insert(columns(&[at_millis(999_999), at_millis(1_000)])));
190		let mut out = change(produced);
191
192		stamp_output_time(&mut out, Some(at_millis(4_000)));
193
194		assert_eq!(
195			out.diffs[0].post().unwrap().time().to_vec(),
196			vec![at_millis(4_000), at_millis(1_000)],
197			"a row above the inherited instant is pulled down; one below keeps its own"
198		);
199	}
200
201	#[test]
202	fn both_sides_of_an_update_are_stamped() {
203		// A pre image left above the inherited instant would advance the watermark through the
204		// pre side alone and make a retention decision see two times for one row.
205		let mut produced = Diffs::new();
206		produced.push(Diff::update(columns(&[at_millis(9_000)]), columns(&[at_millis(10_000)])));
207		let mut out = change(produced);
208
209		stamp_output_time(&mut out, Some(at_millis(7_000)));
210
211		assert_eq!(out.diffs[0].pre().unwrap().time().to_vec(), vec![at_millis(7_000)]);
212		assert_eq!(out.diffs[0].post().unwrap().time().to_vec(), vec![at_millis(7_000)]);
213	}
214
215	#[test]
216	fn a_fan_out_operator_has_every_emitted_row_stamped() {
217		// Every row sits above the inherited instant, so a clamp that visited only the first
218		// would still pass if the rest were left alone.
219		let mut produced = Diffs::new();
220		produced.push(Diff::insert(columns(&[
221			at_millis(9_000),
222			at_millis(10_000),
223			at_millis(11_000),
224			at_millis(12_000),
225			at_millis(13_000),
226		])));
227		let mut out = change(produced);
228
229		stamp_output_time(&mut out, Some(at_millis(8_000)));
230
231		assert_eq!(out.diffs[0].post().unwrap().time().to_vec(), vec![at_millis(8_000); 5]);
232	}
233
234	#[test]
235	fn an_empty_input_leaves_the_output_untouched() {
236		// With nothing to inherit, stamping anyway would write an epoch time that reads as 1970
237		// and is evicted on sight.
238		let empty = change(Diffs::new());
239		assert_eq!(max_input_time(&empty), None);
240
241		let mut produced = Diffs::new();
242		produced.push(Diff::insert(columns(&[at_millis(3_000)])));
243		let mut out = change(produced);
244
245		stamp_output_time(&mut out, None);
246
247		assert_eq!(out.diffs[0].post().unwrap().time().to_vec(), vec![at_millis(3_000)]);
248	}
249
250	#[test]
251	fn an_operator_stamping_above_its_inputs_is_still_overwritten() {
252		// No relaxation of the clamp may reach the above-inputs direction, and the comparison is
253		// strict: one nanosecond over is enough.
254		let inherited = at_millis(5_000);
255		let one_nano_above = DateTime::from_nanos(inherited.to_nanos() + 1);
256
257		let mut produced = Diffs::new();
258		produced.push(Diff::insert(columns(&[one_nano_above])));
259		let mut out = change(produced);
260
261		stamp_output_time(&mut out, Some(inherited));
262
263		assert_eq!(out.diffs[0].post().unwrap().time().to_vec(), vec![inherited]);
264	}
265
266	#[test]
267	fn an_operator_stamping_at_or_below_its_inputs_keeps_its_stamp() {
268		// A window stamps the bucket START, at or below every event it consumed; overwriting it
269		// costs replay stability. Equality must survive - a bucket start can coincide with its
270		// only event.
271		let inherited = at_millis(5_000);
272
273		let mut produced = Diffs::new();
274		produced.push(Diff::insert(columns(&[at_millis(1_000), inherited])));
275		let mut out = change(produced);
276
277		stamp_output_time(&mut out, Some(inherited));
278
279		assert_eq!(
280			out.diffs[0].post().unwrap().time().to_vec(),
281			vec![at_millis(1_000), inherited],
282			"below survives, and equal counts as below"
283		);
284	}
285
286	#[test]
287	fn a_row_stamped_at_the_epoch_keeps_its_own_instant() {
288		// The epoch is an ordinary coordinate here, not a marker for "unstamped". Substituting the
289		// inherited instant for it would silently re-date every row a source legitimately placed in
290		// 1970, and the two cases are already distinguishable without inspecting the value.
291		let mut produced = Diffs::new();
292		produced.push(Diff::insert(columns(&[DateTime::default()])));
293		let mut out = change(produced);
294
295		stamp_output_time(&mut out, Some(at_millis(6_000)));
296
297		assert_eq!(out.diffs[0].post().unwrap().time().to_vec(), vec![DateTime::default()]);
298	}
299
300	#[test]
301	fn a_time_less_batch_stays_time_less_through_stamping() {
302		// A source with no time domain emits rows carrying no #time, and stamping must not invent
303		// one for them. Filling the sidecar here would give a time-less object a clock it never
304		// declared and let its rows start moving watermarks.
305		let mut produced = Diffs::new();
306		produced.push(Diff::insert(untimed_columns(3)));
307		let mut out = change(produced);
308
309		stamp_output_time(&mut out, Some(at_millis(6_000)));
310
311		assert!(out.diffs[0].post().unwrap().time().is_empty(), "#time must stay absent");
312	}
313
314	#[test]
315	fn a_window_row_carries_its_window_start_through_the_apply_wrapper() {
316		// Both paths a guest window emits from inherit an instant at or after the bucket start,
317		// so one rule carries the start through without a special case - which is what lets a
318		// consumer read #time instead of a window_start data column.
319		let window_start = at_millis(60_000);
320		let newest_event_in_bucket = at_millis(119_000);
321		let seal_fires_at = at_millis(120_001);
322
323		let mut on_apply = change({
324			let mut d = Diffs::new();
325			d.push(Diff::insert(columns(&[window_start])));
326			d
327		});
328		stamp_output_time(&mut on_apply, Some(newest_event_in_bucket));
329		assert_eq!(on_apply.diffs[0].post().unwrap().time().to_vec(), vec![window_start]);
330
331		let mut on_timer = change({
332			let mut d = Diffs::new();
333			d.push(Diff::insert(columns(&[window_start])));
334			d
335		});
336		stamp_output_time(&mut on_timer, Some(seal_fires_at));
337		assert_eq!(on_timer.diffs[0].post().unwrap().time().to_vec(), vec![window_start]);
338	}
339
340	#[test]
341	fn the_max_spans_every_diff_in_the_batch() {
342		// An operator fed several diffs must inherit the latest instant anywhere in the batch,
343		// not the first diff's.
344		let mut diffs = Diffs::new();
345		diffs.push(Diff::insert(columns(&[at_millis(1_000)])));
346		diffs.push(Diff::insert(columns(&[at_millis(12_000)])));
347		diffs.push(Diff::insert(columns(&[at_millis(3_000)])));
348
349		assert_eq!(max_input_time(&change(diffs)), Some(at_millis(12_000)));
350	}
351}