reifydb_engine/vm/volcano/join/
natural.rs1use std::collections::{HashMap, HashSet};
5
6use arrow_array::{Array, RecordBatch};
7use arrow_row::RowConverter;
8use reifydb_core::{
9 common::JoinType, error::diagnostic::operation, internal_error, value::column::headers::ColumnHeaders,
10};
11use reifydb_transaction::transaction::Transaction;
12use reifydb_value::{
13 error,
14 fragment::Fragment,
15 reifydb_assertions,
16 value::{system_columns::user_columns, value_type::ValueType},
17};
18use tracing::instrument;
19
20use super::common::{
21 JoinContext, JoinSlot, ensure_join_keyable, join_key_types, load_and_merge_all, materialize_join,
22 resolve_column_names, user_key_columns, user_views,
23};
24use crate::{
25 Result,
26 vm::volcano::{
27 key_rows::{key_arrays, key_rows},
28 query::{QueryContext, QueryNode},
29 },
30};
31
32pub struct NaturalJoinNode {
33 left: Box<dyn QueryNode>,
34 right: Box<dyn QueryNode>,
35 join_type: JoinType,
36 alias: Option<Fragment>,
37 left_name: Option<Fragment>,
38 fragment: Fragment,
39 headers: Option<ColumnHeaders>,
40 context: JoinContext,
41}
42
43impl NaturalJoinNode {
44 pub(crate) fn new(
45 left: Box<dyn QueryNode>,
46 right: Box<dyn QueryNode>,
47 join_type: JoinType,
48 alias: Option<Fragment>,
49 left_name: Option<Fragment>,
50 fragment: Fragment,
51 ) -> Self {
52 Self {
53 left,
54 right,
55 join_type,
56 alias,
57 left_name,
58 fragment,
59 headers: None,
60 context: JoinContext::new(),
61 }
62 }
63
64 fn find_common_columns(left_columns: &RecordBatch, right_columns: &RecordBatch) -> Vec<(String, usize, usize)> {
65 let mut common_columns = Vec::new();
66
67 for (left_idx, (left_field, _)) in user_columns(left_columns).enumerate() {
68 for (right_idx, (right_field, _)) in user_columns(right_columns).enumerate() {
69 if left_field.name() == right_field.name() {
70 common_columns.push((left_field.name().to_string(), left_idx, right_idx));
71 }
72 }
73 }
74
75 common_columns
76 }
77}
78
79impl QueryNode for NaturalJoinNode {
80 #[instrument(name = "volcano::join::natural::initialize", level = "trace", skip_all)]
81 fn initialize<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &QueryContext) -> Result<()> {
82 self.context.set(ctx);
83 self.left.initialize(rx, ctx)?;
84 self.right.initialize(rx, ctx)?;
85 Ok(())
86 }
87
88 #[instrument(name = "volcano::join::natural::next", level = "trace", skip_all)]
89 fn next<'a>(&mut self, rx: &mut Transaction<'a>, ctx: &mut QueryContext) -> Result<Option<RecordBatch>> {
90 reifydb_assertions! {
91 assert!(self.context.is_initialized(), "NaturalJoinNode::next() called before initialize()");
92 }
93
94 if self.headers.is_some() {
95 return Ok(None);
96 }
97
98 let left_columns = load_and_merge_all(&mut self.left, rx, ctx)?;
99 let right_columns = load_and_merge_all(&mut self.right, rx, ctx)?;
100
101 let left_rows = left_columns.num_rows();
102
103 let common_columns = Self::find_common_columns(&left_columns, &right_columns);
104
105 if common_columns.is_empty() {
106 let left = self.left_name.as_ref().map_or("the left input", |name| name.text());
107 let right = self.alias.as_ref().map_or("the right input", |name| name.text());
108 return Err(error!(operation::natural_join_no_shared_column(
109 self.fragment.clone(),
110 left,
111 right
112 )));
113 }
114
115 let excluded_right_cols: HashSet<usize> =
116 common_columns.iter().map(|(_, _, right_idx)| *right_idx).collect();
117
118 let excluded_indices: Vec<usize> = excluded_right_cols.iter().copied().collect();
119
120 let resolved =
121 resolve_column_names(&left_columns, &right_columns, &self.alias, Some(&excluded_indices));
122
123 let right_col_indices: Vec<usize> = common_columns.iter().map(|(_, _, ri)| *ri).collect();
124 let left_col_indices: Vec<usize> = common_columns.iter().map(|(_, li, _)| *li).collect();
125 let left_views = user_views(&left_columns)?;
126 let right_views = user_views(&right_columns)?;
127 ensure_join_keyable(&left_views, &left_col_indices, |_| self.fragment.clone())?;
128 ensure_join_keyable(&right_views, &right_col_indices, |_| self.fragment.clone())?;
129 let targets = join_key_types(&left_views, &left_col_indices, &right_views, &right_col_indices, |_| {
130 self.fragment.clone()
131 })?;
132
133 let (converter, hash_table) = Self::build(&right_columns, &right_col_indices, &targets)?;
134
135 let (left_picks, right_picks) = self.probe(
136 &left_columns,
137 &ProbeContext {
138 converter: &converter,
139 hash_table: &hash_table,
140 left_col_indices: &left_col_indices,
141 targets: &targets,
142 left_rows,
143 },
144 )?;
145
146 let columns = materialize_join(
147 &resolved.qualified_names,
148 &[JoinSlot {
149 columns: &left_columns,
150 picks: &left_picks,
151 }],
152 &right_columns,
153 &excluded_indices,
154 &right_picks,
155 0,
156 )?;
157
158 self.headers = Some(ColumnHeaders::from_batch(&columns));
159 Ok(Some(columns))
160 }
161
162 fn headers(&self) -> Option<ColumnHeaders> {
163 self.headers.clone()
164 }
165}
166
167type KeyIndex = HashMap<Box<[u8]>, Vec<usize>>;
168
169struct ProbeContext<'a> {
170 converter: &'a RowConverter,
171 hash_table: &'a KeyIndex,
172 left_col_indices: &'a [usize],
173 targets: &'a [ValueType],
174 left_rows: usize,
175}
176
177impl NaturalJoinNode {
178 #[instrument(level = "trace", skip_all, name = "volcano::join::natural::build")]
179 fn build(
180 right_columns: &RecordBatch,
181 right_col_indices: &[usize],
182 targets: &[ValueType],
183 ) -> Result<(RowConverter, KeyIndex)> {
184 let (converter, arrays) = key_rows(&user_key_columns(right_columns, right_col_indices), targets)?;
185 let rows = converter
186 .convert_columns(&arrays)
187 .map_err(|e| internal_error!("Failed to build join keys: {}", e))?;
188 let mut hash_table: KeyIndex = HashMap::new();
189 for j in 0..right_columns.num_rows() {
190 if arrays.iter().any(|array| array.is_null(j)) {
191 continue;
192 }
193 let key = rows.row(j);
194 match hash_table.get_mut(key.as_ref()) {
195 Some(indices) => indices.push(j),
196 None => {
197 hash_table.insert(Box::from(key.as_ref()), vec![j]);
198 }
199 }
200 }
201 Ok((converter, hash_table))
202 }
203
204 #[instrument(level = "trace", skip_all, name = "volcano::join::natural::probe")]
205 fn probe(
206 &self,
207 left_columns: &RecordBatch,
208 probe_ctx: &ProbeContext,
209 ) -> Result<(Vec<usize>, Vec<Option<usize>>)> {
210 let ProbeContext {
211 converter,
212 hash_table,
213 left_col_indices,
214 targets,
215 left_rows,
216 } = probe_ctx;
217 let arrays = key_arrays(&user_key_columns(left_columns, left_col_indices), targets);
218 let rows = converter
219 .convert_columns(&arrays)
220 .map_err(|e| internal_error!("Failed to build join keys: {}", e))?;
221
222 let mut left_picks: Vec<usize> = Vec::new();
223 let mut right_picks: Vec<Option<usize>> = Vec::new();
224
225 for i in 0..*left_rows {
226 let mut matched = false;
227
228 let candidates = if arrays.iter().any(|array| array.is_null(i)) {
229 None
230 } else {
231 hash_table.get(rows.row(i).as_ref())
232 };
233
234 if let Some(indices) = candidates {
235 for &j in indices {
236 left_picks.push(i);
237 right_picks.push(Some(j));
238 matched = true;
239 }
240 }
241
242 if !matched && matches!(self.join_type, JoinType::Left) {
243 left_picks.push(i);
244 right_picks.push(None);
245 }
246 }
247
248 Ok((left_picks, right_picks))
249 }
250}