use crate::error::{QueryError, Result};
use crate::parser::ast::*;
use oxigdal_core::error::OxiGdalError;
use std::collections::HashSet;
use super::{OptimizationRule, collect_column_refs};
pub struct ProjectionPushdown;
impl OptimizationRule for ProjectionPushdown {
fn apply(&self, stmt: SelectStatement) -> Result<SelectStatement> {
if stmt.projection.is_empty() {
return Err(QueryError::optimization(
OxiGdalError::invalid_state_builder(
"Cannot apply projection pushdown with empty projection",
)
.with_operation("projection_pushdown")
.with_suggestion("Ensure SELECT clause has at least one column or wildcard")
.build()
.to_string(),
));
}
let mut referenced_columns = HashSet::new();
for item in &stmt.projection {
match item {
SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
return Ok(stmt);
}
SelectItem::Expr { expr, .. } => {
collect_column_refs(expr, &mut referenced_columns);
}
}
}
if let Some(ref selection) = stmt.selection {
collect_column_refs(selection, &mut referenced_columns);
}
for expr in &stmt.group_by {
collect_column_refs(expr, &mut referenced_columns);
}
if let Some(ref having) = stmt.having {
collect_column_refs(having, &mut referenced_columns);
}
for order in &stmt.order_by {
collect_column_refs(&order.expr, &mut referenced_columns);
}
let mut optimized_stmt = stmt;
if let Some(from) = optimized_stmt.from.take() {
optimized_stmt.from = Some(push_column_projections(from, &referenced_columns));
}
Ok(optimized_stmt)
}
}
fn push_column_projections(
table_ref: TableReference,
referenced_columns: &HashSet<String>,
) -> TableReference {
match table_ref {
TableReference::Subquery { query, alias } => {
let needed: HashSet<String> = referenced_columns
.iter()
.filter_map(|col| {
if let Some(stripped) = col.strip_prefix(&format!("{}.", alias)) {
Some(stripped.to_string())
} else if !col.contains('.') {
Some(col.clone())
} else {
None
}
})
.collect();
if needed.is_empty() {
return TableReference::Subquery { query, alias };
}
let mut new_query = *query;
let has_wildcard = new_query
.projection
.iter()
.any(|p| matches!(p, SelectItem::Wildcard | SelectItem::QualifiedWildcard(_)));
if has_wildcard {
let mut new_projection: Vec<SelectItem> = Vec::new();
for item in &new_query.projection {
match item {
SelectItem::Expr { alias: Some(a), .. } if needed.contains(a.as_str()) => {
new_projection.push(item.clone());
}
SelectItem::Expr {
expr: Expr::Column { name, .. },
alias: None,
} if needed.contains(name.as_str()) => {
new_projection.push(item.clone());
}
SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
}
_ => {}
}
}
let existing: HashSet<String> = new_projection
.iter()
.filter_map(|item| match item {
SelectItem::Expr { alias: Some(a), .. } => Some(a.clone()),
SelectItem::Expr {
expr: Expr::Column { name, .. },
alias: None,
} => Some(name.clone()),
_ => None,
})
.collect();
for col in &needed {
if !existing.contains(col) {
new_projection.push(SelectItem::Expr {
expr: Expr::Column {
table: None,
name: col.clone(),
},
alias: None,
});
}
}
if !new_projection.is_empty() {
new_query.projection = new_projection;
}
} else {
let mut internal_refs = HashSet::new();
if let Some(ref sel) = new_query.selection {
collect_column_refs(sel, &mut internal_refs);
}
for gexpr in &new_query.group_by {
collect_column_refs(gexpr, &mut internal_refs);
}
if let Some(ref hav) = new_query.having {
collect_column_refs(hav, &mut internal_refs);
}
for ord in &new_query.order_by {
collect_column_refs(&ord.expr, &mut internal_refs);
}
new_query.projection.retain(|item| match item {
SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => true,
SelectItem::Expr { alias: Some(a), .. } => needed.contains(a.as_str()),
SelectItem::Expr {
expr: Expr::Column { name, .. },
alias: None,
} => {
needed.contains(name.as_str())
|| internal_refs
.iter()
.any(|r| r == name || r.ends_with(&format!(".{}", name)))
}
SelectItem::Expr { expr, alias: None } => {
let key = format!("{}", expr);
needed.contains(&key)
}
});
if new_query.projection.is_empty() {
new_query.projection = vec![SelectItem::Wildcard];
}
}
if let Some(inner_from) = new_query.from.take() {
let mut sub_refs = HashSet::new();
for item in &new_query.projection {
if let SelectItem::Expr { expr, .. } = item {
collect_column_refs(expr, &mut sub_refs);
}
}
if let Some(ref sel) = new_query.selection {
collect_column_refs(sel, &mut sub_refs);
}
for gexpr in &new_query.group_by {
collect_column_refs(gexpr, &mut sub_refs);
}
if let Some(ref hav) = new_query.having {
collect_column_refs(hav, &mut sub_refs);
}
for ord in &new_query.order_by {
collect_column_refs(&ord.expr, &mut sub_refs);
}
new_query.from = Some(push_column_projections(inner_from, &sub_refs));
}
TableReference::Subquery {
query: Box::new(new_query),
alias,
}
}
TableReference::Join {
left,
right,
join_type,
on,
} => {
let mut extended_refs = referenced_columns.clone();
if let Some(ref on_expr) = on {
collect_column_refs(on_expr, &mut extended_refs);
}
TableReference::Join {
left: Box::new(push_column_projections(*left, &extended_refs)),
right: Box::new(push_column_projections(*right, &extended_refs)),
join_type,
on,
}
}
other => other,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_projection_pushdown_subquery_wildcard() {
let stmt = SelectStatement {
projection: vec![SelectItem::Expr {
expr: Expr::Column {
table: Some("sub".to_string()),
name: "x".to_string(),
},
alias: None,
}],
from: Some(TableReference::Subquery {
query: Box::new(SelectStatement {
projection: vec![SelectItem::Wildcard],
from: Some(TableReference::Table {
name: "t".to_string(),
alias: None,
}),
selection: None,
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
}),
alias: "sub".to_string(),
}),
selection: None,
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
};
let pushdown = ProjectionPushdown;
let result = pushdown.apply(stmt);
assert!(result.is_ok(), "Projection pushdown should succeed");
let result = result.expect("Projection pushdown should succeed");
let Some(TableReference::Subquery { query, .. }) = &result.from else {
panic!("FROM should be a subquery");
};
let has_wildcard = query
.projection
.iter()
.any(|p| matches!(p, SelectItem::Wildcard));
assert!(
!has_wildcard,
"Wildcard should be replaced with specific columns"
);
assert_eq!(query.projection.len(), 1);
}
#[test]
fn test_projection_pushdown_outer_wildcard_skips() {
let stmt = SelectStatement {
projection: vec![SelectItem::Wildcard],
from: Some(TableReference::Subquery {
query: Box::new(SelectStatement {
projection: vec![SelectItem::Wildcard],
from: Some(TableReference::Table {
name: "t".to_string(),
alias: None,
}),
selection: None,
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
}),
alias: "sub".to_string(),
}),
selection: None,
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
};
let pushdown = ProjectionPushdown;
let result = pushdown.apply(stmt);
assert!(result.is_ok(), "Projection pushdown should succeed");
let result = result.expect("Projection pushdown should succeed");
if let Some(TableReference::Subquery { query, .. }) = &result.from {
assert!(
query
.projection
.iter()
.any(|p| matches!(p, SelectItem::Wildcard))
);
}
}
#[test]
fn test_projection_pushdown_with_where_columns() {
let stmt = SelectStatement {
projection: vec![SelectItem::Expr {
expr: Expr::Column {
table: Some("sub".to_string()),
name: "x".to_string(),
},
alias: None,
}],
from: Some(TableReference::Subquery {
query: Box::new(SelectStatement {
projection: vec![SelectItem::Wildcard],
from: Some(TableReference::Table {
name: "t".to_string(),
alias: None,
}),
selection: None,
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
}),
alias: "sub".to_string(),
}),
selection: Some(Expr::BinaryOp {
left: Box::new(Expr::Column {
table: Some("sub".to_string()),
name: "y".to_string(),
}),
op: BinaryOperator::Gt,
right: Box::new(Expr::Literal(Literal::Integer(10))),
}),
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
};
let pushdown = ProjectionPushdown;
let result = pushdown.apply(stmt);
assert!(result.is_ok(), "Projection pushdown should succeed");
let result = result.expect("Projection pushdown should succeed");
if let Some(TableReference::Subquery { query, .. }) = &result.from {
assert_eq!(query.projection.len(), 2, "Subquery should project x and y");
}
}
}