use bytes::{Bytes, BytesMut};
use tokio::io::AsyncRead;
use super::wire;
use crate::error::ReplicationError;
use crate::prelude::*;
#[derive(Debug)]
pub(super) enum CopyOutItem {
Data(Bytes),
Done,
}
pub(super) async fn next_copy_out<S: AsyncRead + Unpin>(
stream: &mut S,
buf: &mut BytesMut,
) -> Result<CopyOutItem, ReplicationError> {
loop {
let msg = wire::read_message(stream, buf).await?;
match msg.first().copied() {
Some(b'd') => return Ok(CopyOutItem::Data(msg.slice(wire::HEADER_LEN..))),
Some(b'c') => {
drain_epilogue(stream, buf).await?;
return Ok(CopyOutItem::Done);
}
Some(b'E') => {
let fields = super::error::parse_error_fields(&msg[wire::HEADER_LEN..]);
let err = ReplicationError::from_sqlstate(
&fields.code,
format!("server error during COPY OUT: {fields}"),
);
let _ = super::query::drain_to_ready(stream, buf).await;
return Err(err);
}
Some(b'N') => {
tracing::info!(
"Server notice during COPY OUT: {}",
super::error::parse_error_fields(&msg[wire::HEADER_LEN..])
);
}
Some(b'S' | b'A') => {}
Some(tag) => {
return Err(ReplicationError::protocol(format!(
"unexpected '{}' message inside COPY OUT",
tag as char
)));
}
None => return Err(ReplicationError::protocol("empty message during COPY OUT")),
}
}
}
async fn drain_epilogue<S: AsyncRead + Unpin>(
stream: &mut S,
buf: &mut BytesMut,
) -> Result<(), ReplicationError> {
let mut deferred: Option<ReplicationError> = None;
loop {
let msg = match wire::read_message(stream, buf).await {
Ok(msg) => msg,
Err(e) => return Err(deferred.unwrap_or(e)),
};
match msg.first().copied() {
Some(b'Z') => break,
Some(b'C') => {}
Some(b'N') => {
tracing::info!(
"Server notice after COPY OUT: {}",
super::error::parse_error_fields(&msg[wire::HEADER_LEN..])
);
}
Some(b'E') => {
let fields = super::error::parse_error_fields(&msg[wire::HEADER_LEN..]);
deferred.get_or_insert_with(|| {
ReplicationError::from_sqlstate(
&fields.code,
format!("COPY OUT failed after CopyDone: {fields}"),
)
});
}
Some(b'S' | b'A') => {}
Some(tag) => {
return Err(ReplicationError::protocol(format!(
"unexpected '{}' message after COPY OUT",
tag as char
)))
}
None => return Err(ReplicationError::protocol("empty message after COPY OUT")),
}
}
match deferred {
Some(err) => Err(err),
None => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncWriteExt;
fn frame(tag: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(wire::HEADER_LEN + payload.len());
out.push(tag);
out.extend_from_slice(&((4 + payload.len()) as i32).to_be_bytes());
out.extend_from_slice(payload);
out
}
fn copy_data(payload: &[u8]) -> Vec<u8> {
frame(b'd', payload)
}
fn copy_done() -> Vec<u8> {
frame(b'c', b"")
}
fn command_complete() -> Vec<u8> {
frame(b'C', b"COPY 2\0")
}
fn ready_for_query() -> Vec<u8> {
frame(b'Z', b"I")
}
fn error_response(code: &str, message: &str) -> Vec<u8> {
let mut payload = Vec::new();
payload.push(b'C');
payload.extend_from_slice(code.as_bytes());
payload.push(0);
payload.push(b'M');
payload.extend_from_slice(message.as_bytes());
payload.push(0);
payload.push(0);
frame(b'E', &payload)
}
fn notice(message: &str) -> Vec<u8> {
let mut payload = Vec::new();
payload.push(b'M');
payload.extend_from_slice(message.as_bytes());
payload.push(0);
payload.push(0);
frame(b'N', &payload)
}
async fn scripted(script: Vec<u8>) -> (tokio::io::DuplexStream, tokio::io::DuplexStream) {
let (client, mut server) = tokio::io::duplex(64 * 1024);
server.write_all(&script).await.unwrap();
server.flush().await.unwrap();
(client, server)
}
async fn drive(script: Vec<u8>) -> (Vec<Bytes>, Result<(), ReplicationError>) {
let (mut stream, _server) = scripted(script).await;
let mut buf = BytesMut::new();
let mut rows = Vec::new();
loop {
match next_copy_out(&mut stream, &mut buf).await {
Ok(CopyOutItem::Data(b)) => rows.push(b),
Ok(CopyOutItem::Done) => return (rows, Ok(())),
Err(e) => return (rows, Err(e)),
}
}
}
#[tokio::test]
async fn reads_data_frames_then_done() {
let script = [
copy_data(b"1\tAlice\n"),
copy_data(b"2\tBob\n"),
copy_done(),
command_complete(),
ready_for_query(),
]
.concat();
let (rows, result) = drive(script).await;
result.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(&rows[0][..], b"1\tAlice\n");
assert_eq!(&rows[1][..], b"2\tBob\n");
}
#[tokio::test]
async fn epilogue_is_consumed_so_the_next_read_is_clean() {
let sentinel = frame(b'T', b"sentinel");
let script = [
copy_data(b"x\n"),
copy_done(),
command_complete(),
ready_for_query(),
sentinel.clone(),
]
.concat();
let (mut stream, _server) = scripted(script).await;
let mut buf = BytesMut::new();
assert!(matches!(
next_copy_out(&mut stream, &mut buf).await.unwrap(),
CopyOutItem::Data(_)
));
assert!(matches!(
next_copy_out(&mut stream, &mut buf).await.unwrap(),
CopyOutItem::Done
));
let next = wire::read_message(&mut stream, &mut buf).await.unwrap();
assert_eq!(next[0], b'T', "epilogue was not fully consumed");
}
#[tokio::test]
async fn zero_row_table_completes() {
let script = [copy_done(), command_complete(), ready_for_query()].concat();
let (rows, result) = drive(script).await;
result.unwrap();
assert!(rows.is_empty());
}
#[tokio::test]
async fn error_response_maps_sqlstate() {
let script = [
error_response("42P01", "relation \"nope\" does not exist"),
ready_for_query(),
]
.concat();
let (_, result) = drive(script).await;
let err = result.unwrap_err();
assert!(matches!(err, ReplicationError::Protocol(_)), "{err:?}");
assert!(format!("{err}").contains("42P01"), "{err}");
}
#[tokio::test]
async fn error_response_classified_as_transient() {
let script = [
error_response("57P01", "terminating connection"),
ready_for_query(),
]
.concat();
let (_, result) = drive(script).await;
let err = result.unwrap_err();
assert!(err.is_transient(), "{err:?}");
}
#[tokio::test]
async fn error_response_drains_to_ready() {
let sentinel = frame(b'T', b"after");
let script = [
error_response("42501", "permission denied"),
ready_for_query(),
sentinel,
]
.concat();
let (mut stream, _server) = scripted(script).await;
let mut buf = BytesMut::new();
assert!(next_copy_out(&mut stream, &mut buf).await.is_err());
let next = wire::read_message(&mut stream, &mut buf).await.unwrap();
assert_eq!(next[0], b'T', "did not drain to ReadyForQuery");
}
#[tokio::test]
async fn notice_is_skipped() {
let script = [
notice("something happened"),
copy_data(b"row\n"),
copy_done(),
command_complete(),
ready_for_query(),
]
.concat();
let (rows, result) = drive(script).await;
result.unwrap();
assert_eq!(rows.len(), 1);
}
#[tokio::test]
async fn parameter_status_between_frames_is_skipped() {
let script = [
frame(b'S', b"param\0value\0"),
copy_data(b"row\n"),
frame(b'A', b"\0\0\0\0chan\0payload\0"),
copy_done(),
command_complete(),
ready_for_query(),
]
.concat();
let (rows, result) = drive(script).await;
result.unwrap();
assert_eq!(rows.len(), 1);
}
#[tokio::test]
async fn an_unsanctioned_tag_between_frames_is_rejected() {
for tag in *b"CZTD" {
let script = [copy_data(b"row\n"), frame(tag, b"x\0"), copy_done()].concat();
let (rows, result) = drive(script).await;
assert_eq!(rows.len(), 1);
let err = result.unwrap_err();
assert!(
matches!(err, ReplicationError::Protocol(_)),
"'{}' must not be silently skipped, got {err:?}",
tag as char
);
}
}
#[tokio::test]
async fn notice_in_epilogue_is_skipped() {
let script = [
copy_data(b"row\n"),
copy_done(),
notice("all done"),
command_complete(),
ready_for_query(),
]
.concat();
let (rows, result) = drive(script).await;
result.unwrap();
assert_eq!(rows.len(), 1);
}
#[tokio::test]
async fn parameter_status_in_epilogue_is_skipped() {
let script = [
copy_done(),
frame(b'S', b"k\0v\0"),
command_complete(),
ready_for_query(),
]
.concat();
let (_, result) = drive(script).await;
result.unwrap();
}
#[tokio::test]
async fn an_unsanctioned_tag_in_the_epilogue_is_rejected() {
let script = [copy_done(), frame(b'd', b"row\n"), ready_for_query()].concat();
let (_, result) = drive(script).await;
let err = result.unwrap_err();
assert!(matches!(err, ReplicationError::Protocol(_)), "{err:?}");
}
#[tokio::test]
async fn error_in_epilogue_propagates_after_ready() {
let script = [
copy_data(b"partial\n"),
copy_done(),
error_response("58030", "could not read block"),
ready_for_query(),
]
.concat();
let (rows, result) = drive(script).await;
assert_eq!(rows.len(), 1, "rows before the failure are still delivered");
let err = result.unwrap_err();
assert!(format!("{err}").contains("after CopyDone"), "{err}");
}
#[tokio::test]
async fn first_epilogue_error_wins() {
let script = [
copy_done(),
error_response("58030", "first"),
error_response("XX000", "second"),
ready_for_query(),
]
.concat();
let (_, result) = drive(script).await;
let err = result.unwrap_err();
assert!(format!("{err}").contains("first"), "{err}");
}
#[tokio::test]
async fn copy_in_response_is_rejected() {
let (_, result) = drive(frame(b'G', b"\0\0\0")).await;
let err = result.unwrap_err();
assert!(matches!(err, ReplicationError::Protocol(_)), "{err:?}");
assert!(format!("{err}").contains('G'), "{err}");
}
#[tokio::test]
async fn copy_both_response_is_rejected() {
let (_, result) = drive(frame(b'W', b"\0\0\0")).await;
assert!(format!("{}", result.unwrap_err()).contains('W'));
}
#[tokio::test]
async fn nested_copy_out_response_is_rejected() {
let (_, result) = drive(frame(b'H', b"\0\0\0")).await;
assert!(format!("{}", result.unwrap_err()).contains('H'));
}
#[tokio::test]
async fn truncated_stream_errors() {
let (mut client, server) = tokio::io::duplex(1024);
{
let mut server = server;
server.write_all(©_data(b"row\n")).await.unwrap();
server.flush().await.unwrap();
}
let mut buf = BytesMut::new();
assert!(matches!(
next_copy_out(&mut client, &mut buf).await.unwrap(),
CopyOutItem::Data(_)
));
assert!(
next_copy_out(&mut client, &mut buf).await.is_err(),
"EOF mid-COPY must be an error, not a silent Done"
);
}
#[tokio::test]
async fn truncated_epilogue_errors() {
let (mut client, server) = tokio::io::duplex(1024);
{
let mut server = server;
server.write_all(©_done()).await.unwrap();
server.flush().await.unwrap();
}
let mut buf = BytesMut::new();
assert!(
next_copy_out(&mut client, &mut buf).await.is_err(),
"EOF before ReadyForQuery must be an error"
);
}
#[tokio::test]
async fn data_payload_is_zero_copy_slice_of_the_frame() {
let script = [copy_data(b"payload\n"), copy_done(), ready_for_query()].concat();
let (mut stream, _server) = scripted(script).await;
let mut buf = BytesMut::new();
let CopyOutItem::Data(data) = next_copy_out(&mut stream, &mut buf).await.unwrap() else {
panic!("expected data");
};
assert_eq!(&data[..], b"payload\n");
}
#[tokio::test]
async fn multiple_rows_in_one_frame_are_passed_through() {
let script = [
copy_data(b"1\n2\n3\n"),
copy_done(),
command_complete(),
ready_for_query(),
]
.concat();
let (rows, result) = drive(script).await;
result.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(&rows[0][..], b"1\n2\n3\n");
}
#[tokio::test]
async fn a_fatal_without_ready_for_query_keeps_its_sqlstate() {
let (mut client, server) = tokio::io::duplex(1024);
{
let mut server = server;
server
.write_all(
&[
copy_data(b"row\n"),
error_response("42501", "must be superuser"),
]
.concat(),
)
.await
.unwrap();
server.flush().await.unwrap();
}
let mut buf = BytesMut::new();
assert!(matches!(
next_copy_out(&mut client, &mut buf).await.unwrap(),
CopyOutItem::Data(_)
));
let err = next_copy_out(&mut client, &mut buf).await.unwrap_err();
assert!(format!("{err}").contains("42501"), "{err}");
assert!(
err.is_permanent(),
"a permanent SQLSTATE must not be laundered into a retryable error: {err:?}"
);
assert!(!err.is_transient(), "{err:?}");
}
#[tokio::test]
async fn a_deferred_epilogue_error_survives_a_truncated_epilogue() {
let (mut client, server) = tokio::io::duplex(1024);
{
let mut server = server;
server
.write_all(&[copy_done(), error_response("42501", "must be superuser")].concat())
.await
.unwrap();
server.flush().await.unwrap();
}
let mut buf = BytesMut::new();
let err = next_copy_out(&mut client, &mut buf).await.unwrap_err();
assert!(format!("{err}").contains("42501"), "{err}");
assert!(err.is_permanent(), "{err:?}");
}
#[tokio::test]
async fn an_error_with_ready_for_query_still_resyncs() {
let sentinel = frame(b'T', b"next query");
let script = [
copy_data(b"row\n"),
error_response("57014", "canceling statement"),
ready_for_query(),
sentinel,
]
.concat();
let (mut stream, _server) = scripted(script).await;
let mut buf = BytesMut::new();
assert!(matches!(
next_copy_out(&mut stream, &mut buf).await.unwrap(),
CopyOutItem::Data(_)
));
let err = next_copy_out(&mut stream, &mut buf).await.unwrap_err();
assert!(format!("{err}").contains("57014"), "{err}");
let next = wire::read_message(&mut stream, &mut buf).await.unwrap();
assert_eq!(next[0], b'T', "the epilogue must have been consumed");
}
}