reifydb_engine/vm/volcano/
patch.rs1use 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}