Skip to main content

alopex_core/sql/
join.rs

1//! Generic JOIN execution primitives.
2//!
3//! This module contains type-independent JOIN control flow. Callers provide
4//! concrete row storage, predicate evaluation, key extraction, row construction,
5//! and null value construction.
6
7use std::collections::{HashMap, HashSet};
8use std::hash::Hash;
9
10/// A generic joined row pair emitted by JOIN control-flow primitives.
11pub type JoinPair<T> = (Option<T>, Option<T>);
12
13/// Generic joined row pairs emitted by JOIN control-flow primitives.
14pub type JoinPairs<T> = Vec<JoinPair<T>>;
15
16/// Minimal row interface required by generic JOIN row-combination helpers.
17pub trait RowLike {
18    /// Cell value type stored by the row.
19    type Value: Clone;
20
21    /// Return the row values in column order.
22    fn values(&self) -> &[Self::Value];
23
24    /// Return the number of columns in the row.
25    fn arity(&self) -> usize {
26        self.values().len()
27    }
28}
29
30/// Combine left and right row values into a caller-owned output row.
31pub fn combine_rows<R, O>(
32    left: &R,
33    right: &R,
34    row_id: u64,
35    build_row: impl FnOnce(u64, Vec<R::Value>) -> O,
36) -> O
37where
38    R: RowLike,
39{
40    let mut values = Vec::with_capacity(left.arity() + right.arity());
41    values.extend(left.values().iter().cloned());
42    values.extend(right.values().iter().cloned());
43    build_row(row_id, values)
44}
45
46/// Combine a left row with right-side null padding.
47pub fn pad_right<R, O, N>(
48    left: &R,
49    right_width: usize,
50    row_id: u64,
51    mut null_value: N,
52    build_row: impl FnOnce(u64, Vec<R::Value>) -> O,
53) -> O
54where
55    R: RowLike,
56    N: FnMut() -> R::Value,
57{
58    let mut values = Vec::with_capacity(left.arity() + right_width);
59    values.extend(left.values().iter().cloned());
60    values.extend((0..right_width).map(|_| null_value()));
61    build_row(row_id, values)
62}
63
64/// Combine a right row with left-side null padding.
65pub fn pad_left<R, O, N>(
66    right: &R,
67    left_width: usize,
68    row_id: u64,
69    mut null_value: N,
70    build_row: impl FnOnce(u64, Vec<R::Value>) -> O,
71) -> O
72where
73    R: RowLike,
74    N: FnMut() -> R::Value,
75{
76    let mut values = Vec::with_capacity(left_width + right.arity());
77    values.extend((0..left_width).map(|_| null_value()));
78    values.extend(right.values().iter().cloned());
79    build_row(row_id, values)
80}
81
82/// Execute a generic nested-loop join.
83///
84/// `include_unmatched_left` and `include_unmatched_right` model LEFT/RIGHT/FULL
85/// outer-row emission while leaving concrete join-type dispatch to callers.
86pub fn nested_loop_join<T, E, P>(
87    left: &[T],
88    right: &[T],
89    include_unmatched_left: bool,
90    include_unmatched_right: bool,
91    mut matches: P,
92) -> Result<JoinPairs<T>, E>
93where
94    T: Clone,
95    P: FnMut(&T, &T) -> Result<bool, E>,
96{
97    let mut output = Vec::new();
98    let mut matched_right = HashSet::new();
99
100    for left_row in left {
101        let mut matched_left = false;
102        for (right_idx, right_row) in right.iter().enumerate() {
103            if matches(left_row, right_row)? {
104                matched_left = true;
105                matched_right.insert(right_idx);
106                output.push((Some(left_row.clone()), Some(right_row.clone())));
107            }
108        }
109        if !matched_left && include_unmatched_left {
110            output.push((Some(left_row.clone()), None));
111        }
112    }
113
114    if include_unmatched_right {
115        for (right_idx, right_row) in right.iter().enumerate() {
116            if !matched_right.contains(&right_idx) {
117                output.push((None, Some(right_row.clone())));
118            }
119        }
120    }
121
122    Ok(output)
123}
124
125/// Execute a generic hash join for equi-joins.
126///
127/// Callers provide key extraction so SQL-specific value and error types remain
128/// outside `alopex-core`.
129pub fn hash_join<T, K, E, L, R>(
130    left: &[T],
131    right: &[T],
132    include_unmatched_left: bool,
133    include_unmatched_right: bool,
134    mut left_key: L,
135    mut right_key: R,
136) -> Result<JoinPairs<T>, E>
137where
138    T: Clone,
139    K: Eq + Hash,
140    L: FnMut(&T) -> Result<K, E>,
141    R: FnMut(&T) -> Result<K, E>,
142{
143    let mut buckets: HashMap<K, Vec<(usize, &T)>> = HashMap::new();
144    for (idx, right_row) in right.iter().enumerate() {
145        let key = right_key(right_row)?;
146        buckets.entry(key).or_default().push((idx, right_row));
147    }
148
149    let mut output = Vec::new();
150    let mut matched_right = HashSet::new();
151    for left_row in left {
152        let key = left_key(left_row)?;
153        let mut matched_left = false;
154        if let Some(matches) = buckets.get(&key) {
155            for (right_idx, right_row) in matches {
156                matched_left = true;
157                matched_right.insert(*right_idx);
158                output.push((Some(left_row.clone()), Some((*right_row).clone())));
159            }
160        }
161        if !matched_left && include_unmatched_left {
162            output.push((Some(left_row.clone()), None));
163        }
164    }
165
166    if include_unmatched_right {
167        for (right_idx, right_row) in right.iter().enumerate() {
168            if !matched_right.contains(&right_idx) {
169                output.push((None, Some(right_row.clone())));
170            }
171        }
172    }
173
174    Ok(output)
175}
176
177#[cfg(test)]
178mod tests {
179    use super::{combine_rows, hash_join, nested_loop_join, pad_left, pad_right, RowLike};
180
181    #[derive(Debug, Clone, PartialEq, Eq)]
182    struct TestRow {
183        row_id: u64,
184        values: Vec<Option<i64>>,
185    }
186
187    impl TestRow {
188        fn new(row_id: u64, values: Vec<Option<i64>>) -> Self {
189            Self { row_id, values }
190        }
191    }
192
193    impl RowLike for TestRow {
194        type Value = Option<i64>;
195
196        fn values(&self) -> &[Self::Value] {
197            &self.values
198        }
199    }
200
201    fn rows(values: &[&[i64]]) -> Vec<TestRow> {
202        values
203            .iter()
204            .enumerate()
205            .map(|(idx, row)| TestRow::new(idx as u64, row.iter().copied().map(Some).collect()))
206            .collect()
207    }
208
209    #[test]
210    fn combine_and_padding_build_join_rows() {
211        let left = TestRow::new(10, vec![Some(1), Some(2)]);
212        let right = TestRow::new(20, vec![Some(3)]);
213
214        assert_eq!(
215            combine_rows(&left, &right, 0, TestRow::new),
216            TestRow::new(0, vec![Some(1), Some(2), Some(3)])
217        );
218        assert_eq!(
219            pad_right(&left, 2, 1, || None, TestRow::new),
220            TestRow::new(1, vec![Some(1), Some(2), None, None])
221        );
222        assert_eq!(
223            pad_left(&right, 2, 2, || None, TestRow::new),
224            TestRow::new(2, vec![None, None, Some(3)])
225        );
226    }
227
228    #[test]
229    fn nested_loop_join_emits_inner_matches() {
230        let left = rows(&[&[1, 10], &[2, 20]]);
231        let right = rows(&[&[1, 100], &[3, 300]]);
232
233        let output = nested_loop_join(&left, &right, false, false, |left, right| {
234            Ok::<_, ()>(left.values[0] == right.values[0])
235        })
236        .unwrap();
237
238        assert_eq!(
239            output,
240            vec![(Some(left[0].clone()), Some(right[0].clone()))]
241        );
242    }
243
244    #[test]
245    fn nested_loop_join_emits_left_right_and_full_rows() {
246        let left = rows(&[&[1], &[2]]);
247        let right = rows(&[&[1], &[3]]);
248
249        let left_output = nested_loop_join(&left, &right, true, false, |left, right| {
250            Ok::<_, ()>(left.values[0] == right.values[0])
251        })
252        .unwrap();
253        assert_eq!(
254            left_output,
255            vec![
256                (Some(left[0].clone()), Some(right[0].clone())),
257                (Some(left[1].clone()), None),
258            ]
259        );
260
261        let right_output = nested_loop_join(&left, &right, false, true, |left, right| {
262            Ok::<_, ()>(left.values[0] == right.values[0])
263        })
264        .unwrap();
265        assert_eq!(
266            right_output,
267            vec![
268                (Some(left[0].clone()), Some(right[0].clone())),
269                (None, Some(right[1].clone())),
270            ]
271        );
272
273        let full_output = nested_loop_join(&left, &right, true, true, |left, right| {
274            Ok::<_, ()>(left.values[0] == right.values[0])
275        })
276        .unwrap();
277        assert_eq!(
278            full_output,
279            vec![
280                (Some(left[0].clone()), Some(right[0].clone())),
281                (Some(left[1].clone()), None),
282                (None, Some(right[1].clone())),
283            ]
284        );
285    }
286
287    #[test]
288    fn hash_join_emits_matches_and_outer_rows() {
289        let left = rows(&[&[1, 10], &[2, 20]]);
290        let right = rows(&[&[1, 100], &[3, 300]]);
291
292        let output = hash_join(
293            &left,
294            &right,
295            true,
296            true,
297            |row| Ok::<_, ()>(row.values[0]),
298            |row| Ok::<_, ()>(row.values[0]),
299        )
300        .unwrap();
301
302        assert_eq!(
303            output,
304            vec![
305                (Some(left[0].clone()), Some(right[0].clone())),
306                (Some(left[1].clone()), None),
307                (None, Some(right[1].clone())),
308            ]
309        );
310    }
311}