Skip to main content

akar_processor/processor/mapper/
map_join.rs

1use super::ExecutionContext;
2use crate::physical_operator::*;
3use akar_common::error::ProcessorError;
4use akar_common::vector::DataChunk;
5use akar_parser::ast::Expression;
6use akar_planner::logical_operator::LogicalOperator;
7
8use crate::processor::join_helpers::derive_join_column_indices;
9use crate::processor::union_helpers::{flatten_union_child, merge_optional_chunks};
10
11pub fn map_and_execute_join(
12    op: &LogicalOperator,
13    current_input: Vec<DataChunk>,
14    ctx: &mut ExecutionContext,
15) -> Result<Vec<DataChunk>, ProcessorError> {
16    match op {
17        LogicalOperator::HashJoin(h) => {
18            let left_ops = flatten_union_child(&h.build_side);
19            let right_ops = flatten_union_child(&h.probe_side);
20
21            let build_chunks = ctx.execute_children(&left_ops)?;
22            let probe_chunks = ctx.execute_children(&right_ops)?;
23
24            let (build_cols, probe_cols) = derive_join_column_indices(&h.join_keys, &build_chunks, &probe_chunks);
25            let join = PhysicalHashJoin::new(build_cols, probe_cols);
26            let result = join.execute_binary(&build_chunks, &probe_chunks)?;
27            Ok(result)
28        }
29        LogicalOperator::SemiJoin(s) => {
30            let left_ops = flatten_union_child(&s.left);
31            let right_ops = flatten_union_child(&s.right);
32
33            let build_chunks = ctx.execute_children(&left_ops)?;
34            let probe_chunks = ctx.execute_children(&right_ops)?;
35
36            let (build_cols, probe_cols) = derive_join_column_indices(&s.join_keys, &build_chunks, &probe_chunks);
37            let semi = PhysicalSemiJoin {
38                build_columns: build_cols,
39                probe_columns: probe_cols,
40            };
41            let result = semi.execute_binary(&build_chunks, &probe_chunks)?;
42            Ok(result)
43        }
44        LogicalOperator::AntiJoin(a) => {
45            let left_ops = flatten_union_child(&a.left);
46            let right_ops = flatten_union_child(&a.right);
47
48            let build_chunks = ctx.execute_children(&left_ops)?;
49            let probe_chunks = ctx.execute_children(&right_ops)?;
50
51            let (build_cols, probe_cols) = derive_join_column_indices(&a.join_keys, &build_chunks, &probe_chunks);
52            let anti = PhysicalAntiJoin {
53                build_columns: build_cols,
54                probe_columns: probe_cols,
55            };
56            let result = anti.execute_binary(&build_chunks, &probe_chunks)?;
57            Ok(result)
58        }
59        LogicalOperator::Intersect(ic) => {
60            // Build side is a (possibly nested) Union of per-pattern pipelines,
61            // each executed independently so the intersect has one hash table per
62            // pattern. Probe side is the shared-node scan.
63            let build_sides = collect_union_sides(&ic.left);
64            let probe_ops = flatten_union_child(&ic.right);
65
66            let build_chunk_sides: Vec<Vec<DataChunk>> = build_sides
67                .iter()
68                .map(|ops| ctx.execute_children(ops))
69                .collect::<Result<_, _>>()?;
70            let probe_chunks = ctx.execute_children(&probe_ops)?;
71
72            let (probe_key_col, build_key_col) =
73                resolve_intersect_key_cols(&ic.build_key_exprs, &probe_chunks, &build_chunk_sides);
74
75            let intersect = PhysicalIntersect {
76                num_build_sides: build_chunk_sides.len() as u32,
77                probe_key_col,
78                build_key_col,
79            };
80            let result = intersect.execute_sides(&build_chunk_sides, &probe_chunks)?;
81            Ok(result)
82        }
83        LogicalOperator::CrossProduct(cp) => {
84            let left_ops = flatten_union_child(&cp.left);
85            let right_ops = flatten_union_child(&cp.right);
86            let build_chunks = ctx.execute_children(&left_ops)?;
87            let probe_chunks = ctx.execute_children(&right_ops)?;
88            let cross = PhysicalCrossProduct;
89            let result = cross.execute_binary(&build_chunks, &probe_chunks)?;
90            Ok(result)
91        }
92        LogicalOperator::OptionalMatch(om) => {
93            // Execute left (required) subtree
94            let left_ops = flatten_union_child(&om.left);
95            let left_result = ctx.execute_children(&left_ops)?;
96
97            // Execute right (optional) subtree
98            let right_ops = flatten_union_child(&om.right);
99            let right_result = ctx.execute_children(&right_ops)?;
100
101            // Combine: use flattened row-level merge
102            let merged = merge_optional_chunks(left_result, right_result)?;
103            Ok(merged)
104        }
105        LogicalOperator::RecursiveExtend(re) => {
106            let scan = PhysicalRecursiveExtend {
107                source_table_id: re.source_table_id,
108                rel_table_ids: re.rel_table_ids.clone(),
109                lower_bound: re.lower_bound,
110                upper_bound: re.upper_bound,
111                direction: re.direction,
112                semantic: re.semantic,
113                table_catalog: ctx.table_catalog.clone(),
114                weight_property: re.weight_property.clone(),
115                cost_output_name: re.cost_output_name.clone(),
116            };
117            let result = scan.execute(current_input)?;
118            Ok(result)
119        }
120        _ => Err(format!("Not a join operator: {:?}", op).into()),
121    }
122}
123
124/// Flatten a (possibly nested) `Union` subtree into a list of independent
125/// operator pipelines — one per WCOJ build side.
126fn collect_union_sides(op: &LogicalOperator) -> Vec<Vec<LogicalOperator>> {
127    match op {
128        LogicalOperator::Union(u) => {
129            let mut sides = collect_union_sides(&u.left);
130            sides.extend(collect_union_sides(&u.right));
131            sides
132        }
133        other => vec![flatten_union_child(other)],
134    }
135}
136
137/// Resolve the shared-node key column index on the probe and build sides.
138///
139/// The key is derived from the first build key expression (a reference to the
140/// shared variable, e.g. `a`), resolved against `field_names` as `a.id`.
141fn resolve_intersect_key_cols(
142    build_key_exprs: &[Expression],
143    probe_chunks: &[DataChunk],
144    build_sides: &[Vec<DataChunk>],
145) -> (u32, u32) {
146    let var = build_key_exprs.first().and_then(|e| match e {
147        Expression::Variable(v) => Some(v.clone()),
148        Expression::PropertyAccess(obj, _) => {
149            if let Expression::Variable(v) = &**obj {
150                Some(v.clone())
151            } else {
152                None
153            }
154        }
155        _ => None,
156    });
157
158    let probe_names: Vec<&str> = probe_chunks
159        .first()
160        .map(|c| c.field_names.iter().map(|s| s.as_str()).collect())
161        .unwrap_or_default();
162    let build_names: Vec<&str> = build_sides
163        .first()
164        .and_then(|s| s.first())
165        .map(|c| c.field_names.iter().map(|s| s.as_str()).collect())
166        .unwrap_or_default();
167
168    let mut probe_col = 0u32;
169    let mut build_col = 0u32;
170    if let Some(var) = var {
171        let candidates = [format!("{var}._id"), format!("{var}.id"), var];
172        for c in &candidates {
173            if let Some(idx) = probe_names.iter().position(|n| n == c) {
174                probe_col = idx as u32;
175                break;
176            }
177        }
178        for c in &candidates {
179            if let Some(idx) = build_names.iter().position(|n| n == c) {
180                build_col = idx as u32;
181                break;
182            }
183        }
184    }
185    (probe_col, build_col)
186}