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 postcard::to_stdvec;
7use reifydb_core::value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns};
8use reifydb_transaction::transaction::Transaction;
9use reifydb_value::{
10	fragment::Fragment,
11	util::hash::{Hash128, xxh3_128},
12	value::Value,
13};
14
15use crate::{
16	Result,
17	expression::{compile::CompiledExpr, context::EvalContext},
18	vm::volcano::query::{QueryContext, QueryNode},
19};
20
21pub(crate) fn load_and_merge_all<'a>(
22	node: &mut Box<dyn QueryNode>,
23	rx: &mut Transaction<'a>,
24	ctx: &mut QueryContext,
25) -> Result<Columns> {
26	let mut result: Option<Columns> = None;
27
28	while let Some(columns) = node.next(rx, ctx)? {
29		if let Some(mut acc) = result.take() {
30			acc.append_columns(columns)?;
31			result = Some(acc);
32		} else {
33			result = Some(columns);
34		}
35	}
36	let result = result.unwrap_or_else(Columns::empty);
37	Ok(result)
38}
39
40pub struct ResolvedColumnNames {
41	pub qualified_names: Vec<String>,
42}
43
44pub fn resolve_column_names(
45	left_columns: &Columns,
46	right_columns: &Columns,
47	alias: &Option<Fragment>,
48	excluded_right_indices: Option<&[usize]>,
49) -> ResolvedColumnNames {
50	let mut qualified_names = Vec::new();
51
52	for col in left_columns.iter() {
53		qualified_names.push(col.name().text().to_string());
54	}
55
56	for (idx, col) in right_columns.iter().enumerate() {
57		if let Some(excluded) = excluded_right_indices
58			&& excluded.contains(&idx)
59		{
60			continue;
61		}
62
63		let col_name = col.name().text();
64
65		let alias_text = alias.as_ref().map(|a| a.text()).unwrap_or("other");
66		let prefixed_name = format!("{}_{}", alias_text, col_name);
67
68		let mut final_name = prefixed_name.clone();
69		if qualified_names.contains(&final_name) {
70			let mut counter = 2;
71			loop {
72				let candidate = format!("{}_{}", prefixed_name, counter);
73				if !qualified_names.contains(&candidate) {
74					final_name = candidate;
75					break;
76				}
77				counter += 1;
78			}
79		}
80
81		qualified_names.push(final_name);
82	}
83
84	ResolvedColumnNames {
85		qualified_names,
86	}
87}
88
89pub fn build_eval_columns(
90	left_columns: &Columns,
91	right_columns: &Columns,
92	left_row: &[Value],
93	right_row: &[Value],
94	alias: &Option<Fragment>,
95) -> Vec<ColumnWithName> {
96	let mut eval_columns = Vec::new();
97
98	for (idx, col) in left_columns.iter().enumerate() {
99		let data = match &left_row[idx] {
100			Value::None {
101				..
102			} => ColumnBuffer::typed_none(&col.get_type()),
103			value => ColumnBuffer::from(value.clone()),
104		};
105		eval_columns.push(ColumnWithName::new(col.name().clone(), data));
106	}
107
108	for (idx, col) in right_columns.iter().enumerate() {
109		let data = match &right_row[idx] {
110			Value::None {
111				..
112			} => ColumnBuffer::typed_none(&col.get_type()),
113			value => ColumnBuffer::from(value.clone()),
114		};
115		if let Some(alias) = alias {
116			let aliased_name = Fragment::internal(format!("{}.{}", alias.text(), col.name().text()));
117			eval_columns.push(ColumnWithName {
118				name: aliased_name,
119				data,
120			});
121		} else {
122			eval_columns.push(ColumnWithName::new(col.name().clone(), data));
123		}
124	}
125
126	eval_columns
127}
128
129pub struct JoinContext {
130	pub context: Option<Arc<QueryContext>>,
131	pub compiled: Vec<CompiledExpr>,
132}
133
134impl Default for JoinContext {
135	fn default() -> Self {
136		Self::new()
137	}
138}
139
140impl JoinContext {
141	pub fn new() -> Self {
142		Self {
143			context: None,
144			compiled: vec![],
145		}
146	}
147
148	pub fn set(&mut self, ctx: &QueryContext) {
149		self.context = Some(Arc::new(ctx.clone()));
150	}
151
152	pub fn get(&self) -> &Arc<QueryContext> {
153		self.context.as_ref().expect("Join context not initialized")
154	}
155
156	pub fn is_initialized(&self) -> bool {
157		self.context.is_some()
158	}
159}
160
161pub(crate) fn compute_join_hash(
162	columns: &Columns,
163	col_indices: &[usize],
164	row_idx: usize,
165	buf: &mut Vec<u8>,
166) -> Option<Hash128> {
167	buf.clear();
168	for &idx in col_indices {
169		let value = columns[idx].get_value(row_idx);
170		if matches!(value, Value::None { .. }) {
171			return None;
172		}
173		let bytes = to_stdvec(&value).ok()?;
174		buf.extend_from_slice(&bytes);
175	}
176	Some(xxh3_128(buf))
177}
178
179pub(crate) fn keys_equal_by_index(
180	left: &Columns,
181	left_row: usize,
182	left_indices: &[usize],
183	right: &Columns,
184	right_row: usize,
185	right_indices: &[usize],
186) -> bool {
187	for (&li, &ri) in left_indices.iter().zip(right_indices.iter()) {
188		let lv = left[li].get_value(left_row);
189		let rv = right[ri].get_value(right_row);
190		if lv != rv {
191			return false;
192		}
193	}
194	true
195}
196
197pub(crate) fn eval_join_condition(
198	compiled: &[CompiledExpr],
199	left_columns: &Columns,
200	right_columns: &Columns,
201	left_row: &[Value],
202	right_row: &[Value],
203	alias: &Option<Fragment>,
204	ctx: &QueryContext,
205) -> bool {
206	if compiled.is_empty() {
207		return true;
208	}
209	let eval_columns = build_eval_columns(left_columns, right_columns, left_row, right_row, alias);
210	let session = EvalContext::from_query(ctx);
211	let exec_ctx = session.with_eval_join(Columns::new(eval_columns));
212	compiled.iter().all(|compiled_expr| {
213		let col = compiled_expr.execute(&exec_ctx).unwrap();
214		matches!(col.data().get_value(0), Value::Boolean(true))
215	})
216}