Skip to main content

reifydb_core/
common.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	fmt,
6	fmt::{Display, Formatter},
7	num::ParseIntError,
8	str::FromStr,
9};
10
11use reifydb_value::{Result as ValueResult, value::duration::Duration};
12use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
13
14use crate::{error::CoreError, operator_with::ApplyWith};
15
16#[repr(transparent)]
17#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)]
18pub struct CommitVersion(pub u64);
19
20impl FromStr for CommitVersion {
21	type Err = ParseIntError;
22
23	fn from_str(s: &str) -> Result<Self, Self::Err> {
24		Ok(CommitVersion(u64::from_str(s)?))
25	}
26}
27
28impl Display for CommitVersion {
29	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30		Display::fmt(&self.0, f)
31	}
32}
33
34impl PartialEq<i32> for CommitVersion {
35	fn eq(&self, other: &i32) -> bool {
36		self.0 == *other as u64
37	}
38}
39
40impl PartialEq<CommitVersion> for i32 {
41	fn eq(&self, other: &CommitVersion) -> bool {
42		*self as u64 == other.0
43	}
44}
45
46impl PartialEq<u64> for CommitVersion {
47	fn eq(&self, other: &u64) -> bool {
48		self.0.eq(other)
49	}
50}
51
52impl From<CommitVersion> for u64 {
53	fn from(value: CommitVersion) -> Self {
54		value.0
55	}
56}
57
58impl From<i32> for CommitVersion {
59	fn from(value: i32) -> Self {
60		Self(value as u64)
61	}
62}
63
64impl From<u64> for CommitVersion {
65	fn from(value: u64) -> Self {
66		Self(value)
67	}
68}
69
70impl Serialize for CommitVersion {
71	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72	where
73		S: Serializer,
74	{
75		serializer.serialize_u64(self.0)
76	}
77}
78
79impl<'de> Deserialize<'de> for CommitVersion {
80	fn deserialize<D>(deserializer: D) -> Result<CommitVersion, D::Error>
81	where
82		D: Deserializer<'de>,
83	{
84		struct U64Visitor;
85
86		impl Visitor<'_> for U64Visitor {
87			type Value = CommitVersion;
88
89			fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
90				formatter.write_str("an unsigned 64-bit number")
91			}
92
93			fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
94				Ok(CommitVersion(value))
95			}
96		}
97
98		deserializer.deserialize_u64(U64Visitor)
99	}
100}
101
102#[repr(transparent)]
103#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash, Serialize, Deserialize)]
104#[serde(transparent)]
105pub struct SourceVersion(pub u64);
106
107impl From<CommitVersion> for SourceVersion {
108	fn from(version: CommitVersion) -> Self {
109		Self(version.0)
110	}
111}
112
113#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
114pub struct ChangeVersion {
115	pub commit: CommitVersion,
116	pub source: SourceVersion,
117}
118
119impl From<CommitVersion> for ChangeVersion {
120	fn from(commit: CommitVersion) -> Self {
121		Self {
122			commit,
123			source: SourceVersion::from(commit),
124		}
125	}
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
129pub enum JoinType {
130	Inner,
131	#[default]
132	Left,
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
136pub enum IndexType {
137	#[default]
138	Index,
139	Unique,
140	Primary,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub enum WindowSize {
145	Duration(Duration),
146	Count(u64),
147}
148
149impl WindowSize {
150	pub fn is_count(&self) -> bool {
151		matches!(self, WindowSize::Count(_))
152	}
153
154	pub fn as_duration(&self) -> Option<Duration> {
155		match self {
156			WindowSize::Duration(d) => Some(*d),
157			_ => None,
158		}
159	}
160
161	pub fn as_count(&self) -> Option<u64> {
162		match self {
163			WindowSize::Count(c) => Some(*c),
164			_ => None,
165		}
166	}
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170pub enum TimeDomain {
171	None,
172	Event,
173	Processing,
174}
175
176impl TimeDomain {
177	pub fn to_u8(self) -> u8 {
178		match self {
179			TimeDomain::None => 0,
180			TimeDomain::Event => 1,
181			TimeDomain::Processing => 2,
182		}
183	}
184
185	pub fn from_u8(value: u8) -> Self {
186		match value {
187			1 => TimeDomain::Event,
188			2 => TimeDomain::Processing,
189			_ => TimeDomain::None,
190		}
191	}
192
193	pub fn as_str(&self) -> &'static str {
194		match self {
195			TimeDomain::None => "none",
196			TimeDomain::Event => "event",
197			TimeDomain::Processing => "processing",
198		}
199	}
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum OperatorClass {
204	Managed,
205	Unmanaged,
206	Nostate,
207	Windowed,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum WindowSizeDomain {
212	Time,
213	Slots,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct WindowRequirements {
218	pub takes_window: bool,
219	pub kinds: &'static [&'static str],
220	pub domain: WindowSizeDomain,
221	pub needs_pane: bool,
222}
223
224impl OperatorClass {
225	pub fn to_u8(self) -> u8 {
226		match self {
227			OperatorClass::Managed => 1,
228			OperatorClass::Unmanaged => 2,
229			OperatorClass::Nostate => 3,
230			OperatorClass::Windowed => 4,
231		}
232	}
233
234	pub fn from_u8(value: u8) -> Option<Self> {
235		match value {
236			1 => Some(OperatorClass::Managed),
237			2 => Some(OperatorClass::Unmanaged),
238			3 => Some(OperatorClass::Nostate),
239			4 => Some(OperatorClass::Windowed),
240			_ => None,
241		}
242	}
243}
244
245impl WindowSizeDomain {
246	pub fn to_u8(self) -> u8 {
247		match self {
248			WindowSizeDomain::Time => 1,
249			WindowSizeDomain::Slots => 2,
250		}
251	}
252
253	pub fn from_u8(value: u8) -> Option<Self> {
254		match value {
255			1 => Some(WindowSizeDomain::Time),
256			2 => Some(WindowSizeDomain::Slots),
257			_ => None,
258		}
259	}
260}
261
262const WINDOW_KIND_NAMES: [&str; 4] = ["tumbling", "sliding", "session", "rolling"];
263
264const WINDOW_KIND_SETS: [&[&str]; 16] = [
265	&[],
266	&["tumbling"],
267	&["sliding"],
268	&["tumbling", "sliding"],
269	&["session"],
270	&["tumbling", "session"],
271	&["sliding", "session"],
272	&["tumbling", "sliding", "session"],
273	&["rolling"],
274	&["tumbling", "rolling"],
275	&["sliding", "rolling"],
276	&["tumbling", "sliding", "rolling"],
277	&["session", "rolling"],
278	&["tumbling", "session", "rolling"],
279	&["sliding", "session", "rolling"],
280	&["tumbling", "sliding", "session", "rolling"],
281];
282
283impl WindowRequirements {
284	pub fn kinds_bitmask(&self) -> Option<u32> {
285		self.kinds.iter().try_fold(0u32, |mask, kind| {
286			WINDOW_KIND_NAMES.iter().position(|name| name == kind).map(|bit| mask | 1 << bit)
287		})
288	}
289
290	pub fn kinds_from_bitmask(mask: u32) -> Option<&'static [&'static str]> {
291		WINDOW_KIND_SETS.get(usize::try_from(mask).ok()?).copied()
292	}
293
294	pub fn check(&self, with: &ApplyWith) -> ValueResult<()> {
295		let kind = match (&with.window, self.takes_window) {
296			(Some(kind), true) => kind,
297			(Some(kind), false) => {
298				return Err(CoreError::OperatorWithWindowNotSupported {
299					kind: kind.name(),
300				}
301				.into());
302			}
303			(None, true) => return Err(CoreError::OperatorWithWindowMissing.into()),
304			(None, false) => return Ok(()),
305		};
306		if !self.kinds.contains(&kind.name()) {
307			return Err(CoreError::OperatorWithWindowKindUnsupported {
308				kind: kind.name(),
309				supported: self.kinds.join(", "),
310			}
311			.into());
312		}
313		match (self.domain, kind.size()) {
314			(WindowSizeDomain::Time, Some(WindowSize::Count(count))) => {
315				return Err(CoreError::OperatorWithWindowSizeCount {
316					count: *count,
317				}
318				.into());
319			}
320			(WindowSizeDomain::Slots, Some(WindowSize::Duration(size))) => {
321				return Err(CoreError::OperatorWithWindowSizeDuration {
322					size: *size,
323				}
324				.into());
325			}
326			_ => {}
327		}
328		if self.needs_pane
329			&& matches!(
330				kind,
331				WindowKind::Rolling {
332					pane: None,
333					..
334				}
335			) {
336			return Err(CoreError::OperatorWithPaneMissing.into());
337		}
338		with.check_session_window()
339	}
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
343pub enum TimeSource {
344	None,
345	Event {
346		ts: String,
347	},
348	Processing,
349}
350
351impl TimeSource {
352	pub fn domain(&self) -> TimeDomain {
353		match self {
354			TimeSource::None => TimeDomain::None,
355			TimeSource::Event {
356				..
357			} => TimeDomain::Event,
358			TimeSource::Processing => TimeDomain::Processing,
359		}
360	}
361
362	pub fn ts(&self) -> Option<&str> {
363		match self {
364			TimeSource::Event {
365				ts,
366			} => Some(ts.as_str()),
367			TimeSource::None | TimeSource::Processing => None,
368		}
369	}
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub enum WindowKind {
374	Tumbling {
375		size: WindowSize,
376	},
377
378	Sliding {
379		size: WindowSize,
380		slide: WindowSize,
381	},
382
383	Rolling {
384		size: WindowSize,
385		#[serde(default)]
386		lag: Option<Duration>,
387		#[serde(default)]
388		pane: Option<Duration>,
389	},
390
391	Session {
392		gap: Duration,
393	},
394}
395
396impl WindowKind {
397	pub fn size(&self) -> Option<&WindowSize> {
398		match self {
399			WindowKind::Tumbling {
400				size,
401				..
402			} => Some(size),
403			WindowKind::Sliding {
404				size,
405				..
406			} => Some(size),
407			WindowKind::Rolling {
408				size,
409				..
410			} => Some(size),
411			WindowKind::Session {
412				..
413			} => None,
414		}
415	}
416
417	pub fn name(&self) -> &'static str {
418		match self {
419			WindowKind::Tumbling {
420				..
421			} => "tumbling",
422			WindowKind::Sliding {
423				..
424			} => "sliding",
425			WindowKind::Rolling {
426				..
427			} => "rolling",
428			WindowKind::Session {
429				..
430			} => "session",
431		}
432	}
433}
434
435#[cfg(test)]
436mod tests {
437	use super::*;
438
439	#[test]
440	fn every_operator_class_round_trips_through_its_byte_and_zero_is_refused() {
441		// a zeroed or foreign class byte read as a real class would admit a guest to the wrong keyspaces
442		for class in [
443			OperatorClass::Managed,
444			OperatorClass::Unmanaged,
445			OperatorClass::Nostate,
446			OperatorClass::Windowed,
447		] {
448			assert_eq!(OperatorClass::from_u8(class.to_u8()), Some(class));
449		}
450		assert_eq!(OperatorClass::from_u8(0), None);
451		assert_eq!(OperatorClass::from_u8(5), None);
452	}
453
454	#[test]
455	fn every_window_size_domain_round_trips_through_its_byte_and_zero_is_refused() {
456		// a domain read backwards fires FLOW_068 and FLOW_069 on exactly the views that are correct
457		for domain in [WindowSizeDomain::Time, WindowSizeDomain::Slots] {
458			assert_eq!(WindowSizeDomain::from_u8(domain.to_u8()), Some(domain));
459		}
460		assert_eq!(WindowSizeDomain::from_u8(0), None);
461		assert_eq!(WindowSizeDomain::from_u8(3), None);
462	}
463
464	fn requirements(kinds: &'static [&'static str]) -> WindowRequirements {
465		WindowRequirements {
466			takes_window: true,
467			kinds,
468			domain: WindowSizeDomain::Time,
469			needs_pane: false,
470		}
471	}
472
473	#[test]
474	fn every_published_kind_list_survives_the_bitmask() {
475		// a swapped bit would let CREATE accept a kind the driver cannot run and refuse one it can
476		for kinds in [
477			&["tumbling"][..],
478			&["rolling"],
479			&["tumbling", "sliding"],
480			&["tumbling", "sliding", "rolling"],
481			&["tumbling", "sliding", "session"],
482			&["tumbling", "sliding", "session", "rolling"],
483			&[],
484		] {
485			let mask = requirements(kinds).kinds_bitmask().unwrap();
486			assert_eq!(WindowRequirements::kinds_from_bitmask(mask), Some(kinds));
487		}
488	}
489
490	#[test]
491	fn each_kind_owns_exactly_one_bit() {
492		// two kinds sharing a bit would make the host unable to tell them apart
493		let bits: Vec<u32> = [&["tumbling"][..], &["sliding"], &["session"], &["rolling"]]
494			.into_iter()
495			.map(|kinds| requirements(kinds).kinds_bitmask().unwrap())
496			.collect();
497		assert_eq!(bits, vec![1, 2, 4, 8]);
498	}
499
500	#[test]
501	fn an_unknown_kind_name_or_bit_is_refused() {
502		// a mask the host cannot map back must fail the load, not shrink to the kinds it knows
503		assert_eq!(requirements(&["hopping"]).kinds_bitmask(), None);
504		assert_eq!(requirements(&["tumbling", "hopping"]).kinds_bitmask(), None);
505		assert_eq!(WindowRequirements::kinds_from_bitmask(16), None);
506		assert_eq!(WindowRequirements::kinds_from_bitmask(u32::MAX), None);
507	}
508}