Skip to main content

reifydb_engine/vm/volcano/
map.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{mem, sync::Arc};
5
6use reifydb_core::{
7	interface::{evaluate::TargetColumn, resolved::ResolvedColumn},
8	value::column::{ColumnWithName, cast::cast_column_data, columns::Columns, headers::ColumnHeaders},
9};
10use reifydb_evaluate::expression::{
11	compile::{CompiledExpr, compile_expression},
12	context::{CompileContext, EvalContext},
13};
14use reifydb_extension::transform::{Transform, context::TransformContext};
15use reifydb_rql::expression::{Expression, name::display_label};
16use reifydb_transaction::transaction::Transaction;
17use reifydb_value::{fragment::Fragment, reifydb_assertions, value::system_columns::SystemColumns};
18use tracing::instrument;
19
20use super::NoopNode;
21use crate::{
22	Result,
23	vm::volcano::{
24		query::{QueryContext, QueryNode, eval_context_from_query, eval_context_from_transform},
25		udf::{UdfEvalNode, evaluate_udfs_no_input, strip_udf_columns},
26	},
27};
28
29pub(crate) struct MapNode {
30	input: Box<dyn QueryNode>,
31	expressions: Vec<Expression>,
32	udf_names: Vec<String>,
33	headers: Option<ColumnHeaders>,
34	context: Option<(Arc<QueryContext>, Vec<CompiledExpr>)>,
35}
36
37impl MapNode {
38	pub fn new(input: Box<dyn QueryNode>, expressions: Vec<Expression>) -> Self {
39		Self {
40			input,
41			expressions,
42			udf_names: Vec::new(),
43			headers: None,
44			context: None,
45		}
46	}
47}
48
49impl QueryNode for MapNode {
50	#[instrument(name = "volcano::map::initialize", level = "trace", skip_all)]
51	fn initialize<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &QueryContext) -> Result<()> {
52		let (input, expressions, udf_names) = UdfEvalNode::wrap_if_needed(
53			mem::replace(&mut self.input, Box::new(NoopNode)),
54			&self.expressions,
55			&ctx.symbols,
56		);
57		self.input = input;
58		self.expressions = expressions;
59		self.udf_names = udf_names;
60
61		let compile_ctx = CompileContext {
62			symbols: &ctx.symbols,
63		};
64		let compiled = self
65			.expressions
66			.iter()
67			.map(|e| compile_expression(&compile_ctx, e).expect("compile"))
68			.collect();
69		self.context = Some((Arc::new(ctx.clone()), compiled));
70		let column_names = self.expressions.iter().map(display_label).collect();
71		self.headers = Some(ColumnHeaders {
72			columns: column_names,
73		});
74		self.input.initialize(rx, ctx)?;
75		Ok(())
76	}
77
78	#[instrument(name = "volcano::map::next", level = "trace", skip_all)]
79	fn next<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &mut QueryContext) -> Result<Option<Columns>> {
80		reifydb_assertions! {
81			assert!(self.context.is_some(), "MapNode::next() called before initialize()");
82		}
83
84		if let Some(columns) = self.input.next(rx, ctx)? {
85			let stored_ctx = &self.context.as_ref().unwrap().0;
86			let transform_ctx = TransformContext {
87				routines: &ctx.services.routines,
88				runtime_context: &stored_ctx.services.runtime_context,
89				params: &stored_ctx.params,
90			};
91			let mut result = self.apply(&transform_ctx, columns)?;
92			strip_udf_columns(&mut result, &self.udf_names);
93
94			Ok(Some(result))
95		} else {
96			Ok(None)
97		}
98	}
99
100	fn headers(&self) -> Option<ColumnHeaders> {
101		self.headers.clone().or(self.input.headers())
102	}
103}
104
105impl Transform for MapNode {
106	fn apply(&self, ctx: &TransformContext, input: Columns) -> Result<Columns> {
107		let (stored_ctx, compiled) =
108			self.context.as_ref().expect("MapNode::apply() called before initialize()");
109
110		let row_count = input.row_count();
111		let session = eval_context_from_transform(ctx, stored_ctx);
112		let mut new_columns = Vec::with_capacity(compiled.len());
113
114		for (expr, compiled_expr) in self.expressions.iter().zip(compiled.iter()) {
115			let mut exec_ctx = Self::eval_context(&session, &input, row_count);
116
117			if let (Expression::Alias(alias_expr), Some(source)) = (expr, &stored_ctx.source) {
118				let alias_name = alias_expr.alias.name();
119				if let Some(table_column) = source.columns().iter().find(|col| col.name == alias_name) {
120					let column_ident = Fragment::internal(&table_column.name);
121					let resolved_column =
122						ResolvedColumn::new(column_ident, source.clone(), table_column.clone());
123					exec_ctx.target = Some(TargetColumn::Resolved(resolved_column));
124				}
125			}
126
127			let mut column = Self::eval_projection(compiled_expr, &exec_ctx)?;
128
129			if let Some(target_type) = exec_ctx.target.as_ref().map(|t| t.column_type())
130				&& column.data.get_type() != target_type
131			{
132				let data =
133					cast_column_data(&exec_ctx, &column.data, target_type, &expr.lazy_fragment())?;
134				column = ColumnWithName {
135					name: column.name,
136					data,
137				};
138			}
139
140			new_columns.push(column);
141		}
142
143		Ok(Self::assemble(input.system, new_columns))
144	}
145}
146
147impl MapNode {
148	#[instrument(level = "trace", skip_all, name = "volcano::map::eval_context")]
149	fn eval_context<'e>(session: &EvalContext<'e>, input: &Columns, row_count: usize) -> EvalContext<'e> {
150		session.with_eval(input.clone(), row_count)
151	}
152
153	#[instrument(level = "trace", skip_all, name = "volcano::map::eval")]
154	fn eval_projection(compiled: &CompiledExpr, exec_ctx: &EvalContext) -> Result<ColumnWithName> {
155		compiled.execute(exec_ctx)
156	}
157
158	#[instrument(level = "trace", skip_all, name = "volcano::map::assemble")]
159	fn assemble(system: SystemColumns, new_columns: Vec<ColumnWithName>) -> Columns {
160		let mut names_vec = Vec::with_capacity(new_columns.len());
161		let mut buffers_vec = Vec::with_capacity(new_columns.len());
162		for c in new_columns {
163			names_vec.push(c.name);
164			buffers_vec.push(c.data);
165		}
166		Columns {
167			system,
168			columns: buffers_vec,
169			names: names_vec,
170		}
171	}
172}
173
174pub(crate) struct MapWithoutInputNode {
175	expressions: Vec<Expression>,
176	headers: Option<ColumnHeaders>,
177
178	udf_columns: Option<Columns>,
179	context: Option<(Arc<QueryContext>, Vec<CompiledExpr>)>,
180}
181
182impl MapWithoutInputNode {
183	pub fn new(expressions: Vec<Expression>) -> Self {
184		Self {
185			expressions,
186			headers: None,
187			udf_columns: None,
188			context: None,
189		}
190	}
191}
192
193impl QueryNode for MapWithoutInputNode {
194	#[instrument(name = "volcano::map::noinput::initialize", level = "trace", skip_all)]
195	fn initialize<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &QueryContext) -> Result<()> {
196		if let Some((rewritten, udf_cols)) = evaluate_udfs_no_input(&self.expressions, ctx, rx)? {
197			self.expressions = rewritten;
198			self.udf_columns = Some(udf_cols);
199		}
200
201		let compile_ctx = CompileContext {
202			symbols: &ctx.symbols,
203		};
204		let compiled = self
205			.expressions
206			.iter()
207			.map(|e| compile_expression(&compile_ctx, e).expect("compile"))
208			.collect();
209		self.context = Some((Arc::new(ctx.clone()), compiled));
210		Ok(())
211	}
212
213	#[instrument(name = "volcano::map::noinput::next", level = "trace", skip_all)]
214	fn next<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<Columns>> {
215		reifydb_assertions! {
216			assert!(self.context.is_some(), "MapWithoutInputNode::next() called before initialize()");
217		}
218		let (stored_ctx, compiled) = self.context.as_ref().unwrap();
219
220		if self.headers.is_some() {
221			return Ok(None);
222		}
223
224		let session = eval_context_from_query(stored_ctx);
225		let mut columns = vec![];
226
227		for compiled_expr in compiled {
228			let exec_ctx = match &self.udf_columns {
229				Some(udf_cols) => session.with_eval(udf_cols.clone(), 1),
230				None => session.with_eval_empty(),
231			};
232
233			let column = compiled_expr.execute(&exec_ctx)?;
234
235			columns.push(column);
236		}
237
238		let columns = Columns::new(columns);
239		self.headers = Some(ColumnHeaders::from_columns(&columns));
240		Ok(Some(columns))
241	}
242
243	fn headers(&self) -> Option<ColumnHeaders> {
244		self.headers.clone()
245	}
246}