use std::ops::ControlFlow;
use sqlparser::ast::{self, SetExpr};
use crate::error::{Result, SqlError};
use crate::functions::registry::FunctionRegistry;
use crate::parser::normalize::normalize_ident;
use crate::planner::ast_helpers::{flatten_and_expr, rebuild_and_expr};
use crate::planner::select::{convert_projection, plan_query};
use crate::resolver::ColumnScope;
use crate::resolver::columns::TableScope;
use crate::resolver::expr::convert_expr;
use crate::types::*;
use super::extract::SubqueryJoin;
const SET_OPERATION: &str = "EXISTS over a set operation (UNION / INTERSECT / EXCEPT) \
is not supported; wrap the set operation in a derived table and select from that";
const OUTER_ONLY_EQUALITY: &str = "an equality naming only outer relations inside EXISTS \
is not supported; move it to the enclosing WHERE clause";
const CORRELATION_UNDER_OR: &str = "a correlated reference under OR inside EXISTS \
is not supported; each correlation must be an AND-ed equality";
pub(super) fn plan_exists_subquery(
subquery: &ast::Query,
negated: bool,
outer: &TableScope,
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: crate::TemporalScope,
) -> Result<SubqueryJoin> {
let SetExpr::Select(select) = &*subquery.body else {
return Err(SqlError::Unsupported {
detail: SET_OPERATION.into(),
});
};
let local = TableScope::resolve_from(catalog, functions, temporal, &select.from)?;
let nested = local.clone().nested_in(outer.clone());
convert_projection(&select.projection, &nested)?;
let mut on = Vec::new();
let mut local_conjuncts = Vec::new();
if let Some(where_expr) = &select.selection {
let mut conjuncts = Vec::new();
flatten_and_expr(where_expr, &mut conjuncts);
for conjunct in conjuncts {
convert_expr(&conjunct, &ColumnScope::Relations(&nested))?;
match correlation_pair(&conjunct, &local, outer)? {
Some(pair) => on.push(pair),
None => local_conjuncts.push(conjunct),
}
}
}
let inner_plan = plan_local_query(
subquery,
select,
local_conjuncts,
catalog,
functions,
temporal,
)?;
Ok(SubqueryJoin {
on,
inner_plan,
join_type: if negated {
JoinType::Anti
} else {
JoinType::Semi
},
})
}
fn plan_local_query(
subquery: &ast::Query,
select: &ast::Select,
local_conjuncts: Vec<ast::Expr>,
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: crate::TemporalScope,
) -> Result<SqlPlan> {
let mut inner_select = select.clone();
inner_select.projection = vec![ast::SelectItem::Wildcard(
ast::WildcardAdditionalOptions::default(),
)];
inner_select.selection = if local_conjuncts.is_empty() {
None
} else {
Some(rebuild_and_expr(local_conjuncts))
};
let mut inner_query = subquery.clone();
*inner_query.body = SetExpr::Select(Box::new(inner_select));
plan_query(&inner_query, catalog, functions, temporal)
}
enum Side {
Inner(String),
Outer(String),
Other,
}
fn correlation_pair(
expr: &ast::Expr,
inner: &TableScope,
outer: &TableScope,
) -> Result<Option<(String, String)>> {
let bare = unnest(expr);
if let ast::Expr::BinaryOp { left, op, right } = bare {
let l = classify(left, inner, outer);
let r = classify(right, inner, outer);
match (op, &l, &r) {
(ast::BinaryOperator::Eq, Side::Inner(ic), Side::Outer(oc))
| (ast::BinaryOperator::Eq, Side::Outer(oc), Side::Inner(ic)) => {
return Ok(Some((oc.clone(), ic.clone())));
}
(ast::BinaryOperator::Eq, Side::Outer(_), Side::Outer(_)) => {
return Err(SqlError::Unsupported {
detail: OUTER_ONLY_EQUALITY.into(),
});
}
(ast::BinaryOperator::Or, _, _)
if references_outer(left, inner, outer)
|| references_outer(right, inner, outer) =>
{
return Err(SqlError::Unsupported {
detail: CORRELATION_UNDER_OR.into(),
});
}
(op, Side::Inner(_), Side::Outer(_)) | (op, Side::Outer(_), Side::Inner(_))
if is_inequality(op) =>
{
return Err(SqlError::Unsupported {
detail: format!(
"a correlated inequality ('{op}') inside EXISTS is not supported; \
a semi join probes on equality keys only"
),
});
}
_ => {}
}
}
if references_outer(bare, inner, outer) {
return Err(SqlError::Unsupported {
detail: format!(
"the correlated predicate '{expr}' inside EXISTS is not supported; \
a correlation must be an AND-ed equality between one outer column \
and one inner column"
),
});
}
Ok(None)
}
fn classify(expr: &ast::Expr, inner: &TableScope, outer: &TableScope) -> Side {
match expr {
ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => {
let qualifier = normalize_ident(&parts[0]);
let column = normalize_ident(&parts[1]);
if inner.table_by_ref(&qualifier).is_some() {
Side::Inner(column)
} else if outer.table_by_ref(&qualifier).is_some() {
Side::Outer(column)
} else {
Side::Other
}
}
ast::Expr::Identifier(ident) => {
let column = normalize_ident(ident);
if inner.check_name(None, &column).is_ok() {
Side::Inner(column)
} else if outer.check_name(None, &column).is_ok() {
Side::Outer(column)
} else {
Side::Other
}
}
ast::Expr::Nested(nested) => classify(nested, inner, outer),
_ => Side::Other,
}
}
fn references_outer(expr: &ast::Expr, inner: &TableScope, outer: &TableScope) -> bool {
let walk = ast::visit_expressions(expr, |node| {
if matches!(classify(node, inner, outer), Side::Outer(_)) {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
});
walk.is_break()
}
fn unnest(expr: &ast::Expr) -> &ast::Expr {
match expr {
ast::Expr::Nested(inner) => unnest(inner),
other => other,
}
}
fn is_inequality(op: &ast::BinaryOperator) -> bool {
matches!(
op,
ast::BinaryOperator::Gt
| ast::BinaryOperator::GtEq
| ast::BinaryOperator::Lt
| ast::BinaryOperator::LtEq
| ast::BinaryOperator::NotEq
)
}