Skip to content

Commit 4e99ce1

Browse files
committed
Invalided cached prepared statements on RevalidateCachedQuery and 26000
1 parent 1d674f5 commit 4e99ce1

3 files changed

Lines changed: 114 additions & 23 deletions

File tree

sqlx-core/src/common/statement_cache.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ impl<T> StatementCache<T> {
2121
self.inner.get_mut(k)
2222
}
2323

24+
/// Removes statement by key, returning removed statement if removed
25+
pub fn remove(&mut self, k: &str) -> Option<T> {
26+
self.inner.remove(k)
27+
}
28+
2429
/// Inserts a new statement to the cache, returning the least recently used
2530
/// statement id if the cache is full, or if inserting with an existing key,
2631
/// the replaced existing statement.

sqlx-postgres/src/connection/executor.rs

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@ use crate::error::Error;
22
use crate::executor::{Execute, Executor};
33
use crate::io::{PortalId, StatementId};
44
use crate::logger::QueryLogger;
5-
use crate::message::{
6-
self, BackendMessageFormat, Bind, Close, CommandComplete, DataRow, ParameterDescription, Parse,
7-
ParseComplete, RowDescription,
8-
};
5+
use crate::message::{self, BackendMessageFormat, Bind, Close, CommandComplete, DataRow, ParameterDescription, Parse, ParseComplete, ReceivedMessage, RowDescription};
96
use crate::statement::PgStatementMetadata;
10-
use crate::{
11-
statement::PgStatement, PgArguments, PgConnection, PgQueryResult, PgRow, PgTypeInfo,
12-
PgValueFormat, Postgres,
13-
};
7+
use crate::{statement::PgStatement, PgArguments, PgConnection, PgDatabaseError, PgQueryResult, PgRow, PgTypeInfo, PgValueFormat, Postgres};
148
use futures_core::future::BoxFuture;
159
use futures_core::stream::BoxStream;
1610
use futures_core::Stream;
@@ -19,6 +13,7 @@ use sqlx_core::arguments::Arguments;
1913
use sqlx_core::sql_str::SqlStr;
2014
use sqlx_core::Either;
2115
use std::{pin::pin, sync::Arc};
16+
use sqlx_core::connection::Connection;
2217

2318
async fn prepare(
2419
conn: &mut PgConnection,
@@ -196,22 +191,16 @@ impl PgConnection {
196191
Ok(statement)
197192
}
198193

199-
pub(crate) async fn run<'e, 'c: 'e, 'q: 'e>(
194+
async fn try_get_or_prepare<'e, 'c: 'e, 'q: 'e>(
200195
&'c mut self,
201-
query: SqlStr,
202-
arguments: Option<PgArguments>,
196+
sql: &str,
197+
arguments: Option<&mut PgArguments>,
203198
persistent: bool,
204-
metadata_opt: Option<Arc<PgStatementMetadata>>,
205-
) -> Result<impl Stream<Item = Result<Either<PgQueryResult, PgRow>, Error>> + 'e, Error> {
206-
let mut logger = QueryLogger::new(query, self.inner.log_settings.clone());
207-
let sql = logger.sql().as_str();
208-
209-
// before we continue, wait until we are "ready" to accept more queries
210-
self.wait_until_ready().await?;
211-
212-
let mut metadata: Arc<PgStatementMetadata>;
199+
metadata_opt: Option<Arc<PgStatementMetadata>>
200+
) -> Result<(PgValueFormat, Arc<PgStatementMetadata>), Error> {
201+
let metadata: Arc<PgStatementMetadata>;
213202

214-
let format = if let Some(mut arguments) = arguments {
203+
let format = if let Some(arguments) = arguments {
215204
// Check this before we write anything to the stream.
216205
//
217206
// Note: Postgres actually interprets this value as unsigned,
@@ -292,10 +281,62 @@ impl PgConnection {
292281

293282
self.inner.stream.flush().await?;
294283

284+
Ok((format, metadata))
285+
}
286+
287+
pub(crate) async fn run<'e, 'c: 'e, 'q: 'e>(
288+
&'c mut self,
289+
query: SqlStr,
290+
mut arguments: Option<PgArguments>,
291+
persistent: bool,
292+
metadata_opt: Option<Arc<PgStatementMetadata>>,
293+
) -> Result<impl Stream<Item = Result<Either<PgQueryResult, PgRow>, Error>> + 'e, Error> {
294+
let mut logger = QueryLogger::new(query, self.inner.log_settings.clone());
295+
let sql = logger.sql().as_str();
296+
297+
// before we continue, wait until we are "ready" to accept more queries
298+
self.wait_until_ready().await?;
299+
300+
let (mut format, mut metadata) = self.try_get_or_prepare(
301+
sql,
302+
arguments.as_mut(),
303+
persistent,
304+
metadata_opt.clone()
305+
).await?;
306+
307+
let mut message = match self.inner.stream.recv().await {
308+
Ok(msg) => msg,
309+
Err(err) => {
310+
if let Some(clear_backend_cache) = check_stale_plan(&err) {
311+
// Save transaction mode. It will be lost after invalidating
312+
let is_in_tx = self.in_transaction();
313+
314+
self.invalidate_cached_statement(sql, clear_backend_cache).await?;
315+
316+
// If we were in transaction mode we can't retry statement,
317+
// so we can immediately return err
318+
if is_in_tx {
319+
return Err(err)
320+
}
321+
322+
// Otherwise we can retry statement in hope everything is ok.
323+
(format, metadata) = self.try_get_or_prepare(
324+
sql,
325+
// It should be safe to retry `patch` on the same arguments
326+
arguments.as_mut(),
327+
persistent,
328+
metadata_opt.clone()
329+
).await?;
330+
331+
self.inner.stream.recv().await?
332+
} else {
333+
return Err(err)
334+
}
335+
}
336+
};
337+
295338
Ok(try_stream! {
296339
loop {
297-
let message = self.inner.stream.recv().await?;
298-
299340
match message.format {
300341
BackendMessageFormat::BindComplete
301342
| BackendMessageFormat::ParseComplete
@@ -369,6 +410,8 @@ impl PgConnection {
369410
));
370411
}
371412
}
413+
414+
message = self.inner.stream.recv().await?;
372415
}
373416

374417
Ok(())
@@ -485,3 +528,27 @@ impl<'c> Executor<'c> for &'c mut PgConnection {
485528
})
486529
}
487530
}
531+
532+
// Returns:
533+
// - `None` - if not 'stale query plan'
534+
// - `Some(false)` - if it is stale plan, but we don't need to deallocate objects on backend.
535+
// It can happen because of `DISCARD ALL`, `DEALLOCATE` or due to pgbouncer in
536+
// transaction pooling mode
537+
// - `Some(true)` - if we should invalidate both backend and frontend caches
538+
fn check_stale_plan(error: &Error) -> Option<bool> {
539+
let Some(db_err) = error.as_database_error() else {
540+
return None;
541+
};
542+
let Some(pg) = db_err.try_downcast_ref::<PgDatabaseError>() else {
543+
return None;
544+
};
545+
546+
match (pg.code(), pg.routine()) {
547+
// "cached plan must not change result type"
548+
("0A000", Some("RevalidateCachedQuery")) => Some(true),
549+
// DISCARD ALL / DEALLOCATE / pgbouncer
550+
("26000", _) => Some(false),
551+
_ => None,
552+
}
553+
}
554+

sqlx-postgres/src/connection/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,25 @@ impl PgConnection {
146146
TransactionStatus::Error | TransactionStatus::Idle => false,
147147
}
148148
}
149+
150+
pub(crate) async fn invalidate_cached_statement(&mut self, sql: &str, backend: bool) -> Result<(), Error> {
151+
self.wait_until_ready().await?;
152+
153+
let Some((statement_id, _)) = self.inner.cache_statement.remove(sql) else {
154+
return Ok(())
155+
};
156+
157+
if backend {
158+
self.inner.stream.write_msg(Close::Statement(statement_id))?;
159+
self.write_sync();
160+
self.inner.stream.flush().await?;
161+
162+
self.wait_for_close_complete(1).await?;
163+
self.recv_ready_for_query().await?;
164+
}
165+
166+
Ok(())
167+
}
149168
}
150169

151170
impl Debug for PgConnection {

0 commit comments

Comments
 (0)