#![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)
}
#[tokio::test]
async fn install_schema_creates_tables() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_install";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_name}"), &[])
.await
.expect("set search_path");
heeranjid::postgres_schema::install_schema(&client)
.await
.expect("install_schema should succeed");
let tables: Vec<String> = client
.query_opt(
"SELECT tablename FROM pg_tables WHERE schemaname = $1 AND tablename = 'heer_nodes'",
&[&schema_name],
)
.await
.expect("query pg_tables")
.iter()
.map(|row| row.get(0))
.collect();
assert!(
!tables.is_empty(),
"heer_nodes table should exist after install"
);
heeranjid::postgres_schema::install_schema(&client)
.await
.expect("install_schema should be idempotent");
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn seed_default_node_creates_row() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_seed";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_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 should succeed");
let count: i64 = client
.query_one("SELECT count(*) FROM heer_nodes WHERE node_id = 1", &[])
.await
.expect("query heer_nodes")
.get(0);
assert_eq!(
count, 1,
"default node (node_id = 1) should exist after seed"
);
heeranjid::postgres_schema::seed_default_node(&client)
.await
.expect("seed_default_node should be idempotent");
let count_after_reseed: i64 = client
.query_one("SELECT count(*) FROM heer_nodes WHERE node_id = 1", &[])
.await
.expect("query heer_nodes after reseed")
.get(0);
assert_eq!(
count_after_reseed, 1,
"default node should not be duplicated on re-seed"
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn desc_flip_round_trips_inside_postgres() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_desc_flip";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_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");
heeranjid::postgres_schema::install_all_desc_support(&client)
.await
.expect("install_all_desc_support");
let row = client
.query_one("SELECT heerid_to_asc(heerid_to_desc(1234567::bigint))", &[])
.await
.expect("round-trip query");
let back: i64 = row.get(0);
assert_eq!(back, 1_234_567, "heerid_to_asc(heerid_to_desc(x)) == x");
let row = client
.query_one("SELECT heerid_flip_mask()", &[])
.await
.expect("flip mask query");
let mask: i64 = row.get(0);
assert_eq!(
mask, 9_223_372_036_850_589_695,
"heerid_flip_mask() == documented constant"
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn generate_id_after_seed() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_genid";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_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");
client
.execute("SELECT set_heer_node_id(1)", &[])
.await
.expect("set session node_id");
let id: i64 = client
.query_one("SELECT generate_id()", &[])
.await
.expect("generate_id")
.get(0);
assert!(id > 0, "generated ID should be positive");
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn autofill_trigger_populates_desc_column_on_insert_and_update() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_autofill_trigger";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_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");
heeranjid::postgres_schema::install_all_desc_support(&client)
.await
.expect("install_all_desc_support");
client
.batch_execute("CREATE TABLE trig_test (id bigint PRIMARY KEY, id_desc bigint)")
.await
.expect("create trig_test");
heeranjid::postgres_schema::install_autofill_trigger_for_table(
&client,
"trig_test",
&[heeranjid::postgres_schema::ColumnPair {
src: "id",
dst: "id_desc",
}],
heeranjid::postgres_schema::IdKind::Heer,
)
.await
.expect("install_autofill_trigger_for_table");
client
.execute("INSERT INTO trig_test (id) VALUES ($1)", &[&1000_i64])
.await
.expect("insert row");
let expected: i64 = client
.query_one("SELECT heerid_to_desc($1::bigint)", &[&1000_i64])
.await
.expect("expected id_desc for 1000")
.get(0);
let got: i64 = client
.query_one("SELECT id_desc FROM trig_test WHERE id = $1", &[&1000_i64])
.await
.expect("read id_desc after insert")
.get(0);
assert_eq!(
got, expected,
"INSERT trigger should populate id_desc via heerid_to_desc(id)"
);
client
.execute(
"UPDATE trig_test SET id = $1 WHERE id = $2",
&[&2000_i64, &1000_i64],
)
.await
.expect("update row");
let expected2: i64 = client
.query_one("SELECT heerid_to_desc($1::bigint)", &[&2000_i64])
.await
.expect("expected id_desc for 2000")
.get(0);
let got2: i64 = client
.query_one("SELECT id_desc FROM trig_test WHERE id = $1", &[&2000_i64])
.await
.expect("read id_desc after update")
.get(0);
assert_eq!(
got2, expected2,
"UPDATE trigger should recompute id_desc when source changes"
);
heeranjid::postgres_schema::drop_autofill_trigger_for_table(&client, "trig_test")
.await
.expect("drop_autofill_trigger_for_table");
let remaining: i64 = client
.query_one(
"SELECT count(*) FROM pg_trigger \
WHERE tgname = 'zzz_trig_test_autofill_desc' AND NOT tgisinternal",
&[],
)
.await
.expect("check trigger removal")
.get(0);
assert_eq!(remaining, 0, "trigger should be gone after drop helper");
client
.execute(
"UPDATE trig_test SET id = $1 WHERE id = $2",
&[&3000_i64, &2000_i64],
)
.await
.expect("update row post-drop");
let stale: i64 = client
.query_one("SELECT id_desc FROM trig_test WHERE id = $1", &[&3000_i64])
.await
.expect("read id_desc after post-drop update")
.get(0);
assert_eq!(
stale, expected2,
"after drop, id_desc must not be recomputed by a trigger"
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn generate_ids_desc_returns_flipped_batch() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_bulk_heerid_desc";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_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");
heeranjid::postgres_schema::install_all_desc_support(&client)
.await
.expect("install_all_desc_support");
client
.execute("SELECT set_heer_node_id(1)", &[])
.await
.expect("set_heer_node_id");
let requested: i32 = 8;
let desc_rows = client
.query(
"SELECT id FROM generate_ids_desc($1::integer)",
&[&requested],
)
.await
.expect("bulk generate_ids_desc");
assert_eq!(
desc_rows.len(),
requested as usize,
"generate_ids_desc($1) must return exactly $1 rows"
);
let desc_ids: Vec<i64> = desc_rows.iter().map(|r| r.get::<_, i64>(0)).collect();
for d in &desc_ids {
let roundtrip_row = client
.query_one("SELECT heerid_to_desc(heerid_to_asc($1::bigint))", &[d])
.await
.expect("heerid_to_desc(heerid_to_asc(d)) round-trip");
let roundtrip: i64 = roundtrip_row.get(0);
assert_eq!(
*d, roundtrip,
"heerid_to_desc(heerid_to_asc(d)) must equal d — wrapper must apply the flip"
);
}
let mut asc_ids: Vec<i64> = Vec::with_capacity(desc_ids.len());
for d in &desc_ids {
let asc_row = client
.query_one("SELECT heerid_to_asc($1::bigint)", &[d])
.await
.expect("flip back to asc");
let asc: i64 = asc_row.get(0);
heeranjid::HeerId::from_i64(asc)
.expect("asc-shape round-trip must parse as a valid HeerId");
asc_ids.push(asc);
}
for window in asc_ids.windows(2) {
assert!(
window[0] < window[1],
"asc-flipped sequence must be strictly monotonic increasing; \
got {} then {} — wrapper may have skipped the desc flip",
window[0],
window[1],
);
}
let mut sorted = desc_ids.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
sorted.len(),
desc_ids.len(),
"generate_ids_desc must return distinct IDs"
);
let node_rows = client
.query(
"SELECT id FROM generate_ids_desc($1::integer, $2::integer, true)",
&[&1_i32, &requested],
)
.await
.expect("bulk generate_ids_desc with explicit node");
assert_eq!(
node_rows.len(),
requested as usize,
"generate_ids_desc(node, n, spanning) must return n rows"
);
let mut node_asc_ids: Vec<i64> = Vec::with_capacity(node_rows.len());
for row in &node_rows {
let d: i64 = row.get(0);
let asc_row = client
.query_one("SELECT heerid_to_asc($1::bigint)", &[&d])
.await
.expect("flip back to asc — explicit-node overload");
let asc: i64 = asc_row.get(0);
heeranjid::HeerId::from_i64(asc)
.expect("asc-shape must parse as a valid HeerId — explicit-node overload");
node_asc_ids.push(asc);
}
for window in node_asc_ids.windows(2) {
assert!(
window[0] < window[1],
"explicit-node overload: asc-flipped sequence must be strictly monotonic increasing; \
got {} then {} — wrapper may have skipped the desc flip",
window[0],
window[1],
);
}
let no_span_rows = client
.query(
"SELECT id FROM generate_ids_desc($1::integer, $2::boolean)",
&[&requested, &false],
)
.await
.expect("generate_ids_desc(n, false)");
assert_eq!(
no_span_rows.len(),
requested as usize,
"generate_ids_desc(n, false) must return n rows"
);
let mut no_span_asc_ids: Vec<i64> = Vec::with_capacity(no_span_rows.len());
for row in &no_span_rows {
let d: i64 = row.get(0);
let asc_row = client
.query_one("SELECT heerid_to_asc($1::bigint)", &[&d])
.await
.expect("flip back to asc — allow_spanning=false overload");
let asc: i64 = asc_row.get(0);
heeranjid::HeerId::from_i64(asc)
.expect("asc-shape must parse as a valid HeerId — allow_spanning=false overload");
no_span_asc_ids.push(asc);
}
for window in no_span_asc_ids.windows(2) {
assert!(
window[0] < window[1],
"allow_spanning=false overload: asc-flipped sequence must be strictly monotonic \
increasing; got {} then {} — wrapper may have skipped the desc flip",
window[0],
window[1],
);
}
let zero_err = client
.query("SELECT id FROM generate_ids_desc($1::integer)", &[&0_i32])
.await;
assert!(
zero_err.is_err(),
"generate_ids_desc(0) must propagate requested_count error"
);
let pg_err = zero_err.unwrap_err();
let db_err = pg_err
.as_db_error()
.expect("generate_ids_desc(0) must raise a Postgres-level error");
assert!(
db_err
.message()
.contains("requested_count must be greater than zero"),
"error message must mention requested_count; got: {}",
db_err.message()
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn generate_ranjids_desc_returns_flipped_batch() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_bulk_ranjid_desc";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_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");
heeranjid::postgres_schema::install_all_desc_support(&client)
.await
.expect("install_all_desc_support");
client
.execute("SELECT set_heer_ranj_node_id(1)", &[])
.await
.expect("set_heer_ranj_node_id");
let requested: i32 = 8;
let desc_rows = client
.query(
"SELECT id FROM generate_ranjids_desc($1::integer)",
&[&requested],
)
.await
.expect("bulk generate_ranjids_desc");
assert_eq!(
desc_rows.len(),
requested as usize,
"generate_ranjids_desc($1) must return exactly $1 rows"
);
let desc_ids: Vec<uuid::Uuid> = desc_rows
.iter()
.map(|r| r.get::<_, uuid::Uuid>(0))
.collect();
for d in &desc_ids {
let roundtrip_row = client
.query_one("SELECT ranjid_to_desc(ranjid_to_asc($1::uuid))", &[d])
.await
.expect("ranjid_to_desc(ranjid_to_asc(d)) round-trip");
let roundtrip: uuid::Uuid = roundtrip_row.get(0);
assert_eq!(
*d, roundtrip,
"ranjid_to_desc(ranjid_to_asc(d)) must equal d — wrapper must apply the flip"
);
}
let mut asc_ids: Vec<uuid::Uuid> = Vec::with_capacity(desc_ids.len());
for d in &desc_ids {
let asc_row = client
.query_one("SELECT ranjid_to_asc($1::uuid)", &[d])
.await
.expect("flip back to asc");
let asc: uuid::Uuid = asc_row.get(0);
heeranjid::RanjId::from_uuid(asc)
.expect("asc-shape round-trip must parse as a valid RanjId");
asc_ids.push(asc);
}
for window in asc_ids.windows(2) {
assert!(
window[0] < window[1],
"asc-flipped sequence must be strictly monotonic increasing; \
got {} then {} — wrapper may have skipped the desc flip",
window[0],
window[1],
);
}
let mut sorted = desc_ids.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
desc_ids.len(),
"generate_ranjids_desc must return distinct IDs"
);
let node_rows = client
.query(
"SELECT id FROM generate_ranjids_desc($1::integer, $2::integer, true)",
&[&1_i32, &requested],
)
.await
.expect("bulk generate_ranjids_desc with explicit node");
assert_eq!(
node_rows.len(),
requested as usize,
"generate_ranjids_desc(node, n, spanning) must return n rows"
);
let mut node_asc_ids: Vec<uuid::Uuid> = Vec::with_capacity(node_rows.len());
for row in &node_rows {
let d: uuid::Uuid = row.get(0);
let asc_row = client
.query_one("SELECT ranjid_to_asc($1::uuid)", &[&d])
.await
.expect("flip back to asc — explicit-node overload");
let asc: uuid::Uuid = asc_row.get(0);
heeranjid::RanjId::from_uuid(asc)
.expect("asc-shape must parse as a valid RanjId — explicit-node overload");
node_asc_ids.push(asc);
}
for window in node_asc_ids.windows(2) {
assert!(
window[0] < window[1],
"explicit-node overload: asc-flipped sequence must be strictly monotonic increasing; \
got {} then {} — wrapper may have skipped the desc flip",
window[0],
window[1],
);
}
let no_span_rows = client
.query(
"SELECT id FROM generate_ranjids_desc($1::integer, $2::boolean)",
&[&requested, &false],
)
.await
.expect("generate_ranjids_desc(n, false)");
assert_eq!(
no_span_rows.len(),
requested as usize,
"generate_ranjids_desc(n, false) must return n rows"
);
let mut no_span_asc_ids: Vec<uuid::Uuid> = Vec::with_capacity(no_span_rows.len());
for row in &no_span_rows {
let d: uuid::Uuid = row.get(0);
let asc_row = client
.query_one("SELECT ranjid_to_asc($1::uuid)", &[&d])
.await
.expect("flip back to asc — allow_spanning=false overload");
let asc: uuid::Uuid = asc_row.get(0);
heeranjid::RanjId::from_uuid(asc)
.expect("asc-shape must parse as a valid RanjId — allow_spanning=false overload");
no_span_asc_ids.push(asc);
}
for window in no_span_asc_ids.windows(2) {
assert!(
window[0] < window[1],
"allow_spanning=false overload: asc-flipped sequence must be strictly monotonic \
increasing; got {} then {} — wrapper may have skipped the desc flip",
window[0],
window[1],
);
}
let zero_err = client
.query(
"SELECT id FROM generate_ranjids_desc($1::integer)",
&[&0_i32],
)
.await;
assert!(
zero_err.is_err(),
"generate_ranjids_desc(0) must propagate requested_count error"
);
let pg_err = zero_err.unwrap_err();
let db_err = pg_err
.as_db_error()
.expect("generate_ranjids_desc(0) must raise a Postgres-level error");
assert!(
db_err
.message()
.contains("requested_count must be greater than zero"),
"error message must mention requested_count; got: {}",
db_err.message()
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
}
#[tokio::test]
async fn install_configure_and_call_heer_configure() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_configure";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_name}"), &[])
.await
.expect("set search_path");
heeranjid::postgres_schema::install_schema(&client)
.await
.expect("install_schema");
client
.execute(
"INSERT INTO heer_config (id, epoch, precision) \
VALUES (1, CURRENT_TIMESTAMP - INTERVAL '1 day', 'us')",
&[],
)
.await
.expect("insert heer_config");
client
.execute(
"INSERT INTO heer_nodes (node_id, name, description, is_active) \
VALUES (1, 'default', 'Default single-node instance', true)",
&[],
)
.await
.expect("insert heer_nodes");
client
.execute("INSERT INTO heer_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_node_state");
client
.execute("INSERT INTO heer_ranj_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_ranj_node_state");
heeranjid::postgres_schema::install_configure(&client)
.await
.expect("install_configure should succeed");
client
.execute("SELECT heer_configure()", &[])
.await
.expect("heer_configure() should succeed without error");
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema (configure)");
}
#[tokio::test]
async fn decoded_ranjid_timestamp_is_current() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_ranjid_ts";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_name}"), &[])
.await
.expect("set search_path");
heeranjid::postgres_schema::install_schema(&client)
.await
.expect("install_schema");
client
.execute(
"INSERT INTO heer_config (id, epoch, precision) \
VALUES (1, CURRENT_TIMESTAMP - INTERVAL '1 day', 'us')",
&[],
)
.await
.expect("insert heer_config");
client
.execute(
"INSERT INTO heer_nodes (node_id, name, description, is_active) \
VALUES (1, 'default', 'Default single-node instance', true)",
&[],
)
.await
.expect("insert heer_nodes");
client
.execute("INSERT INTO heer_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_node_state");
client
.execute("INSERT INTO heer_ranj_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_ranj_node_state");
let before_micros = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time is after epoch")
.as_micros();
let uuid: uuid::Uuid = client
.query_one("SELECT generate_ranjid(1)", &[])
.await
.expect("generate_ranjid(1)")
.get(0);
let after_micros = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time is after epoch")
.as_micros();
let ranj = heeranjid::RanjId::from_uuid(uuid).expect("database returned a valid RanjId UUID");
let epoch_micros: u128 = client
.query_one(
"SELECT FLOOR(EXTRACT(EPOCH FROM epoch) * 1000000)::BIGINT \
FROM heer_config WHERE id = 1",
&[],
)
.await
.expect("fetch epoch_micros")
.get::<_, i64>(0) as u128;
let decoded_unix_micros = epoch_micros + ranj.timestamp_micros();
const TOLERANCE_MICROS: u128 = 5_000_000; assert!(
decoded_unix_micros >= before_micros.saturating_sub(TOLERANCE_MICROS),
"decoded timestamp {} µs is more than 5 s before generation start {} µs",
decoded_unix_micros,
before_micros,
);
assert!(
decoded_unix_micros <= after_micros + TOLERANCE_MICROS,
"decoded timestamp {} µs is more than 5 s after generation end {} µs",
decoded_unix_micros,
after_micros,
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema (ranjid_ts)");
}
#[tokio::test]
async fn configured_ranjid_path_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_configured_rollback";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_name}"), &[])
.await
.expect("set search_path");
heeranjid::postgres_schema::install_schema(&client)
.await
.expect("install_schema");
client
.execute(
"INSERT INTO heer_config (id, epoch, precision) \
VALUES (1, CURRENT_TIMESTAMP - INTERVAL '1 day', 'us')",
&[],
)
.await
.expect("insert heer_config");
client
.execute(
"INSERT INTO heer_nodes (node_id, name, description, is_active) \
VALUES (1, 'default', 'Default single-node instance', true)",
&[],
)
.await
.expect("insert heer_nodes");
client
.execute("INSERT INTO heer_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_node_state");
client
.execute("INSERT INTO heer_ranj_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_ranj_node_state");
heeranjid::postgres_schema::install_configure(&client)
.await
.expect("install_configure");
client
.execute("SELECT heer_configure()", &[])
.await
.expect("heer_configure() should succeed");
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 with future timestamp");
let error = heeranjid::postgres_generate::generate_ranjid(&client, 1)
.await
.unwrap_err();
assert!(
matches!(
error,
heeranjid::postgres_generate::GenerateError::HardClockRollback { .. }
),
"expected HardClockRollback on configured path, got {:?}",
error,
);
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema (configured_rollback)");
}
#[tokio::test]
async fn heer_configure_upgrade_drops_old_overload() {
let Some(client) = connect().await else {
eprintln!("SKIP: DATABASE_URL not set; skipping live database test");
return;
};
let schema_name = "test_heeranjid_configure_upgrade";
client
.execute(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"), &[])
.await
.expect("drop test schema");
client
.execute(&format!("CREATE SCHEMA {schema_name}"), &[])
.await
.expect("create test schema");
client
.execute(&format!("SET search_path TO {schema_name}"), &[])
.await
.expect("set search_path");
heeranjid::postgres_schema::install_schema(&client)
.await
.expect("install_schema");
client
.execute(
"INSERT INTO heer_config (id, epoch, precision) \
VALUES (1, CURRENT_TIMESTAMP - INTERVAL '1 day', 'us')",
&[],
)
.await
.expect("insert heer_config");
client
.execute(
"INSERT INTO heer_nodes (node_id, name, description, is_active) \
VALUES (1, 'default', 'Default single-node instance', true)",
&[],
)
.await
.expect("insert heer_nodes");
client
.execute("INSERT INTO heer_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_node_state");
client
.execute("INSERT INTO heer_ranj_node_state (node_id) VALUES (1)", &[])
.await
.expect("insert heer_ranj_node_state");
client
.batch_execute(
"CREATE FUNCTION heer_configure() RETURNS VOID LANGUAGE plpgsql AS $$ \
BEGIN RAISE EXCEPTION 'old overload still present'; END; $$",
)
.await
.expect("create old zero-arg heer_configure overload");
heeranjid::postgres_schema::install_configure(&client)
.await
.expect("install_configure should succeed and drop the old overload");
client
.execute("SELECT heer_configure()", &[])
.await
.expect("heer_configure() must call the new overload, not the old zero-arg one");
client
.execute("SELECT heer_configure(false)", &[])
.await
.expect("heer_configure(false) should succeed");
client
.execute(&format!("DROP SCHEMA {schema_name} CASCADE"), &[])
.await
.expect("drop test schema (configure_upgrade)");
}