Skip to main content

reifydb_routine/procedure/rql/
explain.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{fmt::Display, sync::LazyLock};
5
6use bumpalo::Bump;
7use reifydb_core::{
8	common::JoinType,
9	interface::resolved::ResolvedShape,
10	value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns},
11};
12use reifydb_rql::{
13	ast::parse_str,
14	nodes::AlterSequenceNode,
15	optimize::optimize_physical,
16	plan::{
17		logical::compile_logical,
18		physical::{
19			AggregateNode, AppendPhysicalNode, ApplyNode, AssertNode, DistinctNode, ExtendNode, FilterNode,
20			GateNode, JoinInnerNode, JoinLeftNode, JoinNaturalNode, MapNode, PatchNode, PhysicalPlan,
21			SortNode, TakeNode, compile_physical,
22		},
23	},
24};
25use reifydb_type::value::r#type::Type;
26
27use crate::{
28	procedure::rql::extract_query,
29	routine::{Routine, RoutineInfo, context::ProcedureContext, error::RoutineError},
30};
31
32static INFO: LazyLock<RoutineInfo> = LazyLock::new(|| RoutineInfo::new("rql::explain"));
33
34pub struct RqlExplain;
35
36impl Default for RqlExplain {
37	fn default() -> Self {
38		Self::new()
39	}
40}
41
42impl RqlExplain {
43	pub fn new() -> Self {
44		Self
45	}
46}
47
48impl<'a, 'tx> Routine<ProcedureContext<'a, 'tx>> for RqlExplain {
49	fn info(&self) -> &RoutineInfo {
50		&INFO
51	}
52
53	fn return_type(&self, _input_types: &[Type]) -> Type {
54		Type::Any
55	}
56
57	fn attaches_row_metadata(&self) -> bool {
58		false
59	}
60
61	fn execute(&self, ctx: &mut ProcedureContext<'a, 'tx>, _args: &Columns) -> Result<Columns, RoutineError> {
62		let query = extract_query(ctx.params, "rql::explain")?;
63
64		let bump = Bump::new();
65		let statements = parse_str(&bump, query.as_str())?;
66
67		let mut walker = PhysicalWalker::default();
68		for statement in statements {
69			let logical = compile_logical(&bump, ctx.catalog, ctx.tx, statement)?;
70			if let Some(mut plan) = compile_physical(&bump, ctx.catalog, ctx.tx, logical)? {
71				optimize_physical(&mut plan);
72				walker.walk(&plan, 0, None);
73			}
74		}
75
76		Ok(walker.into_columns())
77	}
78}
79
80#[derive(Default)]
81struct PhysicalWalker {
82	idx: Vec<i32>,
83	depth: Vec<i32>,
84	parent: Vec<Option<i32>>,
85	kind: Vec<String>,
86	detail: Vec<String>,
87}
88
89impl PhysicalWalker {
90	fn emit(&mut self, depth: i32, parent: Option<i32>, kind: &str, detail: String) -> i32 {
91		let next = self.idx.len() as i32;
92		self.idx.push(next);
93		self.depth.push(depth);
94		self.parent.push(parent);
95		self.kind.push(kind.to_string());
96		self.detail.push(detail);
97		next
98	}
99
100	fn into_columns(self) -> Columns {
101		Columns::new(vec![
102			ColumnWithName::int4("idx", self.idx),
103			ColumnWithName::int4("depth", self.depth),
104			ColumnWithName::new("parent", ColumnBuffer::int4_optional(self.parent)),
105			ColumnWithName::utf8("kind", self.kind),
106			ColumnWithName::utf8("detail", self.detail),
107		])
108	}
109
110	fn walk(&mut self, plan: &PhysicalPlan<'_>, depth: i32, parent: Option<i32>) {
111		let (kind, detail) = describe(plan);
112		let me = self.emit(depth, parent, kind, detail);
113		self.recurse_children(plan, depth + 1, Some(me));
114	}
115
116	fn recurse_children(&mut self, plan: &PhysicalPlan<'_>, depth: i32, parent: Option<i32>) {
117		match plan {
118			PhysicalPlan::Aggregate(AggregateNode {
119				input,
120				..
121			}) => self.walk(input, depth, parent),
122			PhysicalPlan::Filter(FilterNode {
123				input,
124				..
125			}) => self.walk(input, depth, parent),
126			PhysicalPlan::Gate(GateNode {
127				input,
128				..
129			}) => self.walk(input, depth, parent),
130			PhysicalPlan::Assert(AssertNode {
131				input: Some(input),
132				..
133			}) => {
134				self.walk(input, depth, parent);
135			}
136			PhysicalPlan::Take(TakeNode {
137				input,
138				..
139			}) => self.walk(input, depth, parent),
140			PhysicalPlan::Sort(SortNode {
141				input,
142				..
143			}) => self.walk(input, depth, parent),
144			PhysicalPlan::Map(MapNode {
145				input: Some(input),
146				..
147			}) => {
148				self.walk(input, depth, parent);
149			}
150			PhysicalPlan::Extend(ExtendNode {
151				input: Some(input),
152				..
153			}) => {
154				self.walk(input, depth, parent);
155			}
156			PhysicalPlan::Patch(PatchNode {
157				input: Some(input),
158				..
159			}) => {
160				self.walk(input, depth, parent);
161			}
162			PhysicalPlan::JoinInner(JoinInnerNode {
163				left,
164				right,
165				..
166			}) => {
167				self.walk(left, depth, parent);
168				self.walk(right, depth, parent);
169			}
170			PhysicalPlan::JoinLeft(JoinLeftNode {
171				left,
172				right,
173				..
174			}) => {
175				self.walk(left, depth, parent);
176				self.walk(right, depth, parent);
177			}
178			PhysicalPlan::JoinNatural(JoinNaturalNode {
179				left,
180				right,
181				..
182			}) => {
183				self.walk(left, depth, parent);
184				self.walk(right, depth, parent);
185			}
186			PhysicalPlan::Apply(ApplyNode {
187				input: Some(input),
188				..
189			}) => {
190				self.walk(input, depth, parent);
191			}
192			PhysicalPlan::Distinct(DistinctNode {
193				input,
194				..
195			}) => self.walk(input, depth, parent),
196			PhysicalPlan::Window(node) => {
197				if let Some(input) = &node.input {
198					self.walk(input, depth, parent);
199				}
200			}
201			PhysicalPlan::Conditional(node) => {
202				self.walk(&node.then_branch, depth, parent);
203				for else_if in node.else_ifs.iter() {
204					self.walk(&else_if.then_branch, depth, parent);
205				}
206				if let Some(else_branch) = &node.else_branch {
207					self.walk(else_branch, depth, parent);
208				}
209			}
210			PhysicalPlan::Scalarize(scalarize) => self.walk(&scalarize.input, depth, parent),
211			PhysicalPlan::DefineFunction(def) => {
212				for child in def.body.iter() {
213					self.walk(child, depth, parent);
214				}
215			}
216			PhysicalPlan::Append(AppendPhysicalNode::Query {
217				left,
218				right,
219			}) => {
220				self.walk(left, depth, parent);
221				self.walk(right, depth, parent);
222			}
223			_ => {}
224		}
225	}
226}
227
228fn describe(plan: &PhysicalPlan<'_>) -> (&'static str, String) {
229	match plan {
230		PhysicalPlan::Loop(_) => ("Loop", String::new()),
231		PhysicalPlan::While(_) => ("While", String::new()),
232		PhysicalPlan::For(_) => ("For", String::new()),
233		PhysicalPlan::Break => ("Break", String::new()),
234		PhysicalPlan::Continue => ("Continue", String::new()),
235		PhysicalPlan::CreateDeferredView(_) => ("CreateDeferredView", String::new()),
236		PhysicalPlan::CreateTransactionalView(_) => ("CreateTransactionalView", String::new()),
237		PhysicalPlan::CreateNamespace(_) => ("CreateNamespace", String::new()),
238		PhysicalPlan::CreateRemoteNamespace(_) => ("CreateRemoteNamespace", String::new()),
239		PhysicalPlan::CreateTable(_) => ("CreateTable", String::new()),
240		PhysicalPlan::CreateRingBuffer(_) => ("CreateRingBuffer", String::new()),
241		PhysicalPlan::CreateDictionary(_) => ("CreateDictionary", String::new()),
242		PhysicalPlan::CreateSumType(_) => ("CreateSumType", String::new()),
243		PhysicalPlan::CreateSubscription(_) => ("CreateSubscription", String::new()),
244		PhysicalPlan::DropNamespace(_) => ("DropNamespace", String::new()),
245		PhysicalPlan::DropTable(_) => ("DropTable", String::new()),
246		PhysicalPlan::DropView(_) => ("DropView", String::new()),
247		PhysicalPlan::DropRingBuffer(_) => ("DropRingBuffer", String::new()),
248		PhysicalPlan::DropDictionary(_) => ("DropDictionary", String::new()),
249		PhysicalPlan::DropSumType(_) => ("DropSumType", String::new()),
250		PhysicalPlan::DropSubscription(_) => ("DropSubscription", String::new()),
251		PhysicalPlan::DropSeries(_) => ("DropSeries", String::new()),
252		PhysicalPlan::DropProcedure(_) => ("DropProcedure", String::new()),
253		PhysicalPlan::DropHandler(_) => ("DropHandler", String::new()),
254		PhysicalPlan::DropTest(_) => ("DropTest", String::new()),
255		PhysicalPlan::CreateSource(_) => ("CreateSource", String::new()),
256		PhysicalPlan::CreateSink(_) => ("CreateSink", String::new()),
257		PhysicalPlan::CreateBinding(_) => ("CreateBinding", String::new()),
258		PhysicalPlan::DropSource(_) => ("DropSource", String::new()),
259		PhysicalPlan::DropSink(_) => ("DropSink", String::new()),
260		PhysicalPlan::DropBinding(_) => ("DropBinding", String::new()),
261		PhysicalPlan::CreateIdentity(n) => ("CreateIdentity", format!("name={}", n.name.text())),
262		PhysicalPlan::CreateRole(n) => ("CreateRole", format!("name={}", n.name.text())),
263		PhysicalPlan::Grant(n) => ("Grant", format!("role={} user={}", n.role.text(), n.user.text())),
264		PhysicalPlan::Revoke(n) => ("Revoke", format!("role={} user={}", n.role.text(), n.user.text())),
265		PhysicalPlan::DropIdentity(n) => {
266			("DropIdentity", format!("name={} if_exists={}", n.name.text(), n.if_exists))
267		}
268		PhysicalPlan::DropRole(n) => ("DropRole", format!("name={} if_exists={}", n.name.text(), n.if_exists)),
269		PhysicalPlan::CreateAuthentication(n) => {
270			("CreateAuthentication", format!("user={} method={}", n.user.text(), n.method.text()))
271		}
272		PhysicalPlan::DropAuthentication(n) => (
273			"DropAuthentication",
274			format!("user={} method={} if_exists={}", n.user.text(), n.method.text(), n.if_exists),
275		),
276		PhysicalPlan::CreatePolicy(n) => {
277			let name = n.name.as_ref().map(|f| f.text()).unwrap_or("<unnamed>");
278			("CreatePolicy", format!("name={} type={}", name, n.target_type))
279		}
280		PhysicalPlan::AlterPolicy(n) => ("AlterPolicy", format!("name={} enabled={}", n.name.text(), n.enable)),
281		PhysicalPlan::DropPolicy(n) => {
282			("DropPolicy", format!("name={} if_exists={}", n.name.text(), n.if_exists))
283		}
284		PhysicalPlan::AlterSequence(AlterSequenceNode {
285			sequence,
286			column,
287			value,
288		}) => ("AlterSequence", format!("{}.{} = {}", sequence.def().name, column.name(), value)),
289		PhysicalPlan::Delete(_) => ("Delete", String::new()),
290		PhysicalPlan::DeleteRingBuffer(_) => ("DeleteRingBuffer", String::new()),
291		PhysicalPlan::InsertTable(_) => ("InsertTable", String::new()),
292		PhysicalPlan::InsertRingBuffer(_) => ("InsertRingBuffer", String::new()),
293		PhysicalPlan::InsertDictionary(_) => ("InsertDictionary", String::new()),
294		PhysicalPlan::DeleteSeries(_) => ("DeleteSeries", String::new()),
295		PhysicalPlan::InsertSeries(_) => ("InsertSeries", String::new()),
296		PhysicalPlan::Update(_) => ("Update", String::new()),
297		PhysicalPlan::UpdateRingBuffer(_) => ("UpdateRingBuffer", String::new()),
298		PhysicalPlan::UpdateSeries(_) => ("UpdateSeries", String::new()),
299		PhysicalPlan::Aggregate(AggregateNode {
300			by,
301			map,
302			..
303		}) => {
304			let by_str = by.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ");
305			let map_str = map.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ");
306			("Aggregate", format!("by=[{}] map=[{}]", by_str, map_str))
307		}
308		PhysicalPlan::Filter(FilterNode {
309			conditions,
310			..
311		}) => ("Filter", expressions_inline(conditions)),
312		PhysicalPlan::Gate(GateNode {
313			conditions,
314			..
315		}) => ("Gate", expressions_inline(conditions)),
316		PhysicalPlan::Assert(AssertNode {
317			conditions,
318			message,
319			..
320		}) => {
321			let cond = expressions_inline(conditions);
322			match message {
323				Some(msg) => ("Assert", format!("\"{}\" [{}]", msg, cond)),
324				None => ("Assert", format!("[{}]", cond)),
325			}
326		}
327		PhysicalPlan::AssertBlock(node) => {
328			let kind = if node.expect_error {
329				"AssertError"
330			} else {
331				"AssertBlock"
332			};
333			let msg = node.message.as_deref().unwrap_or("");
334			(kind, format!("\"{}\"", msg))
335		}
336		PhysicalPlan::Take(TakeNode {
337			take,
338			..
339		}) => ("Take", take.to_string()),
340		PhysicalPlan::Sort(SortNode {
341			by,
342			..
343		}) => ("Sort", expressions_inline(by)),
344		PhysicalPlan::Map(MapNode {
345			map,
346			..
347		}) => ("Map", expressions_inline(map)),
348		PhysicalPlan::Extend(ExtendNode {
349			extend,
350			..
351		}) => ("Extend", expressions_inline(extend)),
352		PhysicalPlan::Patch(PatchNode {
353			assignments,
354			..
355		}) => ("Patch", expressions_inline(assignments)),
356		PhysicalPlan::JoinInner(JoinInnerNode {
357			on,
358			..
359		}) => ("JoinInner", format!("on=[{}]", expressions_inline(on))),
360		PhysicalPlan::JoinLeft(JoinLeftNode {
361			on,
362			..
363		}) => ("JoinLeft", format!("on=[{}]", expressions_inline(on))),
364		PhysicalPlan::JoinNatural(JoinNaturalNode {
365			join_type,
366			..
367		}) => {
368			let kind = match join_type {
369				JoinType::Inner => "Inner",
370				JoinType::Left => "Left",
371			};
372			("JoinNatural", format!("type={}", kind))
373		}
374		PhysicalPlan::IndexScan(node) => (
375			"IndexScan",
376			format!("{}::{}::{}", node.source.namespace().name(), node.source.name(), node.index_name),
377		),
378		PhysicalPlan::TableScan(node) => {
379			("TableScan", format!("{}::{}", node.source.namespace().name(), node.source.name()))
380		}
381		PhysicalPlan::ViewScan(node) => {
382			("ViewScan", format!("{}::{}", node.source.namespace().name(), node.source.name()))
383		}
384		PhysicalPlan::RingBufferScan(node) => {
385			("RingBufferScan", format!("{}::{}", node.source.namespace().name(), node.source.name()))
386		}
387		PhysicalPlan::DictionaryScan(node) => {
388			("DictionaryScan", format!("{}::{}", node.source.namespace().name(), node.source.name()))
389		}
390		PhysicalPlan::SeriesScan(node) => {
391			("SeriesScan", format!("{}.{}", node.source.namespace().name(), node.source.name()))
392		}
393		PhysicalPlan::Apply(ApplyNode {
394			operator,
395			expressions,
396			..
397		}) => {
398			let summary = if expressions.is_empty() {
399				"no args".to_string()
400			} else {
401				format!("{} args", expressions.len())
402			};
403			("Apply", format!("operator={} {}", operator.text(), summary))
404		}
405		PhysicalPlan::RemoteScan(node) => (
406			"RemoteScan",
407			format!(
408				"{}::{} @ {} rql=\"{}\"",
409				node.local_namespace, node.remote_name, node.address, node.remote_rql
410			),
411		),
412		PhysicalPlan::InlineData(node) => {
413			let total_fields: usize = node.rows.iter().map(|row| row.len()).sum();
414			("InlineData", format!("rows={} fields={}", node.rows.len(), total_fields))
415		}
416		PhysicalPlan::Distinct(DistinctNode {
417			columns,
418			..
419		}) => {
420			let detail = if columns.is_empty() {
421				"primary key".to_string()
422			} else {
423				columns.iter().map(|c| c.name().to_string()).collect::<Vec<_>>().join(", ")
424			};
425			("Distinct", detail)
426		}
427		PhysicalPlan::CreatePrimaryKey(_) => ("CreatePrimaryKey", String::new()),
428		PhysicalPlan::CreateColumnProperty(_) => ("CreateColumnProperty", String::new()),
429		PhysicalPlan::CreateProcedure(_) => ("CreateProcedure", String::new()),
430		PhysicalPlan::CreateSeries(_) => ("CreateSeries", String::new()),
431		PhysicalPlan::CreateEvent(_) => ("CreateEvent", String::new()),
432		PhysicalPlan::CreateTag(_) => ("CreateTag", String::new()),
433		PhysicalPlan::CreateTest(_) => ("CreateTest", String::new()),
434		PhysicalPlan::RunTests(_) => ("RunTests", String::new()),
435		PhysicalPlan::CreateMigration(_) => ("CreateMigration", String::new()),
436		PhysicalPlan::Migrate(_) => ("Migrate", String::new()),
437		PhysicalPlan::RollbackMigration(_) => ("RollbackMigration", String::new()),
438		PhysicalPlan::Dispatch(_) => ("Dispatch", String::new()),
439		PhysicalPlan::AlterTable(node) => {
440			("AlterTable", format!("{}.{}", node.namespace.name(), node.table.text()))
441		}
442		PhysicalPlan::AlterRemoteNamespace(_) => ("AlterRemoteNamespace", String::new()),
443		PhysicalPlan::TableVirtualScan(node) => {
444			("TableVirtualScan", format!("{}::{}", node.source.namespace().name(), node.source.name()))
445		}
446		PhysicalPlan::Generator(node) => ("Generator", node.name.text().to_string()),
447		PhysicalPlan::Window(node) => ("Window", format!("kind={:?}", node.kind)),
448		PhysicalPlan::Declare(node) => ("Declare", format!("{} = {}", node.name.text(), node.value)),
449		PhysicalPlan::Assign(node) => ("Assign", format!("{} = {}", node.name.text(), node.value)),
450		PhysicalPlan::Variable(node) => ("Variable", node.variable_expr.fragment.text().to_string()),
451		PhysicalPlan::Conditional(node) => ("Conditional", format!("if {}", node.condition)),
452		PhysicalPlan::Scalarize(_) => ("Scalarize", String::new()),
453		PhysicalPlan::Environment(_) => ("Environment", String::new()),
454		PhysicalPlan::RowPointLookup(lookup) => {
455			let source_name = source_name_of(&lookup.source);
456			("RowPointLookup", format!("source={} row={}", source_name, lookup.row_number))
457		}
458		PhysicalPlan::RowListLookup(lookup) => {
459			let source_name = source_name_of(&lookup.source);
460			let rows = lookup.row_numbers.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", ");
461			("RowListLookup", format!("source={} rows=[{}]", source_name, rows))
462		}
463		PhysicalPlan::RowRangeScan(scan) => {
464			let source_name = source_name_of(&scan.source);
465			("RowRangeScan", format!("source={} range={}..={}", source_name, scan.start, scan.end))
466		}
467		PhysicalPlan::DefineFunction(def) => {
468			let params: Vec<String> = def
469				.parameters
470				.iter()
471				.map(|p| {
472					if let Some(ref tc) = p.type_constraint {
473						format!("${}: {:?}", p.name.text(), tc)
474					} else {
475						format!("${}", p.name.text())
476					}
477				})
478				.collect();
479			let ret = if let Some(ref rt) = def.return_type {
480				format!(" -> {:?}", rt)
481			} else {
482				String::new()
483			};
484			("DefineFunction", format!("{}[{}]{}", def.name.text(), params.join(", "), ret))
485		}
486		PhysicalPlan::Return(ret) => {
487			let value = ret.value.as_ref().map(|expr| expr.to_string()).unwrap_or_default();
488			("Return", value)
489		}
490		PhysicalPlan::CallFunction(call) => {
491			let args: Vec<String> = call.arguments.iter().map(|a| format!("{}", a)).collect();
492			("CallFunction", format!("{}({})", call.name.text(), args.join(", ")))
493		}
494		PhysicalPlan::Append(node) => match node {
495			AppendPhysicalNode::IntoVariable {
496				target,
497				..
498			} => ("Append", format!("${}", target.text())),
499			AppendPhysicalNode::Query {
500				..
501			} => ("Append", String::new()),
502		},
503		PhysicalPlan::DefineClosure(node) => {
504			let params: Vec<String> = node
505				.parameters
506				.iter()
507				.map(|p| {
508					if let Some(ref tc) = p.type_constraint {
509						format!("${}: {:?}", p.name.text(), tc)
510					} else {
511						format!("${}", p.name.text())
512					}
513				})
514				.collect();
515			("DefineClosure", format!("[{}]", params.join(", ")))
516		}
517	}
518}
519
520fn source_name_of(source: &ResolvedShape) -> String {
521	match source {
522		ResolvedShape::Table(t) => t.identifier().text().to_string(),
523		ResolvedShape::View(v) => v.identifier().text().to_string(),
524		ResolvedShape::RingBuffer(rb) => rb.identifier().text().to_string(),
525		_ => "unknown".to_string(),
526	}
527}
528
529fn expressions_inline<E: Display>(items: &[E]) -> String {
530	items.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ")
531}