Skip to main content

reifydb_engine/vm/volcano/join/
common.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::Arc;
5
6use arrow_array::{ArrayRef, RecordBatch, UInt64Array};
7use arrow_schema::FieldRef;
8use reifydb_core::{
9	error::diagnostic::operation,
10	value::{
11		batch::{batch, concat, data_size, take_rows, take_rows_or_none},
12		column::{
13			factory::{datetime, from_one, rename, typed_none},
14			view::group_by::common_key_type,
15		},
16	},
17};
18use reifydb_evaluate::lower::LoweredExpr;
19use reifydb_transaction::transaction::Transaction;
20use reifydb_value::{
21	error,
22	fragment::Fragment,
23	value::{
24		Value,
25		column_view::ColumnView,
26		datetime::DateTime,
27		system_columns::{
28			SystemColumn, is_system_field, stamp_system_columns, system_column, time, user_columns,
29		},
30		value_type::ValueType,
31	},
32};
33
34use crate::{
35	Result,
36	vm::volcano::query::{QueryContext, QueryNode, charge_query_memory_bytes, eval_context_from_query},
37};
38
39pub(crate) fn load_and_merge_all<'a>(
40	node: &mut Box<dyn QueryNode>,
41	rx: &mut Transaction<'a>,
42	ctx: &mut QueryContext,
43) -> Result<RecordBatch> {
44	let mut batches = Vec::new();
45	let mut charged = 0usize;
46	let mut total = 0usize;
47
48	while let Some(columns) = node.next(rx, ctx)? {
49		total += data_size(&columns)?;
50		charge_query_memory_bytes(&ctx.memory, &mut charged, total)?;
51		batches.push(columns);
52	}
53	concat(&batches)
54}
55
56pub(crate) fn user_views(columns: &RecordBatch) -> Result<Vec<ColumnView<'_>>> {
57	user_columns(columns).map(|(field, array)| ColumnView::try_from((array, field.as_ref()))).collect()
58}
59
60pub(crate) fn user_row(views: &[ColumnView<'_>], index: usize) -> Vec<Value> {
61	views.iter().map(|view| view.get_value(index)).collect()
62}
63
64pub(crate) fn user_key_columns<'b>(columns: &'b RecordBatch, indices: &[usize]) -> Vec<(&'b FieldRef, &'b ArrayRef)> {
65	let user: Vec<(&FieldRef, &ArrayRef)> = user_columns(columns).collect();
66	indices.iter().map(|&index| user[index]).collect()
67}
68
69pub(crate) struct JoinSlot<'a> {
70	pub columns: &'a RecordBatch,
71	pub picks: &'a [usize],
72}
73
74pub(crate) fn materialize_join(
75	qualified_names: &[String],
76	left_slots: &[JoinSlot<'_>],
77	right_columns: &RecordBatch,
78	right_excluded: &[usize],
79	right_picks: &[Option<usize>],
80	emitted: u64,
81) -> Result<RecordBatch> {
82	let parts = left_slots.iter().map(|slot| take_rows(slot.columns, slot.picks)).collect::<Result<Vec<_>>>()?;
83	let left = concat(&parts)?;
84	let right = take_rows_or_none(right_columns, right_picks)?;
85	let mut picked: Vec<(FieldRef, ArrayRef)> = Vec::with_capacity(left.num_columns() + right.num_columns());
86
87	for (field, array) in user_columns(&left) {
88		let name = &qualified_names[picked.len()];
89		picked.push(rename((field.clone(), array.clone()), name));
90	}
91
92	for (index, (field, array)) in user_columns(&right).enumerate() {
93		if right_excluded.contains(&index) {
94			continue;
95		}
96		let name = &qualified_names[picked.len()];
97		picked.push(rename((field.clone(), array.clone()), name));
98	}
99
100	for (field, array) in left.schema_ref().fields().iter().zip(left.columns()) {
101		if is_system_field(field) {
102			picked.push((field.clone(), array.clone()));
103		}
104	}
105
106	let mut stamps: Vec<(SystemColumn, ArrayRef)> = Vec::new();
107	if system_column(&left, SystemColumn::RowNumbers).is_some() {
108		let row_numbers = UInt64Array::from_iter_values((1..=right_picks.len() as u64).map(|i| emitted + i));
109		stamps.push((SystemColumn::RowNumbers, Arc::new(row_numbers)));
110	}
111	let left_time = time(&left)?;
112	let right_time = time(right_columns)?;
113	if !left_time.is_empty() && !right_time.is_empty() {
114		let merged: Vec<DateTime> = left_time
115			.iter()
116			.zip(right_picks)
117			.map(|(&time, &pick)| match pick {
118				None => time,
119				Some(index) => time.max(right_time[index]),
120			})
121			.collect();
122		let (_, array) = datetime(SystemColumn::Time.name(), merged);
123		stamps.push((SystemColumn::Time, array));
124	}
125	stamp_system_columns(batch(picked)?, stamps)
126}
127
128pub struct ResolvedColumnNames {
129	pub qualified_names: Vec<String>,
130}
131
132pub fn resolve_column_names(
133	left_columns: &RecordBatch,
134	right_columns: &RecordBatch,
135	alias: &Option<Fragment>,
136	excluded_right_indices: Option<&[usize]>,
137) -> ResolvedColumnNames {
138	let mut qualified_names = Vec::new();
139
140	for (field, _) in user_columns(left_columns) {
141		qualified_names.push(field.name().to_string());
142	}
143
144	for (idx, (field, _)) in user_columns(right_columns).enumerate() {
145		if let Some(excluded) = excluded_right_indices
146			&& excluded.contains(&idx)
147		{
148			continue;
149		}
150
151		let col_name = field.name();
152
153		let alias_text = alias.as_ref().map(|a| a.text()).unwrap_or("other");
154		let prefixed_name = format!("{}_{}", alias_text, col_name);
155
156		let mut final_name = prefixed_name.clone();
157		if qualified_names.contains(&final_name) {
158			let mut counter = 2;
159			loop {
160				let candidate = format!("{}_{}", prefixed_name, counter);
161				if !qualified_names.contains(&candidate) {
162					final_name = candidate;
163					break;
164				}
165				counter += 1;
166			}
167		}
168
169		qualified_names.push(final_name);
170	}
171
172	ResolvedColumnNames {
173		qualified_names,
174	}
175}
176
177pub fn build_eval_columns(
178	left_columns: &[ColumnView<'_>],
179	right_columns: &[ColumnView<'_>],
180	left_row: &[Value],
181	right_row: &[Value],
182	alias: &Option<Fragment>,
183) -> Vec<(FieldRef, ArrayRef)> {
184	let mut eval_columns = Vec::new();
185
186	for (idx, col) in left_columns.iter().enumerate() {
187		let name = col.field.name();
188		let data = match &left_row[idx] {
189			Value::None {
190				..
191			} => typed_none(name, &col.get_type()),
192			value => from_one(name, value.clone()),
193		};
194		eval_columns.push((Arc::new(col.field.clone()), data.1));
195	}
196
197	for (idx, col) in right_columns.iter().enumerate() {
198		let name = match alias {
199			Some(alias) => format!("{}.{}", alias.text(), col.field.name()),
200			None => col.field.name().clone(),
201		};
202		let data = match &right_row[idx] {
203			Value::None {
204				..
205			} => typed_none(&name, &col.get_type()),
206			value => from_one(&name, value.clone()),
207		};
208		eval_columns.push((Arc::new(col.field.clone().with_name(name)), data.1));
209	}
210
211	eval_columns
212}
213
214pub struct JoinContext {
215	pub context: Option<Arc<QueryContext>>,
216	pub lowered: Vec<LoweredExpr>,
217}
218
219impl Default for JoinContext {
220	fn default() -> Self {
221		Self::new()
222	}
223}
224
225impl JoinContext {
226	pub fn new() -> Self {
227		Self {
228			context: None,
229			lowered: vec![],
230		}
231	}
232
233	pub fn set(&mut self, ctx: &QueryContext) {
234		self.context = Some(Arc::new(ctx.clone()));
235	}
236
237	pub fn get(&self) -> &Arc<QueryContext> {
238		self.context.as_ref().expect("Join context not initialized")
239	}
240
241	pub fn is_initialized(&self) -> bool {
242		self.context.is_some()
243	}
244}
245
246pub(crate) fn ensure_join_keyable(
247	columns: &[ColumnView<'_>],
248	key_indices: &[usize],
249	fragment: impl Fn(usize) -> Fragment,
250) -> Result<()> {
251	for (key, &idx) in key_indices.iter().enumerate() {
252		let ty = columns[idx].get_type();
253		if matches!(ty.inner_type(), ValueType::Digest { .. }) {
254			return Err(error!(operation::join_key_unkeyable(fragment(key), ty)));
255		}
256	}
257	Ok(())
258}
259
260pub(crate) fn join_key_types(
261	left: &[ColumnView<'_>],
262	left_indices: &[usize],
263	right: &[ColumnView<'_>],
264	right_indices: &[usize],
265	fragment: impl Fn(usize) -> Fragment,
266) -> Result<Vec<ValueType>> {
267	let mut targets = Vec::with_capacity(left_indices.len());
268	for (key, (&li, &ri)) in left_indices.iter().zip(right_indices.iter()).enumerate() {
269		let left_ty = left[li].get_type();
270		let right_ty = right[ri].get_type();
271		match common_key_type(left_ty.inner_type(), right_ty.inner_type()) {
272			Some(target) => targets.push(target),
273			None => {
274				return Err(error!(operation::join_key_type_mismatch(
275					fragment(key),
276					left_ty.inner_type().clone(),
277					right_ty.inner_type().clone()
278				)));
279			}
280		}
281	}
282	Ok(targets)
283}
284
285pub(crate) fn eval_join_condition(
286	lowered: &[LoweredExpr],
287	left_columns: &[ColumnView<'_>],
288	right_columns: &[ColumnView<'_>],
289	left_row: &[Value],
290	right_row: &[Value],
291	alias: &Option<Fragment>,
292	ctx: &QueryContext,
293) -> Result<bool> {
294	if lowered.is_empty() {
295		return Ok(true);
296	}
297	let eval_columns = build_eval_columns(left_columns, right_columns, left_row, right_row, alias);
298	let session = eval_context_from_query(ctx);
299	let exec_ctx = session.with_eval_join(batch(eval_columns)?);
300	for lowered_expr in lowered {
301		let col = lowered_expr.evaluate(&exec_ctx)?;
302		if !matches!(ColumnView::try_from(&col)?.get_value(0), Value::Boolean(true)) {
303			return Ok(false);
304		}
305	}
306	Ok(true)
307}