#![cfg(feature = "postgres")]
use std::env;
use tokio_postgres::NoTls;
async fn connect() -> Option<tokio_postgres::Client> {
let url = env::var("DATABASE_URL").ok()?;
let (client, conn) = tokio_postgres::connect(&url, NoTls).await.ok()?;
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("postgres connection error: {e}");
}
});
Some(client)
}
async fn setup_schema(client: &tokio_postgres::Client, name: &str) {
client
.execute(&format!("DROP SCHEMA IF EXISTS {name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {name}"), &[])
.await
.expect("set search_path");
heeranjid::postgres_schema::install_schema(client)
.await
.expect("install_schema");
heeranjid::postgres_schema::seed_default_node(client)
.await
.expect("seed_default_node");
}
async fn teardown_schema(client: &tokio_postgres::Client, name: &str) {
client
.execute(&format!("DROP SCHEMA {name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn generate_heerid_returns_valid_id() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_generate_heerid";
setup_schema(&client, schema_name).await;
let id = heeranjid::postgres_generate::generate_heerid(&client, 1)
.await
.expect("generate_heerid should succeed");
let raw = id.as_i64();
assert!(raw > 0, "generated HeerId must have a positive raw value");
let round_tripped =
heeranjid::HeerId::from_i64(raw).expect("generated i64 must parse as a HeerId");
assert_eq!(id, round_tripped, "HeerId must round-trip through i64");
assert_eq!(id.into_parts().node_id, 1, "node_id must be 1");
teardown_schema(&client, schema_name).await;
}
#[tokio::test]
async fn generate_ranjid_returns_valid_id() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_generate_ranjid";
setup_schema(&client, schema_name).await;
let id = heeranjid::postgres_generate::generate_ranjid(&client, 1)
.await
.expect("generate_ranjid should succeed");
let uuid = id.as_uuid();
let round_tripped =
heeranjid::RanjId::from_uuid(uuid).expect("generated uuid must parse as a RanjId");
assert_eq!(id, round_tripped, "RanjId must round-trip through uuid");
assert_eq!(id.into_parts().node_id, 1, "node_id must be 1");
teardown_schema(&client, schema_name).await;
}
#[tokio::test]
async fn generate_heerids_returns_requested_count() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_generate_heerids";
setup_schema(&client, schema_name).await;
let count = 5_i32;
let ids = heeranjid::postgres_generate::generate_heerids(&client, 1, count)
.await
.expect("generate_heerids should succeed");
assert_eq!(
ids.len(),
count as usize,
"batch generate_heerids must return exactly `count` rows"
);
for id in &ids {
let raw = id.as_i64();
assert!(raw > 0, "each generated HeerId must be positive");
heeranjid::HeerId::from_i64(raw).expect("each raw i64 must parse as a HeerId");
assert_eq!(id.into_parts().node_id, 1, "each id must belong to node 1");
}
for pair in ids.windows(2) {
assert!(
pair[0] < pair[1],
"batch-generated HeerIds must be strictly ascending"
);
}
teardown_schema(&client, schema_name).await;
}
#[tokio::test]
async fn generate_ranjids_returns_requested_count() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_generate_ranjids";
setup_schema(&client, schema_name).await;
let count = 5_i32;
let ids = heeranjid::postgres_generate::generate_ranjids(&client, 1, count)
.await
.expect("generate_ranjids should succeed");
assert_eq!(
ids.len(),
count as usize,
"batch generate_ranjids must return exactly `count` rows"
);
for id in &ids {
let uuid = id.as_uuid();
heeranjid::RanjId::from_uuid(uuid).expect("each uuid must parse as a RanjId");
assert_eq!(id.into_parts().node_id, 1, "each id must belong to node 1");
}
for pair in ids.windows(2) {
assert!(
pair[0] < pair[1],
"batch-generated RanjIds must be strictly ascending"
);
}
teardown_schema(&client, schema_name).await;
}
#[tokio::test]
async fn generate_heerid_surfaces_typed_rollback() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_typed_rollback";
setup_schema(&client, schema_name).await;
client
.execute(
"INSERT INTO heer_node_state (node_id, last_id_time, last_sequence) VALUES (1, 999999999999, 0) ON CONFLICT (node_id) DO UPDATE SET last_id_time = EXCLUDED.last_id_time, last_sequence = EXCLUDED.last_sequence",
&[],
)
.await
.expect("seed heer_node_state");
let error = heeranjid::postgres_generate::generate_heerid(&client, 1)
.await
.unwrap_err();
assert!(matches!(
error,
heeranjid::postgres_generate::GenerateError::HardClockRollback { .. }
));
teardown_schema(&client, schema_name).await;
}
#[tokio::test]
async fn generate_ranjid_surfaces_hard_clock_rollback() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_ranj_hard_rollback";
setup_schema(&client, schema_name).await;
client
.execute(
"INSERT INTO heer_ranj_node_state (node_id, last_id_time, last_sequence) VALUES (1, 999999999999999, 0) ON CONFLICT (node_id) DO UPDATE SET last_id_time = EXCLUDED.last_id_time, last_sequence = EXCLUDED.last_sequence",
&[],
)
.await
.expect("seed heer_ranj_node_state");
let error = heeranjid::postgres_generate::generate_ranjid(&client, 1)
.await
.unwrap_err();
match error {
heeranjid::postgres_generate::GenerateError::HardClockRollback { .. } => {
}
_ => panic!("expected HardClockRollback, got {:?}", error),
}
teardown_schema(&client, schema_name).await;
}