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