Skip to main content

reifydb_engine/flow/compiler/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_catalog::{catalog::Catalog, store::operator_settings::create::create_operator_settings};
5use reifydb_core::{
6	error::diagnostic::{
7		flow::{
8			flow_ephemeral_id_capacity_exceeded, flow_remote_source_unsupported,
9			flow_sort_must_be_terminal, flow_source_required,
10		},
11		subscription::subscription_operation_unsupported,
12	},
13	interface::catalog::{
14		flow::{FlowEdge, FlowEdgeId, FlowId, FlowNode, FlowNodeId},
15		id::SubscriptionId,
16		view::View,
17	},
18	internal,
19	row::{JoinTtl, OperatorSettings, Ttl},
20};
21use reifydb_routine::routine::registry::Routines;
22use reifydb_rql::{
23	flow::{
24		flow::{FlowBuilder, FlowDag},
25		node::{self, FlowNodeType},
26	},
27	query::QueryPlan,
28};
29use reifydb_value::{Result, error::Error, value::blob::Blob};
30
31pub mod operator;
32pub mod primitive;
33
34use postcard::to_stdvec;
35use reifydb_transaction::transaction::{Transaction, admin::AdminTransaction};
36
37use crate::flow::compiler::{
38	operator::{
39		aggregate::AggregateCompiler, append::AppendCompiler, apply::ApplyCompiler, distinct::DistinctCompiler,
40		extend::ExtendCompiler, filter::FilterCompiler, gate::GateCompiler, join::JoinCompiler,
41		map::MapCompiler, sort::SortCompiler, take::TakeCompiler, window::WindowCompiler,
42	},
43	primitive::{
44		dictionary_scan::DictionaryScanCompiler, inline_data::InlineDataCompiler,
45		ringbuffer_scan::RingBufferScanCompiler, series_scan::SeriesScanCompiler,
46		table_scan::TableScanCompiler, view_scan::ViewScanCompiler,
47	},
48};
49
50pub fn compile_flow(
51	catalog: &Catalog,
52	routines: &Routines,
53	txn: &mut AdminTransaction,
54	plan: QueryPlan,
55	sink: Option<&View>,
56	flow_id: FlowId,
57) -> Result<FlowDag> {
58	let compiler = FlowCompiler::new(catalog.clone(), routines.clone(), flow_id);
59	compiler.compile(&mut Transaction::Admin(txn), plan, sink)
60}
61
62pub fn compile_subscription_flow_ephemeral(
63	catalog: &Catalog,
64	routines: &Routines,
65	txn: &mut Transaction<'_>,
66	plan: QueryPlan,
67	subscription_id: SubscriptionId,
68	flow_id: FlowId,
69) -> Result<FlowDag> {
70	let compiler = FlowCompiler::new_ephemeral(catalog.clone(), routines.clone(), flow_id);
71	compiler.compile_with_subscription_id(txn, plan, subscription_id)
72}
73
74pub(crate) struct FlowCompiler {
75	pub(crate) catalog: Catalog,
76
77	pub(crate) routines: Routines,
78
79	builder: FlowBuilder,
80
81	pub(crate) sink: Option<View>,
82
83	ephemeral: bool,
84
85	local_node_counter: u64,
86
87	local_edge_counter: u64,
88
89	local_id_limit: u64,
90}
91
92impl FlowCompiler {
93	pub fn new(catalog: Catalog, routines: Routines, flow_id: FlowId) -> Self {
94		Self {
95			catalog,
96			routines,
97			builder: FlowDag::builder(flow_id),
98			sink: None,
99			ephemeral: false,
100			local_node_counter: 0,
101			local_edge_counter: 0,
102			local_id_limit: 0,
103		}
104	}
105
106	pub fn new_ephemeral(catalog: Catalog, routines: Routines, flow_id: FlowId) -> Self {
107		let base = flow_id.0 * 100;
108		Self {
109			catalog,
110			routines,
111			builder: FlowDag::builder(flow_id),
112			sink: None,
113			ephemeral: true,
114			local_node_counter: base,
115			local_edge_counter: base,
116			local_id_limit: base + 99,
117		}
118	}
119
120	fn next_node_id(&mut self, txn: &mut Transaction<'_>) -> Result<FlowNodeId> {
121		if self.ephemeral {
122			if self.local_node_counter >= self.local_id_limit {
123				return Err(Error(Box::new(flow_ephemeral_id_capacity_exceeded(self.builder.id().0))));
124			}
125			self.local_node_counter += 1;
126			Ok(FlowNodeId(self.local_node_counter))
127		} else {
128			self.catalog.next_flow_node_id(txn.admin_mut())
129		}
130	}
131
132	fn next_edge_id(&mut self, txn: &mut Transaction<'_>) -> Result<FlowEdgeId> {
133		if self.ephemeral {
134			if self.local_edge_counter >= self.local_id_limit {
135				return Err(Error(Box::new(flow_ephemeral_id_capacity_exceeded(self.builder.id().0))));
136			}
137			self.local_edge_counter += 1;
138			Ok(FlowEdgeId(self.local_edge_counter))
139		} else {
140			self.catalog.next_flow_edge_id(txn.admin_mut())
141		}
142	}
143
144	pub(crate) fn add_edge(&mut self, txn: &mut Transaction<'_>, from: &FlowNodeId, to: &FlowNodeId) -> Result<()> {
145		let edge_id = self.next_edge_id(txn)?;
146		let flow_id = self.builder.id();
147
148		if !self.ephemeral {
149			let edge_def = FlowEdge {
150				id: edge_id,
151				flow: flow_id,
152				source: *from,
153				target: *to,
154			};
155
156			self.catalog.create_flow_edge(txn.admin_mut(), &edge_def)?;
157		}
158
159		self.builder.add_edge(node::FlowEdge::new(edge_id, *from, *to))?;
160		Ok(())
161	}
162
163	pub(crate) fn add_node(&mut self, txn: &mut Transaction<'_>, node_type: FlowNodeType) -> Result<FlowNodeId> {
164		let node_id = self.next_node_id(txn)?;
165		let flow_id = self.builder.id();
166
167		if !self.ephemeral {
168			let data = to_stdvec(&node_type)
169				.map_err(|e| Error(Box::new(internal!("Failed to serialize FlowNodeType: {}", e))))?;
170
171			let node_def = FlowNode {
172				id: node_id,
173				flow: flow_id,
174				node_type: node_type.discriminator(),
175				data: Blob::from(data),
176			};
177
178			self.catalog.create_flow_node(txn.admin_mut(), &node_def)?;
179		}
180
181		self.builder.add_node(node::FlowNode::new(node_id, node_type));
182		Ok(node_id)
183	}
184
185	pub(crate) fn write_operator_settings(
186		&self,
187		txn: &mut Transaction<'_>,
188		node_id: FlowNodeId,
189		ttl: Option<Ttl>,
190	) -> Result<()> {
191		if self.ephemeral {
192			return Ok(());
193		}
194		if let Some(ttl) = ttl {
195			create_operator_settings(
196				txn.admin_mut(),
197				node_id,
198				&OperatorSettings {
199					ttl: Some(ttl),
200					join: None,
201				},
202			)?;
203		}
204		Ok(())
205	}
206
207	pub(crate) fn write_operator_settings_join(
208		&self,
209		txn: &mut Transaction<'_>,
210		node_id: FlowNodeId,
211		join: Option<JoinTtl>,
212	) -> Result<()> {
213		if self.ephemeral {
214			return Ok(());
215		}
216		let Some(join) = join else {
217			return Ok(());
218		};
219		if join.left.is_none() && join.right.is_none() {
220			return Ok(());
221		}
222		create_operator_settings(
223			txn.admin_mut(),
224			node_id,
225			&OperatorSettings {
226				ttl: None,
227				join: Some(join),
228			},
229		)?;
230		Ok(())
231	}
232
233	pub(crate) fn compile(
234		mut self,
235		txn: &mut Transaction<'_>,
236		plan: QueryPlan,
237		sink: Option<&View>,
238	) -> Result<FlowDag> {
239		validate_sort_terminal(&plan)?;
240		self.sink = sink.cloned();
241		let root_node_id = self.compile_plan(txn, plan)?;
242
243		if let Some(sink_view) = sink {
244			self.attach_sink_node(txn, sink_view, &root_node_id)?;
245		}
246
247		self.build_validated_flow()
248	}
249
250	#[inline]
251	fn attach_sink_node(
252		&mut self,
253		txn: &mut Transaction<'_>,
254		sink_view: &View,
255		root_node_id: &FlowNodeId,
256	) -> Result<()> {
257		let node_type = match sink_view {
258			View::Table(t) => FlowNodeType::SinkTableView {
259				view: sink_view.id(),
260				table: t.underlying,
261			},
262			View::RingBuffer(rb) => FlowNodeType::SinkRingBufferView {
263				view: sink_view.id(),
264				ringbuffer: rb.underlying,
265				capacity: rb.capacity,
266				propagate_evictions: rb.propagate_evictions,
267			},
268			View::Series(s) => FlowNodeType::SinkSeriesView {
269				view: sink_view.id(),
270				series: s.underlying,
271				key: s.key.clone(),
272			},
273		};
274		let result_node = self.add_node(txn, node_type)?;
275		self.add_edge(txn, root_node_id, &result_node)
276	}
277
278	#[inline]
279	fn build_validated_flow(self) -> Result<FlowDag> {
280		let flow = self.builder.build();
281
282		if !has_real_source(&flow) {
283			return Err(Error(Box::new(flow_source_required())));
284		}
285
286		Ok(flow)
287	}
288
289	pub(crate) fn compile_with_subscription_id(
290		mut self,
291		txn: &mut Transaction<'_>,
292		plan: QueryPlan,
293		subscription_id: SubscriptionId,
294	) -> Result<FlowDag> {
295		validate_subscription_plan(&plan)?;
296		let root_node_id = self.compile_plan(txn, plan)?;
297
298		let result_node = self.add_node(
299			txn,
300			FlowNodeType::SinkSubscription {
301				subscription: subscription_id,
302			},
303		)?;
304
305		self.add_edge(txn, &root_node_id, &result_node)?;
306
307		let flow = self.builder.build();
308
309		if !has_real_source(&flow) {
310			return Err(Error(Box::new(flow_source_required())));
311		}
312
313		Ok(flow)
314	}
315
316	pub(crate) fn compile_plan(&mut self, txn: &mut Transaction<'_>, plan: QueryPlan) -> Result<FlowNodeId> {
317		match plan {
318			QueryPlan::IndexScan(_index_scan) => {
319				// TODO: Implement IndexScanCompiler for flow
320				unimplemented!("IndexScan compilation not yet implemented for flow")
321			}
322			QueryPlan::TableScan(table_scan) => TableScanCompiler::from(table_scan).compile(self, txn),
323			QueryPlan::ViewScan(view_scan) => ViewScanCompiler::from(view_scan).compile(self, txn),
324			QueryPlan::InlineData(inline_data) => InlineDataCompiler::from(inline_data).compile(self, txn),
325			QueryPlan::Filter(filter) => FilterCompiler::from(filter).compile(self, txn),
326			QueryPlan::Gate(gate) => GateCompiler::from(gate).compile(self, txn),
327			QueryPlan::Map(map) => MapCompiler::from(map).compile(self, txn),
328			QueryPlan::Extend(extend) => ExtendCompiler::from(extend).compile(self, txn),
329			QueryPlan::Apply(apply) => ApplyCompiler::from(apply).compile(self, txn),
330			QueryPlan::Aggregate(aggregate) => AggregateCompiler::from(aggregate).compile(self, txn),
331			QueryPlan::Distinct(distinct) => DistinctCompiler::from(distinct).compile(self, txn),
332			QueryPlan::Take(take) => TakeCompiler::from(take).compile(self, txn),
333			QueryPlan::Sort(sort) => SortCompiler::from(sort).compile(self, txn),
334			QueryPlan::JoinInner(join) => JoinCompiler::from(join).compile(self, txn),
335			QueryPlan::JoinLeft(join) => JoinCompiler::from(join).compile(self, txn),
336			QueryPlan::JoinNatural(join) => JoinCompiler::from(join).compile(self, txn),
337			QueryPlan::Append(append) => AppendCompiler::from(append).compile(self, txn),
338			QueryPlan::Patch(_) => {
339				unimplemented!("Patch compilation not yet implemented for flow")
340			}
341			QueryPlan::TableVirtualScan(_scan) => {
342				// TODO: Implement VirtualScanCompiler
343				unimplemented!("VirtualScan compilation not yet implemented")
344			}
345			QueryPlan::RingBufferScan(scan) => RingBufferScanCompiler::from(scan).compile(self, txn),
346			QueryPlan::Generator(_generator) => {
347				// TODO: Implement GeneratorCompiler for flow
348				unimplemented!("Generator compilation not yet implemented for flow")
349			}
350			QueryPlan::Window(window) => WindowCompiler::from(window).compile(self, txn),
351			QueryPlan::Variable(_) => {
352				panic!("Variable references are not supported in flow graphs");
353			}
354			QueryPlan::Scalarize(_) => {
355				panic!("Scalarize operations are not supported in flow graphs");
356			}
357			QueryPlan::Environment(_) => {
358				panic!("Environment operations are not supported in flow graphs");
359			}
360			QueryPlan::RowPointLookup(_) => {
361				// TODO: Implement optimized row point lookup for flow graphs
362				unimplemented!("RowPointLookup compilation not yet implemented for flow")
363			}
364			QueryPlan::RowListLookup(_) => {
365				// TODO: Implement optimized row list lookup for flow graphs
366				unimplemented!("RowListLookup compilation not yet implemented for flow")
367			}
368			QueryPlan::RowRangeScan(_) => {
369				// TODO: Implement optimized row range scan for flow graphs
370				unimplemented!("RowRangeScan compilation not yet implemented for flow")
371			}
372			QueryPlan::DictionaryScan(dictionary_scan) => {
373				DictionaryScanCompiler::from(dictionary_scan).compile(self, txn)
374			}
375			QueryPlan::Assert(_) => {
376				unimplemented!("Assert compilation not yet implemented for flow")
377			}
378			QueryPlan::SeriesScan(series_scan) => SeriesScanCompiler::from(series_scan).compile(self, txn),
379			QueryPlan::RemoteScan(_) => Err(Error(Box::new(flow_remote_source_unsupported()))),
380			QueryPlan::RunTests(_) => {
381				panic!("RunTests is not supported in flow graphs");
382			}
383			QueryPlan::CallFunction(_) => {
384				panic!("CallFunction is not supported in flow graphs");
385			}
386		}
387	}
388}
389
390fn validate_subscription_plan(plan: &QueryPlan) -> Result<()> {
391	match plan {
392		QueryPlan::Filter(n) => validate_subscription_plan(&n.input),
393		QueryPlan::Gate(n) => validate_subscription_plan(&n.input),
394		QueryPlan::Take(n) => validate_subscription_plan(&n.input),
395		QueryPlan::Distinct(n) => validate_subscription_plan(&n.input),
396		QueryPlan::Map(n) => match &n.input {
397			Some(input) => validate_subscription_plan(input),
398			None => Ok(()),
399		},
400		QueryPlan::Extend(n) => match &n.input {
401			Some(input) => validate_subscription_plan(input),
402			None => Ok(()),
403		},
404		QueryPlan::TableScan(_)
405		| QueryPlan::ViewScan(_)
406		| QueryPlan::RingBufferScan(_)
407		| QueryPlan::SeriesScan(_)
408		| QueryPlan::DictionaryScan(_)
409		| QueryPlan::InlineData(_) => Ok(()),
410		other => Err(Error(Box::new(subscription_operation_unsupported(other.name())))),
411	}
412}
413
414fn validate_sort_terminal(plan: &QueryPlan) -> Result<()> {
415	let has_deeper_sort = match plan {
416		QueryPlan::Sort(n) => contains_sort(&n.input),
417		other => contains_sort(other),
418	};
419	if has_deeper_sort {
420		return Err(Error(Box::new(flow_sort_must_be_terminal())));
421	}
422	Ok(())
423}
424
425fn contains_sort(plan: &QueryPlan) -> bool {
426	matches!(plan, QueryPlan::Sort(_)) || child_plans(plan).iter().any(|child| contains_sort(child))
427}
428
429fn child_plans(plan: &QueryPlan) -> Vec<&QueryPlan> {
430	match plan {
431		QueryPlan::Filter(n) => vec![&n.input],
432		QueryPlan::Gate(n) => vec![&n.input],
433		QueryPlan::Aggregate(n) => vec![&n.input],
434		QueryPlan::Distinct(n) => vec![&n.input],
435		QueryPlan::Sort(n) => vec![&n.input],
436		QueryPlan::Take(n) => vec![&n.input],
437		QueryPlan::Scalarize(n) => vec![&n.input],
438		QueryPlan::Map(n) => n.input.as_deref().into_iter().collect(),
439		QueryPlan::Extend(n) => n.input.as_deref().into_iter().collect(),
440		QueryPlan::Patch(n) => n.input.as_deref().into_iter().collect(),
441		QueryPlan::Apply(n) => n.input.as_deref().into_iter().collect(),
442		QueryPlan::Assert(n) => n.input.as_deref().into_iter().collect(),
443		QueryPlan::Window(n) => n.input.as_deref().into_iter().collect(),
444		QueryPlan::JoinInner(n) => vec![&n.left, &n.right],
445		QueryPlan::JoinLeft(n) => vec![&n.left, &n.right],
446		QueryPlan::JoinNatural(n) => vec![&n.left, &n.right],
447		QueryPlan::Append(n) => vec![&n.left, &n.right],
448		QueryPlan::RemoteScan(_)
449		| QueryPlan::TableScan(_)
450		| QueryPlan::TableVirtualScan(_)
451		| QueryPlan::ViewScan(_)
452		| QueryPlan::RingBufferScan(_)
453		| QueryPlan::DictionaryScan(_)
454		| QueryPlan::SeriesScan(_)
455		| QueryPlan::IndexScan(_)
456		| QueryPlan::RowPointLookup(_)
457		| QueryPlan::RowListLookup(_)
458		| QueryPlan::RowRangeScan(_)
459		| QueryPlan::InlineData(_)
460		| QueryPlan::Generator(_)
461		| QueryPlan::Variable(_)
462		| QueryPlan::Environment(_)
463		| QueryPlan::RunTests(_)
464		| QueryPlan::CallFunction(_) => vec![],
465	}
466}
467
468fn has_real_source(flow: &FlowDag) -> bool {
469	flow.get_node_ids().any(|node_id| {
470		if let Some(node) = flow.get_node(&node_id) {
471			matches!(
472				node.ty,
473				FlowNodeType::SourceTable { .. }
474					| FlowNodeType::SourceView { .. } | FlowNodeType::SourceFlow { .. }
475					| FlowNodeType::SourceRingBuffer { .. }
476					| FlowNodeType::SourceSeries { .. } | FlowNodeType::SourceDictionary { .. }
477			)
478		} else {
479			false
480		}
481	})
482}
483
484pub(crate) trait CompileOperator {
485	fn compile(self, compiler: &mut FlowCompiler, txn: &mut Transaction<'_>) -> Result<FlowNodeId>;
486}