use crate::error::{ReplicationError, Result};
use crate::protocol::build_hot_standby_feedback_message;
use crate::types::{format_lsn, BaseBackupOptions, ReplicationSlotOptions, SlotType, XLogRecPtr};
use bytes::{BufMut, Bytes, BytesMut};
use pq_sys::*;
use std::collections::VecDeque;
use std::ffi::{CStr, CString};
use std::os::raw::c_void;
use std::os::unix::io::RawFd;
use std::{ptr, slice};
use tokio::io::unix::AsyncFd;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
#[derive(Debug)]
enum ReadResult {
Data(Bytes),
WouldBlock,
CopyDone,
CopyFailed(String, String),
}
#[derive(Debug, PartialEq)]
enum DrainResult {
Progressed,
WouldBlock,
}
#[derive(Debug)]
struct CopyStream {
pending: VecDeque<Bytes>,
end: Option<CopyEnd>,
}
impl Default for CopyStream {
fn default() -> Self {
Self {
pending: VecDeque::with_capacity(MAX_DRAIN_BATCH),
end: None,
}
}
}
#[derive(Debug, Default)]
enum CopyMode {
#[default]
Idle,
Replication(CopyStream),
Out(CopyStream),
}
impl CopyMode {
#[inline]
fn stream_mut(&mut self) -> Option<&mut CopyStream> {
match self {
CopyMode::Idle => None,
CopyMode::Replication(s) | CopyMode::Out(s) => Some(s),
}
}
#[inline]
fn is_replication(&self) -> bool {
matches!(self, CopyMode::Replication(_))
}
#[inline]
fn is_out(&self) -> bool {
matches!(self, CopyMode::Out(_))
}
}
#[derive(Debug, Clone, PartialEq)]
enum CopyEnd {
Done,
Failed(String, String),
}
impl CopyEnd {
fn to_error(&self) -> ReplicationError {
match self {
CopyEnd::Done => ReplicationError::transient_connection(
"replication stream ended by server (CopyDone)",
),
CopyEnd::Failed(sqlstate, message) => ReplicationError::from_sqlstate(
sqlstate,
format!("replication stream terminated by server: {message}"),
),
}
}
}
const MAX_DRAIN_BATCH: usize = 4096;
const READ_BUF_INITIAL_CAPACITY: usize = 256 * 1024;
const COPY_IN_CHUNK: usize = 64 * 1024;
pub struct PgReplicationConnection {
conn: *mut PGconn,
async_fd: Option<AsyncFd<RawFd>>,
read_buf: BytesMut,
copy: CopyMode,
dead: bool,
}
impl PgReplicationConnection {
pub fn connect(conninfo: &str) -> Result<Self> {
unsafe {
let library_version = PQlibVersion();
debug!("Using libpq version: {}", library_version);
}
let c_conninfo = CString::new(conninfo)
.map_err(|e| ReplicationError::connection(format!("Invalid connection string: {e}")))?;
let conn = unsafe { PQconnectdb(c_conninfo.as_ptr()) };
if conn.is_null() {
return Err(ReplicationError::transient_connection(
"Failed to allocate PostgreSQL connection object".to_string(),
));
}
let status = unsafe { PQstatus(conn) };
if status != ConnStatusType::CONNECTION_OK {
let error_msg = unsafe {
let error_ptr = PQerrorMessage(conn);
if error_ptr.is_null() {
"Unknown connection error".to_string()
} else {
CStr::from_ptr(error_ptr).to_string_lossy().into_owned()
}
};
unsafe { PQfinish(conn) };
let error_msg_lower = error_msg.to_lowercase();
if error_msg_lower.contains("authentication failed")
|| error_msg_lower.contains("password authentication failed")
|| error_msg_lower.contains("role does not exist")
{
return Err(ReplicationError::authentication(format!(
"PostgreSQL authentication failed: {error_msg}"
)));
} else if error_msg_lower.contains("database does not exist")
|| error_msg_lower.contains("invalid connection string")
|| error_msg_lower.contains("unsupported")
{
return Err(ReplicationError::permanent_connection(format!(
"PostgreSQL connection failed (permanent): {error_msg}"
)));
} else {
return Err(ReplicationError::transient_connection(format!(
"PostgreSQL connection failed (transient): {error_msg}"
)));
}
}
if unsafe { PQsetClientEncoding(conn, c"UTF8".as_ptr()) } != 0 {
let error_msg = unsafe {
let error_ptr = PQerrorMessage(conn);
if error_ptr.is_null() {
"Unknown error".to_string()
} else {
CStr::from_ptr(error_ptr).to_string_lossy().into_owned()
}
};
unsafe { PQfinish(conn) };
return Err(ReplicationError::permanent_connection(format!(
"Failed to set client_encoding=UTF8: {error_msg}"
)));
}
let server_version = unsafe { PQserverVersion(conn) };
if server_version < 140000 {
unsafe { PQfinish(conn) };
return Err(ReplicationError::permanent_connection(format!(
"PostgreSQL version {server_version} is not supported. Logical replication requires PostgreSQL 14+"
)));
}
debug!("Connected to PostgreSQL server version: {}", server_version);
Ok(Self {
conn,
async_fd: None,
copy: CopyMode::Idle,
read_buf: BytesMut::with_capacity(READ_BUF_INITIAL_CAPACITY),
dead: false,
})
}
pub fn exec(&mut self, query: &str) -> Result<PgResult> {
let c_query = CString::new(query)
.map_err(|e| ReplicationError::protocol(format!("Invalid query string: {e}")))?;
let result = unsafe { PQexec(self.conn, c_query.as_ptr()) };
if result.is_null() {
return Err(ReplicationError::protocol(
"Query execution failed - null result".to_string(),
));
}
let pg_result = PgResult::new(result);
let status = pg_result.status();
info!(
"query : {} pg_result.status() : {:?}",
query,
pg_result.status()
);
if !matches!(
status,
ExecStatusType::PGRES_TUPLES_OK
| ExecStatusType::PGRES_COMMAND_OK
| ExecStatusType::PGRES_COPY_BOTH
| ExecStatusType::PGRES_COPY_OUT
) {
let error_msg = pg_result
.error_message()
.unwrap_or_else(|| "Unknown error".to_string());
return Err(ReplicationError::from_sqlstate(
&pg_result.error_sqlstate(),
format!("Query execution failed: {error_msg}"),
));
}
Ok(pg_result)
}
pub fn identify_system(&mut self) -> Result<PgResult> {
debug!("Sending IDENTIFY_SYSTEM command");
let result = self.exec("IDENTIFY_SYSTEM")?;
if result.ntuples() > 0 {
if let (Some(systemid), Some(timeline), Some(xlogpos)) = (
result.get_value(0, 0),
result.get_value(0, 1),
result.get_value(0, 2),
) {
debug!(
"System identification: systemid={}, timeline={}, xlogpos={}",
systemid, timeline, xlogpos
);
}
}
Ok(result)
}
pub fn start_replication(
&mut self,
slot_name: &str,
start_lsn: XLogRecPtr,
options: &[(&str, &str)],
) -> Result<()> {
let sql = crate::sql_builder::build_start_replication_sql(slot_name, start_lsn, options)?;
debug!("Starting replication: {}", sql);
let _result = self.exec(&sql)?;
self.dead = false;
self.initialize_async_socket()?;
self.copy = CopyMode::Replication(CopyStream::default());
debug!("Replication started successfully");
Ok(())
}
pub async fn send_standby_status_update(
&mut self,
received_lsn: XLogRecPtr,
flushed_lsn: XLogRecPtr,
applied_lsn: XLogRecPtr,
reply_requested: bool,
) -> Result<()> {
self.ensure_replication_mode()?;
let reply_data = crate::protocol::build_standby_status_update_message(
received_lsn,
flushed_lsn,
applied_lsn,
reply_requested,
);
self.put_copy_data_and_flush(&reply_data).await?;
info!(
"Sent standby status update: received={}, flushed={}, applied={}, reply_requested={}",
format_lsn(received_lsn),
format_lsn(flushed_lsn),
format_lsn(applied_lsn),
reply_requested
);
Ok(())
}
pub(crate) fn copy_out_begin(&mut self, sql: &str) -> Result<()> {
match self.copy {
CopyMode::Idle => {}
CopyMode::Out(_) => {
return Err(ReplicationError::protocol(
"a COPY OUT stream is already in progress on this connection".to_string(),
))
}
CopyMode::Replication(_) => {
return Err(ReplicationError::protocol(
"cannot start a COPY OUT on a connection that is streaming replication"
.to_string(),
))
}
}
if !self.is_alive() {
return Err(ReplicationError::protocol(
"cannot start a COPY OUT on a connection that is no longer usable".to_string(),
));
}
let result = self.exec(sql)?;
if !matches!(result.status(), ExecStatusType::PGRES_COPY_OUT) {
return Err(ReplicationError::protocol(format!(
"{sql} did not enter COPY OUT mode (status {:?})",
result.status()
)));
}
self.initialize_async_socket()?;
self.copy = CopyMode::Out(CopyStream::default());
Ok(())
}
pub(crate) async fn copy_out_next(
&mut self,
cancellation_token: &CancellationToken,
) -> Result<Option<Bytes>> {
if !self.copy.is_out() {
return Err(ReplicationError::protocol(
"no COPY OUT stream is in progress on this connection".to_string(),
));
}
let result = self.pump_copy(cancellation_token).await;
match &result {
Ok(Some(_)) => {}
Ok(None) => self.copy = CopyMode::Idle,
Err(_) => {
self.copy = CopyMode::Idle;
self.mark_dead();
}
}
if matches!(result, Ok(None)) {
if let Err(e) = self.copy_out_drain_epilogue(cancellation_token).await {
self.mark_dead();
return Err(e);
}
}
result
}
async fn copy_out_drain_epilogue(
&mut self,
cancellation_token: &CancellationToken,
) -> Result<()> {
loop {
while unsafe { PQisBusy(self.conn) } == 0 {
let raw = unsafe { PQgetResult(self.conn) };
if raw.is_null() {
return Ok(());
}
let res = PgResult::new(raw);
match res.status() {
ExecStatusType::PGRES_FATAL_ERROR => {
let message = res
.error_message()
.unwrap_or_else(|| "Unknown error".to_string());
let sqlstate = res.error_sqlstate();
warn!("COPY OUT failed after CopyDone [{sqlstate}]: {message}");
return Err(ReplicationError::from_sqlstate(
&sqlstate,
format!("COPY OUT failed after CopyDone: {message}"),
));
}
ExecStatusType::PGRES_COPY_IN
| ExecStatusType::PGRES_COPY_OUT
| ExecStatusType::PGRES_COPY_BOTH => return Ok(()),
_ => {}
}
}
let async_fd = self
.async_fd
.as_ref()
.ok_or_else(|| ReplicationError::protocol("AsyncFd not initialized".to_string()))?;
tokio::select! {
biased;
_ = cancellation_token.cancelled() => {
return Err(ReplicationError::Cancelled(
"COPY OUT cancelled while draining the epilogue".to_string(),
));
}
guard_result = async_fd.readable() => {
let mut guard = guard_result.map_err(|e| {
ReplicationError::protocol(format!(
"Failed to wait for socket readability: {e}"
))
})?;
if unsafe { PQconsumeInput(self.conn) } == 0 {
let error_msg = self.last_error_message();
return Err(ReplicationError::transient_connection(format!(
"PQconsumeInput failed while draining the COPY epilogue: {error_msg}"
)));
}
guard.clear_ready();
}
}
}
}
pub(crate) fn copy_out_abort(&mut self) {
if self.copy.is_out() {
self.copy = CopyMode::Idle;
self.mark_dead();
}
}
fn initialize_async_socket(&mut self) -> Result<()> {
if self.async_fd.is_some() {
return Ok(());
}
let sock: RawFd = unsafe { PQsocket(self.conn) };
if sock < 0 {
return Err(ReplicationError::protocol(
"Invalid PostgreSQL socket".to_string(),
));
}
let ret = unsafe { PQsetnonblocking(self.conn, 1) };
if ret != 0 {
return Err(ReplicationError::protocol(
"Failed to set non-blocking mode on PostgreSQL connection".to_string(),
));
}
let async_fd = AsyncFd::new(sock)
.map_err(|e| ReplicationError::protocol(format!("Failed to create AsyncFd: {e}")))?;
self.async_fd = Some(async_fd);
Ok(())
}
pub async fn get_copy_data_async(
&mut self,
cancellation_token: &CancellationToken,
) -> Result<Bytes> {
self.ensure_replication_mode()?;
match self.pump_copy(cancellation_token).await? {
Some(payload) => Ok(payload),
None => Err(CopyEnd::Done.to_error()),
}
}
async fn pump_copy(&mut self, cancellation_token: &CancellationToken) -> Result<Option<Bytes>> {
loop {
if let Some(msg) = self.pop_pending() {
return Ok(Some(msg));
}
if let Some(end) = self.copy_end() {
return match end {
CopyEnd::Done => Ok(None),
failed => Err(failed.to_error()),
};
}
match Self::drain_fields(self.conn, &mut self.copy, &mut self.read_buf)? {
DrainResult::Progressed => continue,
DrainResult::WouldBlock => {} }
let async_fd = self
.async_fd
.as_ref()
.ok_or_else(|| ReplicationError::protocol("AsyncFd not initialized".to_string()))?;
tokio::select! {
biased;
_ = cancellation_token.cancelled() => {
return self.handle_cancellation();
}
guard_result = async_fd.readable() => {
let mut guard = guard_result.map_err(|e| {
ReplicationError::protocol(format!("Failed to wait for socket readability: {e}"))
})?;
let consumed = unsafe { PQconsumeInput(self.conn) };
if consumed == 0 {
let error_msg = self.last_error_message();
return Err(ReplicationError::transient_connection(format!(
"PQconsumeInput failed: {error_msg}"
)));
}
match Self::drain_fields(self.conn, &mut self.copy, &mut self.read_buf)? {
DrainResult::Progressed => {
}
DrainResult::WouldBlock => {
guard.clear_ready();
}
}
}
}
}
}
#[inline]
fn drain_fields(
conn: *mut PGconn,
copy: &mut CopyMode,
read_buf: &mut BytesMut,
) -> Result<DrainResult> {
let stream = copy
.stream_mut()
.ok_or_else(|| ReplicationError::protocol("no COPY in progress".to_string()))?;
drain_buffered_messages(conn, &mut stream.pending, read_buf, &mut stream.end)
}
#[inline]
fn pop_pending(&mut self) -> Option<Bytes> {
self.copy.stream_mut()?.pending.pop_front()
}
#[inline]
fn has_pending(&self) -> bool {
match &self.copy {
CopyMode::Idle => false,
CopyMode::Replication(s) | CopyMode::Out(s) => !s.pending.is_empty(),
}
}
#[inline]
fn copy_end(&self) -> Option<&CopyEnd> {
match &self.copy {
CopyMode::Idle => None,
CopyMode::Replication(s) | CopyMode::Out(s) => s.end.as_ref(),
}
}
fn handle_cancellation(&mut self) -> Result<Option<Bytes>> {
debug!("Cancellation detected in get_copy_data_async");
if let Some(msg) = self.pop_pending() {
info!("Found queued data after cancellation, returning it");
return Ok(Some(msg));
}
let drained = if matches!(self.copy, CopyMode::Idle) {
Ok(DrainResult::WouldBlock)
} else {
Self::drain_fields(self.conn, &mut self.copy, &mut self.read_buf)
};
match drained {
Ok(DrainResult::Progressed) => {
if let Some(msg) = self.pop_pending() {
info!("Found buffered data after cancellation, returning it");
return Ok(Some(msg));
}
if let Some(end) = self.copy_end() {
info!("COPY stream ended during cancellation check");
return match end {
CopyEnd::Done => Ok(None),
failed => Err(failed.to_error()),
};
}
}
Ok(DrainResult::WouldBlock) => {
info!("Cancellation token triggered with no buffered data");
}
Err(e) => {
if let Some(msg) = self.pop_pending() {
info!("Delivering data queued before the cancellation drain failed");
return Ok(Some(msg));
}
debug!("Transport failed during cancellation drain, reporting cancel: {e}");
}
}
Err(ReplicationError::Cancelled(
"Operation cancelled".to_string(),
))
}
fn last_error_message(&self) -> String {
unsafe {
let error_ptr = PQerrorMessage(self.conn);
if error_ptr.is_null() {
"Unknown error".to_string()
} else {
CStr::from_ptr(error_ptr).to_string_lossy().into_owned()
}
}
}
#[inline]
fn ensure_replication_mode(&self) -> Result<()> {
if !self.copy.is_replication() {
return Err(ReplicationError::protocol(
"Connection is not in replication mode".to_string(),
));
}
Ok(())
}
async fn put_copy_data_and_flush(&mut self, data: &[u8]) -> Result<()> {
let result = unsafe {
PQputCopyData(
self.conn,
data.as_ptr() as *const std::os::raw::c_char,
data.len() as i32,
)
};
if result != 1 {
let error_msg = self.last_error_message();
return Err(ReplicationError::protocol(format!(
"Failed to send data via COPY protocol: {error_msg}"
)));
}
loop {
let flush_result = unsafe { PQflush(self.conn) };
match flush_result {
0 => return Ok(()),
1 => {
let async_fd = self.async_fd.as_ref().ok_or_else(|| {
ReplicationError::protocol("AsyncFd not initialized".to_string())
})?;
let mut guard = async_fd.writable().await.map_err(|e| {
ReplicationError::protocol(format!(
"Failed to wait for socket writability: {e}"
))
})?;
guard.clear_ready();
}
_ => {
let error_msg = self.last_error_message();
return Err(ReplicationError::protocol(format!(
"Failed to flush connection: {error_msg}"
)));
}
}
}
}
pub(crate) async fn end_copy(&mut self) -> Result<()> {
if !self.copy.is_replication() {
return Ok(());
}
self.copy = CopyMode::Idle;
loop {
let r = unsafe { PQputCopyEnd(self.conn, ptr::null()) };
match r {
1 => break,
0 => {
loop {
let flush = unsafe { PQflush(self.conn) };
match flush {
0 => break,
1 => {
let async_fd = self.async_fd.as_ref().ok_or_else(|| {
ReplicationError::protocol(
"AsyncFd not initialized".to_string(),
)
})?;
let mut guard = async_fd.writable().await.map_err(|e| {
ReplicationError::protocol(format!("wait writable failed: {e}"))
})?;
guard.clear_ready();
}
_ => {
let msg = self.last_error_message();
return Err(ReplicationError::protocol(format!(
"PQflush failed: {msg}"
)));
}
}
}
}
_ => {
let msg = self.last_error_message();
return Err(ReplicationError::protocol(format!(
"PQputCopyEnd failed: {msg}"
)));
}
}
}
loop {
let flush = unsafe { PQflush(self.conn) };
match flush {
0 => break,
1 => {
let async_fd = self.async_fd.as_ref().ok_or_else(|| {
ReplicationError::protocol("AsyncFd not initialized".to_string())
})?;
let mut guard = async_fd.writable().await.map_err(|e| {
ReplicationError::protocol(format!("wait writable failed: {e}"))
})?;
guard.clear_ready();
}
_ => {
let msg = self.last_error_message();
return Err(ReplicationError::protocol(format!("PQflush failed: {msg}")));
}
}
}
Ok(())
}
pub(crate) fn is_alive(&self) -> bool {
if self.conn.is_null() || self.dead {
return false;
}
if self.copy_end().is_some() && !self.has_pending() {
return false;
}
unsafe { PQstatus(self.conn) == ConnStatusType::CONNECTION_OK }
}
pub(crate) fn mark_dead(&mut self) {
self.dead = true;
}
pub fn server_version(&self) -> i32 {
unsafe { PQserverVersion(self.conn) }
}
pub fn create_replication_slot_with_options(
&mut self,
slot_name: &str,
slot_type: SlotType,
output_plugin: Option<&str>,
options: &ReplicationSlotOptions,
) -> Result<PgResult> {
let sql = crate::sql_builder::prepare_create_slot(
self.server_version(),
slot_name,
slot_type,
output_plugin,
options,
)?;
debug!("Creating replication slot: {}", sql);
self.exec(&sql)
}
pub fn alter_replication_slot(
&mut self,
slot_name: &str,
two_phase: Option<bool>,
failover: Option<bool>,
) -> Result<PgResult> {
let alter_slot_sql = crate::sql_builder::prepare_alter_slot(
self.server_version(),
slot_name,
two_phase,
failover,
)?;
debug!("Altering replication slot: {}", alter_slot_sql);
let result = self.exec(&alter_slot_sql)?;
debug!("Replication slot {} altered", slot_name);
Ok(result)
}
pub fn drop_replication_slot(&mut self, slot_name: &str, wait: bool) -> Result<()> {
let sql = crate::sql_builder::build_drop_slot_sql(slot_name, wait)?;
debug!("Dropping replication slot: {}", sql);
let result = self.exec(&sql)?;
if !result.is_ok() {
return Err(ReplicationError::replication_slot(format!(
"Failed to drop replication slot '{}': {}",
slot_name,
result
.error_message()
.unwrap_or_else(|| "unknown error".to_string())
)));
}
debug!("Replication slot {} dropped", slot_name);
Ok(())
}
pub fn read_replication_slot(
&mut self,
slot_name: &str,
) -> Result<crate::types::ReplicationSlotInfo> {
let sql = crate::sql_builder::prepare_read_slot(self.server_version(), slot_name)?;
debug!("Reading replication slot: {}", sql);
let result = self.exec(&sql)?;
if !result.is_ok() {
return Err(ReplicationError::replication_slot(format!(
"Failed to read replication slot '{}': {}",
slot_name,
result
.error_message()
.unwrap_or_else(|| "unknown error".to_string())
)));
}
let slot_type = result.get_value(0, 0);
let restart_lsn = result
.get_value(0, 1)
.and_then(|s| crate::types::parse_lsn(&s).ok())
.map(crate::types::Lsn::new);
let restart_tli = result.get_value(0, 2).and_then(|s| s.parse::<i32>().ok());
Ok(crate::types::ReplicationSlotInfo {
slot_type,
restart_lsn,
restart_tli,
})
}
pub fn start_physical_replication(
&mut self,
slot_name: Option<&str>,
start_lsn: XLogRecPtr,
timeline_id: Option<u32>,
) -> Result<()> {
let sql = crate::sql_builder::build_start_physical_replication_sql(
slot_name,
start_lsn,
timeline_id,
)?;
debug!("Starting physical replication: {}", sql);
let _result = self.exec(&sql)?;
self.initialize_async_socket()?;
self.copy = CopyMode::Replication(CopyStream::default());
debug!("Physical replication started successfully");
Ok(())
}
pub async fn send_hot_standby_feedback(
&mut self,
xmin: u32,
xmin_epoch: u32,
catalog_xmin: u32,
catalog_xmin_epoch: u32,
) -> Result<()> {
self.ensure_replication_mode()?;
let feedback_data =
build_hot_standby_feedback_message(xmin, xmin_epoch, catalog_xmin, catalog_xmin_epoch)?;
self.put_copy_data_and_flush(&feedback_data).await?;
debug!(
"Sent hot standby feedback: xmin={}, catalog_xmin={}",
xmin, catalog_xmin
);
Ok(())
}
pub fn upload_manifest(&mut self, manifest: &[u8]) -> Result<()> {
crate::sql_builder::check_upload_manifest_version(self.server_version())?;
let c_query = CString::new("UPLOAD_MANIFEST")
.map_err(|e| ReplicationError::protocol(format!("Invalid query string: {e}")))?;
let was_nonblocking = unsafe { PQisnonblocking(self.conn) } == 1;
if was_nonblocking && unsafe { PQsetnonblocking(self.conn, 0) } != 0 {
return Err(ReplicationError::protocol(
"Failed to set blocking mode for UPLOAD_MANIFEST".to_string(),
));
}
let restore = |conn| {
if was_nonblocking {
unsafe { PQsetnonblocking(conn, 1) };
}
};
debug!("Uploading backup manifest ({} bytes)", manifest.len());
let res = unsafe { PQexec(self.conn, c_query.as_ptr()) };
if res.is_null() {
restore(self.conn);
return Err(ReplicationError::protocol(
"UPLOAD_MANIFEST failed - null result".to_string(),
));
}
let pg_result = PgResult::new(res);
if !matches!(pg_result.status(), ExecStatusType::PGRES_COPY_IN) {
restore(self.conn);
let error_msg = pg_result
.error_message()
.unwrap_or_else(|| "Unknown error".to_string());
return Err(ReplicationError::from_sqlstate(
&pg_result.error_sqlstate(),
format!("UPLOAD_MANIFEST did not enter CopyIn mode: {error_msg}"),
));
}
drop(pg_result);
for chunk in manifest.chunks(COPY_IN_CHUNK) {
let sent = unsafe {
PQputCopyData(
self.conn,
chunk.as_ptr().cast::<std::os::raw::c_char>(),
chunk.len() as i32,
)
};
if sent != 1 {
let error_msg = self.last_error_message();
restore(self.conn);
return Err(ReplicationError::protocol(format!(
"PQputCopyData failed while uploading manifest: {error_msg}"
)));
}
}
if unsafe { PQputCopyEnd(self.conn, ptr::null()) } != 1 {
let error_msg = self.last_error_message();
restore(self.conn);
return Err(ReplicationError::protocol(format!(
"PQputCopyEnd failed while uploading manifest: {error_msg}"
)));
}
let mut failure = None;
loop {
let raw = unsafe { PQgetResult(self.conn) };
if raw.is_null() {
break;
}
let res = PgResult::new(raw);
if !matches!(res.status(), ExecStatusType::PGRES_COMMAND_OK) && failure.is_none() {
failure = Some(ReplicationError::from_sqlstate(
&res.error_sqlstate(),
format!(
"UPLOAD_MANIFEST failed: {}",
res.error_message()
.unwrap_or_else(|| "Unknown error".to_string())
),
));
}
}
restore(self.conn);
match failure {
Some(e) => Err(e),
None => {
debug!("Backup manifest uploaded");
Ok(())
}
}
}
pub fn base_backup(&mut self, options: &BaseBackupOptions) -> Result<PgResult> {
let base_backup_sql =
crate::sql_builder::prepare_base_backup(self.server_version(), options)?;
debug!("Starting base backup: {}", base_backup_sql);
let result = self.exec(&base_backup_sql)?;
self.initialize_async_socket()?;
self.copy = CopyMode::Replication(CopyStream::default());
debug!("Base backup started successfully");
Ok(result)
}
fn close_replication_connection(&mut self) {
if !self.conn.is_null() {
info!("Closing PostgreSQL replication connection");
if self.copy.is_replication() {
debug!("Ending COPY mode before closing connection");
unsafe {
let result = PQputCopyEnd(self.conn, ptr::null());
if result != 1 {
warn!(
"Failed to end COPY mode gracefully: {}",
self.last_error_message()
);
} else {
debug!("COPY mode ended gracefully");
}
}
self.copy = CopyMode::Idle;
}
self.async_fd = None;
unsafe {
PQfinish(self.conn);
}
self.conn = std::ptr::null_mut();
if let Some(stream) = self.copy.stream_mut() {
stream.pending.clear();
}
info!("PostgreSQL replication connection closed and cleaned up");
} else {
info!("Connection already closed or was never initialized");
}
}
}
impl Drop for PgReplicationConnection {
fn drop(&mut self) {
self.close_replication_connection();
}
}
unsafe impl Send for PgReplicationConnection {}
#[cfg(test)]
impl PgReplicationConnection {
pub(crate) fn null_for_testing() -> Self {
Self {
conn: std::ptr::null_mut(),
async_fd: None,
copy: CopyMode::Idle,
read_buf: BytesMut::new(),
dead: false,
}
}
fn push_pending_message_for_testing(&mut self, msg: Bytes) {
if self.copy.stream_mut().is_none() {
self.copy = CopyMode::Replication(CopyStream::default());
}
self.copy
.stream_mut()
.expect("just set above")
.pending
.push_back(msg);
}
pub(crate) fn null_for_testing_with_frames(frames: Vec<Bytes>) -> Self {
let mut conn = Self::null_for_testing();
conn.copy = CopyMode::Replication(CopyStream::default());
conn.copy
.stream_mut()
.expect("null_for_testing_with_frames sets Replication mode above")
.pending
.extend(frames);
conn
}
}
pub struct PgResult {
result: *mut PGresult,
}
impl PgResult {
fn new(result: *mut PGresult) -> Self {
Self { result }
}
pub fn status(&self) -> ExecStatusType {
unsafe { PQresultStatus(self.result) }
}
pub fn is_ok(&self) -> bool {
matches!(
self.status(),
ExecStatusType::PGRES_TUPLES_OK | ExecStatusType::PGRES_COMMAND_OK
)
}
pub fn ntuples(&self) -> i32 {
unsafe { PQntuples(self.result) }
}
pub fn nfields(&self) -> i32 {
unsafe { PQnfields(self.result) }
}
pub fn get_value(&self, row: i32, col: i32) -> Option<String> {
let bytes = self.get_bytes(row, col)?;
Some(String::from_utf8_lossy(bytes).into_owned())
}
pub fn get_bytes(&self, row: i32, col: i32) -> Option<&[u8]> {
if row < 0 || col < 0 || row >= self.ntuples() || col >= self.nfields() {
return None;
}
if unsafe { PQgetisnull(self.result, row, col) } != 0 {
return None;
}
let ptr = unsafe { PQgetvalue(self.result, row, col) };
if ptr.is_null() {
return None;
}
let len = unsafe { PQgetlength(self.result, row, col) };
if len < 0 {
return None;
}
Some(unsafe { core::slice::from_raw_parts(ptr.cast::<u8>(), len as usize) })
}
pub fn get_bytes_owned(&self, row: i32, col: i32) -> Option<Vec<u8>> {
self.get_bytes(row, col).map(<[u8]>::to_vec)
}
pub fn error_message(&self) -> Option<String> {
let error_ptr = unsafe { PQresultErrorMessage(self.result) };
if error_ptr.is_null() {
None
} else {
unsafe { Some(CStr::from_ptr(error_ptr).to_string_lossy().into_owned()) }
}
}
pub(crate) fn error_sqlstate(&self) -> String {
let ptr = unsafe { PQresultErrorField(self.result, PG_DIAG_SQLSTATE as i32) };
if ptr.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
}
}
}
impl Drop for PgResult {
fn drop(&mut self) {
if !self.result.is_null() {
unsafe {
PQclear(self.result);
}
}
}
}
unsafe impl Send for PgResult {}
fn copy_end_status(conn: *mut PGconn) -> ReadResult {
let mut end = ReadResult::CopyDone;
while unsafe { PQisBusy(conn) } == 0 {
let raw = unsafe { PQgetResult(conn) };
if raw.is_null() {
break;
}
let res = PgResult::new(raw);
match res.status() {
ExecStatusType::PGRES_COPY_IN | ExecStatusType::PGRES_COPY_BOTH => {
debug!("COPY stream ended by server; sending client CopyDone to leave copy mode");
unsafe { PQputCopyEnd(conn, ptr::null()) };
break;
}
ExecStatusType::PGRES_COPY_OUT => break,
ExecStatusType::PGRES_FATAL_ERROR if matches!(end, ReadResult::CopyDone) => {
let message = res
.error_message()
.unwrap_or_else(|| "Unknown error".to_string());
let sqlstate = res.error_sqlstate();
warn!("COPY stream terminated by server [{sqlstate}]: {message}");
end = ReadResult::CopyFailed(sqlstate, message);
}
_ => {}
}
}
if matches!(end, ReadResult::CopyDone) {
debug!("COPY stream finished gracefully (PQgetCopyData returned -1)");
}
end
}
#[inline]
fn try_read_buffered_data_raw(conn: *mut PGconn, read_buf: &mut BytesMut) -> Result<ReadResult> {
let mut buffer: *mut std::os::raw::c_char = ptr::null_mut();
let result = unsafe { PQgetCopyData(conn, &mut buffer, 1) };
match result {
len if len > 0 => {
if buffer.is_null() {
return Err(ReplicationError::buffer(
"Received null buffer from PQgetCopyData".to_string(),
));
}
let len = len as usize;
let src = unsafe { slice::from_raw_parts(buffer as *const u8, len) };
read_buf.reserve(len);
read_buf.put_slice(src);
let data = read_buf.split().freeze();
unsafe { PQfreemem(buffer as *mut c_void) };
Ok(ReadResult::Data(data))
}
0 => Ok(ReadResult::WouldBlock),
-1 => Ok(copy_end_status(conn)),
-2 => {
let error_msg = unsafe {
let error_ptr = PQerrorMessage(conn);
if error_ptr.is_null() {
"Unknown error".to_string()
} else {
CStr::from_ptr(error_ptr).to_string_lossy().into_owned()
}
};
Err(ReplicationError::transient_connection(format!(
"PQgetCopyData error: {error_msg}"
)))
}
other => Err(ReplicationError::protocol(format!(
"Unexpected PQgetCopyData result: {other}"
))),
}
}
#[inline]
fn drain_buffered_messages(
conn: *mut PGconn,
pending_messages: &mut VecDeque<Bytes>,
read_buf: &mut BytesMut,
copy_end: &mut Option<CopyEnd>,
) -> Result<DrainResult> {
let mut drained = false;
for _ in 0..MAX_DRAIN_BATCH {
match try_read_buffered_data_raw(conn, read_buf) {
Ok(ReadResult::Data(data)) => {
pending_messages.push_back(data);
drained = true;
}
Ok(ReadResult::WouldBlock) => break,
Ok(ReadResult::CopyDone) => {
*copy_end = Some(CopyEnd::Done);
break;
}
Ok(ReadResult::CopyFailed(sqlstate, message)) => {
*copy_end = Some(CopyEnd::Failed(sqlstate, message));
break;
}
Err(e) => return Err(e),
}
}
Ok(if drained || copy_end.is_some() {
DrainResult::Progressed
} else {
DrainResult::WouldBlock
})
}
#[cfg(test)]
mod tests {
use super::*;
fn set_copy_end(conn: &mut PgReplicationConnection, end: CopyEnd) {
if conn.copy.stream_mut().is_none() {
conn.copy = CopyMode::Replication(CopyStream::default());
}
conn.copy.stream_mut().expect("just set").end = Some(end);
}
use crate::sql_builder::quote_literal;
fn sanitize_sql_string_value(value: &str) -> String {
let quoted = quote_literal(value).unwrap();
quoted[1..quoted.len() - 1].to_owned()
}
fn quote_sql_string_value(value: &str) -> String {
quote_literal(value).unwrap()
}
#[test]
fn test_sanitize_sql_string_value_no_quotes() {
let input = "test_value";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "test_value");
}
#[test]
fn test_sanitize_sql_string_value_single_quote() {
let input = "test'value";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "test''value");
}
#[test]
fn test_sanitize_sql_string_value_multiple_quotes() {
let input = "test'value'with'quotes";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "test''value''with''quotes");
}
#[test]
fn test_sanitize_sql_string_value_sql_injection_attempt() {
let input = "'; DROP TABLE users; --";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "''; DROP TABLE users; --");
}
#[test]
fn test_sanitize_sql_string_value_empty() {
let input = "";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "");
}
#[test]
fn test_sanitize_sql_string_value_only_quote() {
let input = "'";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "''");
}
#[test]
fn test_sanitize_sql_string_value_consecutive_quotes() {
let input = "''";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "''''");
}
#[test]
fn test_quote_sql_string_value_basic() {
let input = "test_value";
let quoted = quote_sql_string_value(input);
assert_eq!(quoted, "'test_value'");
}
#[test]
fn test_quote_sql_string_value_with_quotes() {
let input = "test'value";
let quoted = quote_sql_string_value(input);
assert_eq!(quoted, "'test''value'");
}
#[test]
fn test_quote_sql_string_value_sql_injection() {
let input = "'; DROP TABLE users; --";
let quoted = quote_sql_string_value(input);
assert_eq!(quoted, "'''; DROP TABLE users; --'");
}
#[test]
fn test_quote_sql_string_value_empty() {
let input = "";
let quoted = quote_sql_string_value(input);
assert_eq!(quoted, "''");
}
#[test]
fn test_sanitize_complex_injection_attempt() {
let input = "value' OR '1'='1";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "value'' OR ''1''=''1");
let quoted = quote_sql_string_value(input);
assert_eq!(quoted, "'value'' OR ''1''=''1'");
}
#[test]
fn test_sanitize_unicode_with_quotes() {
let input = "test'值'测试";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "test''值''测试");
}
#[test]
fn test_sanitize_special_chars_without_quotes() {
let input = "test;value--comment/**/";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "test;value--comment/**/");
}
#[test]
fn test_quote_backslash_and_quote() {
assert_eq!(
quote_sql_string_value("test\\'value"),
r#" E'test\\''value'"#
);
}
#[test]
fn test_sanitize_newlines_and_quotes() {
let input = "line1'quote\nline2'quote";
let sanitized = sanitize_sql_string_value(input);
assert_eq!(sanitized, "line1''quote\nline2''quote");
}
#[test]
fn test_build_sql_options_empty() {
let options: Vec<String> = vec![];
let result = crate::sql_builder::build_sql_options(&options);
assert_eq!(result, "");
}
#[test]
fn test_build_sql_options_single() {
let options = vec!["proto_version '2'".to_string()];
let result = crate::sql_builder::build_sql_options(&options);
assert_eq!(result, " (proto_version '2')");
}
#[test]
fn test_build_sql_options_multiple() {
let options = vec![
"proto_version '2'".to_string(),
"publication_names '\"my_pub\"'".to_string(),
"streaming 'on'".to_string(),
];
let result = crate::sql_builder::build_sql_options(&options);
assert_eq!(
result,
" (proto_version '2', publication_names '\"my_pub\"', streaming 'on')"
);
}
#[test]
fn test_ensure_replication_mode_fails_when_not_replication() {
let conn = PgReplicationConnection::null_for_testing();
let err = conn.ensure_replication_mode().unwrap_err();
assert!(
err.to_string().contains("not in replication mode"),
"Expected replication mode error, got: {err}"
);
}
#[test]
fn test_is_alive_returns_false_for_null_conn() {
let conn = PgReplicationConnection::null_for_testing();
assert!(!conn.is_alive());
}
#[test]
fn test_close_replication_connection_null_conn() {
let mut conn = PgReplicationConnection::null_for_testing();
conn.close_replication_connection(); assert!(conn.conn.is_null());
}
#[tokio::test]
async fn end_copy_noop_when_not_replication_conn() {
let mut conn = PgReplicationConnection::null_for_testing();
assert!(conn.end_copy().await.is_ok());
}
#[test]
fn test_drop_null_conn_does_not_panic() {
let conn = PgReplicationConnection::null_for_testing();
drop(conn); }
#[test]
fn test_read_result_data_variant_with_bytes() {
use bytes::Bytes;
let data = Bytes::from(vec![1u8, 2, 3, 4, 5]);
let result = ReadResult::Data(data.clone());
match result {
ReadResult::Data(b) => {
assert_eq!(b.len(), 5);
assert_eq!(b[0], 1);
assert_eq!(b[4], 5);
assert_eq!(b, data);
}
_ => panic!("Expected ReadResult::Data"),
}
}
#[test]
fn test_read_result_data_bytes_zero_copy_slice() {
use bytes::Bytes;
let original = Bytes::from(vec![10u8, 20, 30, 40, 50, 60, 70, 80]);
let result = ReadResult::Data(original.clone());
match result {
ReadResult::Data(b) => {
let slice = b.slice(2..6);
assert_eq!(slice, Bytes::from_static(&[30, 40, 50, 60]));
assert_eq!(b.len(), 8);
}
_ => panic!("Expected ReadResult::Data"),
}
}
#[test]
fn test_read_result_data_empty_bytes() {
use bytes::Bytes;
let result = ReadResult::Data(Bytes::new());
match result {
ReadResult::Data(b) => {
assert!(b.is_empty());
assert_eq!(b.len(), 0);
}
_ => panic!("Expected ReadResult::Data"),
}
}
#[test]
fn test_read_result_would_block_variant() {
let result = ReadResult::WouldBlock;
assert!(matches!(result, ReadResult::WouldBlock));
}
#[test]
fn test_read_result_copy_done_variant() {
let result = ReadResult::CopyDone;
assert!(matches!(result, ReadResult::CopyDone));
}
#[test]
fn test_read_result_data_bytes_copy_from_slice() {
use bytes::Bytes;
let raw_data: Vec<u8> = (0..100).collect();
let bytes = Bytes::copy_from_slice(&raw_data);
let result = ReadResult::Data(bytes);
match result {
ReadResult::Data(b) => {
assert_eq!(b.len(), 100);
for (i, &byte) in b.iter().enumerate() {
assert_eq!(byte, i as u8);
}
}
_ => panic!("Expected ReadResult::Data"),
}
}
#[test]
fn test_read_result_data_large_payload() {
use bytes::Bytes;
let raw_data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
let bytes = Bytes::copy_from_slice(&raw_data);
let result = ReadResult::Data(bytes.clone());
match result {
ReadResult::Data(b) => {
assert_eq!(b.len(), 4096);
let header = b.slice(0..25);
assert_eq!(header.len(), 25);
let payload = b.slice(25..);
assert_eq!(payload.len(), 4096 - 25);
}
_ => panic!("Expected ReadResult::Data"),
}
}
#[test]
fn test_read_result_debug_format() {
use bytes::Bytes;
let result = ReadResult::Data(Bytes::from_static(b"test"));
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("Data"));
let result = ReadResult::WouldBlock;
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("WouldBlock"));
let result = ReadResult::CopyDone;
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("CopyDone"));
}
#[test]
fn test_get_copy_data_async_return_type_is_bytes() {
fn _assert_return_type<'a>(
conn: &'a mut PgReplicationConnection,
token: &'a CancellationToken,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = crate::error::Result<bytes::Bytes>> + 'a>,
> {
Box::pin(conn.get_copy_data_async(token))
}
}
#[test]
fn status_enum_abi_values_match_libpq() {
assert_eq!(ConnStatusType::CONNECTION_OK as i32, 0);
assert_eq!(ExecStatusType::PGRES_COMMAND_OK as i32, 1);
assert_eq!(ExecStatusType::PGRES_TUPLES_OK as i32, 2);
assert_eq!(ExecStatusType::PGRES_COPY_OUT as i32, 3);
assert_eq!(ExecStatusType::PGRES_COPY_BOTH as i32, 8);
}
#[test]
fn test_drain_result_variants() {
assert_eq!(DrainResult::Progressed, DrainResult::Progressed);
assert_ne!(DrainResult::Progressed, DrainResult::WouldBlock);
}
#[test]
fn test_drain_result_debug_format() {
assert!(format!("{:?}", DrainResult::Progressed).contains("Progressed"));
assert!(format!("{:?}", DrainResult::WouldBlock).contains("WouldBlock"));
}
#[test]
fn test_copy_end_to_error() {
let done = CopyEnd::Done.to_error();
assert!(
matches!(done, ReplicationError::TransientConnection(_)),
"server-side stream end must not look like user cancellation: {done:?}"
);
assert!(!done.is_cancelled());
assert!(done.is_transient());
let invalidated =
CopyEnd::Failed("55000".to_string(), "can no longer access slot".to_string())
.to_error();
assert!(matches!(invalidated, ReplicationError::ReplicationSlot(_)));
assert!(invalidated.is_permanent());
assert!(invalidated.to_string().contains("55000"), "{invalidated}");
let other = CopyEnd::Failed("57P01".to_string(), "terminating".to_string()).to_error();
assert!(
matches!(other, ReplicationError::TransientConnection(_)),
"{other:?}"
);
assert!(!other.is_permanent());
assert!(other.is_transient());
}
#[test]
fn copy_end_status_terminates() {
assert!(matches!(
copy_end_status(std::ptr::null_mut()),
ReadResult::CopyDone
));
}
#[test]
fn mark_dead_forces_is_alive_false() {
let mut conn =
PgReplicationConnection::null_for_testing_with_frames(vec![Bytes::from_static(
b"frame",
)]);
conn.conn = std::ptr::null_mut();
conn.mark_dead();
assert!(!conn.is_alive());
assert!(conn.dead);
}
#[test]
fn drain_throws_transport_failure_instead_of_reporting_would_block() {
let mut pending = VecDeque::new();
let mut read_buf = BytesMut::new();
let mut copy_end = None;
let result = drain_buffered_messages(
std::ptr::null_mut(),
&mut pending,
&mut read_buf,
&mut copy_end,
);
let err = result.expect_err("a -2 transport failure must propagate as Err");
assert!(
matches!(err, ReplicationError::TransientConnection(_)),
"must classify transient so the stream reconnects: {err:?}"
);
assert!(err.to_string().contains("PQgetCopyData error"), "{err}");
assert!(
copy_end.is_none(),
"the failure is thrown, not latched into CopyEnd"
);
}
#[test]
fn drain_error_preserves_already_queued_messages() {
let mut pending = VecDeque::new();
pending.push_back(Bytes::from_static(b"received before the failure"));
let mut read_buf = BytesMut::new();
let mut copy_end = None;
let result = drain_buffered_messages(
std::ptr::null_mut(),
&mut pending,
&mut read_buf,
&mut copy_end,
);
assert!(result.is_err(), "a null conn must fail the drain");
assert_eq!(
pending.len(),
1,
"a transport failure must not discard already-received frames"
);
assert_eq!(&pending[0][..], b"received before the failure");
}
#[test]
fn test_latched_copy_end_surfaces_only_after_the_queue_drains() {
let mut conn = PgReplicationConnection::null_for_testing();
conn.push_pending_message_for_testing(Bytes::from_static(b"first"));
conn.push_pending_message_for_testing(Bytes::from_static(b"second"));
set_copy_end(&mut conn, CopyEnd::Done);
assert_eq!(conn.handle_cancellation().unwrap().unwrap(), &b"first"[..]);
assert_eq!(conn.handle_cancellation().unwrap().unwrap(), &b"second"[..]);
}
fn block_on<F: std::future::Future>(fut: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(fut)
}
#[test]
fn test_latched_copy_done_is_a_clean_terminal_for_copy_out() {
let mut conn = PgReplicationConnection::null_for_testing();
set_copy_end(&mut conn, CopyEnd::Done);
assert!(
block_on(conn.pump_copy(&CancellationToken::new()))
.unwrap()
.is_none(),
"a clean CopyDone must not surface as an error"
);
}
#[test]
fn test_latched_copy_done_is_still_an_error_for_replication() {
let mut conn = PgReplicationConnection::null_for_testing();
conn.copy = CopyMode::Replication(CopyStream::default());
set_copy_end(&mut conn, CopyEnd::Done);
let err = block_on(conn.get_copy_data_async(&CancellationToken::new())).unwrap_err();
assert!(format!("{err}").contains("CopyDone"), "{err}");
}
#[test]
fn test_latched_copy_failure_still_errors() {
let mut conn = PgReplicationConnection::null_for_testing();
set_copy_end(
&mut conn,
CopyEnd::Failed("57P01".to_string(), "gone".to_string()),
);
let err = block_on(conn.pump_copy(&CancellationToken::new())).unwrap_err();
assert!(err.is_transient(), "{err:?}");
}
#[test]
fn test_pump_copy_drains_queue_before_the_latch() {
let mut conn = PgReplicationConnection::null_for_testing();
conn.push_pending_message_for_testing(Bytes::from_static(b"first"));
set_copy_end(&mut conn, CopyEnd::Done);
let token = CancellationToken::new();
assert_eq!(
&block_on(conn.pump_copy(&token)).unwrap().unwrap()[..],
b"first"
);
assert!(block_on(conn.pump_copy(&token)).unwrap().is_none());
}
#[test]
fn test_copy_out_next_without_begin_errors() {
let mut conn = PgReplicationConnection::null_for_testing();
let err = block_on(conn.copy_out_next(&CancellationToken::new())).unwrap_err();
assert!(format!("{err}").contains("no COPY OUT stream"), "{err}");
}
#[test]
fn test_copy_out_begin_rejects_a_replication_connection() {
let mut conn = PgReplicationConnection::null_for_testing();
conn.copy = CopyMode::Replication(CopyStream::default());
let err = conn.copy_out_begin("COPY t TO STDOUT").unwrap_err();
assert!(
format!("{err}").contains("streaming replication"),
"expected the replication-mode rejection, got: {err}"
);
assert!(
conn.copy.is_replication(),
"a rejected COPY OUT must leave replication mode intact"
);
}
#[test]
fn test_initialize_async_socket_is_idempotent() {
use std::os::fd::AsRawFd;
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let sock = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let mut conn = PgReplicationConnection::null_for_testing();
conn.async_fd = Some(
tokio::io::unix::AsyncFd::new(sock.as_raw_fd())
.expect("first registration succeeds"),
);
conn.initialize_async_socket()
.expect("a second call must be a no-op, not EEXIST");
assert!(conn.async_fd.is_some());
});
}
#[test]
fn test_copy_out_abort_marks_dead_and_is_idempotent() {
let mut conn = PgReplicationConnection::null_for_testing();
conn.copy = CopyMode::Out(CopyStream::default());
conn.copy_out_abort();
assert!(!conn.is_alive());
assert!(!conn.copy.is_out());
conn.copy_out_abort(); }
#[test]
fn test_copy_end_latch_is_sticky() {
let end = CopyEnd::Failed("55000".to_string(), "gone".to_string());
assert_eq!(end.to_error().to_string(), end.to_error().to_string());
}
#[test]
fn test_handle_cancellation_returns_queued_message() {
let mut conn = PgReplicationConnection::null_for_testing();
let msg = Bytes::from_static(b"queued message");
conn.push_pending_message_for_testing(msg.clone());
let result = conn.handle_cancellation();
assert!(result.is_ok());
assert_eq!(result.unwrap().unwrap(), msg);
}
#[test]
fn test_handle_cancellation_returns_first_queued_message() {
let mut conn = PgReplicationConnection::null_for_testing();
let msg1 = Bytes::from_static(b"first");
let msg2 = Bytes::from_static(b"second");
conn.push_pending_message_for_testing(msg1.clone());
conn.push_pending_message_for_testing(msg2.clone());
let result = conn.handle_cancellation();
assert!(result.is_ok());
assert_eq!(result.unwrap().unwrap(), msg1);
assert_eq!(
conn.copy
.stream_mut()
.expect("a COPY is in flight")
.pending
.len(),
1
);
}
#[test]
fn test_handle_cancellation_returns_cancelled_when_empty() {
let mut conn = PgReplicationConnection::null_for_testing();
let result = conn.handle_cancellation();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("cancelled")
|| err.to_string().contains("Cancelled")
|| err.to_string().contains("Operation cancelled"),
"Expected cancellation error, got: {err}"
);
}
#[test]
fn test_alter_replication_slot_rejects_null_byte() {
let mut conn = PgReplicationConnection::null_for_testing();
let result = conn.alter_replication_slot("slot\0x", Some(true), None);
let err = result.err().expect("expected error");
assert!(err.to_string().contains("null bytes"));
}
#[test]
fn test_drop_replication_slot_rejects_null_byte() {
let mut conn = PgReplicationConnection::null_for_testing();
let err = conn.drop_replication_slot("slot\0x", false).unwrap_err();
assert!(err.to_string().contains("null bytes"));
}
#[test]
fn test_read_replication_slot_rejects_null_byte() {
let mut conn = PgReplicationConnection::null_for_testing();
let err = conn.read_replication_slot("slot\0x").unwrap_err();
assert!(err.to_string().contains("null bytes"));
}
#[test]
fn test_start_physical_replication_rejects_null_byte() {
let mut conn = PgReplicationConnection::null_for_testing();
let err = conn
.start_physical_replication(Some("slot\0x"), 0, None)
.unwrap_err();
assert!(err.to_string().contains("null bytes"));
}
#[test]
fn test_base_backup_rejects_null_byte_in_label() {
let mut conn = PgReplicationConnection::null_for_testing();
let opts = BaseBackupOptions {
label: Some("label\0x".to_string()),
..Default::default()
};
let result = conn.base_backup(&opts);
let err = result.err().expect("expected error");
assert!(err.to_string().contains("null bytes"));
}
#[test]
fn test_start_replication_rejects_null_byte() {
let mut conn = PgReplicationConnection::null_for_testing();
let err = conn
.start_replication("slot\0x", 0, &[("proto_version", "1")])
.unwrap_err();
assert!(err.to_string().contains("null bytes"));
}
#[test]
fn test_create_replication_slot_rejects_null_byte() {
let mut conn = PgReplicationConnection::null_for_testing();
let opts = ReplicationSlotOptions::default();
let result = conn.create_replication_slot_with_options(
"slot\0x",
SlotType::Logical,
Some("pgoutput"),
&opts,
);
let err = result.err().expect("expected error");
assert!(err.to_string().contains("null bytes"));
}
fn make_bytea_result(cells: &[Option<&[u8]>]) -> PgResult {
use pq_sys::{
ExecStatusType, PGresAttDesc, PQmakeEmptyPGresult, PQsetResultAttrs, PQsetvalue,
};
use std::os::raw::{c_char, c_int};
let names: Vec<CString> = (0..cells.len())
.map(|i| CString::new(format!("c{i}")).unwrap())
.collect();
let mut attrs: Vec<PGresAttDesc> = names
.iter()
.map(|name| PGresAttDesc {
name: name.as_ptr() as *mut c_char,
tableid: 0,
columnid: 0,
format: 1, typid: 17, typlen: -1,
atttypmod: -1,
})
.collect();
unsafe {
let res = PQmakeEmptyPGresult(std::ptr::null_mut(), ExecStatusType::PGRES_TUPLES_OK);
assert!(!res.is_null(), "PQmakeEmptyPGresult returned null");
assert_ne!(
PQsetResultAttrs(res, cells.len() as c_int, attrs.as_mut_ptr()),
0,
"PQsetResultAttrs failed"
);
for (col, cell) in cells.iter().enumerate() {
let rc = match cell {
None => PQsetvalue(res, 0, col as c_int, std::ptr::null_mut(), -1),
Some(bytes) => PQsetvalue(
res,
0,
col as c_int,
bytes.as_ptr() as *mut c_char,
bytes.len() as c_int,
),
};
assert_ne!(rc, 0, "PQsetvalue failed for col {col}");
}
PgResult::new(res)
}
}
#[test]
fn get_bytes_reads_ascii_payload() {
let res = make_bytea_result(&[Some(b"hello")]);
assert_eq!(res.get_bytes(0, 0), Some(&b"hello"[..]));
}
#[test]
fn get_bytes_preserves_non_utf8() {
let res = make_bytea_result(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
assert_eq!(res.get_bytes(0, 0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
}
#[test]
fn get_bytes_preserves_embedded_nul() {
let res = make_bytea_result(&[Some(&[0x00, 0x01, 0x00, 0x02])]);
assert_eq!(res.get_bytes(0, 0), Some(&[0x00, 0x01, 0x00, 0x02][..]));
}
#[test]
fn get_bytes_distinguishes_null_from_empty() {
let res = make_bytea_result(&[None, Some(&[])]);
assert_eq!(res.get_bytes(0, 0), None, "SQL NULL must be None");
assert_eq!(
res.get_bytes(0, 1),
Some(&[][..]),
"empty bytea must be Some(&[])"
);
}
#[test]
fn get_bytes_out_of_range_and_negative_return_none() {
let res = make_bytea_result(&[Some(b"x")]);
assert_eq!(res.ntuples(), 1);
assert_eq!(res.nfields(), 1);
assert_eq!(res.get_bytes(1, 0), None, "row past end");
assert_eq!(res.get_bytes(0, 1), None, "col past end");
assert_eq!(res.get_bytes(-1, 0), None, "negative row");
assert_eq!(res.get_bytes(0, -1), None, "negative col");
}
#[test]
fn get_bytes_owned_matches_borrowed() {
let res = make_bytea_result(&[Some(&[0x00, 0xFF, 0x10])]);
let borrowed = res.get_bytes(0, 0).map(<[u8]>::to_vec);
let owned = res.get_bytes_owned(0, 0);
assert_eq!(owned, Some(vec![0x00, 0xFF, 0x10]));
assert_eq!(owned, borrowed);
let res_null = make_bytea_result(&[None]);
assert_eq!(res_null.get_bytes_owned(0, 0), None);
}
#[test]
fn get_value_decodes_utf8_and_handles_null() {
let res = make_bytea_result(&[Some(b"hello"), None]);
assert_eq!(res.get_value(0, 0), Some("hello".to_string()));
assert_eq!(res.get_value(0, 1), None);
assert_eq!(res.get_value(-1, 0), None);
}
#[test]
fn get_value_is_lossy_for_non_utf8() {
let res = make_bytea_result(&[Some(&[0xFF, 0x00, 0xFE])]);
let s = res.get_value(0, 0).expect("non-null");
assert_ne!(s.as_bytes(), &[0xFF, 0x00, 0xFE][..]);
}
}