Skip to main content

reifydb_engine/vm/volcano/
inline.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	collections::{BTreeSet, HashMap, HashSet},
6	mem,
7	sync::Arc,
8};
9
10use reifydb_core::{
11	interface::{catalog::sumtype::SumType, evaluate::TargetColumn, resolved::ResolvedObject},
12	value::column::{
13		ColumnWithName, buffer::ColumnBuffer, cast::cast_column_data, columns::Columns, headers::ColumnHeaders,
14	},
15};
16use reifydb_evaluate::expression::{context::EvalContext, eval::evaluate};
17use reifydb_rql::expression::{AliasExpression, ConstantExpression, Expression, IdentExpression};
18use reifydb_transaction::transaction::Transaction;
19use reifydb_value::{
20	fragment::Fragment,
21	reifydb_assertions,
22	value::{Value, constraint::Constraint, value_type::ValueType},
23};
24use tracing::instrument;
25
26use crate::{
27	Result,
28	vm::volcano::query::{QueryContext, QueryNode, eval_context_from_query},
29};
30
31pub(crate) struct InlineDataNode {
32	rows: Vec<Vec<AliasExpression>>,
33	headers: Option<ColumnHeaders>,
34	context: Option<Arc<QueryContext>>,
35	executed: bool,
36}
37
38impl InlineDataNode {
39	pub fn new(rows: Vec<Vec<AliasExpression>>, context: Arc<QueryContext>) -> Self {
40		let cloned_context = context.clone();
41		let headers = cloned_context.source.as_ref().map(|source| {
42			let mut layout = Self::create_columns_layout_from_source(source);
43
44			if matches!(source, ResolvedObject::Series(_)) {
45				let existing: HashSet<String> =
46					layout.columns.iter().map(|c| c.text().to_string()).collect();
47				for row in &rows {
48					for alias in row {
49						let name = alias.alias.0.text().to_string();
50						if !existing.contains(&name) {
51							layout.columns.push(Fragment::internal(&name));
52						}
53					}
54				}
55			}
56			layout
57		});
58
59		Self {
60			rows,
61			headers,
62			context: Some(context),
63			executed: false,
64		}
65	}
66
67	fn create_columns_layout_from_source(source: &ResolvedObject) -> ColumnHeaders {
68		ColumnHeaders {
69			columns: source.columns().iter().map(|col| Fragment::internal(&col.name)).collect(),
70		}
71	}
72
73	#[instrument(level = "trace", skip_all, name = "volcano::inline::expand_sumtypes")]
74	fn expand_sumtype_constructors<'a>(&mut self, txn: &mut Transaction<'a>) -> Result<()> {
75		let Some(ctx) = self.context.as_ref().cloned() else {
76			return Ok(());
77		};
78		if !rows_need_sumtype_expansion(&self.rows) {
79			return Ok(());
80		}
81		for row in &mut self.rows {
82			let original = mem::take(row);
83			let mut expanded = Vec::with_capacity(original.len());
84			for alias_expr in original {
85				match alias_expr.expression.as_ref() {
86					Expression::SumTypeConstructor(_) => {
87						expand_sumtype_ctor(&ctx, txn, alias_expr, &mut expanded)?;
88					}
89					Expression::Column(_) => {
90						expand_unit_variant_column(&ctx, txn, alias_expr, &mut expanded)?;
91					}
92					_ => expanded.push(alias_expr),
93				}
94			}
95			*row = expanded;
96		}
97		Ok(())
98	}
99}
100
101#[inline]
102fn rows_need_sumtype_expansion(rows: &[Vec<AliasExpression>]) -> bool {
103	for row in rows {
104		for alias_expr in row {
105			if matches!(
106				alias_expr.expression.as_ref(),
107				Expression::SumTypeConstructor(_) | Expression::Column(_)
108			) {
109				return true;
110			}
111		}
112	}
113	false
114}
115
116fn expand_sumtype_ctor<'a>(
117	ctx: &Arc<QueryContext>,
118	txn: &mut Transaction<'a>,
119	alias_expr: AliasExpression,
120	expanded: &mut Vec<AliasExpression>,
121) -> Result<()> {
122	let col_name = alias_expr.alias.0.text().to_string();
123	let fragment = alias_expr.fragment.clone();
124
125	let Expression::SumTypeConstructor(ctor) = *alias_expr.expression else {
126		unreachable!()
127	};
128
129	let is_unresolved = ctor.namespace.text() == ctor.variant_name.text()
130		&& ctor.sumtype_name.text() == ctor.variant_name.text();
131
132	let sumtype = if is_unresolved {
133		resolve_unresolved_sumtype(ctx, txn, &col_name)?
134	} else {
135		let ns_name = ctor.namespace.text();
136		let ns = ctx.services.catalog.find_namespace_by_name(txn, ns_name)?.unwrap();
137		let sumtype_name = ctor.sumtype_name.text();
138		ctx.services.catalog.find_sumtype_by_name(txn, ns.id(), sumtype_name)?.unwrap()
139	};
140
141	let variant_name_lower = ctor.variant_name.text().to_lowercase();
142	let variant = sumtype.variants.iter().find(|v| v.name == variant_name_lower).unwrap();
143
144	expanded.push(AliasExpression {
145		alias: IdentExpression(Fragment::internal(format!("{}_tag", col_name))),
146		expression: Box::new(Expression::Constant(ConstantExpression::Number {
147			fragment: Fragment::internal(variant.tag.to_string()),
148		})),
149		fragment: fragment.clone(),
150	});
151
152	for (field_name, field_expr) in ctor.columns {
153		let phys_col_name = format!("{}_{}_{}", col_name, variant_name_lower, field_name.text().to_lowercase());
154		expanded.push(AliasExpression {
155			alias: IdentExpression(Fragment::internal(phys_col_name)),
156			expression: Box::new(field_expr),
157			fragment: fragment.clone(),
158		});
159	}
160
161	Ok(())
162}
163
164#[inline]
165fn resolve_unresolved_sumtype<'a>(
166	ctx: &Arc<QueryContext>,
167	txn: &mut Transaction<'a>,
168	col_name: &str,
169) -> Result<SumType> {
170	let tag_col_name = format!("{}_tag", col_name);
171	let source = ctx.source.as_ref().expect("source required for unresolved sumtype");
172
173	if let Some(tag_col) = source.columns().iter().find(|c| c.name == tag_col_name) {
174		let Some(Constraint::SumType(id)) = tag_col.constraint.constraint() else {
175			panic!("expected SumType constraint on tag column")
176		};
177		ctx.services.catalog.get_sumtype(txn, *id)
178	} else if let ResolvedObject::Series(series) = source {
179		let tag_id = series.def().tag.expect("series tag expected");
180		ctx.services.catalog.get_sumtype(txn, tag_id)
181	} else {
182		panic!("tag column not found: {}", tag_col_name)
183	}
184}
185
186fn expand_unit_variant_column<'a>(
187	ctx: &Arc<QueryContext>,
188	txn: &mut Transaction<'a>,
189	alias_expr: AliasExpression,
190	expanded: &mut Vec<AliasExpression>,
191) -> Result<()> {
192	let col_name = alias_expr.alias.0.text().to_string();
193
194	let resolved = if let Some(source) = ctx.source.as_ref() {
195		let Expression::Column(col) = alias_expr.expression.as_ref() else {
196			unreachable!()
197		};
198		try_resolve_unit_variant(ctx, txn, source, &col_name, col.0.name.text())?
199	} else {
200		None
201	};
202
203	let Some((sumtype, tag)) = resolved else {
204		expanded.push(alias_expr);
205		return Ok(());
206	};
207
208	let fragment = alias_expr.fragment.clone();
209	expanded.push(AliasExpression {
210		alias: IdentExpression(Fragment::internal(format!("{}_tag", col_name))),
211		expression: Box::new(Expression::Constant(ConstantExpression::Number {
212			fragment: Fragment::internal(tag.to_string()),
213		})),
214		fragment: fragment.clone(),
215	});
216	for v in &sumtype.variants {
217		for field in &v.fields {
218			let phys_col_name =
219				format!("{}_{}_{}", col_name, v.name.to_lowercase(), field.name.to_lowercase());
220			expanded.push(AliasExpression {
221				alias: IdentExpression(Fragment::internal(phys_col_name)),
222				expression: Box::new(Expression::Constant(ConstantExpression::None {
223					fragment: fragment.clone(),
224				})),
225				fragment: fragment.clone(),
226			});
227		}
228	}
229	Ok(())
230}
231
232#[inline]
233fn try_resolve_unit_variant<'a>(
234	ctx: &Arc<QueryContext>,
235	txn: &mut Transaction<'a>,
236	source: &ResolvedObject,
237	col_name: &str,
238	alias_text: &str,
239) -> Result<Option<(SumType, u8)>> {
240	let tag_col_name = format!("{}_tag", col_name);
241
242	if let Some(tag_col) = source.columns().iter().find(|c| c.name == tag_col_name) {
243		let Some(Constraint::SumType(id)) = tag_col.constraint.constraint() else {
244			return Ok(None);
245		};
246		let sumtype = ctx.services.catalog.get_sumtype(txn, *id)?;
247		let variant_name_lower = alias_text.to_lowercase();
248		let maybe_tag =
249			sumtype.variants.iter().find(|v| v.name.to_lowercase() == variant_name_lower).map(|v| v.tag);
250		return Ok(maybe_tag.map(|tag| (sumtype, tag)));
251	}
252
253	if let ResolvedObject::Series(series) = source
254		&& let Some(tag_id) = series.def().tag
255	{
256		let sumtype = ctx.services.catalog.get_sumtype(txn, tag_id)?;
257		let variant_name_lower = alias_text.to_lowercase();
258		let maybe_tag =
259			sumtype.variants.iter().find(|v| v.name.to_lowercase() == variant_name_lower).map(|v| v.tag);
260		return Ok(maybe_tag.map(|tag| (sumtype, tag)));
261	}
262
263	Ok(None)
264}
265
266impl QueryNode for InlineDataNode {
267	#[instrument(level = "trace", skip_all, name = "volcano::inline::initialize")]
268	fn initialize<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
269		self.expand_sumtype_constructors(rx)?;
270		Ok(())
271	}
272
273	#[instrument(level = "trace", skip_all, name = "volcano::inline::next")]
274	fn next<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<Columns>> {
275		reifydb_assertions! {
276			assert!(self.context.is_some(), "InlineDataNode::next() called before initialize()");
277		}
278		let stored_ctx = self.context.as_ref().unwrap().clone();
279
280		if self.executed {
281			return Ok(None);
282		}
283
284		self.executed = true;
285
286		if self.rows.is_empty() {
287			let columns = Columns::empty();
288			if self.headers.is_none() {
289				self.headers = Some(ColumnHeaders::from_columns(&columns));
290			}
291			return Ok(Some(columns));
292		}
293
294		if self.headers.is_some() {
295			self.next_with_source(&stored_ctx)
296		} else {
297			self.next_infer_namespace(&stored_ctx)
298		}
299	}
300
301	fn headers(&self) -> Option<ColumnHeaders> {
302		self.headers.clone()
303	}
304}
305
306impl InlineDataNode {
307	fn find_optimal_integer_type(column: &ColumnBuffer) -> ValueType {
308		let mut min_val = i128::MAX;
309		let mut max_val = i128::MIN;
310		let mut has_values = false;
311
312		for value in column.iter() {
313			match value {
314				Value::Int16(v) => {
315					has_values = true;
316					min_val = min_val.min(v);
317					max_val = max_val.max(v);
318				}
319				Value::None {
320					..
321				} => {}
322				_ => {
323					return ValueType::Int16;
324				}
325			}
326		}
327
328		if !has_values {
329			return ValueType::Int1;
330		}
331
332		if min_val >= i8::MIN as i128 && max_val <= i8::MAX as i128 {
333			ValueType::Int1
334		} else if min_val >= i16::MIN as i128 && max_val <= i16::MAX as i128 {
335			ValueType::Int2
336		} else if min_val >= i32::MIN as i128 && max_val <= i32::MAX as i128 {
337			ValueType::Int4
338		} else if min_val >= i64::MIN as i128 && max_val <= i64::MAX as i128 {
339			ValueType::Int8
340		} else {
341			ValueType::Int16
342		}
343	}
344
345	#[instrument(level = "trace", skip_all, name = "volcano::inline::column_names")]
346	fn collect_column_names(rows: &[Vec<AliasExpression>]) -> BTreeSet<String> {
347		let mut all_columns: BTreeSet<String> = BTreeSet::new();
348
349		for row in rows {
350			for keyed_expr in row {
351				let column_name = keyed_expr.alias.0.text().to_string();
352				all_columns.insert(column_name);
353			}
354		}
355
356		all_columns
357	}
358
359	#[instrument(level = "trace", skip_all, name = "volcano::inline::row_maps")]
360	fn build_row_maps(rows: &[Vec<AliasExpression>]) -> Vec<HashMap<String, &AliasExpression>> {
361		let mut rows_data: Vec<HashMap<String, &AliasExpression>> = Vec::new();
362
363		for row in rows {
364			let mut row_map: HashMap<String, &AliasExpression> = HashMap::new();
365			for alias_expr in row {
366				let column_name = alias_expr.alias.0.text().to_string();
367				row_map.insert(column_name, alias_expr);
368			}
369			rows_data.push(row_map);
370		}
371
372		rows_data
373	}
374
375	#[instrument(level = "trace", skip_all, name = "volcano::inline::eval")]
376	fn eval_column_values(
377		session: &EvalContext<'_>,
378		rows_data: &[HashMap<String, &AliasExpression>],
379		column_name: &str,
380	) -> Result<(Vec<Value>, Option<ValueType>, Option<Fragment>)> {
381		let mut all_values = Vec::new();
382		let mut first_value_type: Option<ValueType> = None;
383		let mut column_fragment: Option<Fragment> = None;
384
385		for row_data in rows_data {
386			if let Some(alias_expr) = row_data.get(column_name) {
387				if column_fragment.is_none() {
388					column_fragment = Some(alias_expr.fragment.clone());
389				}
390				let eval_ctx = session.with_eval_empty();
391
392				let evaluated = evaluate(&eval_ctx, &alias_expr.expression)?;
393
394				let mut iter = evaluated.data().iter();
395				if let Some(value) = iter.next() {
396					if first_value_type.is_none() && !matches!(value, Value::None { .. }) {
397						first_value_type = Some(value.get_type());
398					}
399					all_values.push(value);
400				} else {
401					all_values.push(Value::none());
402				}
403			} else {
404				all_values.push(Value::none());
405			}
406		}
407
408		Ok((all_values, first_value_type, column_fragment))
409	}
410
411	#[instrument(level = "trace", skip_all, name = "volcano::inline::materialize")]
412	fn materialize_inferred_column(
413		session: &EvalContext<'_>,
414		all_values: &[Value],
415		first_value_type: Option<ValueType>,
416	) -> ColumnBuffer {
417		let wide_type = if let Some(ref fvt) = first_value_type {
418			if *fvt == ValueType::Decimal {
419				Some(ValueType::Decimal)
420			} else if *fvt == ValueType::Int {
421				Some(ValueType::Int)
422			} else if *fvt == ValueType::Uint {
423				Some(ValueType::Uint)
424			} else if fvt.is_integer() {
425				Some(ValueType::Int16)
426			} else if fvt.is_floating_point() {
427				Some(ValueType::Float8)
428			} else if *fvt == ValueType::Utf8 {
429				Some(ValueType::Utf8)
430			} else if *fvt == ValueType::Boolean {
431				Some(ValueType::Boolean)
432			} else {
433				None
434			}
435		} else {
436			None
437		};
438
439		let mut column_data = if wide_type.is_none() {
440			ColumnBuffer::none_typed(ValueType::Boolean, all_values.len())
441		} else {
442			let mut data = ColumnBuffer::with_capacity(wide_type.clone().unwrap(), 0);
443
444			for value in all_values {
445				if matches!(value, Value::None { .. }) {
446					data.push_none();
447				} else if wide_type.as_ref().is_some_and(|wt| value.get_type() == *wt) {
448					data.push_value(value.clone());
449				} else {
450					let temp_data = ColumnBuffer::from(value.clone());
451					let eval_ctx = session.with_eval_empty();
452
453					match cast_column_data(
454						&eval_ctx,
455						&temp_data,
456						wide_type.clone().unwrap(),
457						Fragment::none,
458					) {
459						Ok(casted) => {
460							if let Some(casted_value) = casted.iter().next() {
461								data.push_value(casted_value);
462							} else {
463								data.push_none();
464							}
465						}
466						Err(_) => {
467							data.push_none();
468						}
469					}
470				}
471			}
472
473			data
474		};
475
476		if wide_type == Some(ValueType::Int16) {
477			let optimal_type = Self::find_optimal_integer_type(&column_data);
478			if optimal_type != ValueType::Int16 {
479				let eval_ctx = session.with_eval(Columns::empty(), column_data.len());
480
481				if let Ok(demoted) =
482					cast_column_data(&eval_ctx, &column_data, optimal_type, Fragment::none)
483				{
484					column_data = demoted;
485				}
486			}
487		}
488
489		column_data
490	}
491
492	fn next_infer_namespace(&mut self, ctx: &QueryContext) -> Result<Option<Columns>> {
493		let all_columns = Self::collect_column_names(&self.rows);
494		let rows_data = Self::build_row_maps(&self.rows);
495
496		let session = eval_context_from_query(ctx);
497
498		let mut columns = Vec::new();
499
500		for column_name in all_columns {
501			let (all_values, first_value_type, column_fragment) =
502				Self::eval_column_values(&session, &rows_data, &column_name)?;
503
504			let column_data = Self::materialize_inferred_column(&session, &all_values, first_value_type);
505
506			columns.push(ColumnWithName::new(
507				column_fragment.unwrap_or_else(|| Fragment::internal(column_name)),
508				column_data,
509			));
510		}
511
512		let columns = Columns::new(columns);
513		self.headers = Some(ColumnHeaders::from_columns(&columns));
514
515		Ok(Some(columns))
516	}
517
518	fn next_with_source(&mut self, ctx: &QueryContext) -> Result<Option<Columns>> {
519		let source = ctx.source.as_ref().unwrap();
520		let headers = self.headers.as_ref().unwrap();
521		let session = eval_context_from_query(ctx);
522
523		let rows_data = Self::build_row_maps(&self.rows);
524
525		let mut columns = Vec::new();
526
527		for column_name in &headers.columns {
528			columns.push(Self::build_source_column(&session, source, &rows_data, column_name)?);
529		}
530
531		let columns = Columns::new(columns);
532
533		Ok(Some(columns))
534	}
535
536	#[instrument(level = "trace", skip_all, name = "volcano::inline::source_column")]
537	fn build_source_column(
538		session: &EvalContext<'_>,
539		source: &ResolvedObject,
540		rows_data: &[HashMap<String, &AliasExpression>],
541		column_name: &Fragment,
542	) -> Result<ColumnWithName> {
543		let table_column = source.columns().iter().find(|col| col.name == column_name.text());
544
545		let mut column_data = if let Some(tc) = table_column {
546			ColumnBuffer::none_typed(tc.constraint.get_type(), 0)
547		} else {
548			ColumnBuffer::with_capacity(ValueType::Int16, 0)
549		};
550		let mut column_fragment: Option<Fragment> = None;
551
552		for row_data in rows_data {
553			if let Some(alias_expr) = row_data.get(column_name.text()) {
554				if column_fragment.is_none() {
555					column_fragment = Some(alias_expr.fragment.clone());
556				}
557				let mut eval_ctx = session.with_eval_empty();
558				eval_ctx.target = table_column.map(|tc| TargetColumn::Partial {
559					source_name: Some(source.identifier().text().to_string()),
560					column_name: Some(tc.name.clone()),
561					column_type: tc.constraint.get_type(),
562					properties: tc.properties.iter().map(|cp| cp.property.clone()).collect(),
563				});
564
565				let evaluated = evaluate(&eval_ctx, &alias_expr.expression)?;
566
567				let eval_len = evaluated.data().len();
568				if table_column.is_some() {
569					if eval_len == 1 {
570						column_data.extend(evaluated.data().clone())?;
571					} else if eval_len == 0 {
572						column_data.push_value(Value::none());
573					} else {
574						let first_value =
575							evaluated.data().iter().next().unwrap_or(Value::none());
576						column_data.push_value(first_value);
577					}
578				} else {
579					let value = if eval_len > 0 {
580						evaluated.data().iter().next().unwrap_or(Value::none())
581					} else {
582						Value::none()
583					};
584					match &value {
585						Value::None {
586							..
587						} => column_data.push_none(),
588						Value::Int16(_) => column_data.push_value(value),
589						_ => {
590							let temp = ColumnBuffer::from(value.clone());
591							match cast_column_data(
592								&eval_ctx,
593								&temp,
594								ValueType::Int16,
595								Fragment::none,
596							) {
597								Ok(casted) => {
598									if let Some(v) = casted.iter().next() {
599										column_data.push_value(v);
600									} else {
601										column_data.push_none();
602									}
603								}
604								Err(_) => column_data.push_value(value),
605							}
606						}
607					}
608				}
609			} else {
610				column_data.push_value(Value::none());
611			}
612		}
613
614		if table_column.is_none() {
615			let optimal_type = Self::find_optimal_integer_type(&column_data);
616			if optimal_type != ValueType::Int16 {
617				let eval_ctx = session.with_eval(Columns::empty(), column_data.len());
618				if let Ok(demoted) =
619					cast_column_data(&eval_ctx, &column_data, optimal_type, Fragment::none)
620				{
621					column_data = demoted;
622				}
623			}
624		}
625
626		Ok(ColumnWithName::new(
627			column_fragment.map(|f| f.with_text(column_name.text())).unwrap_or_else(|| column_name.clone()),
628			column_data,
629		))
630	}
631}