Apply ORDER BY, detecting search-triggering sort expressions. `select_items` is the raw SELECT list from the AST. It is required so that an ORDER BY referencing an alias (`ORDER BY score DESC` where the SELECT carries `rrf_score(...) AS score`) can be resolved back to the underlying function call before the search-trigger check runs. Without this resolution the search trigger would only fire when
(
plan: &SqlPlan,
order_by: &ast::OrderBy,
functions: &FunctionRegistry,
select_items: &[ast::SelectItem],
)
| 25 | /// function call appears in ORDER BY — a shape no SQL author would write |
| 26 | /// when the same expression is also being projected. |
| 27 | pub(in crate::planner::select) fn apply_order_by( |
| 28 | plan: &SqlPlan, |
| 29 | order_by: &ast::OrderBy, |
| 30 | functions: &FunctionRegistry, |
| 31 | select_items: &[ast::SelectItem], |
| 32 | ) -> Result<SqlPlan> { |
| 33 | let exprs = match &order_by.kind { |
| 34 | ast::OrderByKind::Expressions(exprs) => exprs, |
| 35 | ast::OrderByKind::All(_) => return Ok(plan.clone()), |
| 36 | }; |
| 37 | |
| 38 | if exprs.is_empty() { |
| 39 | return Ok(plan.clone()); |
| 40 | } |
| 41 | |
| 42 | // Two resolution rules apply before the trigger check: |
| 43 | // (a) Bare-identifier ORDER BY → look up the alias in the SELECT |
| 44 | // projection and substitute the underlying expression. |
| 45 | // (b) Literal function-call ORDER BY → also check the SELECT for the |
| 46 | // same call under an alias, and propagate that alias. |
| 47 | let first = &exprs[0]; |
| 48 | let (resolved_expr, score_alias) = resolve_order_by_target(&first.expr, select_items); |
| 49 | if let Some(search_plan) = |
| 50 | try_extract_sort_search(resolved_expr, plan, functions, score_alias.as_deref())? |
| 51 | { |
| 52 | return Ok(search_plan); |
| 53 | } |
| 54 | |
| 55 | // Normal sort keys. |
| 56 | let sort_keys: Vec<SortKey> = exprs |
| 57 | .iter() |
| 58 | .map(|o| { |
| 59 | Ok(SortKey { |
| 60 | expr: convert_expr(&o.expr)?, |
| 61 | ascending: o.options.asc.unwrap_or(true), |
| 62 | nulls_first: o.options.nulls_first.unwrap_or(false), |
| 63 | }) |
| 64 | }) |
| 65 | .collect::<Result<_>>()?; |
| 66 | |
| 67 | match plan { |
| 68 | SqlPlan::Scan { |
| 69 | collection, |
| 70 | alias, |
| 71 | engine, |
| 72 | filters, |
| 73 | projection, |
| 74 | limit, |
| 75 | offset, |
| 76 | distinct, |
| 77 | window_functions, |
| 78 | temporal, |
| 79 | .. |
| 80 | } => Ok(SqlPlan::Scan { |
| 81 | collection: collection.clone(), |
| 82 | alias: alias.clone(), |
| 83 | engine: *engine, |
| 84 | filters: filters.clone(), |
no test coverage detected