Skip to main content

reifydb_flow/window/
coord.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::fmt::Debug;
5
6use reifydb_core::{
7	metrics::heap::HeapSize,
8	state::timer::{StateStore, TimerStore},
9};
10use reifydb_macro::operator_state;
11use reifydb_value::{
12	Result,
13	value::{datetime::DateTime, duration::Duration, row_number::RowNumber},
14};
15
16use crate::{
17	operator::state::seal::{
18		coord::{Coord, IsZero},
19		domain::SealDomain,
20		ledger::SealLedger,
21	},
22	window::span::Slot,
23};
24
25pub trait TimeStamped {
26	fn row_time(&self) -> DateTime;
27}
28
29impl TimeStamped for DateTime {
30	fn row_time(&self) -> DateTime {
31		*self
32	}
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct EventCoord(DateTime);
37
38impl EventCoord {
39	pub fn of(row: &impl TimeStamped) -> Self {
40		Self(row.row_time())
41	}
42
43	pub fn at(self) -> DateTime {
44		self.0
45	}
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
49pub struct RowSpan {
50	rows: u64,
51}
52
53impl RowSpan {
54	pub const ZERO: Self = Self {
55		rows: 0,
56	};
57
58	pub fn of(rows: u64) -> Self {
59		Self {
60			rows,
61		}
62	}
63
64	pub fn rows(self) -> u64 {
65		self.rows
66	}
67}
68
69impl IsZero for RowSpan {
70	#[inline]
71	fn is_zero(&self) -> bool {
72		self.rows == 0
73	}
74}
75
76#[operator_state]
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub struct OrdinalCoord {
79	ordinal: u64,
80}
81
82impl OrdinalCoord {
83	pub fn from_arrival_counter(ordinal: u64) -> Self {
84		Self {
85			ordinal,
86		}
87	}
88
89	pub fn from_row_number(row_number: RowNumber) -> Self {
90		Self {
91			ordinal: row_number.0,
92		}
93	}
94
95	pub fn value(self) -> u64 {
96		self.ordinal
97	}
98}
99
100impl HeapSize for OrdinalCoord {
101	fn heap_size(&self) -> usize {
102		0
103	}
104}
105
106impl Coord for OrdinalCoord {
107	type Span = RowSpan;
108
109	const MAX: Self = Self {
110		ordinal: u64::MAX,
111	};
112
113	fn saturating_sub_span(self, span: RowSpan) -> Self {
114		Self {
115			ordinal: self.ordinal.saturating_sub(span.rows),
116		}
117	}
118
119	fn checked_sub_span(self, span: RowSpan) -> Option<Self> {
120		self.ordinal.checked_sub(span.rows).map(|ordinal| Self {
121			ordinal,
122		})
123	}
124
125	fn add_span(self, span: RowSpan) -> Self {
126		Self {
127			ordinal: self.ordinal + span.rows,
128		}
129	}
130
131	fn floor_to(self, span: RowSpan) -> Self {
132		Self {
133			ordinal: self.ordinal - (self.ordinal % span.rows),
134		}
135	}
136
137	fn span_since(self, earlier: Self) -> RowSpan {
138		RowSpan {
139			rows: self.ordinal - earlier.ordinal,
140		}
141	}
142
143	fn to_order(self) -> u64 {
144		self.ordinal
145	}
146
147	fn from_order(order: u64) -> Self {
148		Self {
149			ordinal: order,
150		}
151	}
152
153	fn span_millis(_span: RowSpan) -> Option<u64> {
154		None
155	}
156}
157
158impl SealDomain for OrdinalCoord {
159	type Lateness = RowSpan;
160
161	fn arms_timer() -> bool {
162		false
163	}
164
165	fn lateness_duration(_lateness: RowSpan) -> Option<Duration> {
166		None
167	}
168
169	fn observe(store: &mut (impl StateStore + TimerStore), newest: Self, _lateness: RowSpan) -> Result<()> {
170		SealLedger::observe(store, newest.to_order())?;
171		Ok(())
172	}
173
174	fn frontier(store: &mut (impl StateStore + TimerStore)) -> Result<Self> {
175		Ok(Self::from_order(SealLedger::read_order(store)?.unwrap_or(0)))
176	}
177
178	fn horizon(frontier: Self, lateness: RowSpan) -> Self {
179		frontier.saturating_sub_span(lateness)
180	}
181}
182
183impl Slot for OrdinalCoord {
184	type Coord = OrdinalCoord;
185
186	fn order_key(&self) -> OrdinalCoord {
187		*self
188	}
189
190	fn from_order_key(coord: OrdinalCoord) -> Self {
191		coord
192	}
193}
194
195#[cfg(test)]
196mod tests {
197	use reifydb_codec::row::operator::state::encode;
198
199	use super::*;
200	use crate::operator::state::mock::MockStore;
201
202	struct Row {
203		time: DateTime,
204		other_column: DateTime,
205	}
206
207	impl TimeStamped for Row {
208		fn row_time(&self) -> DateTime {
209			self.time
210		}
211	}
212
213	#[test]
214	fn an_event_coordinate_can_only_come_from_the_row_time() {
215		// `of` is the only constructor, so a coordinate cannot come from a data column, a config
216		// value or a clock read. The row's second DateTime is what an operator would be tempted to
217		// bucket by, keying the window on something the substrate can neither see nor seal against.
218		let row = Row {
219			time: DateTime::from_millis(5_000),
220			other_column: DateTime::from_millis(9_999),
221		};
222
223		assert_eq!(EventCoord::of(&row).at(), DateTime::from_millis(5_000));
224		assert_ne!(EventCoord::of(&row).at(), row.other_column);
225	}
226
227	#[test]
228	fn event_coordinates_order_by_instant() {
229		// Ordering is what the seal ledger and the admissible-span comparison are built
230		// on, so it must be the instant's order and nothing else.
231		let early = EventCoord::of(&DateTime::from_millis(1));
232		let late = EventCoord::of(&DateTime::from_millis(2));
233
234		assert!(early < late);
235	}
236
237	#[test]
238	fn an_ordinal_encodes_to_the_same_bytes_as_the_bare_count_it_replaced() {
239		// A changed persisted layout is silent: stored buffer keys get reinterpreted, not rejected.
240		let value = 0x0123_4567_89AB_CDEFu64;
241
242		let wrapped = encode(&OrdinalCoord::from_arrival_counter(value)).expect("encode");
243		let bare = encode(&value).expect("encode");
244
245		assert_eq!(wrapped.body(), bare.body(), "the newtype changed the persisted layout");
246	}
247
248	#[test]
249	fn ordinal_arithmetic_counts_rows_and_refuses_to_answer_in_milliseconds() {
250		// A span here is rows, not milliseconds. span_millis answering Some would let a row count
251		// reach the seal horizon as a duration.
252		let coord = OrdinalCoord::from_arrival_counter(100);
253
254		assert_eq!(coord.saturating_sub_span(RowSpan::of(64)), OrdinalCoord::from_arrival_counter(36));
255		assert_eq!(coord.add_span(RowSpan::of(5)), OrdinalCoord::from_arrival_counter(105));
256		assert_eq!(coord.span_since(OrdinalCoord::from_arrival_counter(60)), RowSpan::of(40));
257		assert_eq!(<OrdinalCoord as Coord>::span_millis(RowSpan::of(64)), None);
258	}
259
260	#[test]
261	fn an_ordinal_below_its_own_span_has_no_earlier_coordinate_rather_than_wrapping() {
262		// Wrapping below zero lands near u64::MAX and evicts the whole buffer on the first pass.
263		let coord = OrdinalCoord::from_arrival_counter(10);
264
265		assert_eq!(coord.checked_sub_span(RowSpan::of(11)), None);
266		assert_eq!(coord.checked_sub_span(RowSpan::of(10)), Some(OrdinalCoord::from_arrival_counter(0)));
267		assert_eq!(coord.saturating_sub_span(RowSpan::of(11)), OrdinalCoord::from_arrival_counter(0));
268	}
269
270	#[test]
271	fn a_row_ordinal_seals_without_the_wheel_and_declares_no_wall_clock_lateness() {
272		// a row count fed to duration arithmetic lands just past the epoch, fires at once and rearms forever
273		assert!(!<OrdinalCoord as SealDomain>::arms_timer());
274		assert_eq!(<OrdinalCoord as SealDomain>::lateness_duration(RowSpan::of(64)), None);
275	}
276
277	#[test]
278	fn an_ordinal_frontier_moves_inline_from_the_batch_and_only_forward() {
279		// nothing arms a timer here, so the batch itself must advance the ledger or the horizon never moves
280		let mut store = MockStore::default();
281
282		assert_eq!(OrdinalCoord::frontier(&mut store).unwrap(), OrdinalCoord::from_arrival_counter(0));
283
284		OrdinalCoord::observe(&mut store, OrdinalCoord::from_arrival_counter(90), RowSpan::ZERO).unwrap();
285		OrdinalCoord::observe(&mut store, OrdinalCoord::from_arrival_counter(30), RowSpan::ZERO).unwrap();
286
287		assert_eq!(OrdinalCoord::frontier(&mut store).unwrap(), OrdinalCoord::from_arrival_counter(90));
288		assert_eq!(
289			OrdinalCoord::horizon(OrdinalCoord::from_arrival_counter(90), RowSpan::of(64)),
290			OrdinalCoord::from_arrival_counter(26)
291		);
292	}
293
294	#[test]
295	fn both_ordinal_sources_produce_the_same_domain() {
296		// An ordinal can be minted from a per-group arrival counter or from a RowNumber, and both
297		// must land in one domain type or each count kind would need its own driver.
298		let minted = OrdinalCoord::from_arrival_counter(7);
299		let from_row = OrdinalCoord::from_row_number(RowNumber(7));
300
301		assert_eq!(minted, from_row);
302		assert_eq!(minted.value(), 7);
303	}
304}