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