use bytes::BytesMut;
use tokio::io::{AsyncRead, AsyncWrite};
use super::result::{NativePgResult, NativeResultStatus};
use super::wire;
use crate::error::ReplicationError;
pub(super) async fn simple_query<S: AsyncRead + AsyncWrite + Unpin>(
stream: &mut S,
buf: &mut BytesMut,
sql: &str,
) -> Result<NativePgResult, ReplicationError> {
let query_msg = wire::build_query_message(sql);
wire::write_all(stream, &query_msg).await?;
wire::flush(stream).await?;
let mut result = NativePgResult::new();
loop {
let msg = match wire::read_message(stream, buf).await {
Ok(msg) => msg,
Err(e) => {
if result.status == NativeResultStatus::FatalError {
return Ok(result);
}
return Err(e);
}
};
if msg.is_empty() {
continue;
}
match msg[0] {
b'T' => {
result.parse_row_description(&msg[5..]);
}
b'D' => {
result.parse_data_row(&msg[5..]);
}
b'C' => {
if result.status == NativeResultStatus::Empty {
result.status = NativeResultStatus::CommandOk;
}
}
b'Z' => {
break;
}
b'W' => {
result.status = NativeResultStatus::CopyBoth;
break;
}
b'H' => {
result.status = NativeResultStatus::CopyOut;
break;
}
b'G' => {
let fail = wire::build_copy_fail("CopyIn not supported by this query path");
wire::write_all(stream, &fail).await?;
wire::flush(stream).await?;
drain_to_ready(stream, buf).await?;
return Err(ReplicationError::protocol(format!(
"{sql} requested CopyIn mode, which this call does not support; \
use the CopyIn-aware path (UPLOAD_MANIFEST) instead"
)));
}
b'E' => {
let fields = super::error::parse_error_fields(&msg[5..]);
result.status = NativeResultStatus::FatalError;
result.error_code = Some(fields.code.clone());
result.error_msg = Some(format!("{}", fields));
}
b'N' => {
let fields = super::error::parse_error_fields(&msg[5..]);
tracing::info!("Server notice: {}", fields);
}
_ => {
tracing::debug!("Skipping message type '{}' during query", msg[0] as char);
}
}
}
Ok(result)
}
const COPY_IN_CHUNK: usize = 64 * 1024;
pub(super) async fn simple_query_copy_in<S: AsyncRead + AsyncWrite + Unpin>(
stream: &mut S,
buf: &mut BytesMut,
sql: &str,
payload: &[u8],
) -> Result<NativePgResult, ReplicationError> {
let query_msg = wire::build_query_message(sql);
wire::write_all(stream, &query_msg).await?;
wire::flush(stream).await?;
let mut result = NativePgResult::new();
loop {
let msg = wire::read_message(stream, buf).await?;
if msg.is_empty() {
continue;
}
match msg[0] {
b'G' => break, b'E' => {
let fields = super::error::parse_error_fields(&msg[5..]);
result.status = NativeResultStatus::FatalError;
result.error_code = Some(fields.code.clone());
result.error_msg = Some(format!("{fields}"));
drain_to_ready(stream, buf).await?;
return Ok(result);
}
b'N' => {
let fields = super::error::parse_error_fields(&msg[5..]);
tracing::info!("Server notice: {fields}");
}
b'Z' => {
return Err(ReplicationError::protocol(format!(
"{sql} completed without entering CopyIn mode"
)));
}
other => {
tracing::debug!("Skipping message type '{}' before CopyIn", other as char);
}
}
}
for chunk in payload.chunks(COPY_IN_CHUNK) {
let msg = wire::build_copy_data(chunk);
wire::write_all(stream, &msg).await?;
}
let done = wire::build_copy_done();
wire::write_all(stream, &done).await?;
wire::flush(stream).await?;
loop {
let msg = match wire::read_message(stream, buf).await {
Ok(msg) => msg,
Err(e) => {
if result.status == NativeResultStatus::FatalError {
return Ok(result);
}
return Err(e);
}
};
if msg.is_empty() {
continue;
}
match msg[0] {
b'C' => {
if result.status == NativeResultStatus::Empty {
result.status = NativeResultStatus::CommandOk;
}
}
b'Z' => break,
b'E' => {
let fields = super::error::parse_error_fields(&msg[5..]);
result.status = NativeResultStatus::FatalError;
result.error_code = Some(fields.code.clone());
result.error_msg = Some(format!("{fields}"));
}
b'N' => {
let fields = super::error::parse_error_fields(&msg[5..]);
tracing::info!("Server notice: {fields}");
}
other => {
tracing::debug!("Skipping message type '{}' after CopyIn", other as char);
}
}
}
Ok(result)
}
pub(super) async fn drain_to_ready<S: AsyncRead + Unpin>(
stream: &mut S,
buf: &mut BytesMut,
) -> Result<(), ReplicationError> {
loop {
let msg = wire::read_message(stream, buf).await?;
if msg.first() == Some(&b'Z') {
return Ok(());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn test_result_status_variants() {
assert_ne!(NativeResultStatus::CommandOk, NativeResultStatus::TuplesOk);
assert_ne!(NativeResultStatus::CopyBoth, NativeResultStatus::FatalError);
}
fn build_command_complete(tag: &str) -> Vec<u8> {
let mut payload = tag.as_bytes().to_vec();
payload.push(0);
let len = (4 + payload.len()) as i32;
let mut msg = vec![b'C'];
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&payload);
msg
}
fn build_ready_for_query(status: u8) -> Vec<u8> {
vec![b'Z', 0, 0, 0, 5, status]
}
fn build_copy_both_response() -> Vec<u8> {
let mut msg = vec![b'W'];
msg.extend_from_slice(&7i32.to_be_bytes());
msg.push(0); msg.extend_from_slice(&0i16.to_be_bytes()); msg
}
fn build_copy_out_response() -> Vec<u8> {
let mut msg = vec![b'H'];
msg.extend_from_slice(&7i32.to_be_bytes());
msg.push(0); msg.extend_from_slice(&0i16.to_be_bytes()); msg
}
fn build_error_response(severity: &str, code: &str, message: &str) -> Vec<u8> {
let mut payload = Vec::new();
payload.push(b'S');
payload.extend_from_slice(severity.as_bytes());
payload.push(0);
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);
let len = (4 + payload.len()) as i32;
let mut msg = vec![b'E'];
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&payload);
msg
}
fn build_row_description(names: &[&str]) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&(names.len() as i16).to_be_bytes());
for name in names {
payload.extend_from_slice(name.as_bytes());
payload.push(0); payload.extend_from_slice(&0i32.to_be_bytes()); payload.extend_from_slice(&0i16.to_be_bytes()); payload.extend_from_slice(&25i32.to_be_bytes()); payload.extend_from_slice(&(-1i16).to_be_bytes()); payload.extend_from_slice(&0i32.to_be_bytes()); payload.extend_from_slice(&0i16.to_be_bytes()); }
let len = (4 + payload.len()) as i32;
let mut msg = vec![b'T'];
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&payload);
msg
}
fn build_data_row(values: &[&str]) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&(values.len() as i16).to_be_bytes());
for val in values {
let bytes = val.as_bytes();
payload.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
payload.extend_from_slice(bytes);
}
let len = (4 + payload.len()) as i32;
let mut msg = vec![b'D'];
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&payload);
msg
}
fn build_copy_in_response() -> Vec<u8> {
let mut msg = vec![b'G'];
msg.extend_from_slice(&7i32.to_be_bytes());
msg.push(0); msg.extend_from_slice(&0i16.to_be_bytes()); msg
}
#[tokio::test]
async fn simple_query_keeps_sqlstate_when_server_fatals_and_closes() {
let (mut client, mut server) = tokio::io::duplex(65536);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server
.write_all(&build_error_response(
"FATAL",
"57P01",
"terminating connection due to administrator command",
))
.await
.unwrap();
server.flush().await.unwrap();
drop(server);
});
let mut buf = BytesMut::new();
let res = tokio::time::timeout(
std::time::Duration::from_secs(5),
simple_query(&mut client, &mut buf, "IDENTIFY_SYSTEM"),
)
.await
.expect("simple_query hung");
let result = res.expect("the parsed FATAL must survive the close");
assert_eq!(result.status, NativeResultStatus::FatalError);
assert_eq!(result.error_code.as_deref(), Some("57P01"));
}
#[tokio::test]
async fn copy_in_keeps_sqlstate_when_server_fatals_after_the_payload() {
let (mut client, mut server) = tokio::io::duplex(65536);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_in_response()).await.unwrap();
server.flush().await.unwrap();
let _ = drain_client(&mut server, 5 + 3 + 5).await;
server
.write_all(&build_error_response("FATAL", "XX000", "manifest rejected"))
.await
.unwrap();
server.flush().await.unwrap();
drop(server);
});
let mut buf = BytesMut::new();
let res = tokio::time::timeout(
std::time::Duration::from_secs(5),
simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", b"abc"),
)
.await
.expect("copy_in hung");
let result = res.expect("the parsed FATAL must survive the close");
assert_eq!(result.status, NativeResultStatus::FatalError);
assert_eq!(result.error_code.as_deref(), Some("XX000"));
}
async fn drain_client<S: AsyncRead + AsyncWrite + Unpin>(
server: &mut S,
upto: usize,
) -> Vec<u8> {
let mut got = Vec::new();
let mut chunk = vec![0u8; 8192];
while got.len() < upto {
match server.read(&mut chunk).await {
Ok(0) => break,
Ok(n) => got.extend_from_slice(&chunk[..n]),
Err(_) => break,
}
}
got
}
#[tokio::test]
async fn simple_query_fails_the_copy_instead_of_hanging() {
let (mut client, mut server) = tokio::io::duplex(65536);
let handle = tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_in_response()).await.unwrap();
server.flush().await.unwrap();
let sent = drain_client(&mut server, 5).await;
server
.write_all(&build_error_response(
"ERROR",
"57014",
"COPY from stdin failed",
))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
sent
});
let mut buf = BytesMut::new();
let res = tokio::time::timeout(
std::time::Duration::from_secs(5),
simple_query(&mut client, &mut buf, "COPY t FROM STDIN"),
)
.await
.expect("simple_query hung on CopyInResponse");
let err = res.expect_err("CopyIn on the plain query path must be an error");
assert!(
err.to_string().contains("CopyIn"),
"unexpected error: {err}"
);
let sent = handle.await.unwrap();
assert_eq!(sent.first(), Some(&b'f'), "client must send CopyFail");
}
#[tokio::test]
async fn test_simple_query_copy_in_success() {
let (mut client, mut server) = tokio::io::duplex(65536);
let handle = tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_in_response()).await.unwrap();
server.flush().await.unwrap();
let sent = drain_client(&mut server, 5 + 7 + 5).await;
server
.write_all(&build_command_complete("UPLOAD_MANIFEST"))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
sent
});
let mut buf = BytesMut::new();
let result = simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", b"payload")
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::CommandOk);
assert!(result.is_ok());
let sent = handle.await.unwrap();
assert_eq!(sent[0], b'd');
assert_eq!(i32::from_be_bytes(sent[1..5].try_into().unwrap()), 4 + 7);
assert_eq!(&sent[5..12], b"payload");
assert_eq!(sent[12], b'c');
assert_eq!(i32::from_be_bytes(sent[13..17].try_into().unwrap()), 4);
}
#[tokio::test]
async fn test_simple_query_copy_in_chunks_large_payload() {
let payload = vec![b'x'; COPY_IN_CHUNK + 100];
let expect_bytes = 2 * 5 + payload.len() + 5; let (mut client, mut server) = tokio::io::duplex(4 * 1024 * 1024);
let handle = tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_in_response()).await.unwrap();
server.flush().await.unwrap();
let sent = drain_client(&mut server, expect_bytes).await;
server
.write_all(&build_command_complete("UPLOAD_MANIFEST"))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
sent
});
let mut buf = BytesMut::new();
let result = simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", &payload)
.await
.unwrap();
assert!(result.is_ok());
let sent = handle.await.unwrap();
let first = i32::from_be_bytes(sent[1..5].try_into().unwrap()) as usize - 4;
assert_eq!(first, COPY_IN_CHUNK, "first frame is a full chunk");
let second_at = 5 + first;
assert_eq!(sent[second_at], b'd');
let second =
i32::from_be_bytes(sent[second_at + 1..second_at + 5].try_into().unwrap()) as usize - 4;
assert_eq!(
first + second,
payload.len(),
"every byte sent exactly once"
);
assert_eq!(sent[second_at + 5 + second], b'c', "CopyDone follows");
}
#[tokio::test]
async fn test_simple_query_copy_in_error_before_copy_starts() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server
.write_all(&build_error_response(
"ERROR",
"42601",
"syntax error at or near \"UPLOAD_MANIFEST\"",
))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", b"x")
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::FatalError);
assert_eq!(result.error_sqlstate(), "42601");
assert!(result.error_message().unwrap().contains("syntax error"));
}
#[tokio::test]
async fn test_simple_query_copy_in_error_after_copy_completes() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_in_response()).await.unwrap();
server.flush().await.unwrap();
let _ = drain_client(&mut server, 5 + 3 + 5).await;
server
.write_all(&build_error_response(
"ERROR",
"XX000",
"could not parse backup manifest",
))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", b"bad")
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::FatalError);
assert_eq!(result.error_sqlstate(), "XX000");
assert!(result.error_message().unwrap().contains("manifest"));
}
#[tokio::test]
async fn test_simple_query_copy_in_skips_notices() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
let mut notice = build_error_response("NOTICE", "00000", "heads up");
notice[0] = b'N';
server.write_all(¬ice).await.unwrap();
server.write_all(&build_copy_in_response()).await.unwrap();
server.flush().await.unwrap();
let _ = drain_client(&mut server, 5 + 1 + 5).await;
server.write_all(¬ice).await.unwrap();
server
.write_all(&build_command_complete("UPLOAD_MANIFEST"))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", b"m")
.await
.unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_simple_query_copy_in_never_enters_copy_mode() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let err = simple_query_copy_in(&mut client, &mut buf, "UPLOAD_MANIFEST", b"m")
.await
.unwrap_err();
assert!(err.to_string().contains("without entering CopyIn"), "{err}");
}
#[tokio::test]
async fn test_simple_query_command_ok() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server
.write_all(&build_command_complete("SELECT 0"))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query(&mut client, &mut buf, "SELECT 1")
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::CommandOk);
}
#[tokio::test]
async fn test_simple_query_with_rows() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server
.write_all(&build_row_description(&["systemid", "timeline"]))
.await
.unwrap();
server
.write_all(&build_data_row(&["12345", "1"]))
.await
.unwrap();
server
.write_all(&build_data_row(&["67890", "2"]))
.await
.unwrap();
server
.write_all(&build_command_complete("SELECT 2"))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query(&mut client, &mut buf, "IDENTIFY_SYSTEM")
.await
.unwrap();
assert_eq!(result.ntuples(), 2);
assert_eq!(result.nfields(), 2);
assert_eq!(result.get_value(0, 0), Some("12345".to_string()));
assert_eq!(result.get_value(0, 1), Some("1".to_string()));
assert_eq!(result.get_value(1, 0), Some("67890".to_string()));
}
#[tokio::test]
async fn test_simple_query_error_response() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server
.write_all(&build_error_response("ERROR", "42601", "syntax error"))
.await
.unwrap();
server
.write_all(&build_ready_for_query(b'I'))
.await
.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query(&mut client, &mut buf, "INVALID SQL")
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::FatalError);
assert!(result.error_message().is_some());
let err_msg = result.error_message().unwrap();
assert!(err_msg.contains("syntax error"), "Got: {err_msg}");
}
#[tokio::test]
async fn test_simple_query_copy_both() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_both_response()).await.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query(
&mut client,
&mut buf,
"START_REPLICATION SLOT test LOGICAL 0/0",
)
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::CopyBoth);
}
#[tokio::test]
async fn test_simple_query_copy_out() {
let (mut client, mut server) = tokio::io::duplex(8192);
tokio::spawn(async move {
let mut discard = vec![0u8; 1024];
let _ = server.read(&mut discard).await;
server.write_all(&build_copy_out_response()).await.unwrap();
server.flush().await.unwrap();
});
let mut buf = BytesMut::new();
let result = simple_query(&mut client, &mut buf, "BASE_BACKUP")
.await
.unwrap();
assert_eq!(result.status(), &NativeResultStatus::CopyOut);
}
}