Skip to content

fix cross-shard ORDER BY on non-projected columns - #1517

Closed
murex971 wants to merge 7 commits into
pgdogdev:mainfrom
murex971:fix-projection
Closed

murex971 wants to merge 7 commits into
pgdogdev:mainfrom
murex971:fix-projection

Conversation

@murex971

Copy link
Copy Markdown
Contributor

@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.51485% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ontend/router/parser/rewrite/statement/order_by.rs 97.12% 4 Missing ⚠️
pgdog/src/frontend/router/parser/query/select.rs 96.96% 1 Missing ⚠️
pgdog/src/net/messages/bind.rs 90.90% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread pgdog/src/frontend/client/query_engine/query.rs Outdated
}
}

fn drop_projected_columns(

@levkk levkk Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 sgrif left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +147 to +164
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<_>>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're touching this signature, what do you think about changing this to return impl Iterator instead?

Comment on lines +49 to +56
})
.collect::<Vec<_>>();

if helpers.is_empty() {
return;
}

let (targets, mappings): (Vec<_>, Vec<_>) = helpers.into_iter().unzip();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can skip an intermediate vec here.

Suggested change
})
.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;
}

Comment on lines +91 to +107
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,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about this instead? The way qualifiers is constructed feels noisy to me, and we can skip the intermediate vec.

Suggested change
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,
}

@murex971

Copy link
Copy Markdown
Contributor Author

@sgrif I am closing this and handling it in #1545
will handle suggested fixes there

@murex971 murex971 closed this Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Query Engine] ORDER BY on non-projected column can produce incorrect cross-shard ordering

3 participants