Conversation
murex971
commented
Sep 10, 2026
- Temporarily project missing ORDER BY columns during query rewriting.
- Use hidden columns for correct global cross-shard sorting.
- Remove hidden columns before sending rows and RowDescription to clients.
- Support direct-shard results.
- closes [Query Engine] ORDER BY on non-projected column can produce incorrect cross-shard ordering #1135
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| } | ||
| } | ||
|
|
||
| fn drop_projected_columns( |
There was a problem hiding this comment.
I would probably push this concern into the connection binding (e.g.. read method). The architecture is as such that piece of code handles the cross-shard/direct-to-shard message manipulation. The query engine doesn't actually know whether it's talking to one or more servers, so this leaks it here.
sgrif
left a comment
There was a problem hiding this comment.
It might be out of scope for this PR, but a refactor I've been wanting to make is to pass through the original target list of the select statement, and changing the code to keep those columns rather than keep track of which ones to drop. This could probably even just be a length instead of a list since we're always going to append any helpers to the end of the list
| let mut order_by = Self::select_sort(stmt, context.router_context.bind); | ||
| for helper in cached_ast.rewrite_plan.projection.order_by_helpers() { | ||
| let Some((_, column)) = order_by | ||
| .iter_mut() | ||
| .find(|(position, _)| *position == helper.sort_position) | ||
| else { | ||
| continue; | ||
| }; | ||
| *column = if column.asc() { | ||
| OrderBy::Asc(helper.projected_column + 1) | ||
| } else { | ||
| OrderBy::Desc(helper.projected_column + 1) | ||
| }; | ||
| } | ||
| let order_by = order_by | ||
| .into_iter() | ||
| .map(|(_, order_by)| order_by) | ||
| .collect::<Vec<_>>(); |
There was a problem hiding this comment.
| let mut order_by = Self::select_sort(stmt, context.router_context.bind); | |
| for helper in cached_ast.rewrite_plan.projection.order_by_helpers() { | |
| let Some((_, column)) = order_by | |
| .iter_mut() | |
| .find(|(position, _)| *position == helper.sort_position) | |
| else { | |
| continue; | |
| }; | |
| *column = if column.asc() { | |
| OrderBy::Asc(helper.projected_column + 1) | |
| } else { | |
| OrderBy::Desc(helper.projected_column + 1) | |
| }; | |
| } | |
| let order_by = order_by | |
| .into_iter() | |
| .map(|(_, order_by)| order_by) | |
| .collect::<Vec<_>>(); | |
| let helpers = cached_ast.rewrite_plan.projection.order_by_helpers(); | |
| let order_by = Self::select_sort(stmt, router_context.bind) | |
| .into_iter() | |
| .map(|(position, column)| { | |
| if let Some(helper) = order_by.iter().find(|helper| position == helper.sort_position) { | |
| if column.asc() { | |
| OrderBy::Asc(helper.projected_column + 1) | |
| } else { | |
| OrderBy::Desc(helper.projected_column + 1) | |
| } | |
| } else { | |
| column | |
| } | |
| }) | |
| .collect::<Vec<_>>(); | |
| stmt: &nodes::SelectStmt, | ||
| params: Option<StatementParameters<'_>>, | ||
| ) -> Vec<OrderBy> { | ||
| ) -> Vec<(usize, OrderBy)> { |
There was a problem hiding this comment.
While we're touching this signature, what do you think about changing this to return impl Iterator instead?
| }) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| if helpers.is_empty() { | ||
| return; | ||
| } | ||
|
|
||
| let (targets, mappings): (Vec<_>, Vec<_>) = helpers.into_iter().unzip(); |
There was a problem hiding this comment.
We can skip an intermediate vec here.
| }) | |
| .collect::<Vec<_>>(); | |
| if helpers.is_empty() { | |
| return; | |
| } | |
| let (targets, mappings): (Vec<_>, Vec<_>) = helpers.into_iter().unzip(); | |
| }); | |
| let (targets, mappings): (Vec<_>, Vec<_>) = helpers.unzip(); | |
| if targets.is_empty() { | |
| return; | |
| } |
| let fields = column_ref.fields(); | ||
| if !matches!(fields.iter().next_back(), Some(Node::A_Star(_))) { | ||
| return false; | ||
| } | ||
|
|
||
| let qualifiers = fields | ||
| .iter() | ||
| .take(fields.len().saturating_sub(1)) | ||
| .filter_map(Node::as_str) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| match qualifiers.as_slice() { | ||
| [] => true, | ||
| [table] => order_by.table == Some(*table), | ||
| [schema, table] => order_by.schema == Some(*schema) && order_by.table == Some(*table), | ||
| _ => false, | ||
| } |
There was a problem hiding this comment.
What do you think about this instead? The way qualifiers is constructed feels noisy to me, and we can skip the intermediate vec.
| let fields = column_ref.fields(); | |
| if !matches!(fields.iter().next_back(), Some(Node::A_Star(_))) { | |
| return false; | |
| } | |
| let qualifiers = fields | |
| .iter() | |
| .take(fields.len().saturating_sub(1)) | |
| .filter_map(Node::as_str) | |
| .collect::<Vec<_>>(); | |
| match qualifiers.as_slice() { | |
| [] => true, | |
| [table] => order_by.table == Some(*table), | |
| [schema, table] => order_by.schema == Some(*schema) && order_by.table == Some(*table), | |
| _ => false, | |
| } | |
| let mut fields = column_ref.fields().iter(); | |
| if !matches!(fields.next_back(), Some(Node::A_Star(_))) { | |
| return false; | |
| } | |
| let table = fields.next_back(); | |
| let schema = fields.next_back(); | |
| match (schema, table) { | |
| (None, None) => true, | |
| (Some(table), None) => order_by.table == Some(*table), | |
| (schema, table) => order_by.schema == schema && order_by.table == table, | |
| } |