Skip to main content

akar_processor/processor/
join_helpers.rs

1use akar_common::vector::DataChunk;
2use akar_parser::ast::Expression;
3
4/// Resolved join key binding for one equality condition: which build/probe
5/// columns participate, and whether the column holds a map/struct from which a
6/// key must be extracted (e.g. `UNWIND $rows AS row MATCH (m:Memory
7/// {id: row.id})` binds the probe column `row` with map key `id`, P53.26).
8#[derive(Debug, Clone)]
9pub struct JoinKeyBinding {
10    pub build_col: u32,
11    pub probe_col: u32,
12    /// Map key to extract from the build-side column value (None = raw cell).
13    pub build_map_key: Option<String>,
14    /// Map key to extract from the probe-side column value (None = raw cell).
15    pub probe_map_key: Option<String>,
16}
17
18/// Resolve the build/probe column indices for a join condition. Returns one
19/// binding per `Equal` key.
20///
21/// Column resolution tries, in order:
22/// 1. the fully-qualified property (`m.id` / `row.id`),
23/// 2. the bare property name (`id` — scan columns use plain column names),
24/// 3. a variable column holding map/struct values, with the key to extract
25///    (`row` + key `id`).
26///
27/// The cross-side fallback (try the opposite side's expression) preserves the
28/// legacy behaviour when chunk field names are asymmetric.
29pub fn derive_join_bindings(
30    join_keys: &[Expression],
31    build_chunks: &[DataChunk],
32    probe_chunks: &[DataChunk],
33) -> Vec<JoinKeyBinding> {
34    let build_names: Vec<&str> = build_chunks
35        .first()
36        .map(|c| c.field_names.iter().map(|s| s.as_str()).collect())
37        .unwrap_or_default();
38    let probe_names: Vec<&str> = probe_chunks
39        .first()
40        .map(|c| c.field_names.iter().map(|s| s.as_str()).collect())
41        .unwrap_or_default();
42
43    let mut bindings = Vec::new();
44    for key in join_keys {
45        if let Expression::BinaryOp(akar_parser::ast::BinaryOp::Equal, left, right) = key {
46            let (lp, lvar, lprop) = split_prop(left);
47            let (rp, rvar, rprop) = split_prop(right);
48
49            let (build_col, build_map_key) = resolve_side(&lp, &lvar, &lprop, &build_names)
50                .or_else(|| resolve_side(&rp, &rvar, &rprop, &build_names))
51                .unwrap_or((0, None));
52            let (probe_col, probe_map_key) = resolve_side(&rp, &rvar, &rprop, &probe_names)
53                .or_else(|| resolve_side(&lp, &lvar, &lprop, &probe_names))
54                .unwrap_or((0, None));
55
56            bindings.push(JoinKeyBinding {
57                build_col: build_col as u32,
58                probe_col: probe_col as u32,
59                build_map_key,
60                probe_map_key,
61            });
62        }
63    }
64
65    if bindings.is_empty() {
66        bindings.push(JoinKeyBinding {
67            build_col: 0,
68            probe_col: 0,
69            build_map_key: None,
70            probe_map_key: None,
71        });
72    }
73    bindings
74}
75
76/// Split a join-key expression into `(full_prop, base_variable, key)`.
77/// A plain variable `iid` yields `("iid", "iid", "iid")`; a property access
78/// `row.id` yields `("row.id", "row", "id")`.
79fn split_prop(expr: &Expression) -> (String, String, String) {
80    match expr {
81        Expression::PropertyAccess(obj, prop) => {
82            if let Expression::Variable(var) = &**obj {
83                (format!("{var}.{prop}"), var.clone(), prop.clone())
84            } else {
85                (prop.clone(), String::new(), prop.clone())
86            }
87        }
88        Expression::Variable(name) => (name.clone(), name.clone(), name.clone()),
89        _ => (String::new(), String::new(), String::new()),
90    }
91}
92
93/// Resolve one side of a join key against the side's chunk field names.
94/// Returns `(column_index, map_key_to_extract)`.
95fn resolve_side(full: &str, var: &str, key: &str, names: &[&str]) -> Option<(usize, Option<String>)> {
96    if let Some(i) = names.iter().position(|n| *n == full) {
97        return Some((i, None));
98    }
99    if let Some(i) = names.iter().position(|n| *n == key) {
100        return Some((i, None));
101    }
102    if !var.is_empty()
103        && let Some(i) = names.iter().position(|n| *n == var)
104    {
105        return Some((i, Some(key.to_string())));
106    }
107    None
108}