use crate::error::{Result, SqlError};
use crate::planner::qualified_name;
use crate::types::{
ArrayProjectPlan, ArraySlicePlan, DocumentIndexLookupPlan, HybridSearchPlan,
HybridSearchTriplePlan, LateralLoopPlan, LateralTopKPlan, RangeScanPlan, RecursiveScanPlan,
TextSearchPlan, TimeseriesScanPlan,
};
use crate::types::{Projection, SortKey, SqlExpr, SqlPlan};
pub(in crate::planner::select) fn post_process(
input: SqlPlan,
sort_keys: Vec<SortKey>,
limit: Option<usize>,
offset: usize,
) -> Result<SqlPlan> {
let mut input = input;
let projection = retain_sort_columns(&mut input, &sort_keys)?;
Ok(SqlPlan::Subquery {
input: Box::new(input),
filters: Vec::new(),
projection,
window_functions: Vec::new(),
sort_keys,
offset,
distinct: false,
limit,
})
}
fn retain_sort_columns(input: &mut SqlPlan, sort_keys: &[SortKey]) -> Result<Vec<Projection>> {
if sort_keys.is_empty() {
return Ok(Vec::new());
}
let variant = input.variant_name();
if let SqlPlan::ArraySlice(ArraySlicePlan {
attr_projection, ..
})
| SqlPlan::ArrayProject(ArrayProjectPlan {
attr_projection, ..
}) = input
&& !attr_projection.is_empty()
&& let Some(missing) = first_missing_attr(attr_projection, sort_keys)
{
return Err(SqlError::Unsupported {
detail: format!(
"ORDER BY '{missing}' over a {variant} plan that projects only \
[{}]; add the column to the attribute list",
attr_projection.join(", ")
),
});
}
let Some(projection) = body_projection_mut(input) else {
return Ok(Vec::new());
};
if projection.is_empty()
|| projection
.iter()
.any(|p| matches!(p, Projection::Star | Projection::QualifiedStar(_)))
{
return Ok(Vec::new());
}
let original = projection.clone();
let mut widened = false;
for key in sort_keys {
match &key.expr {
SqlExpr::Column { table, name } => {
if !projects_column(&original, table.as_deref(), name) {
projection.push(Projection::Column(qualified_name(table.as_deref(), name)));
widened = true;
}
}
_ => {
return Err(SqlError::Unsupported {
detail: format!(
"ORDER BY over a computed expression on a {variant} plan whose \
projection narrows the row; project the sort expression's columns \
in the SELECT list"
),
});
}
}
}
Ok(if widened { original } else { Vec::new() })
}
fn projects_column(projection: &[Projection], table: Option<&str>, name: &str) -> bool {
let qualified = qualified_name(table, name);
projection.iter().any(|p| match p {
Projection::Column(projected) => {
projected.eq_ignore_ascii_case(&qualified) || bare(projected).eq_ignore_ascii_case(name)
}
Projection::Computed { alias, .. } | Projection::CpComputed { alias, .. } => {
alias.eq_ignore_ascii_case(name)
}
Projection::Star | Projection::QualifiedStar(_) => true,
})
}
fn bare(name: &str) -> &str {
name.rsplit('.').next().unwrap_or(name)
}
fn first_missing_attr(attrs: &[String], sort_keys: &[SortKey]) -> Option<String> {
sort_keys.iter().find_map(|key| match &key.expr {
SqlExpr::Column { name, .. } => {
if attrs.iter().any(|a| a.eq_ignore_ascii_case(name)) {
None
} else {
Some(name.clone())
}
}
_ => None,
})
}
fn body_projection_mut(plan: &mut SqlPlan) -> Option<&mut Vec<Projection>> {
match plan {
SqlPlan::Scan { projection, .. }
| SqlPlan::Join { projection, .. }
| SqlPlan::PointGet { projection, .. }
| SqlPlan::RangeScan(RangeScanPlan { projection, .. })
| SqlPlan::DocumentIndexLookup(DocumentIndexLookupPlan { projection, .. })
| SqlPlan::TimeseriesScan(TimeseriesScanPlan { projection, .. })
| SqlPlan::VectorSearch { projection, .. }
| SqlPlan::MultiVectorSearch { projection, .. }
| SqlPlan::SparseSearch { projection, .. }
| SqlPlan::TextSearch(TextSearchPlan { projection, .. })
| SqlPlan::HybridSearch(HybridSearchPlan { projection, .. })
| SqlPlan::HybridSearchTriple(HybridSearchTriplePlan { projection, .. })
| SqlPlan::SpatialScan { projection, .. }
| SqlPlan::RecursiveScan(RecursiveScanPlan { projection, .. })
| SqlPlan::LateralTopK(LateralTopKPlan { projection, .. })
| SqlPlan::LateralLoop(LateralLoopPlan { projection, .. })
| SqlPlan::Subquery { projection, .. } => Some(projection),
_ => None,
}
}