Skip to content

feat: partial sharding support for PREPARE and EXECUTE - #1368

Merged
levkk merged 22 commits into
mainfrom
levkk-prepared-simple-routing
Aug 17, 2026
Merged

feat: partial sharding support for PREPARE and EXECUTE#1368
levkk merged 22 commits into
mainfrom
levkk-prepared-simple-routing

Conversation

@levkk

@levkk levkk commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Prepared statements

Correctly store statements sent with PREPARE in the global cache. We use the whole query string with data type specifications as key, while anonymizing the statement name. This is equivalent to using Parse as cache key.

PREPARE __stmt_1(int) AS SELECT $1;

becomes

PREPARE __pgdog_template_name(int) AS SELECT $1;

It's then dynamically rewritten to

PREPARE __pgdog_1(int) AS SELECT $1

at execution time.

This is because it's kinda hard to parse the data types out of the string and we need a unique key identifying the prepared statement in the cache.

Sharding

Handle EXECUTE by extracting parameters and passing them to our query router. This works with I think most of our sharding features, i.e., direct-to-shard and cross-shard queries.

TODOs

  1. Comment-based routing isn't supported. This is because we parse and deparse the original statement to put it in the prepared statement cache, which erases the comment.
  2. Query rewrites, e.g., insert split, don't work yet.

Notes for reviewers

  1. Most of the diff in client/prepared_statements module is from moving stuff around. There is some diff for handling PREPARE, that's where I would focus the review.
  2. Simulated messages sent by Server now get processed through the server state machine; this was necessary for correct state management for handling PREPARE statements; seems to have no bad side effects.

@levkk
levkk requested a review from meskill August 16, 2026 20:02
@levkk
levkk marked this pull request as ready for review August 16, 2026 20:03
@levkk levkk changed the title feat: sharding support for PREPARE and EXECUTE feat: partial sharding support for PREPARE and EXECUTE Aug 17, 2026
@levkk
levkk force-pushed the levkk-prepared-simple-routing branch from e2e9044 to ebcc0c7 Compare August 17, 2026 18:48
@levkk
levkk merged commit 0c8cf53 into main Aug 17, 2026
7 checks passed
@levkk
levkk deleted the levkk-prepared-simple-routing branch August 17, 2026 23:09
Comment on lines +43 to +61
impl FromBytes for Prepare {
fn from_bytes(_bytes: Bytes) -> Result<Self, Error> {
unreachable!("Prepare must be constructed manually")
}
}

impl Protocol for Prepare {
fn code(&self) -> char {
'Q'
}

fn message(&self) -> Result<Message, Error> {
Ok(Message::new(self.to_bytes()).frontend())
}

fn streaming(&self) -> bool {
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.

is this actually needed?

Comment on lines +63 to +74
impl ToBytes for Prepare {
fn to_bytes(&self) -> Bytes {
let query = self.query();
let name = self.name();
// This is safe because the statement looks like this:
// PREPARE __pgdog_template_name AS [...]
// so the template name will always match first.
let query = query.replacen(PREPARE_TEMPLATE_NAME, name, 1);

Query::new(query).to_bytes()
}
}

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.

nit: I understand why we need the "fake" messages, but I don't like much that we imitate that it's actual message that has byte representation. I'd prefer if we separate this kind of message in the parent enum to make it explicit and do the conversion to the some other real ProtocolMessage to use it's implementation. I.e. instead of ToBytes implement into_query() and call to_bytes on it

}
}

_ => BindParameter::new_null(),

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 should handle TypeCast as well and not only ConstValue, so "445-45::uuid" works and the compute_shard_for_table won't trigger broadcasting.
I think there also could FuncCalls and this probably should be revisited later on routing sharding, erroring etc., but anyway null is too destructive for this and misleading

Comment on lines +12 to +15
/// A `Simple` key comes from SQL `PREPARE` and matches nothing but itself.
/// Its declared argument types are not captured, so two of those
/// statements are never known to be the same.
///

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.

that's not true anymore

let stmt_params = StatementParameters::Execute(&params);

// Create new parser context.
let mut context = context.clone();

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.

that's looks fishy - we clone to mutate and then after mutation we drop this. I don't know maybe it's covered somehow or not harmful, but that is very brittle. I noticed only because the derive(Clone) appeared on the Context

Comment on lines +114 to +115
// Will use parameters for replacing args, not materialize values.
self.extended = true;

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.

won't this cause any issues with the actual extended protocol? yeah, like using PREPARE inside Parse 😄

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oh god I hope Postgres triggers an error when that happens....

Comment on lines +115 to +116
#[error("prepare statement can only be DML")]
PrepareNotDml,

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.

unused

if self.contains(name) {
ProtocolMessage::PrepareFromClient(prepare) => {
use crate::net::{CommandComplete, ReadyForQuery};
if self.contains(prepare.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 we need to use check_prepared at some extend - the ttl functionality and parses deduplication is not working for prepared rn

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good call!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 169 to 172
if let NodeMut::SelectStmt(mut select) = stmt.stmt_mut() {
self.rewrite_aggregates(&mut select, mem, &mut plan, self.db_schema)?;
self.limit_offset(&select, &mut 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.

well, should we do something specific for the prepare/execute here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah, probably doesn't work currently.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

// INVARIANT: omni dedup in multi_shard relies on this being process-unique;
// never substitute a non-unique value here.
return Ok(message.backend(self.id));
break message.backend(self.id);

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 this change? that seems like simulated prepared will now be processed and setting sync_prepared=true that will cause more work on check-in

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Because we need CommandComplete and ReadyForQuery to reset the server state back to Idle when we prepare a statement. Otherwise, it gets stuck in reading data. Preparing a statement using Query basically executes a separate query before the client's request, so the state management is a bit more complex.

We just push whatever message we "simulate" through the state manager though and it seems to work.

levkk added a commit that referenced this pull request Aug 18, 2026
#1368 introduced support for `PREPARE`, but handling of it was
duplicating an internal state tracker which could prevent the server
connection from being returned into the pool after the transaction was
done.

Also removed some dead code.
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