Skip to content

Port StatementRewrite::sharding_key_update to the new parser#1136

Open
sgrif wants to merge 9 commits into
mainfrom
sg-port-stuff
Open

Port StatementRewrite::sharding_key_update to the new parser#1136
sgrif wants to merge 9 commits into
mainfrom
sg-port-stuff

Conversation

@sgrif

@sgrif sgrif commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

More specifically, this ports over the glue code as well as the last subroutine of that function, create_stmts. This should be the beefiest function we have to port (dear god please let this be the beefiest one).

The new implementation diverges from the old in a couple of ways:

  • We deparse things lazily instead of eagerly
    • The old code was only deparsing eagerly due to the slowness of deparsing. This should no longer be slow enough to justify that. If needed we can memoize the result, but I'd rather have it be a memoized method than something upstream code cares about
  • We do not rewrite the bind parameters, instead making every query use all of them
    • The old code did a recursive mutable AST walk to look for any bind parameters in the query, rewrite/compress the numbers to be a contiguous list starting from 1, and then mapepd that to the original. I really don't want to have a mutable AST walk for the new parser, so instead I just shove a no-op CTE that uses all the params in the queries and copy the original bind over.
  • ShardingKeyUpdate holds the entire original UpdateStmt rather than smaller pieces
    • I actually tried having this hold onto references of the original query, but ultimately this struct ends up in a field that can't is a sibling of the original AST, so it can't hold a lifetime like that. I probably will revist that at some point, but for now copying the whole thing made more sense than copying each smaller piece into its own allocation

The unit tests look a little funky now to allow the new code to pass. What they're testing is technically a little bit weaker than before (it just checks that the bind parameters are all in the right places, it gives zero shits about the numbers). However, once the port is finished, I'll likely delete all but one of those unit tests. The complex logic they were testing simply no longer exists, and we really just need one test to assert that the where clause gets stuck on all 3 subqueries, and that all 3 queries use the same bind parameters as the original update

The CTE hack for the bind params really is funky, but I couldn't come up with any less hacky way to do it. I would very much prefer to stick SELECT $1, $2, ... somewhere instead, but that won't work. We can't rely on the client having sent us OIDs for the bind parameters in their Parse message, and any binds that appear in a top level select will get inferred to text. Since we can't assume any random blob of bytes is valid UTF-8, that would cause the query to fail. Putting the original update with false AND in the where clause was the simplest way I could think of to ensure the query uses every bind parameter, and keep the same type inference that we had before.

Other than the caveats mentioned above, the actual logic is mostly a line for line port. The syntax is certainly very different for constructing these trees, but it's a clean mapping each way.

@sgrif sgrif requested a review from levkk July 3, 2026 19:06
@sgrif

sgrif commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Once again going to assume wait_for_replication_finishes_with_unrelated_writes failing is unrelated to my change.

Comment thread pgdog/src/frontend/router/parser/rewrite/statement/update.rs Outdated
Comment thread pgdog/src/frontend/router/parser/rewrite/statement/update.rs Outdated
Comment thread pgdog/src/frontend/router/parser/rewrite/statement/update.rs Outdated

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

Provisionally agreed, but this will need cleanup - we got some more features coming in here soon and they will conflict - our current implementation is pretty easy to mod, this one might not be, I'm not sure. N.B.: I only had one coffee so far so I may be hallucinating issues.

@sgrif

sgrif commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

I'll for sure look at adding a transforming AST walk (which is different than mutating, it will return a new ast) It'll probably after the port is done unless some other part of the code desperately needs it. I'll revisit this once I have that

Though I'm gonna be honest, despite how hacky it is I don't agree that it's obviously worse than what the old code was doing. The sheer number of tests it has for various edge cases that just aren't necessary here says a lot.

@sgrif

sgrif commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

I'm gonna spike on both transforming and (limited) mutating AST walks on Monday. The more I think about it the more I realize at least one of those two is inevitable. My main concern is ensuring that they can be done safely, I don't want to have to have every ast walk be in an unsafe block. It's possible I'm being overly cautious.

Anyway the more I think about it the more I realize it doesn't make sense to merge this until I've fully explored that problem.

@sgrif sgrif marked this pull request as draft July 4, 2026 19:17
sgrif added 3 commits July 5, 2026 09:14
More specifically, this ports over the glue code as well as the last
subroutine of that function, `create_stmts`. This should be the beefiest
function we have to port (dear god please let this be the beefiest one).

The new implementation diverges from the old in a couple of ways:

- We deparse things lazily instead of eagerly
  - The old code was only deparsing eagerly due to the slowness of
    deparsing. This should no longer be slow enough to justify that. If
    needed we can memoize the result, but I'd rather have it be a
    memoized method than something upstream code cares about
- We do not rewrite the bind parameters, instead making every query use
  all of them
  - The old code did a recursive mutable AST walk to look for any bind
    parameters in the query, rewrite/compress the numbers to be a
    contiguous list starting from 1, and then mapepd that to the
    original. I *really* don't want to have a mutable AST walk for the
    new parser, so instead I just shove a no-op CTE that uses all the
    params in the queries and copy the original bind over.
- ShardingKeyUpdate holds the entire original UpdateStmt rather than
  smaller pieces
  - I actually tried having this hold onto references of the original
    query, but ultimately this struct ends up in a field that can't is a
    sibling of the original AST, so it can't hold a lifetime like that.
    I probably will revist that at some point, but for now copying the
    whole thing made more sense than copying each smaller piece into its
    own allocation

The unit tests look a little funky now to allow the new code to pass.
What they're testing is technically a little bit weaker than before (it
just checks that the bind parameters are all in the right places, it
gives zero shits about the numbers). However, once the port is finished,
I'll likely delete all but one of those unit tests. The complex logic
they were testing simply no longer exists, and we really just need one
test to assert that the where clause gets stuck on all 3 subqueries, and
that all 3 queries use the same bind parameters as the original update

The CTE hack for the bind params really is funky, but I couldn't come up
with any less hacky way to do it. I would very much prefer to stick
`SELECT $1, $2, ...` somewhere instead, but that won't work. We can't
rely on the client having sent us OIDs for the bind parameters in their
`Parse` message, and any binds that appear in a top level select will
get inferred to `text`. Since we can't assume any random blob of bytes
is valid UTF-8, that would cause the query to fail. Putting the original
update with `false AND` in the where clause was the simplest way I could
think of to ensure the query uses every bind parameter, and keep the
same type inference that we had before.

Other than the caveats mentioned above, the actual logic is mostly a
line for line port. The syntax is certainly very different for
constructing these trees, but it's a clean mapping each way.
@sgrif sgrif marked this pull request as ready for review July 5, 2026 19:44
@sgrif sgrif requested a review from levkk July 5, 2026 19:44
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

sgrif added 4 commits July 5, 2026 15:11
We're not caching the prepared statement, we're always using text format
anyway, there's just no reason to do anything other than put the exact
nodes we got in for the output
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.

2 participants