use std::sync::atomic::{AtomicU32, Ordering};
use vibesql_ast::{
BinaryOperator, ColumnIdentifier, CommonTableExpr, CteMaterialization, Expression, FromClause,
GroupByClause, JoinType, SelectItem, SelectStmt,
};
use super::extract_table_names;
static CTE_COUNTER: AtomicU32 = AtomicU32::new(0);
#[derive(Debug)]
struct DecorrelationResult {
cte: CommonTableExpr,
join_on: Expression,
cte_table: FromClause,
replacement_expr: Expression,
}
fn is_comparison_op(op: &BinaryOperator) -> bool {
matches!(
op,
BinaryOperator::GreaterThan
| BinaryOperator::GreaterThanOrEqual
| BinaryOperator::LessThan
| BinaryOperator::LessThanOrEqual
| BinaryOperator::Equal
| BinaryOperator::NotEqual
)
}
fn is_constant(expr: &Expression) -> bool {
match expr {
Expression::Literal(_) => true,
Expression::UnaryOp { expr, .. } => is_constant(expr),
Expression::BinaryOp { left, right, .. } => is_constant(left) && is_constant(right),
_ => false,
}
}
fn try_decorrelate_subquery(
subquery: &SelectStmt,
outer_tables: &[String],
) -> Option<DecorrelationResult> {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!(
"[SCALAR_DECORRELATION] try_decorrelate_subquery: set_op={}, group_by={}, having={}, limit={}, offset={}",
subquery.set_operation.is_some(),
subquery.group_by.is_some(),
subquery.having.is_some(),
subquery.limit.is_some(),
subquery.offset.is_some()
);
}
if subquery.set_operation.is_some()
|| subquery.group_by.is_some()
|| subquery.having.is_some()
|| subquery.limit.is_some()
|| subquery.offset.is_some()
{
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Skipping due to set_op/group_by/having/limit/offset");
}
return None;
}
let (inner_table, inner_alias) = match &subquery.from {
Some(FromClause::Table { name, alias, .. }) => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Inner table: {}, alias: {:?}", name, alias);
}
(name.clone(), alias.clone().unwrap_or_else(|| name.clone()))
}
_ => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Skipping - FROM clause is not a simple table");
}
return None;
}
};
let (agg_func, _agg_alias) = match extract_single_aggregate(&subquery.select_list) {
Some(result) => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Found aggregate function");
}
result
}
None => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Skipping - no single aggregate in SELECT list");
}
return None;
}
};
let where_clause = match subquery.where_clause.as_ref() {
Some(w) => w,
None => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Skipping - no WHERE clause");
}
return None;
}
};
let (inner_col, outer_col, remaining_predicates) =
match extract_correlation_predicate(where_clause, &inner_alias, outer_tables) {
Some(result) => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Found correlation predicate");
}
result
}
None => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!(
"[SCALAR_DECORRELATION] Skipping - no correlation predicate found in WHERE"
);
}
return None;
}
};
let counter = CTE_COUNTER.fetch_add(1, Ordering::SeqCst);
let cte_alias = format!("_decorr_{}", counter);
let agg_col_alias = format!("_agg_{}", counter);
let inner_col_name = match &inner_col {
Expression::ColumnRef(col_id) => col_id.column_canonical().to_string(),
_ => return None,
};
let original_alias = match &subquery.from {
Some(FromClause::Table { alias, .. }) => alias.clone(),
_ => None,
};
let corr_col_alias = format!("_corr_{}", counter);
let cte_select = SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![
SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple(&inner_col_name, false)),
alias: Some(corr_col_alias.clone()),
source_text: None,
},
SelectItem::Expression {
expr: agg_func,
alias: Some(agg_col_alias.clone()),
source_text: None,
},
],
into_table: None,
into_variables: None,
from: Some(FromClause::Table {
index_hint: None,
name: inner_table.clone(),
alias: original_alias, column_aliases: None,
quoted: false,
}),
where_clause: remaining_predicates,
group_by: Some(GroupByClause::Simple(vec![Expression::ColumnRef(
ColumnIdentifier::simple(&inner_col_name, false),
)])),
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
};
let cte = CommonTableExpr {
name: cte_alias.clone(),
columns: None,
query: Box::new(cte_select),
recursive: false,
materialization: CteMaterialization::Default,
};
let join_on = Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(outer_col.clone()),
right: Box::new(Expression::ColumnRef(ColumnIdentifier::qualified(
&cte_alias,
false,
&corr_col_alias,
false,
))),
};
let cte_table = FromClause::Table {
index_hint: None,
name: cte_alias.clone(),
alias: None,
column_aliases: None,
quoted: false,
};
let replacement_expr = Expression::ColumnRef(ColumnIdentifier::qualified(
&cte_alias,
false,
&agg_col_alias,
false,
));
Some(DecorrelationResult { cte, join_on, cte_table, replacement_expr })
}
fn extract_single_aggregate(select_list: &[SelectItem]) -> Option<(Expression, String)> {
if select_list.len() != 1 {
return None;
}
match &select_list[0] {
SelectItem::Expression { expr, alias, .. } => {
if is_aggregate_expr(expr) {
let agg_alias = alias.clone().unwrap_or_else(|| "agg".to_string());
Some((expr.clone(), agg_alias))
} else {
None
}
}
_ => None,
}
}
fn is_aggregate_expr(expr: &Expression) -> bool {
match expr {
Expression::AggregateFunction { .. } => true,
Expression::Cast { expr, .. } => is_aggregate_expr(expr),
_ => false,
}
}
fn extract_correlation_predicate(
expr: &Expression,
inner_table: &str,
outer_tables: &[String],
) -> Option<(Expression, Expression, Option<Expression>)> {
match expr {
Expression::BinaryOp { op: BinaryOperator::Equal, left, right } => {
if is_from_table(left, inner_table) && is_from_outer(right, outer_tables) {
return Some(((**left).clone(), (**right).clone(), None));
}
if is_from_table(right, inner_table) && is_from_outer(left, outer_tables) {
return Some(((**right).clone(), (**left).clone(), None));
}
None
}
Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
if let Some((inner, outer, left_remaining)) =
extract_correlation_predicate(left, inner_table, outer_tables)
{
let remaining = combine_predicates(left_remaining, Some((**right).clone()));
return Some((inner, outer, remaining));
}
if let Some((inner, outer, right_remaining)) =
extract_correlation_predicate(right, inner_table, outer_tables)
{
let remaining = combine_predicates(Some((**left).clone()), right_remaining);
return Some((inner, outer, remaining));
}
None
}
_ => None,
}
}
fn is_from_table(expr: &Expression, table: &str) -> bool {
match expr {
Expression::ColumnRef(col_id) => match col_id.table_canonical() {
Some(t) => t.eq_ignore_ascii_case(table),
None => true, },
_ => false,
}
}
fn is_from_outer(expr: &Expression, outer_tables: &[String]) -> bool {
match expr {
Expression::ColumnRef(col_id) => match col_id.table_canonical() {
Some(t) => outer_tables.iter().any(|ot| ot.eq_ignore_ascii_case(t)),
None => false, },
_ => false,
}
}
fn combine_predicates(left: Option<Expression>, right: Option<Expression>) -> Option<Expression> {
match (left, right) {
(Some(l), Some(r)) => Some(Expression::BinaryOp {
op: BinaryOperator::And,
left: Box::new(l),
right: Box::new(r),
}),
(Some(l), None) => Some(l),
(None, Some(r)) => Some(r),
(None, None) => None,
}
}
pub fn apply_scalar_decorrelation(stmt: &SelectStmt) -> SelectStmt {
let outer_tables = extract_table_names(&stmt.from);
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Checking query, outer_tables: {:?}", outer_tables);
}
if outer_tables.is_empty() {
return stmt.clone();
}
let where_clause = match &stmt.where_clause {
Some(w) => w,
None => return stmt.clone(),
};
if let Some((new_where, decorrelations)) =
find_and_decorrelate_in_where(where_clause, &outer_tables)
{
if decorrelations.is_empty() {
return stmt.clone();
}
let mut result = stmt.clone();
let new_ctes: Vec<CommonTableExpr> = decorrelations.iter().map(|d| d.cte.clone()).collect();
result.with_clause = match &stmt.with_clause {
Some(existing_ctes) => Some(existing_ctes.iter().cloned().chain(new_ctes).collect()),
None => Some(new_ctes),
};
if let Some(from) = &result.from {
let mut new_from = from.clone();
for decorrelation in &decorrelations {
new_from = FromClause::Join {
join_type: JoinType::Inner,
left: Box::new(new_from),
right: Box::new(decorrelation.cte_table.clone()),
condition: Some(decorrelation.join_on.clone()),
using_columns: None,
natural: false,
alias: None,
};
}
result.from = Some(new_from);
}
result.where_clause = new_where;
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!(
"[SCALAR_DECORRELATION] Decorrelated {} scalar subqueries",
decorrelations.len()
);
}
result
} else {
stmt.clone()
}
}
fn find_and_decorrelate_in_where(
expr: &Expression,
outer_tables: &[String],
) -> Option<(Option<Expression>, Vec<DecorrelationResult>)> {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] find_and_decorrelate_in_where: checking expression");
}
let mut decorrelations = Vec::new();
let new_expr = rewrite_expr_with_decorrelation(expr, outer_tables, &mut decorrelations);
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!(
"[SCALAR_DECORRELATION] find_and_decorrelate_in_where: found {} decorrelations",
decorrelations.len()
);
}
if decorrelations.is_empty() {
None
} else {
Some((Some(new_expr), decorrelations))
}
}
fn rewrite_expr_with_decorrelation(
expr: &Expression,
outer_tables: &[String],
decorrelations: &mut Vec<DecorrelationResult>,
) -> Expression {
match expr {
Expression::BinaryOp { op, left, right } if is_comparison_op(op) => {
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!(
"[SCALAR_DECORRELATION] Found comparison op {:?}, checking for scalar subquery",
op
);
}
if let Some((original_subq, multiplier)) =
extract_scalar_subquery_with_multiplier(right)
{
if std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!("[SCALAR_DECORRELATION] Found scalar subquery on right side");
}
if let Some(decorrelation) = try_decorrelate_subquery(original_subq, outer_tables) {
let replacement = if let Some(mult) = multiplier {
Expression::BinaryOp {
op: BinaryOperator::Multiply,
left: Box::new(mult),
right: Box::new(decorrelation.replacement_expr.clone()),
}
} else {
decorrelation.replacement_expr.clone()
};
decorrelations.push(decorrelation);
return Expression::BinaryOp {
op: op.clone(),
left: left.clone(),
right: Box::new(replacement),
};
}
}
if let Some((original_subq, multiplier)) = extract_scalar_subquery_with_multiplier(left)
{
if let Some(decorrelation) = try_decorrelate_subquery(original_subq, outer_tables) {
let replacement = if let Some(mult) = multiplier {
Expression::BinaryOp {
op: BinaryOperator::Multiply,
left: Box::new(mult),
right: Box::new(decorrelation.replacement_expr.clone()),
}
} else {
decorrelation.replacement_expr.clone()
};
decorrelations.push(decorrelation);
return Expression::BinaryOp {
op: op.clone(),
left: Box::new(replacement),
right: right.clone(),
};
}
}
Expression::BinaryOp {
op: op.clone(),
left: Box::new(rewrite_expr_with_decorrelation(left, outer_tables, decorrelations)),
right: Box::new(rewrite_expr_with_decorrelation(
right,
outer_tables,
decorrelations,
)),
}
}
Expression::BinaryOp { op, left, right } => Expression::BinaryOp {
op: op.clone(),
left: Box::new(rewrite_expr_with_decorrelation(left, outer_tables, decorrelations)),
right: Box::new(rewrite_expr_with_decorrelation(right, outer_tables, decorrelations)),
},
_ => expr.clone(),
}
}
fn extract_scalar_subquery_with_multiplier(
expr: &Expression,
) -> Option<(&SelectStmt, Option<Expression>)> {
match expr {
Expression::ScalarSubquery(subquery) => Some((subquery, None)),
Expression::BinaryOp { op: BinaryOperator::Multiply, left, right } => {
if let Expression::ScalarSubquery(subquery) = right.as_ref() {
if is_constant(left) {
return Some((subquery, Some((**left).clone())));
}
}
if let Expression::ScalarSubquery(subquery) = left.as_ref() {
if is_constant(right) {
return Some((subquery, Some((**right).clone())));
}
}
None
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_constant() {
assert!(is_constant(&Expression::Literal(vibesql_types::SqlValue::Integer(42))));
assert!(is_constant(&Expression::Literal(vibesql_types::SqlValue::Float(1.2))));
assert!(!is_constant(&Expression::ColumnRef(ColumnIdentifier::simple("x", false))));
}
fn select_stmt(from: Option<FromClause>) -> SelectStmt {
SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("total", false)),
alias: None,
source_text: None,
}],
into_table: None,
into_variables: None,
from,
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
}
}
fn correlated_avg_subquery() -> SelectStmt {
let mut subq = select_stmt(Some(FromClause::Table {
index_hint: None,
name: "lineitem".to_string(),
alias: Some("l".to_string()),
column_aliases: None,
quoted: false,
}));
subq.select_list = vec![SelectItem::Expression {
expr: Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("AVG"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::qualified(
"l", false, "amount", false,
))],
order_by: None,
filter: None,
},
alias: None,
source_text: None,
}];
subq.where_clause = Some(Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::qualified(
"l", false, "order_id", false,
))),
right: Box::new(Expression::ColumnRef(ColumnIdentifier::qualified(
"orders", false, "order_id", false,
))),
});
subq
}
#[test]
fn test_decorrelation_with_unaliased_outer_table() {
let mut stmt = select_stmt(Some(FromClause::Table {
index_hint: None,
name: "orders".to_string(),
alias: None,
column_aliases: None,
quoted: false,
}));
stmt.where_clause = Some(Expression::BinaryOp {
op: BinaryOperator::GreaterThan,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::qualified(
"orders", false, "total", false,
))),
right: Box::new(Expression::ScalarSubquery(Box::new(correlated_avg_subquery()))),
});
let result = apply_scalar_decorrelation(&stmt);
let ctes = result.with_clause.as_ref().expect("decorrelation should add a CTE");
assert_eq!(ctes.len(), 1, "expected exactly one decorrelation CTE");
assert!(
matches!(result.from, Some(FromClause::Join { .. })),
"decorrelation should rewrite FROM into a JOIN with the CTE"
);
}
}