Skip to content

refactor: apply route-dependent rewrites after routing - #1545

Open
murex971 wants to merge 12 commits into
pgdogdev:mainfrom
murex971:refactor-parse-route
Open

murex971 wants to merge 12 commits into
pgdogdev:mainfrom
murex971:refactor-parse-route

Conversation

@murex971

@murex971 murex971 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Aggregate helpers, ORDER BY helpers, and LIMIT/OFFSET pushdown now run only after the route is known, so direct shard execution keeps original SQL and does not leak helper columns.

closes #1425 closes #1135

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

@murex971
murex971 marked this pull request as ready for review September 14, 2026 20:01
@levkk

levkk commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

👀

describe.anonymize();
}
if let Some(describe) = describe {
return Ok(HandleResult::PrependProtocolMessageRewrite {

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.

Q: why do we need this? I'm not too familiar with this piece of code, would be good to have a small example explaining.

@levkk levkk left a comment

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.

This looks good to me I think. It would be good to get feedback from @jkaczman after we merge #1541 (both touch the rewriter heavily). Should be easy to resolve conflicts, but just want to double check one doesn't break the other and vice versa.

}))
}

fn internal_describe(&mut self, name: &str) -> Option<ProtocolMessage> {

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.

Oh I see. The cross-shard Describe has different columns that direct-to-shard, so we need to rewrite it. I'm remembering now.

Comment thread pgdog/src/frontend/prepared_statements/global_cache.rs Outdated
Comment thread pgdog/src/frontend/prepared_statements/global_cache.rs Outdated
@murex971
murex971 requested a review from jkaczman September 16, 2026 13:33

@jkaczman jkaczman 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.

LGTM!

It would be great if you could document certain decisions e.g. why you chose to move from an explicit A_Const node with the limit + offset to an A_Expr executing on Postgres. I think I understand the tradeoffs, but it would be great to see the author's POV when reading the code.

I would also be curious to know if the benchmark from before vs after changed at all.

@murex971

Copy link
Copy Markdown
Contributor Author

Benchmarked release builds of main and this PR using pgbench prepared protocol against the same three shards. Each case used a 3s warmup followed by three 15s runs at 10 and 50 clients.

Results were effectively neutral: direct and cross-shard AVG ranged from −0.6% to +1.0%; comparable ORDER BY was ~1% slower (expected from post-route bookkeeping). CPU was unchanged (+0.13%), with ~476 KiB higher peak RSS.

@murex971
murex971 requested review from jkaczman and levkk September 17, 2026 18:36
pub(super) fn select(
&mut self,
cached_ast: &Ast,
_cached_ast: &Ast,

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.

I think we're fine to remove this as it's no longer needed in this func

}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HelperKind {
Count,

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.

Why remove the comments that were originally in aggregate/plan.rs?

@@ -0,0 +1,199 @@
use pg_raw_parse::{ConstValue, Node, make, nodes};

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 would be great to have some comments explaining some of the logic in this file.

}

#[derive(Debug, Default, Clone)]
pub(crate) struct RewriteOutput {

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 looks like this struct is just a basic wrapper around ProjectionRewritePlan. Why not just use ProjectionRewritePlan by itself?

)
}

pub(crate) fn aggregate_helpers(&self) -> &[AggregateHelper] {

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.

These fields can be exposed pub(crate) and used directly if we're providing basic getters/setters without any special invariants enforced or anything.

&& projected.fields().into_iter().next_back().and_then(Node::as_str)
== name))
})
}

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.

I think this will fail with something like SELECT p.* FROM products p ORDER BY price. Looks like it would work fine if it was SELECT * FROM products ORDER BY price or SELECT p.* FROM products p ORDER BY p.price, but not if we have a mixed case of one being qualified and the other not.

Comment on lines +407 to +412
pub(crate) fn projection_rewrite_plan(&self) -> &ProjectionRewritePlan {
&self.projection_rewrite_plan
}

pub(crate) fn set_rewrite_plan(&mut self, plan: AggregateRewritePlan) {
self.rewrite_plan = plan;
pub(crate) fn set_projection_rewrite_plan(&mut self, plan: ProjectionRewritePlan) {
self.projection_rewrite_plan = plan;

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.

This is another case we could probably get rid of the getter/setter

Comment on lines +16 to +18
fn cross_shard_variant_name(name: &str) -> String {
format!("{name}_cross_shard")
}

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.

I wonder if we could re-use the previous allocation somehow, here's a super simple example (probably not optimal):

buf = "x__pgdog_1"
cross_shard_variant = buf
base = buf[1..]

continue;
}

let projected_column = select.target_list().len() + helpers.len();

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.

I think we shouldn't rely on this column index; it gives us the parse tree entries, but not the actual output columns (considering when * is used). I wonder if it would be better to use what you do in aggregate.rs again (decoder.row_description().field_index(&helper.alias) else {). Right now, I think there are edge cases where this can accidentally delete user columns / sort on the wrong column.


#[test]
fn projects_column_not_covered_by_qualified_star() {
let (sql, plan) = rewrite("SELECT a.* FROM a JOIN b ON a.id = b.a_id ORDER BY b.score");

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.

This should also test the index (going off of my comment above)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants