Skip to main content

nodedb_sql/planner/join/
constraint.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ON-clause / USING-clause extraction: equi-join keys + non-equi residual.
4
5use sqlparser::ast;
6
7use crate::error::{Result, SqlError};
8use crate::parser::normalize::normalize_ident;
9use crate::resolver::expr::convert_expr;
10use crate::types::*;
11
12/// (join_type, equi_keys, non-equi condition)
13pub(super) type JoinSpec = (JoinType, Vec<(String, String)>, Option<SqlExpr>);
14
15/// Extract join type, equi-join keys, and non-equi condition.
16pub(super) fn extract_join_spec(op: &ast::JoinOperator) -> Result<JoinSpec> {
17    match op {
18        ast::JoinOperator::Inner(constraint) | ast::JoinOperator::Join(constraint) => {
19            let (keys, cond) = extract_join_constraint(constraint)?;
20            Ok((JoinType::Inner, keys, cond))
21        }
22        ast::JoinOperator::Left(constraint) | ast::JoinOperator::LeftOuter(constraint) => {
23            let (keys, cond) = extract_join_constraint(constraint)?;
24            Ok((JoinType::Left, keys, cond))
25        }
26        ast::JoinOperator::Right(constraint) | ast::JoinOperator::RightOuter(constraint) => {
27            let (keys, cond) = extract_join_constraint(constraint)?;
28            Ok((JoinType::Right, keys, cond))
29        }
30        ast::JoinOperator::FullOuter(constraint) => {
31            let (keys, cond) = extract_join_constraint(constraint)?;
32            Ok((JoinType::Full, keys, cond))
33        }
34        ast::JoinOperator::CrossJoin(constraint) => {
35            let (keys, cond) = extract_join_constraint(constraint)?;
36            Ok((JoinType::Cross, keys, cond))
37        }
38        ast::JoinOperator::Semi(constraint) | ast::JoinOperator::LeftSemi(constraint) => {
39            let (keys, cond) = extract_join_constraint(constraint)?;
40            Ok((JoinType::Semi, keys, cond))
41        }
42        ast::JoinOperator::Anti(constraint) | ast::JoinOperator::LeftAnti(constraint) => {
43            let (keys, cond) = extract_join_constraint(constraint)?;
44            Ok((JoinType::Anti, keys, cond))
45        }
46        _ => Err(SqlError::Unsupported {
47            detail: format!("join type: {op:?}"),
48        }),
49    }
50}
51
52/// (equi_keys, non-equi condition)
53type JoinConstraintResult = (Vec<(String, String)>, Option<SqlExpr>);
54
55fn extract_join_constraint(constraint: &ast::JoinConstraint) -> Result<JoinConstraintResult> {
56    match constraint {
57        ast::JoinConstraint::On(expr) => {
58            let mut keys = Vec::new();
59            let mut non_equi = Vec::new();
60            extract_equi_keys(expr, &mut keys, &mut non_equi)?;
61            let cond = if non_equi.is_empty() {
62                None
63            } else {
64                let mut combined = convert_expr(&non_equi[0])?;
65                for pred in &non_equi[1..] {
66                    combined = SqlExpr::BinaryOp {
67                        left: Box::new(combined),
68                        op: crate::types::BinaryOp::And,
69                        right: Box::new(convert_expr(pred)?),
70                    };
71                }
72                Some(combined)
73            };
74            Ok((keys, cond))
75        }
76        ast::JoinConstraint::Using(columns) => {
77            let keys = columns
78                .iter()
79                .map(|c| {
80                    let name = crate::parser::normalize::normalize_object_name_checked(c)?;
81                    Ok((name.clone(), name))
82                })
83                .collect::<Result<Vec<_>>>()?;
84            Ok((keys, None))
85        }
86        ast::JoinConstraint::Natural => Err(SqlError::Unsupported {
87            detail: "NATURAL JOIN is not supported; use explicit ON or USING clause".into(),
88        }),
89        ast::JoinConstraint::None => Err(SqlError::Unsupported {
90            detail: "implicit cross join (no ON/USING clause) is not supported".into(),
91        }),
92    }
93}
94
95fn extract_equi_keys(
96    expr: &ast::Expr,
97    keys: &mut Vec<(String, String)>,
98    non_equi: &mut Vec<ast::Expr>,
99) -> Result<()> {
100    match expr {
101        ast::Expr::BinaryOp {
102            left,
103            op: ast::BinaryOperator::And,
104            right,
105        } => {
106            extract_equi_keys(left, keys, non_equi)?;
107            extract_equi_keys(right, keys, non_equi)?;
108        }
109        ast::Expr::BinaryOp {
110            left,
111            op: ast::BinaryOperator::Eq,
112            right,
113        } => {
114            if let (Some(l), Some(r)) = (extract_col_ref(left), extract_col_ref(right)) {
115                keys.push((l, r));
116            } else {
117                non_equi.push(expr.clone());
118            }
119        }
120        _ => {
121            non_equi.push(expr.clone());
122        }
123    }
124    Ok(())
125}
126
127/// Orient equi-join keys to the FROM order: `(left_side_key, right_side_key)`.
128///
129/// `extract_equi_keys` records each `a = b` pair in the textual order it
130/// appears in the ON clause (`(a, b)`), which is independent of which side of
131/// the FROM clause each operand belongs to. Downstream physical-plan
132/// conversion assumes `on.0` references the left input and `on.1` the right
133/// input (it builds the hash index on the right key and probes with the left).
134/// A query that writes `ON right.k = left.k` would otherwise build the index
135/// on the wrong side and match zero rows.
136///
137/// For each key pair, if the first operand is qualified by an alias/name
138/// belonging to the right side (and the second is not), swap so the right-side
139/// operand is always `on.1`. `right_ids` is the set of normalized
140/// alias/table-name identifiers for the right relation.
141pub(super) fn orient_keys_to_sides(keys: &mut [(String, String)], right_ids: &[String]) {
142    let qualifier = |k: &str| k.rsplit_once('.').map(|(q, _)| q.to_string());
143    let on_right = |k: &str| {
144        qualifier(k)
145            .map(|q| right_ids.iter().any(|id| id == &q))
146            .unwrap_or(false)
147    };
148    for (l, r) in keys.iter_mut() {
149        // Swap only when the left operand clearly belongs to the right side and
150        // the right operand does not — i.e. the pair is written reversed.
151        if on_right(l) && !on_right(r) {
152            std::mem::swap(l, r);
153        }
154    }
155}
156
157fn extract_col_ref(expr: &ast::Expr) -> Option<String> {
158    match expr {
159        ast::Expr::Identifier(ident) => Some(normalize_ident(ident)),
160        // Two-part: table.column — preserve the qualified form as the join key.
161        ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => Some(format!(
162            "{}.{}",
163            normalize_ident(&parts[0]),
164            normalize_ident(&parts[1])
165        )),
166        // Three or more parts: schema.table.column — reject by returning None;
167        // the caller will push the expression into non_equi which convert_expr
168        // then rejects with SqlError::Unsupported.
169        ast::Expr::CompoundIdentifier(_) => None,
170        _ => None,
171    }
172}