#![cfg(any(feature = "libpq", feature = "rustls-tls"))]
use pg_walstream::{PgReplicationConnection, ReplicationError};
fn replication_conn_string() -> String {
std::env::var("DATABASE_URL").unwrap_or_else(|_| {
"postgresql://postgres:postgres@localhost:5432/test_walstream?replication=database"
.to_string()
})
}
fn regular_conn_string() -> String {
std::env::var("DATABASE_URL_REGULAR").unwrap_or_else(|_| {
let repl = replication_conn_string();
repl.replace("?replication=database", "")
.replace("&replication=database", "")
})
}
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.try_init();
}
fn drop_slot(slot_name: &str) {
if let Ok(mut conn) = PgReplicationConnection::connect(&replication_conn_string()) {
let _ = conn.exec(&format!(
"SELECT pg_drop_replication_slot('{slot_name}') \
WHERE EXISTS (SELECT 1 FROM pg_replication_slots WHERE slot_name = '{slot_name}')"
));
}
}
#[test]
#[ignore = "requires live PostgreSQL with wal_level=logical"]
fn missing_slot_is_a_permanent_replication_slot_error() {
init_tracing();
let slot = "it_errcls_absent_slot";
drop_slot(slot);
let mut repl = PgReplicationConnection::connect(&replication_conn_string())
.expect("replication connection");
let err = repl
.start_replication(
slot,
0,
&[("proto_version", "2"), ("publication_names", "p")],
)
.expect_err("START_REPLICATION on an absent slot must fail");
assert!(
matches!(err, ReplicationError::ReplicationSlot(_)),
"expected ReplicationSlot, got {err:?}"
);
assert!(
err.is_permanent(),
"a missing slot must not be retried: {err}"
);
assert!(
err.to_string().contains("42704"),
"the SQLSTATE must reach the caller: {err}"
);
}
#[test]
#[ignore = "requires live PostgreSQL with wal_level=logical"]
fn wrong_slot_type_is_a_permanent_replication_slot_error() {
init_tracing();
let slot = "it_errcls_physical_slot";
drop_slot(slot);
struct SlotGuard<'a>(&'a str);
impl Drop for SlotGuard<'_> {
fn drop(&mut self) {
drop_slot(self.0);
}
}
let _guard = SlotGuard(slot);
let mut regular =
PgReplicationConnection::connect(®ular_conn_string()).expect("regular connection");
regular
.exec(&format!(
"SELECT pg_create_physical_replication_slot('{slot}')"
))
.expect("create physical slot");
let mut repl = PgReplicationConnection::connect(&replication_conn_string())
.expect("replication connection");
let err = repl
.start_replication(
slot,
0,
&[("proto_version", "2"), ("publication_names", "p")],
)
.expect_err("logical replication on a physical slot must fail");
assert!(
matches!(err, ReplicationError::ReplicationSlot(_)),
"expected ReplicationSlot, got {err:?}"
);
assert!(err.is_permanent(), "55000 must be permanent: {err}");
assert!(
err.to_string().contains("55000"),
"the SQLSTATE must reach the caller: {err}"
);
}
#[test]
#[ignore = "requires live PostgreSQL"]
fn ordinary_sql_error_stays_retryable() {
init_tracing();
let mut regular =
PgReplicationConnection::connect(®ular_conn_string()).expect("regular connection");
let err = match regular.exec("SELECT * FROM it_errcls_table_that_does_not_exist") {
Ok(_) => panic!("querying a missing table must fail"),
Err(e) => e,
};
assert!(
matches!(err, ReplicationError::Protocol(_)),
"expected Protocol, got {err:?}"
);
assert!(
!err.is_permanent(),
"an ordinary SQL error must stay retryable: {err}"
);
assert!(
err.to_string().contains("42P01"),
"the SQLSTATE must reach the caller: {err}"
);
}