feat: partial sharding support for PREPARE and EXECUTE - #1368
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
e2e9044 to
ebcc0c7
Compare
| 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 | ||
| } | ||
| } |
| 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() | ||
| } | ||
| } |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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
| /// 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. | ||
| /// |
| let stmt_params = StatementParameters::Execute(¶ms); | ||
|
|
||
| // Create new parser context. | ||
| let mut context = context.clone(); |
There was a problem hiding this comment.
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
| // Will use parameters for replacing args, not materialize values. | ||
| self.extended = true; |
There was a problem hiding this comment.
won't this cause any issues with the actual extended protocol? yeah, like using PREPARE inside Parse 😄
There was a problem hiding this comment.
Oh god I hope Postgres triggers an error when that happens....
| #[error("prepare statement can only be DML")] | ||
| PrepareNotDml, |
| if self.contains(name) { | ||
| ProtocolMessage::PrepareFromClient(prepare) => { | ||
| use crate::net::{CommandComplete, ReadyForQuery}; | ||
| if self.contains(prepare.name()) { |
There was a problem hiding this comment.
I think we need to use check_prepared at some extend - the ttl functionality and parses deduplication is not working for prepared rn
| 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); | ||
| } |
There was a problem hiding this comment.
well, should we do something specific for the prepare/execute here?
There was a problem hiding this comment.
Ah yeah, probably doesn't work currently.
| // 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
#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.
Prepared statements
Correctly store statements sent with
PREPAREin the global cache. We use the whole query string with data type specifications as key, while anonymizing the statement name. This is equivalent to usingParseas cache key.becomes
It's then dynamically rewritten to
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
EXECUTEby 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
Notes for reviewers
client/prepared_statementsmodule is from moving stuff around. There is some diff for handlingPREPARE, that's where I would focus the review.Servernow get processed through the server state machine; this was necessary for correct state management for handlingPREPAREstatements; seems to have no bad side effects.