Skip to main content

reifydb_rql/flow/
node.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_core::{
5	common::{JoinType, WindowKind},
6	interface::catalog::{
7		flow::{FlowEdgeId, FlowId, FlowNodeId},
8		id::{RingBufferId, SeriesId, SubscriptionId, TableId, ViewId},
9		series::SeriesKey,
10		shape::ShapeId,
11	},
12	sort::SortKey,
13};
14use reifydb_value::value::{dictionary::DictionaryId, duration::Duration};
15use serde::{Deserialize, Serialize};
16
17use crate::expression::Expression;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum FlowNodeType {
21	SourceInlineData {},
22	SourceTable {
23		table: TableId,
24	},
25	SourceView {
26		view: ViewId,
27	},
28	SourceFlow {
29		flow: FlowId,
30	},
31	SourceRingBuffer {
32		ringbuffer: RingBufferId,
33	},
34	SourceSeries {
35		series: SeriesId,
36	},
37	Filter {
38		conditions: Vec<Expression>,
39	},
40	Gate {
41		conditions: Vec<Expression>,
42	},
43	Map {
44		expressions: Vec<Expression>,
45	},
46	Extend {
47		expressions: Vec<Expression>,
48	},
49	Join {
50		join_type: JoinType,
51		left: Vec<Expression>,
52		right: Vec<Expression>,
53		alias: Option<String>,
54		#[serde(default)]
55		snapshot: bool,
56		#[serde(default)]
57		natural: bool,
58		#[serde(default)]
59		latest: bool,
60	},
61	Aggregate {
62		by: Vec<Expression>,
63		map: Vec<Expression>,
64	},
65	Append {},
66	Sort {
67		by: Vec<SortKey>,
68	},
69	Take {
70		limit: usize,
71	},
72	Distinct {
73		expressions: Vec<Expression>,
74	},
75	Apply {
76		operator: String,
77		expressions: Vec<Expression>,
78	},
79	SinkTableView {
80		view: ViewId,
81		table: TableId,
82	},
83	SinkRingBufferView {
84		view: ViewId,
85		ringbuffer: RingBufferId,
86		capacity: u64,
87		propagate_evictions: bool,
88	},
89	SinkSeriesView {
90		view: ViewId,
91		series: SeriesId,
92		key: SeriesKey,
93	},
94	SinkSubscription {
95		subscription: SubscriptionId,
96	},
97	Window {
98		kind: WindowKind,
99		group_by: Vec<Expression>,
100		aggregations: Vec<Expression>,
101		ts: Option<String>,
102		lateness: Option<Duration>,
103		state_cache_size: Option<usize>,
104		internal_state_cache_size: Option<usize>,
105	},
106	SourceDictionary {
107		dictionary: DictionaryId,
108	},
109}
110
111impl FlowNodeType {
112	pub fn ticks(&self) -> bool {
113		matches!(
114			self,
115			FlowNodeType::Append { .. }
116				| FlowNodeType::Distinct { .. }
117				| FlowNodeType::Window { .. }
118				| FlowNodeType::Apply { .. } | FlowNodeType::Join { .. }
119		)
120	}
121
122	pub fn label(&self) -> String {
123		match self {
124			FlowNodeType::SourceInlineData {
125				..
126			} => "SourceInlineData".into(),
127			FlowNodeType::SourceTable {
128				..
129			} => "SourceTable".into(),
130			FlowNodeType::SourceView {
131				..
132			} => "SourceView".into(),
133			FlowNodeType::SourceFlow {
134				..
135			} => "SourceFlow".into(),
136			FlowNodeType::SourceRingBuffer {
137				..
138			} => "SourceRingBuffer".into(),
139			FlowNodeType::SourceSeries {
140				..
141			} => "SourceSeries".into(),
142			FlowNodeType::SourceDictionary {
143				..
144			} => "SourceDictionary".into(),
145			FlowNodeType::Filter {
146				..
147			} => "Filter".into(),
148			FlowNodeType::Gate {
149				..
150			} => "Gate".into(),
151			FlowNodeType::Map {
152				..
153			} => "Map".into(),
154			FlowNodeType::Extend {
155				..
156			} => "Extend".into(),
157			FlowNodeType::Join {
158				..
159			} => "Join".into(),
160			FlowNodeType::Aggregate {
161				..
162			} => "Aggregate".into(),
163			FlowNodeType::Append {
164				..
165			} => "Append".into(),
166			FlowNodeType::Sort {
167				..
168			} => "Sort".into(),
169			FlowNodeType::Take {
170				..
171			} => "Take".into(),
172			FlowNodeType::Distinct {
173				..
174			} => "Distinct".into(),
175			FlowNodeType::Apply {
176				operator,
177				..
178			} => format!("Apply({})", operator),
179			FlowNodeType::SinkTableView {
180				..
181			} => "SinkTableView".into(),
182			FlowNodeType::SinkRingBufferView {
183				..
184			} => "SinkRingBufferView".into(),
185			FlowNodeType::SinkSeriesView {
186				..
187			} => "SinkSeriesView".into(),
188			FlowNodeType::SinkSubscription {
189				..
190			} => "SinkSubscription".into(),
191			FlowNodeType::Window {
192				..
193			} => "Window".into(),
194		}
195	}
196
197	pub fn discriminator(&self) -> u8 {
198		match self {
199			FlowNodeType::SourceInlineData {
200				..
201			} => 0,
202			FlowNodeType::SourceTable {
203				..
204			} => 1,
205			FlowNodeType::SourceView {
206				..
207			} => 2,
208			FlowNodeType::SourceFlow {
209				..
210			} => 3,
211			FlowNodeType::Filter {
212				..
213			} => 4,
214			FlowNodeType::Map {
215				..
216			} => 5,
217			FlowNodeType::Extend {
218				..
219			} => 6,
220			FlowNodeType::Join {
221				..
222			} => 7,
223			FlowNodeType::Aggregate {
224				..
225			} => 8,
226			FlowNodeType::Append {
227				..
228			} => 9,
229			FlowNodeType::Sort {
230				..
231			} => 10,
232			FlowNodeType::Take {
233				..
234			} => 11,
235			FlowNodeType::Distinct {
236				..
237			} => 12,
238			FlowNodeType::Apply {
239				..
240			} => 13,
241			FlowNodeType::SinkSubscription {
242				..
243			} => 14,
244			FlowNodeType::Window {
245				..
246			} => 15,
247			FlowNodeType::SourceRingBuffer {
248				..
249			} => 16,
250			FlowNodeType::SourceSeries {
251				..
252			} => 17,
253			FlowNodeType::Gate {
254				..
255			} => 18,
256			FlowNodeType::SinkTableView {
257				..
258			} => 19,
259			FlowNodeType::SinkRingBufferView {
260				..
261			} => 20,
262			FlowNodeType::SinkSeriesView {
263				..
264			} => 21,
265			FlowNodeType::SourceDictionary {
266				..
267			} => 22,
268		}
269	}
270
271	pub fn primitive_source_shape_id(&self) -> Option<ShapeId> {
272		match self {
273			FlowNodeType::SourceTable {
274				table,
275			} => Some(ShapeId::table(*table)),
276			FlowNodeType::SourceRingBuffer {
277				ringbuffer,
278			} => Some(ShapeId::ringbuffer(*ringbuffer)),
279			FlowNodeType::SourceSeries {
280				series,
281			} => Some(ShapeId::series(*series)),
282			FlowNodeType::SourceDictionary {
283				dictionary,
284			} => Some(ShapeId::dictionary(*dictionary)),
285			FlowNodeType::SourceInlineData {
286				..
287			}
288			| FlowNodeType::SourceView {
289				..
290			}
291			| FlowNodeType::SourceFlow {
292				..
293			}
294			| FlowNodeType::Filter {
295				..
296			}
297			| FlowNodeType::Gate {
298				..
299			}
300			| FlowNodeType::Map {
301				..
302			}
303			| FlowNodeType::Extend {
304				..
305			}
306			| FlowNodeType::Join {
307				..
308			}
309			| FlowNodeType::Aggregate {
310				..
311			}
312			| FlowNodeType::Append {
313				..
314			}
315			| FlowNodeType::Sort {
316				..
317			}
318			| FlowNodeType::Take {
319				..
320			}
321			| FlowNodeType::Distinct {
322				..
323			}
324			| FlowNodeType::Apply {
325				..
326			}
327			| FlowNodeType::SinkTableView {
328				..
329			}
330			| FlowNodeType::SinkRingBufferView {
331				..
332			}
333			| FlowNodeType::SinkSeriesView {
334				..
335			}
336			| FlowNodeType::SinkSubscription {
337				..
338			}
339			| FlowNodeType::Window {
340				..
341			} => None,
342		}
343	}
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct FlowNode {
348	pub id: FlowNodeId,
349	pub ty: FlowNodeType,
350	pub inputs: Vec<FlowNodeId>,
351	pub outputs: Vec<FlowNodeId>,
352}
353
354impl FlowNode {
355	pub fn new(id: impl Into<FlowNodeId>, ty: FlowNodeType) -> Self {
356		Self {
357			id: id.into(),
358			ty,
359			inputs: Vec::new(),
360			outputs: Vec::new(),
361		}
362	}
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
366pub struct FlowEdge {
367	pub id: FlowEdgeId,
368	pub source: FlowNodeId,
369	pub target: FlowNodeId,
370}
371
372impl FlowEdge {
373	pub fn new(id: impl Into<FlowEdgeId>, source: impl Into<FlowNodeId>, target: impl Into<FlowNodeId>) -> Self {
374		Self {
375			id: id.into(),
376			source: source.into(),
377			target: target.into(),
378		}
379	}
380}
381
382#[cfg(test)]
383mod tests {
384	use reifydb_core::common::JoinType;
385
386	use super::FlowNodeType;
387
388	fn join() -> FlowNodeType {
389		FlowNodeType::Join {
390			join_type: JoinType::Inner,
391			left: vec![],
392			right: vec![],
393			alias: None,
394			snapshot: false,
395			natural: false,
396			latest: false,
397		}
398	}
399
400	#[test]
401	fn join_always_requests_ticks() {
402		// Join state TTL is reclaimed by the background operator GC actor (per-side, via
403		// OperatorSettings), not on the flow tick path - so a Join node never requests ticks.
404		assert!(join().ticks());
405	}
406
407	#[test]
408	fn apply_always_requests_ticks() {
409		// Apply nodes always register for flow ticks, regardless of the underlying operator's
410		// tick capability. The graph-level gate cannot see the runtime operator, so it
411		// registers unconditionally; the runtime operator then decides whether tick() actually
412		// runs (an FFI operator without CAPABILITY_TICK reports no interval and is skipped).
413		// Registering here is what lets a tick-capable custom operator be ticked at all.
414		let apply = FlowNodeType::Apply {
415			operator: "compute_swap_volumes".to_string(),
416			expressions: vec![],
417		};
418		assert!(apply.ticks());
419	}
420
421	#[test]
422	fn append_and_distinct_always_request_ticks() {
423		// Their TTL now lives in OperatorSettings (not the node) and is reclaimed on tick when
424		// configured; the graph-level gate cannot see it, so they request ticks unconditionally and
425		// the runtime operator decides whether tick() actually runs.
426		assert!(FlowNodeType::Append {}.ticks());
427		assert!(FlowNodeType::Distinct {
428			expressions: vec![]
429		}
430		.ticks());
431	}
432
433	#[test]
434	fn stateless_nodes_do_not_request_ticks() {
435		assert!(!FlowNodeType::Map {
436			expressions: vec![]
437		}
438		.ticks());
439		assert!(!FlowNodeType::Filter {
440			conditions: vec![]
441		}
442		.ticks());
443	}
444}