use crate::buffer::BufferWriter;
use crate::error::{ReplicationError, Result};
use crate::protocol::build_hot_standby_feedback_message;
use crate::types::{
format_lsn, system_time_to_postgres_timestamp, 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::time::SystemTime;
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,
}
#[derive(Debug, PartialEq)]
enum DrainResult {
Drained,
WouldBlock,
CopyDone,
}
const MAX_DRAIN_BATCH: usize = 4096;
const READ_BUF_INITIAL_CAPACITY: usize = 256 * 1024;
pub struct PgReplicationConnection {
conn: *mut PGconn,
is_replication_conn: bool,
async_fd: Option<AsyncFd<RawFd>>,
pending_messages: VecDeque<Bytes>,
read_buf: BytesMut,
}
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,
is_replication_conn: false,
async_fd: None,
pending_messages: VecDeque::with_capacity(MAX_DRAIN_BATCH),
read_buf: BytesMut::with_capacity(READ_BUF_INITIAL_CAPACITY),
})
}
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::protocol(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.initialize_async_socket()?;
self.is_replication_conn = true;
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 timestamp = system_time_to_postgres_timestamp(SystemTime::now());
let mut buffer = BufferWriter::with_capacity(34);
buffer.write_u8(b'r'); buffer.write_u64(received_lsn);
buffer.write_u64(flushed_lsn);
buffer.write_u64(applied_lsn);
buffer.write_i64(timestamp);
buffer.write_u8(if reply_requested { 1 } else { 0 });
let reply_data = buffer.freeze();
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(())
}
fn initialize_async_socket(&mut self) -> Result<()> {
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()?;
loop {
if let Some(msg) = self.pending_messages.pop_front() {
return Ok(msg);
}
match drain_buffered_messages(self.conn, &mut self.pending_messages, &mut self.read_buf)
{
DrainResult::Drained => continue, DrainResult::CopyDone => {
debug!("COPY stream ended gracefully");
return Err(ReplicationError::Cancelled("COPY stream ended".to_string()));
}
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::protocol(format!(
"PQconsumeInput failed: {error_msg}"
)));
}
match drain_buffered_messages(self.conn, &mut self.pending_messages, &mut self.read_buf) {
DrainResult::Drained => {
}
DrainResult::CopyDone => {
debug!("COPY stream ended after consuming input");
return Err(ReplicationError::Cancelled(
"COPY stream ended".to_string(),
));
}
DrainResult::WouldBlock => {
guard.clear_ready();
}
}
}
}
}
}
fn handle_cancellation(&mut self) -> Result<Bytes> {
debug!("Cancellation detected in get_copy_data_async");
if let Some(msg) = self.pending_messages.pop_front() {
info!("Found queued data after cancellation, returning it");
return Ok(msg);
}
match drain_buffered_messages(self.conn, &mut self.pending_messages, &mut self.read_buf) {
DrainResult::Drained => {
if let Some(msg) = self.pending_messages.pop_front() {
info!("Found buffered data after cancellation, returning it");
return Ok(msg);
}
}
DrainResult::CopyDone => {
info!("COPY stream ended during cancellation check");
return Err(ReplicationError::Cancelled("COPY stream ended".to_string()));
}
DrainResult::WouldBlock => {
info!("Cancellation token triggered with no buffered data");
}
}
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.is_replication_conn {
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.is_replication_conn {
return Ok(());
}
self.is_replication_conn = false;
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 fn is_alive(&self) -> bool {
if self.conn.is_null() {
return false;
}
unsafe { PQstatus(self.conn) == ConnStatusType::CONNECTION_OK }
}
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> {
crate::sql_builder::check_create_slot_version(self.server_version(), slot_type, options)?;
let sql = crate::sql_builder::build_create_slot_sql(
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> {
crate::sql_builder::check_alter_slot_version(self.server_version(), two_phase)?;
let alter_slot_sql =
crate::sql_builder::build_alter_slot_sql(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> {
crate::sql_builder::check_read_slot_version(self.server_version())?;
let sql = crate::sql_builder::build_read_slot_sql(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.is_replication_conn = true;
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 base_backup(&mut self, options: &BaseBackupOptions) -> Result<PgResult> {
crate::sql_builder::check_base_backup_version(self.server_version(), options)?;
let base_backup_sql = crate::sql_builder::build_base_backup_sql(options)?;
debug!("Starting base backup: {}", base_backup_sql);
let result = self.exec(&base_backup_sql)?;
self.initialize_async_socket()?;
self.is_replication_conn = true;
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.is_replication_conn {
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.is_replication_conn = false;
}
unsafe {
PQfinish(self.conn);
}
self.conn = std::ptr::null_mut();
self.async_fd = None;
self.pending_messages.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(),
is_replication_conn: false,
async_fd: None,
pending_messages: VecDeque::new(),
read_buf: BytesMut::new(),
}
}
fn push_pending_message_for_testing(&mut self, msg: Bytes) {
self.pending_messages.push_back(msg);
}
}
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()) }
}
}
}
impl Drop for PgResult {
fn drop(&mut self) {
if !self.result.is_null() {
unsafe {
PQclear(self.result);
}
}
}
}
unsafe impl Send for PgResult {}
#[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 => {
debug!("COPY stream finished (PQgetCopyData returned -1)");
Ok(ReadResult::CopyDone)
}
-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::protocol(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,
) -> 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) => return DrainResult::CopyDone,
Err(_) => break, }
}
if drained {
DrainResult::Drained
} else {
DrainResult::WouldBlock
}
}
#[cfg(test)]
mod tests {
use super::*;
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_drained_variant() {
let result = DrainResult::Drained;
assert_eq!(result, DrainResult::Drained);
assert_ne!(result, DrainResult::WouldBlock);
assert_ne!(result, DrainResult::CopyDone);
}
#[test]
fn test_drain_result_would_block_variant() {
let result = DrainResult::WouldBlock;
assert_eq!(result, DrainResult::WouldBlock);
assert_ne!(result, DrainResult::Drained);
}
#[test]
fn test_drain_result_copy_done_variant() {
let result = DrainResult::CopyDone;
assert_eq!(result, DrainResult::CopyDone);
assert_ne!(result, DrainResult::Drained);
}
#[test]
fn test_drain_result_debug_format() {
let drained = format!("{:?}", DrainResult::Drained);
assert!(drained.contains("Drained"));
let would_block = format!("{:?}", DrainResult::WouldBlock);
assert!(would_block.contains("WouldBlock"));
let copy_done = format!("{:?}", DrainResult::CopyDone);
assert!(copy_done.contains("CopyDone"));
}
#[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(), 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(), msg1);
assert_eq!(conn.pending_messages.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][..]);
}
}