#![cfg(any(feature = "libpq", feature = "rustls-tls"))]
use pg_walstream::{
EventType, LogicalReplicationStream, PgReplicationConnection, ReplicationSlotOptions,
ReplicationStreamConfig, RetryConfig, SlotType, StreamingMode,
};
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use tracing::warn;
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", "")
.replace("?replication=database", "")
})
}
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.try_init();
}
fn server_version_num(conn: &mut PgReplicationConnection) -> i64 {
conn.exec("SHOW server_version_num")
.expect("SHOW server_version_num")
.get_value(0, 0)
.expect("server_version_num present")
.parse()
.expect("server_version_num numeric")
}
fn drop_slot(slot: &str) {
if let Ok(mut conn) = PgReplicationConnection::connect(&replication_conn_string()) {
let _ = conn.exec(&format!(
"SELECT pg_drop_replication_slot('{slot}') WHERE EXISTS \
(SELECT 1 FROM pg_replication_slots WHERE slot_name = '{slot}')"
));
}
}
struct SlotGuard(&'static str);
impl Drop for SlotGuard {
fn drop(&mut self) {
drop_slot(self.0);
}
}
fn core_cfg(slot: &str) -> ReplicationStreamConfig {
ReplicationStreamConfig::new(
slot.to_string(),
"vp_pub".to_string(),
2,
StreamingMode::Off,
Duration::from_secs(10),
Duration::from_secs(30),
Duration::from_secs(60),
RetryConfig::default(),
)
}
#[tokio::test]
#[ignore = "requires live PostgreSQL with wal_level=logical"]
async fn core_cdc_path_streams_and_decodes_insert() {
init_tracing();
let slot = "vp_it_core_slot";
drop_slot(slot);
let _guard = SlotGuard(slot);
let mut regular =
PgReplicationConnection::connect(®ular_conn_string()).expect("regular connection");
let _ = regular.exec(
"CREATE TABLE IF NOT EXISTS vp_events (id SERIAL PRIMARY KEY, payload TEXT NOT NULL)",
);
let _ = regular.exec("TRUNCATE vp_events RESTART IDENTITY");
let _ = regular.exec("DROP PUBLICATION IF EXISTS vp_pub");
regular
.exec("CREATE PUBLICATION vp_pub FOR TABLE vp_events")
.expect("create publication");
let mut stream = LogicalReplicationStream::new(&replication_conn_string(), core_cfg(slot))
.await
.expect("replication stream");
stream.start(None).await.expect("start");
regular
.exec("INSERT INTO vp_events (payload) VALUES ('hello')")
.expect("insert");
let cancel = CancellationToken::new();
let decoded = tokio::time::timeout(Duration::from_secs(60), async {
loop {
let event = stream.next_event(&cancel).await.expect("stream error");
if let EventType::Insert { table, data, .. } = &event.event_type {
assert_eq!(&**table, "vp_events", "insert on the wrong table");
assert!(
data.get("payload").map(|v| *v == "hello").unwrap_or(false),
"payload column must decode to 'hello'"
);
break;
}
}
})
.await;
assert!(decoded.is_ok(), "timed out waiting for the INSERT event");
drop(stream);
tokio::time::sleep(Duration::from_millis(200)).await;
let _ = regular.exec("DROP PUBLICATION IF EXISTS vp_pub");
let _ = regular.exec("DROP TABLE IF EXISTS vp_events");
}
#[test]
#[ignore = "requires live PostgreSQL 17+ with wal_level=logical"]
fn gated_slot_ops_pass_preflight_and_execute_on_pg17plus() {
init_tracing();
let slot = "vp_it_gated_slot";
drop_slot(slot);
let _guard = SlotGuard(slot);
let mut regular =
PgReplicationConnection::connect(®ular_conn_string()).expect("regular connection");
let version = server_version_num(&mut regular);
if version < 170000 {
warn!("skipping gated-ops test: server_version_num {version} < 170000 (FAILOVER is PG17+)");
return;
}
let mut repl =
PgReplicationConnection::connect(&replication_conn_string()).expect("replication conn");
let opts = ReplicationSlotOptions {
failover: true,
snapshot: Some("nothing".to_string()),
..Default::default()
};
repl.create_replication_slot_with_options(slot, SlotType::Logical, Some("pgoutput"), &opts)
.expect("FAILOVER slot create must pass preflight and succeed on PG17+");
repl.alter_replication_slot(slot, None, Some(false))
.expect("ALTER_REPLICATION_SLOT (failover) must pass preflight and succeed on PG17+");
let r = regular
.exec(&format!(
"SELECT failover FROM pg_replication_slots WHERE slot_name = '{slot}'"
))
.expect("query slot");
assert_eq!(r.ntuples(), 1, "slot must exist");
assert_eq!(
r.get_value(0, 0).as_deref(),
Some("f"),
"failover must be off after ALTER"
);
repl.drop_replication_slot(slot, false)
.expect("drop slot must succeed");
}