use sqlparser::ast::{self, SetExpr};
use crate::error::{Result, SqlError};
use crate::functions::registry::FunctionRegistry;
use crate::parser::normalize::{normalize_ident, table_name_from_factor};
use crate::planner::select::CteCatalog;
use crate::resolver::columns::TableScope;
use crate::temporal::TemporalScope;
use crate::types::*;
type RecursiveInfo = (Vec<Filter>, Option<(String, String)>);
pub(super) fn extract_recursive_info(
expr: &SetExpr,
cte_name: &str,
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: TemporalScope,
) -> Result<RecursiveInfo> {
let select = match expr {
SetExpr::Select(s) => s,
_ => {
return Err(SqlError::Unsupported {
detail: "recursive CTE branch must be SELECT".into(),
});
}
};
let mut real_table_alias = None;
let mut cte_alias = None;
let mut join_on_expr = None;
for from in &select.from {
if let Some((name, alias, is_unqualified)) = extract_table_reference(&from.relation)? {
if is_unqualified && name.eq_ignore_ascii_case(cte_name) {
cte_alias = alias.or(Some(name));
} else {
real_table_alias = alias.or(Some(name));
}
}
for join in &from.joins {
if let Some((name, alias, is_unqualified)) = extract_table_reference(&join.relation)? {
if is_unqualified && name.eq_ignore_ascii_case(cte_name) {
cte_alias = alias.or(Some(name));
if let Some(cond) = extract_join_on_condition(&join.join_operator) {
join_on_expr = Some(cond.clone());
}
} else {
real_table_alias = alias.or(Some(name));
if join_on_expr.is_none()
&& let Some(cond) = extract_join_on_condition(&join.join_operator)
{
join_on_expr = Some(cond.clone());
}
}
}
}
}
let arm_catalog = CteCatalog::open(catalog, cte_name);
let scope = TableScope::resolve_from(&arm_catalog, functions, temporal, &select.from)?;
let join_link = if let (Some(real_alias), Some(cte_al), Some(on_expr)) =
(&real_table_alias, &cte_alias, &join_on_expr)
{
extract_equi_link(on_expr, real_alias, cte_al)
} else {
None
};
if let Some((real_field, cte_field)) = &join_link {
if let Some(real_alias) = &real_table_alias {
scope.check_name(Some(real_alias), real_field)?;
}
if let Some(cte_al) = &cte_alias {
scope.check_name(Some(cte_al), cte_field)?;
}
}
let mut filters = Vec::new();
if let Some(where_expr) = &select.selection {
filters.extend(crate::planner::select::convert_where_to_filters(
where_expr, &scope,
)?);
}
Ok((filters, join_link))
}
fn extract_equi_link(
expr: &ast::Expr,
real_alias: &str,
cte_alias: &str,
) -> Option<(String, String)> {
match expr {
ast::Expr::BinaryOp {
left,
op: ast::BinaryOperator::Eq,
right,
} => {
let left_parts = extract_qualified_column(left)?;
let right_parts = extract_qualified_column(right)?;
if left_parts.0.eq_ignore_ascii_case(real_alias)
&& right_parts.0.eq_ignore_ascii_case(cte_alias)
{
Some((left_parts.1, right_parts.1))
} else if right_parts.0.eq_ignore_ascii_case(real_alias)
&& left_parts.0.eq_ignore_ascii_case(cte_alias)
{
Some((right_parts.1, left_parts.1))
} else {
None
}
}
ast::Expr::BinaryOp {
left,
op: ast::BinaryOperator::And,
right,
} => extract_equi_link(left, real_alias, cte_alias)
.or_else(|| extract_equi_link(right, real_alias, cte_alias)),
_ => None,
}
}
fn extract_qualified_column(expr: &ast::Expr) -> Option<(String, String)> {
match expr {
ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => {
Some((normalize_ident(&parts[0]), normalize_ident(&parts[1])))
}
_ => None,
}
}
fn extract_table_reference(
relation: &ast::TableFactor,
) -> Result<Option<(String, Option<String>, bool)>> {
let is_unqualified = matches!(
relation,
ast::TableFactor::Table { name, .. } if name.0.len() == 1
);
Ok(table_name_from_factor(relation)?.map(|(name, alias)| (name, alias, is_unqualified)))
}
fn extract_join_on_condition(op: &ast::JoinOperator) -> Option<&ast::Expr> {
use ast::JoinOperator::*;
let constraint = match op {
Join(c) | Inner(c) | Left(c) | LeftOuter(c) | Right(c) | RightOuter(c) | FullOuter(c) => c,
_ => return None,
};
match constraint {
ast::JoinConstraint::On(expr) => Some(expr),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use sqlparser::dialect::PostgreSqlDialect;
use sqlparser::parser::Parser;
fn link_of(sql: &str) -> Option<(String, String)> {
let statements = Parser::parse_sql(&PostgreSqlDialect {}, sql).expect("parse arm");
let ast::Statement::Query(query) = &statements[0] else {
panic!("expected a query");
};
let SetExpr::Select(select) = query.body.as_ref() else {
panic!("expected a SELECT");
};
let join = &select.from[0].joins[0];
let on = extract_join_on_condition(&join.join_operator)?;
extract_equi_link(on, "c", "t")
}
#[test]
fn every_on_join_spelling_yields_the_link() {
let expected = Some(("parent".to_string(), "id".to_string()));
for join in [
"JOIN",
"INNER JOIN",
"LEFT JOIN",
"LEFT OUTER JOIN",
"RIGHT JOIN",
"RIGHT OUTER JOIN",
"FULL OUTER JOIN",
] {
let sql = format!("SELECT c.* FROM tree c {join} t ON c.parent = t.id");
assert_eq!(link_of(&sql), expected, "{join}");
}
}
#[test]
fn the_link_is_oriented_by_alias() {
assert_eq!(
link_of("SELECT c.* FROM tree c JOIN t ON t.id = c.parent"),
Some(("parent".to_string(), "id".to_string()))
);
}
}