mod common;
use common::pgwire_harness::TestServer;
use bytes::Bytes;
use futures::SinkExt;
use futures::StreamExt;
const TENANT: u64 = 1;
const FLUSH_THRESHOLD: usize = 4;
const TOTAL_ROWS: usize = FLUSH_THRESHOLD * 2;
async fn drain_backup(server: &TestServer, tenant: u64) -> Vec<u8> {
let stream = server
.client
.copy_out(&format!("COPY (BACKUP TENANT {tenant}) TO STDOUT"))
.await
.expect("copy_out: BACKUP TENANT");
let mut bytes = Vec::new();
let mut s = Box::pin(stream);
while let Some(chunk) = s.next().await {
bytes.extend_from_slice(&chunk.expect("copy_out chunk"));
}
bytes
}
async fn push_restore(server: &TestServer, tenant: u64, bytes: Vec<u8>) {
let sink = server
.client
.copy_in::<_, Bytes>(&format!("COPY tenant_restore({tenant}) FROM STDIN"))
.await
.expect("copy_in: RESTORE TENANT");
let mut sink = Box::pin(sink);
sink.as_mut()
.send(Bytes::from(bytes))
.await
.expect("send backup bytes");
sink.as_mut()
.finish()
.await
.expect("finish copy_in: RESTORE TENANT");
}
fn detail(e: tokio_postgres::Error) -> String {
if let Some(db) = e.as_db_error() {
format!("{}: {}", db.code().code(), db.message())
} else {
format!("{e}")
}
}
async fn count_rows(server: &TestServer, table: &str) -> usize {
let msgs = server
.client
.simple_query(&format!("SELECT COUNT(*) FROM {table}"))
.await
.unwrap_or_else(|e| panic!("SELECT COUNT(*) FROM {table}: {}", detail(e)));
for msg in &msgs {
if let tokio_postgres::SimpleQueryMessage::Row(r) = msg
&& let Some(s) = r.get(0)
{
return s.parse::<usize>().expect("COUNT(*) parse");
}
}
0
}
async fn collect_ids(server: &TestServer, sql: &str) -> Vec<String> {
let msgs = server
.client
.simple_query(sql)
.await
.unwrap_or_else(|e| panic!("{sql}: {}", detail(e)));
let mut ids = Vec::new();
for msg in &msgs {
if let tokio_postgres::SimpleQueryMessage::Row(r) = msg
&& let Some(s) = r.get(0)
{
ids.push(s.to_string());
}
}
ids
}
#[tokio::test]
async fn backup_restore_columnar_survives_restart() {
let srv_a = TestServer::start_with_columnar_flush_threshold(FLUSH_THRESHOLD).await;
srv_a
.exec(
"CREATE COLLECTION col_restart \
COLUMNS (id TEXT, region TEXT, value FLOAT, ts BIGINT) \
WITH (engine='columnar')",
)
.await
.expect("CREATE COLLECTION col_restart on srvA");
for i in 0..TOTAL_ROWS {
let region = if i % 2 == 0 { "us" } else { "eu" };
srv_a
.exec(&format!(
"INSERT INTO col_restart (id, region, value, ts) \
VALUES ('r{i}', '{region}', {i}.0, {i})"
))
.await
.unwrap_or_else(|e| panic!("INSERT row {i} into col_restart on srvA: {e}"));
}
assert_eq!(count_rows(&srv_a, "col_restart").await, TOTAL_ROWS);
let backup_bytes = drain_backup(&srv_a, TENANT).await;
assert!(
!backup_bytes.is_empty(),
"backup envelope must not be empty"
);
drop(srv_a);
let srv_b = TestServer::start_with_columnar_flush_threshold(FLUSH_THRESHOLD).await;
push_restore(&srv_b, TENANT, backup_bytes).await;
assert_eq!(
count_rows(&srv_b, "col_restart").await,
TOTAL_ROWS,
"RESTORE must carry columnar data into the clean target before restart"
);
let (srv_b, dir) = srv_b.take_dir();
srv_b.graceful_shutdown().await;
let (srv_c, _dir) =
TestServer::open_on_path_with_columnar_flush_threshold(dir, FLUSH_THRESHOLD).await;
let post_restart_count = count_rows(&srv_c, "col_restart").await;
assert_eq!(
post_restart_count, TOTAL_ROWS,
"restored columnar rows must survive a restart (WAL replay): expected {TOTAL_ROWS}, \
got {post_restart_count}"
);
let ids = collect_ids(&srv_c, "SELECT id FROM col_restart ORDER BY ts").await;
let expected: Vec<String> = (0..TOTAL_ROWS).map(|i| format!("r{i}")).collect();
assert_eq!(
ids, expected,
"every restored row id must be intact after restart; got {ids:?}"
);
}