mod common;
use common::pgwire_harness::TestServer;
use bytes::Bytes;
use futures::SinkExt;
use futures::StreamExt;
const TENANT: u64 = 1;
const TOTAL_VECTORS: usize = 6;
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 nearest_id(server: &TestServer) -> String {
let rows = server
.query_rows(
"SELECT id FROM vec_restart \
ORDER BY vector_distance(embedding, ARRAY[1.0,0.0,0.0,0.0]) \
LIMIT 1",
)
.await
.unwrap_or_else(|e| panic!("ANN search on vec_restart: {e}"));
rows.first()
.and_then(|r| r.first())
.cloned()
.unwrap_or_default()
}
#[tokio::test]
async fn backup_restore_vector_survives_restart() {
let srv_a = TestServer::start().await;
srv_a
.exec("CREATE COLLECTION vec_restart WITH (engine='vector')")
.await
.expect("CREATE COLLECTION vec_restart on srvA");
srv_a
.exec("CREATE INDEX ON vec_restart (embedding)")
.await
.expect("CREATE INDEX on srvA");
for i in 0..TOTAL_VECTORS {
let mut v = [0.0f32; 4];
v[i % 4] = 1.0;
v[(i % 4 + 1) % 4] += i as f32;
let arr = format!("[{},{},{},{}]", v[0], v[1], v[2], v[3]);
srv_a
.exec(&format!(
"INSERT INTO vec_restart {{ id: 'v{i}', embedding: {arr} }}"
))
.await
.unwrap_or_else(|e| panic!("INSERT vector {i} into vec_restart on srvA: {e}"));
}
assert_eq!(count_rows(&srv_a, "vec_restart").await, TOTAL_VECTORS);
assert_eq!(nearest_id(&srv_a).await, "v0");
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().await;
push_restore(&srv_b, TENANT, backup_bytes).await;
assert_eq!(
count_rows(&srv_b, "vec_restart").await,
TOTAL_VECTORS,
"RESTORE must carry vector data into the clean target before restart"
);
assert_eq!(
nearest_id(&srv_b).await,
"v0",
"restored HNSW index must answer ANN search correctly before restart"
);
let (srv_b, dir) = srv_b.take_dir();
srv_b.graceful_shutdown().await;
let (srv_c, _dir) = TestServer::open_on_path(dir).await;
let post_restart_count = count_rows(&srv_c, "vec_restart").await;
assert_eq!(
post_restart_count, TOTAL_VECTORS,
"restored vectors must survive a restart (WAL replay) with no loss and no \
duplication: expected {TOTAL_VECTORS}, got {post_restart_count}"
);
assert_eq!(
nearest_id(&srv_c).await,
"v0",
"restored HNSW index must still answer ANN search correctly after restart"
);
}