Skip to content

Commit 0b62ac1

Browse files
feikesteenbergenjayy-lmao
authored andcommitted
PostgreSQL Bugfix: Ensure connection is usable after failed COPY inside a transaction (launchbadge#3138)
* Include test case for regular subtransactions While using COPY and subtransactions I kept running into errors. This test case documents that error, it currently fails with: Error: encountered unexpected or invalid data: expecting ParseComplete but received CommandComplete * PostgreSQL Copy: Consume ReadyForQuery on error When a COPY statement was in error inside a subtransaction, a Protocol Error used to be raised. By consuming the ReadyForQuery message when there is an error, we no longer have this issue.
1 parent cd4042c commit 0b62ac1

File tree

2 files changed

+78
-9
lines changed

2 files changed

+78
-9
lines changed

sqlx-postgres/src/copy.rs

+13-8
Original file line numberDiff line numberDiff line change
@@ -342,16 +342,21 @@ async fn pg_begin_copy_out<'c, C: DerefMut<Target = PgConnection> + Send + 'c>(
342342

343343
let stream: TryAsyncStream<'c, Bytes> = try_stream! {
344344
loop {
345-
let msg = conn.stream.recv().await?;
346-
match msg.format {
347-
MessageFormat::CopyData => r#yield!(msg.decode::<CopyData<Bytes>>()?.0),
348-
MessageFormat::CopyDone => {
349-
let _ = msg.decode::<CopyDone>()?;
350-
conn.stream.recv_expect(MessageFormat::CommandComplete).await?;
345+
match conn.stream.recv().await {
346+
Err(e) => {
351347
conn.stream.recv_expect(MessageFormat::ReadyForQuery).await?;
352-
return Ok(())
348+
return Err(e);
353349
},
354-
_ => return Err(err_protocol!("unexpected message format during copy out: {:?}", msg.format))
350+
Ok(msg) => match msg.format {
351+
MessageFormat::CopyData => r#yield!(msg.decode::<CopyData<Bytes>>()?.0),
352+
MessageFormat::CopyDone => {
353+
let _ = msg.decode::<CopyDone>()?;
354+
conn.stream.recv_expect(MessageFormat::CommandComplete).await?;
355+
conn.stream.recv_expect(MessageFormat::ReadyForQuery).await?;
356+
return Ok(())
357+
},
358+
_ => return Err(err_protocol!("unexpected message format during copy out: {:?}", msg.format))
359+
}
355360
}
356361
}
357362
};

tests/postgres/postgres.rs

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
use futures::{StreamExt, TryStreamExt};
1+
use futures::{Stream, StreamExt, TryStreamExt};
2+
23
use sqlx::postgres::types::Oid;
34
use sqlx::postgres::{
45
PgAdvisoryLock, PgConnectOptions, PgConnection, PgDatabaseError, PgErrorPosition, PgListener,
56
PgPoolOptions, PgRow, PgSeverity, Postgres,
67
};
78
use sqlx::{Column, Connection, Executor, Row, Statement, TypeInfo};
9+
use sqlx_core::bytes::Bytes;
810
use sqlx_test::{new, pool, setup_if_needed};
911
use std::env;
12+
use std::pin::Pin;
1013
use std::sync::Arc;
1114
use std::time::Duration;
1215

@@ -382,6 +385,67 @@ async fn it_can_query_all_scalar() -> anyhow::Result<()> {
382385
Ok(())
383386
}
384387

388+
#[sqlx_macros::test]
389+
async fn copy_can_work_with_failed_transactions() -> anyhow::Result<()> {
390+
let mut conn = new::<Postgres>().await?;
391+
392+
// We're using a (local) statement_timeout to simulate a runtime failure, as opposed to
393+
// a parse/plan failure.
394+
let mut tx = conn.begin().await?;
395+
let _ = sqlx::query("SELECT pg_catalog.set_config($1, $2, true)")
396+
.bind("statement_timeout")
397+
.bind("1ms")
398+
.execute(tx.as_mut())
399+
.await?;
400+
401+
let mut copy_out: Pin<
402+
Box<dyn Stream<Item = Result<Bytes, sqlx::Error>> + Send>,
403+
> = (&mut tx)
404+
.copy_out_raw("COPY (SELECT nspname FROM pg_catalog.pg_namespace WHERE pg_sleep(0.001) IS NULL) TO STDOUT")
405+
.await?;
406+
407+
while copy_out.try_next().await.is_ok() {}
408+
drop(copy_out);
409+
410+
tx.rollback().await?;
411+
412+
// conn should be usable again, as we explictly rolled back the transaction
413+
let got: i32 = sqlx::query_scalar("SELECT 1")
414+
.fetch_one(conn.as_mut())
415+
.await?;
416+
assert_eq!(1, got);
417+
418+
Ok(())
419+
}
420+
421+
#[sqlx_macros::test]
422+
async fn it_can_work_with_failed_transactions() -> anyhow::Result<()> {
423+
let mut conn = new::<Postgres>().await?;
424+
425+
// We're using a (local) statement_timeout to simulate a runtime failure, as opposed to
426+
// a parse/plan failure.
427+
let mut tx = conn.begin().await?;
428+
let _ = sqlx::query("SELECT pg_catalog.set_config($1, $2, true)")
429+
.bind("statement_timeout")
430+
.bind("1ms")
431+
.execute(tx.as_mut())
432+
.await?;
433+
434+
assert!(sqlx::query("SELECT 1 WHERE pg_sleep(0.30) IS NULL")
435+
.fetch_one(tx.as_mut())
436+
.await
437+
.is_err());
438+
tx.rollback().await?;
439+
440+
// conn should be usable again, as we explictly rolled back the transaction
441+
let got: i32 = sqlx::query_scalar("SELECT 1")
442+
.fetch_one(conn.as_mut())
443+
.await?;
444+
assert_eq!(1, got);
445+
446+
Ok(())
447+
}
448+
385449
#[sqlx_macros::test]
386450
async fn it_can_work_with_transactions() -> anyhow::Result<()> {
387451
let mut conn = new::<Postgres>().await?;

0 commit comments

Comments
 (0)