use sqlparser::ast;
use crate::error::{Result, SqlError};
use crate::parser::normalize::normalize_ident;
use crate::planner::predicate_coerce::coerce_predicate_literals;
use crate::resolver::ColumnScope;
use crate::resolver::columns::{BareColumn, ColumnNaming, TableScope};
use crate::resolver::expr::convert_expr;
use crate::types::*;
pub(super) type JoinSpec = (JoinType, Vec<(String, String)>, Option<SqlExpr>);
pub(super) fn extract_join_spec(
op: &ast::JoinOperator,
scope: &TableScope,
right_ref: Option<&str>,
) -> Result<JoinSpec> {
let (join_type, constraint) = match op {
ast::JoinOperator::Inner(constraint) | ast::JoinOperator::Join(constraint) => {
(JoinType::Inner, constraint)
}
ast::JoinOperator::Left(constraint) | ast::JoinOperator::LeftOuter(constraint) => {
(JoinType::Left, constraint)
}
ast::JoinOperator::Right(constraint) | ast::JoinOperator::RightOuter(constraint) => {
(JoinType::Right, constraint)
}
ast::JoinOperator::FullOuter(constraint) => (JoinType::Full, constraint),
ast::JoinOperator::CrossJoin(ast::JoinConstraint::None) => {
return Ok((JoinType::Cross, Vec::new(), None));
}
ast::JoinOperator::CrossJoin(constraint) => (JoinType::Cross, constraint),
ast::JoinOperator::Semi(constraint) | ast::JoinOperator::LeftSemi(constraint) => {
(JoinType::Semi, constraint)
}
ast::JoinOperator::Anti(constraint) | ast::JoinOperator::LeftAnti(constraint) => {
(JoinType::Anti, constraint)
}
_ => {
return Err(SqlError::Unsupported {
detail: format!("join type: {op:?}"),
});
}
};
let (keys, cond) = extract_join_constraint(constraint, scope, right_ref)?;
Ok((join_type, keys, cond))
}
type JoinConstraintResult = (Vec<(String, String)>, Option<SqlExpr>);
fn extract_join_constraint(
constraint: &ast::JoinConstraint,
scope: &TableScope,
right_ref: Option<&str>,
) -> Result<JoinConstraintResult> {
match constraint {
ast::JoinConstraint::On(expr) => {
let mut keys = Vec::new();
let mut non_equi = Vec::new();
extract_equi_keys(expr, &mut keys, &mut non_equi, scope)?;
let cond = if non_equi.is_empty() {
None
} else {
let mut combined = convert_expr(&non_equi[0], &ColumnScope::Relations(scope))?;
for pred in &non_equi[1..] {
combined = SqlExpr::BinaryOp {
left: Box::new(combined),
op: crate::types::BinaryOp::And,
right: Box::new(convert_expr(pred, &ColumnScope::Relations(scope))?),
};
}
coerce_predicate_literals(&mut combined, scope)?;
Some(combined)
};
Ok((keys, cond))
}
ast::JoinConstraint::Using(columns) => {
let keys = columns
.iter()
.map(|c| {
let name = crate::parser::normalize::normalize_object_name_checked(c)?;
scope.check_using_column(right_ref, &name)?;
Ok((name.clone(), name))
})
.collect::<Result<Vec<_>>>()?;
Ok((keys, None))
}
ast::JoinConstraint::Natural => Err(SqlError::Unsupported {
detail: "NATURAL JOIN is not supported; use explicit ON or USING clause".into(),
}),
ast::JoinConstraint::None => Err(SqlError::Unsupported {
detail: "implicit cross join (no ON/USING clause) is not supported".into(),
}),
}
}
fn extract_equi_keys(
expr: &ast::Expr,
keys: &mut Vec<(String, String)>,
non_equi: &mut Vec<ast::Expr>,
scope: &TableScope,
) -> Result<()> {
match expr {
ast::Expr::BinaryOp {
left,
op: ast::BinaryOperator::And,
right,
} => {
extract_equi_keys(left, keys, non_equi, scope)?;
extract_equi_keys(right, keys, non_equi, scope)?;
}
ast::Expr::BinaryOp {
left,
op: ast::BinaryOperator::Eq,
right,
} => {
if let (Some(l), Some(r)) = (extract_col_ref(left), extract_col_ref(right)) {
keys.push((resolve_join_key(scope, l)?, resolve_join_key(scope, r)?));
} else {
non_equi.push(expr.clone());
}
}
_ => {
non_equi.push(expr.clone());
}
}
Ok(())
}
pub(super) fn orient_keys_to_sides(keys: &mut [(String, String)], right_ids: &[String]) {
let qualifier = |k: &str| k.rsplit_once('.').map(|(q, _)| q.to_string());
let on_right = |k: &str| {
qualifier(k)
.map(|q| right_ids.iter().any(|id| id == &q))
.unwrap_or(false)
};
for (l, r) in keys.iter_mut() {
if on_right(l) && !on_right(r) {
std::mem::swap(l, r);
}
}
}
fn resolve_join_key(scope: &TableScope, key: String) -> Result<String> {
if let Some((table, column)) = key.rsplit_once('.') {
scope.check_name(Some(table), column)?;
return Ok(key);
}
let owner = scope.resolve_bare(&key)?;
Ok(match (scope.column_naming(), owner) {
(ColumnNaming::JoinRow, BareColumn::Relation(table)) => {
format!("{}.{key}", table.ref_name())
}
(ColumnNaming::JoinRow, BareColumn::Output | BareColumn::Outer)
| (ColumnNaming::AsWritten | ColumnNaming::OutputRow, _) => key,
})
}
fn extract_col_ref(expr: &ast::Expr) -> Option<String> {
match expr {
ast::Expr::Identifier(ident) => Some(normalize_ident(ident)),
ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => Some(format!(
"{}.{}",
normalize_ident(&parts[0]),
normalize_ident(&parts[1])
)),
ast::Expr::CompoundIdentifier(_) => None,
_ => None,
}
}