Skip to main content

reifydb_engine/vm/volcano/
patch.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::column::{headers::ColumnHeaders, write::check_digest_write},
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::{
23	fragment::Fragment,
24	reifydb_assertions,
25	value::{column_view::ColumnView, system_columns::user_columns},
26};
27use tracing::instrument;
28
29use super::{NoopNode, user_header_names, with_system_headers, with_user_columns};
30use crate::{
31	Result,
32	vm::volcano::{
33		inline::{expand_aliases, expand_sumtype_assignment, resolve_is_variants},
34		query::{QueryContext, QueryNode, eval_context_from_transform},
35		udf::{UdfEvalNode, strip_udf_columns},
36	},
37};
38
39pub(crate) struct PatchNode {
40	input: Box<dyn QueryNode>,
41	expressions: Vec<Expression>,
42	source: Option<ResolvedObject>,
43	udf_names: Vec<String>,
44	headers: Option<ColumnHeaders>,
45	context: Option<(Arc<QueryContext>, Vec<LoweredExpr>)>,
46}
47
48impl PatchNode {
49	pub fn new(input: Box<dyn QueryNode>, expressions: Vec<Expression>, source: Option<ResolvedObject>) -> Self {
50		Self {
51			input,
52			expressions,
53			source,
54			udf_names: Vec::new(),
55			headers: None,
56			context: None,
57		}
58	}
59}
60
61impl QueryNode for PatchNode {
62	#[instrument(name = "volcano::patch::initialize", level = "trace", skip_all)]
63	fn initialize<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &QueryContext) -> Result<()> {
64		if let Some(source) = ctx.source.as_ref().or(self.source.as_ref()) {
65			(self.expressions, _) =
66				expand_aliases(mem::take(&mut self.expressions), |alias_expr, expanded| {
67					expand_sumtype_assignment(ctx, rx, source, alias_expr, expanded)
68				})?;
69		}
70		if let Some(source) = self.source.as_ref() {
71			for expr in &mut self.expressions {
72				resolve_is_variants(&ctx.services.catalog, rx, source, expr)?;
73			}
74		}
75		let (input, expressions, udf_names) = UdfEvalNode::wrap_if_needed(
76			mem::replace(&mut self.input, Box::new(NoopNode)),
77			&self.expressions,
78			&ctx.symbols,
79		);
80		self.input = input;
81		self.expressions = expressions;
82		self.udf_names = udf_names;
83
84		let lowered = self.expressions.iter().map(|e| LoweredExpr::new(e.clone(), "patch")).collect();
85		self.context = Some((Arc::new(ctx.clone()), lowered));
86		self.input.initialize(rx, ctx)?;
87		Ok(())
88	}
89
90	#[instrument(name = "volcano::patch::next", level = "trace", skip_all)]
91	fn next<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &mut QueryContext) -> Result<Option<RecordBatch>> {
92		reifydb_assertions! {
93			assert!(self.context.is_some(), "PatchNode::next() called before initialize()");
94		}
95
96		if let Some(columns) = self.input.next(rx, ctx)? {
97			let stored_ctx = &self.context.as_ref().unwrap().0;
98			let transform_ctx = TransformContext {
99				routines: &ctx.services.routines,
100				runtime_context: &stored_ctx.services.runtime_context,
101				params: &stored_ctx.params,
102			};
103			let result = self.apply(&transform_ctx, columns)?;
104
105			if self.headers.is_none() {
106				self.headers = Some(ColumnHeaders::from_batch(&result));
107			}
108
109			let result = strip_udf_columns(result, &self.udf_names)?;
110			Ok(Some(result))
111		} else {
112			Ok(None)
113		}
114	}
115
116	fn headers(&self) -> Option<ColumnHeaders> {
117		if let Some(ref headers) = self.headers {
118			return Some(headers.clone());
119		}
120
121		let input_headers = self.input.headers()?;
122		let patch_names: Vec<Fragment> = self.expressions.iter().map(display_label).collect();
123
124		let mut result = Vec::new();
125		for col in &user_header_names(&input_headers) {
126			if let Some(patch_idx) = patch_names.iter().position(|n| n.text() == col.text()) {
127				result.push(patch_names[patch_idx].clone());
128			} else {
129				result.push(col.clone());
130			}
131		}
132
133		for patch_name in &patch_names {
134			if !result.iter().any(|h| h.text() == patch_name.text()) {
135				result.push(patch_name.clone());
136			}
137		}
138
139		Some(with_system_headers(result, &input_headers))
140	}
141}
142
143impl Transform for PatchNode {
144	fn apply(&self, ctx: &TransformContext, input: RecordBatch) -> Result<RecordBatch> {
145		let (stored_ctx, lowered) =
146			self.context.as_ref().expect("PatchNode::apply() called before initialize()");
147
148		let row_count = input.num_rows();
149
150		let patch_names: Vec<Fragment> = self.expressions.iter().map(display_label).collect();
151
152		let session = eval_context_from_transform(ctx, stored_ctx);
153		let mut patch_columns = Vec::with_capacity(self.expressions.len());
154		for (expr, lowered_expr) in self.expressions.iter().zip(lowered.iter()) {
155			let mut exec_ctx = Self::eval_context(&session, &input, row_count);
156
157			if let (Expression::Alias(alias_expr), Some(source)) = (expr, &stored_ctx.source) {
158				let alias_name = alias_expr.alias.name();
159
160				if let Some(table_column) = source.columns().iter().find(|col| col.name == alias_name) {
161					let column_ident = Fragment::internal(&table_column.name);
162					let resolved_column =
163						ResolvedColumn::new(column_ident, source.clone(), table_column.clone());
164					exec_ctx.target = Some(TargetColumn::Resolved(resolved_column));
165				}
166			}
167
168			let mut column = Self::eval_patch(lowered_expr, &exec_ctx)?;
169
170			if let Some(target_type) = exec_ctx.target.as_ref().map(|t| t.column_type()) {
171				let view = ColumnView::try_from(&column)?;
172				if view.get_type() != target_type {
173					check_digest_write(&view, &target_type, expr.lazy_fragment())?;
174					column = cast_for_write(&exec_ctx, &view, target_type, &expr.lazy_fragment())?;
175				}
176			}
177
178			patch_columns.push(column);
179		}
180
181		Self::merge(input, &patch_names, patch_columns)
182	}
183}
184
185impl PatchNode {
186	#[instrument(level = "trace", skip_all, name = "volcano::patch::eval_context")]
187	fn eval_context<'e>(session: &EvalContext<'e>, input: &RecordBatch, row_count: usize) -> EvalContext<'e> {
188		session.with_eval(input.clone(), row_count)
189	}
190
191	#[instrument(level = "trace", skip_all, name = "volcano::patch::eval")]
192	fn eval_patch(lowered: &LoweredExpr, exec_ctx: &EvalContext) -> Result<(FieldRef, ArrayRef)> {
193		lowered.evaluate(exec_ctx)
194	}
195
196	#[instrument(level = "trace", skip_all, name = "volcano::patch::merge")]
197	fn merge(
198		input: RecordBatch,
199		patch_names: &[Fragment],
200		patch_columns: Vec<(FieldRef, ArrayRef)>,
201	) -> Result<RecordBatch> {
202		let mut result_columns: Vec<(FieldRef, ArrayRef)> = Vec::new();
203
204		for (original_field, original_data) in user_columns(&input) {
205			let original_name_text = original_field.name().as_str();
206
207			if let Some(patch_idx) = patch_names.iter().position(|n| n.text() == original_name_text) {
208				result_columns.push(patch_columns[patch_idx].clone());
209			} else {
210				result_columns.push((original_field.clone(), original_data.clone()));
211			}
212		}
213
214		for (patch_idx, patch_name) in patch_names.iter().enumerate() {
215			if !result_columns.iter().any(|(field, _)| field.name() == patch_name.text()) {
216				result_columns.push(patch_columns[patch_idx].clone());
217			}
218		}
219
220		with_user_columns(result_columns, &input)
221	}
222}