#![cfg(feature = "postgres")]
use axon::cancel_token::CancellationFlag;
use axon::ir_nodes::IRAxonStore;
use axon::store::audit_chain::{ChainVerdict, StoreAuditChain, StoreMutationKind};
use axon::store::capability::check_store_capability;
use axon::store::epistemic::{enforce_retrieve_floor, mark_retrieved};
use axon::store::filter::SqlValue;
use axon::store::postgres_backend::PostgresStoreBackend;
use axon::store::registry::{StoreHandle, StoreRegistry};
use axon::store::row_stream::stream_retrieve;
use axon::stream_effect::BackpressurePolicy;
fn nb() -> std::collections::HashMap<String, String> {
std::collections::HashMap::new()
}
fn sc(backend: &PostgresStoreBackend) -> axon::store::store_conn::StoreConn<'_> {
axon::store::store_conn::StoreConn::pool(backend.pool())
}
async fn test_backend() -> Option<PostgresStoreBackend> {
let dsn = match std::env::var("AXON_TEST_DATABASE_URL") {
Ok(d) if !d.trim().is_empty() => d,
_ => {
eprintln!(
"l_2: AXON_TEST_DATABASE_URL unset — skipping \
real-Postgres integration (set it to run; CI always does)"
);
return None;
}
};
let backend = match PostgresStoreBackend::connect(&dsn) {
Ok(b) => b,
Err(e) => {
eprintln!("l_2: backend connect failed ({e}) — skipping");
return None;
}
};
if let Err(e) = backend.ping().await {
eprintln!("l_2: Postgres unreachable ({e}) — skipping");
return None;
}
Some(backend)
}
macro_rules! pg_or_skip {
() => {
match test_backend().await {
Some(b) => b,
None => return,
}
};
}
async fn exec(backend: &PostgresStoreBackend, sql: &str) {
sqlx::query(sql)
.execute(backend.pool())
.await
.unwrap_or_else(|e| panic!("l_2 fixture SQL failed:\n {sql}\n {e}"));
}
async fn fresh_table(backend: &PostgresStoreBackend, table: &str, columns: &str) {
exec(backend, &format!("DROP TABLE IF EXISTS {table}")).await;
exec(backend, &format!("CREATE TABLE {table} ({columns})")).await;
}
async fn drop_table(backend: &PostgresStoreBackend, table: &str) {
exec(backend, &format!("DROP TABLE IF EXISTS {table}")).await;
}
fn text(s: &str) -> SqlValue {
SqlValue::Text(s.to_string())
}
#[tokio::test]
async fn t1_substrate_insert_then_query_round_trip() {
let backend = pg_or_skip!();
let table = "substrate_2";
fresh_table(&backend, table, "id INTEGER, name TEXT, active BOOLEAN").await;
let inserted = backend
.insert(
&mut sc(&backend),
table,
&[
("id".into(), SqlValue::Integer(7)),
("name".into(), text("alice")),
("active".into(), SqlValue::Boolean(true)),
],
)
.await
.expect("insert");
assert_eq!(inserted, 1, "one row persisted");
let rows = backend.query(&mut sc(&backend), table, "id = 7", &nb()).await.expect("query");
assert_eq!(rows.len(), 1, "the persisted row is retrievable");
assert_eq!(rows[0].get("name"), Some(&serde_json::json!("alice")));
assert_eq!(rows[0].get("active"), Some(&serde_json::json!(true)));
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t2_substrate_mutate_updates_real_rows() {
let backend = pg_or_skip!();
let table = "mutate_2";
fresh_table(&backend, table, "id INTEGER, status TEXT").await;
backend
.insert(&mut sc(&backend), table, &[("id".into(), SqlValue::Integer(1)), ("status".into(), text("draft"))])
.await
.expect("seed");
let affected = backend
.mutate(&mut sc(&backend), table, "id = 1", &[("status".into(), text("published"))], &nb())
.await
.expect("mutate");
assert_eq!(affected, 1);
let rows = backend.query(&mut sc(&backend), table, "id = 1", &nb()).await.expect("query");
assert_eq!(rows[0].get("status"), Some(&serde_json::json!("published")));
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t3_substrate_purge_deletes_real_rows() {
let backend = pg_or_skip!();
let table = "purge_2";
fresh_table(&backend, table, "id INTEGER").await;
for i in 1..=5 {
backend
.insert(&mut sc(&backend), table, &[("id".into(), SqlValue::Integer(i))])
.await
.expect("seed");
}
let purged = backend.purge(&mut sc(&backend), table, "id <= 3", &nb()).await.expect("purge");
assert_eq!(purged, 3, "three rows deleted");
let survivors = backend.query(&mut sc(&backend), table, "", &nb()).await.expect("query");
assert_eq!(survivors.len(), 2, "two rows survive");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t4_pillar_i_confidence_floor_filters_real_rows() {
let backend = pg_or_skip!();
let table = "epistemic_2";
fresh_table(&backend, table, "id INTEGER, _confidence NUMERIC").await;
for (id, conf) in [(1, "0.95"), (2, "0.50"), (3, "0.80"), (4, "0.10"), (5, "0.99")] {
exec(
&backend,
&format!("INSERT INTO {table} (id, _confidence) VALUES ({id}, {conf})"),
)
.await;
}
let outcome =
stream_retrieve(&backend, &mut sc(&backend), table, "", "", "", "", "", BackpressurePolicy::DegradeQuality, 1000, &CancellationFlag::new(), &nb())
.await
.expect("stream_retrieve");
assert_eq!(outcome.rows.len(), 5, "all five rows off the cursor");
let floored = enforce_retrieve_floor(mark_retrieved(outcome.rows), Some(0.8));
assert_eq!(floored.trusted.len(), 3, "three rows at or above the floor");
assert_eq!(floored.below_floor.len(), 2, "two sub-floor rows filtered");
drop_table(&backend, table).await;
}
async fn seed_n(backend: &PostgresStoreBackend, table: &str, n: i64) {
fresh_table(backend, table, "id INTEGER").await;
let values: Vec<String> = (1..=n).map(|i| format!("({i})")).collect();
exec(
backend,
&format!("INSERT INTO {table} (id) VALUES {}", values.join(", ")),
)
.await;
}
#[tokio::test]
async fn t5_pillar_iii_drop_oldest_bounds_a_real_cursor() {
let backend = pg_or_skip!();
let table = "dropoldest";
seed_n(&backend, table, 50).await;
let outcome = stream_retrieve(
&backend, &mut sc(&backend), table, "", "", "", "", "", BackpressurePolicy::DropOldest, 10, &CancellationFlag::new(),
&nb(),
)
.await
.expect("stream_retrieve");
assert_eq!(outcome.rows.len(), 10, "drop_oldest bounds to the window");
assert_eq!(outcome.total_seen, 50, "the cursor yielded all 50");
assert_eq!(outcome.dropped, 40, "40 older rows dropped");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t6_pillar_iii_pause_upstream_truncates_a_real_cursor() {
let backend = pg_or_skip!();
let table = "pause";
seed_n(&backend, table, 50).await;
let outcome = stream_retrieve(
&backend, &mut sc(&backend), table, "", "", "", "", "", BackpressurePolicy::PauseUpstream, 10, &CancellationFlag::new(),
&nb(),
)
.await
.expect("stream_retrieve");
assert_eq!(outcome.rows.len(), 10, "pause_upstream stops at the bound");
assert!(outcome.truncated, "more rows existed past the bound");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t7_pillar_iii_fail_errors_past_the_bound() {
let backend = pg_or_skip!();
let table = "fail_2";
seed_n(&backend, table, 50).await;
let result = stream_retrieve(
&backend, &mut sc(&backend), table, "", "", "", "", "", BackpressurePolicy::Fail, 10, &CancellationFlag::new(),
&nb(),
)
.await;
assert!(result.is_err(), "fail policy errors on an over-bound result");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t8_pillar_iii_cancel_stops_the_real_drain() {
let backend = pg_or_skip!();
let table = "cancel_2";
seed_n(&backend, table, 50).await;
let cancel = CancellationFlag::new();
cancel.cancel(); let outcome = stream_retrieve(
&backend, &mut sc(&backend), table, "", "", "", "", "", BackpressurePolicy::DegradeQuality, 1000, &cancel,
&nb(),
)
.await
.expect("stream_retrieve");
assert!(outcome.cancelled, "a pre-cancelled drain reports cancelled");
assert!(outcome.rows.is_empty(), "no row consumed under a fired cancel");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t9_type_mapping_is_json_safe_for_every_supported_type() {
let backend = pg_or_skip!();
let table = "types_2";
fresh_table(
&backend,
table,
"u UUID, ts TIMESTAMPTZ, n NUMERIC, j JSONB, b BYTEA, i INTEGER, f DOUBLE PRECISION",
)
.await;
exec(
&backend,
&format!(
"INSERT INTO {table} (u, ts, n, j, b, i, f) VALUES \
(gen_random_uuid(), now(), 1234.5678, '{{\"k\":1}}'::jsonb, \
'\\xDEADBEEF'::bytea, 42, 2.5)"
),
)
.await;
let rows = backend.query(&mut sc(&backend), table, "", &nb()).await.expect("query");
assert_eq!(rows.len(), 1);
let r = &rows[0];
assert!(r.get("u").unwrap().is_string(), "UUID → string");
assert!(r.get("ts").unwrap().is_string(), "TIMESTAMPTZ → RFC3339 string");
assert!(r.get("n").unwrap().is_string(), "NUMERIC → precision-safe string");
assert!(r.get("j").unwrap().is_object(), "JSONB → JSON object");
assert!(r.get("b").unwrap().is_string(), "BYTEA → base64 string");
assert!(r.get("i").unwrap().is_i64(), "INTEGER → JSON number");
assert!(r.get("f").unwrap().is_f64(), "DOUBLE PRECISION → JSON number");
assert_eq!(r.get("n").unwrap().as_str(), Some("1234.5678"));
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t10_pillar_ii_audit_chain_records_real_mutations() {
let backend = pg_or_skip!();
let table = "audit_2";
fresh_table(&backend, table, "id INTEGER, v TEXT").await;
let mut chain = StoreAuditChain::new();
let n = backend
.insert(&mut sc(&backend), table, &[("id".into(), SqlValue::Integer(1)), ("v".into(), text("a"))])
.await
.expect("insert");
chain.record(StoreMutationKind::Persist, table, &format!("{n} row(s)"));
let n = backend
.mutate(&mut sc(&backend), table, "id = 1", &[("v".into(), text("b"))], &nb())
.await
.expect("mutate");
chain.record(StoreMutationKind::Mutate, table, &format!("{n} row(s)"));
let n = backend.purge(&mut sc(&backend), table, "id = 1", &nb()).await.expect("purge");
chain.record(StoreMutationKind::Purge, table, &format!("{n} row(s)"));
assert_eq!(chain.len(), 3);
assert_eq!(
chain.verify(),
ChainVerdict::Intact,
"the audit chain of three real mutations verifies tamper-free"
);
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t11_pillar_iv_capability_gates_a_real_postgresql_store() {
let backend = pg_or_skip!();
let dsn = std::env::var("AXON_TEST_DATABASE_URL").unwrap();
let _ = &backend;
let registry = StoreRegistry::build(&[IRAxonStore {
node_type: "axonstore",
source_line: 0,
source_column: 0,
name: "vault".to_string(),
backend: "postgresql".to_string(),
connection: dsn,
confidence_floor: None,
isolation: String::new(),
on_breach: String::new(),
capability: "vault.read".to_string(),
class: String::new(),
column_schema: None,
resource_ref: String::new(),
}])
.expect("registry build");
let handle = registry.resolve("vault").expect("resolve");
assert!(matches!(handle, StoreHandle::Postgres(_)));
let required = registry.spec("vault").unwrap().capability.clone();
assert!(
check_store_capability("vault", &required, &["other.cap".to_string()])
.is_err(),
"a caller missing vault.read is denied"
);
assert!(
check_store_capability("vault", &required, &["vault.read".to_string()])
.is_ok(),
"a caller holding vault.read is admitted"
);
}
#[tokio::test]
async fn t12_d4_a_malicious_where_value_cannot_drop_a_real_table() {
let backend = pg_or_skip!();
let table = "injection_2";
fresh_table(&backend, table, "id INTEGER, name TEXT").await;
backend
.insert(&mut sc(&backend), table, &[("id".into(), SqlValue::Integer(1)), ("name".into(), text("safe"))])
.await
.expect("seed");
let malicious = format!("name = '; DROP TABLE {table}; --'");
let rows = backend.query(&mut sc(&backend), table, &malicious, &nb()).await.expect("query runs");
assert!(rows.is_empty(), "no row literally named the payload");
let survivors = backend.query(&mut sc(&backend), table, "", &nb()).await.expect("table intact");
assert_eq!(survivors.len(), 1, "the injection did NOT drop the table");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t13_typed_column_write_and_read_round_trip() {
let backend = pg_or_skip!();
let table = "typed_2";
fresh_table(&backend, table, "tid uuid, kind text, n integer").await;
let uuid = "83d078e1-b372-42ba-9572-ff8dc521386e";
backend
.insert(
&mut sc(&backend),
table,
&[
("tid".into(), text(uuid)),
("kind".into(), text("greeting")),
("n".into(), text("7")),
],
)
.await
.expect("v1.36.2 — persist into a uuid + integer column");
let rows = backend
.query(&mut sc(&backend), table, &format!("tid = '{uuid}'"), &nb())
.await
.expect("retrieve by uuid");
assert_eq!(rows.len(), 1, "the row written to the uuid column round-trips");
let updated = backend
.mutate(
&mut sc(&backend),
table,
&format!("tid = '{uuid}'"),
&[("n".into(), text("99"))],
&nb(),
)
.await
.expect("v1.36.2 — mutate a typed column");
assert_eq!(updated, 1, "mutate updated the row via the typed where + set");
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t14_introspection_resolves_a_table_outside_current_schema() {
let base = match std::env::var("AXON_TEST_DATABASE_URL") {
Ok(d) if !d.trim().is_empty() => d,
_ => {
eprintln!("l_2 t14: AXON_TEST_DATABASE_URL unset — skipping");
return;
}
};
let schema = "axon_legacy_probe";
let sep = if base.contains('?') { '&' } else { '?' };
let dsn =
format!("{base}{sep}options=-c%20search_path%3Dpublic%2C{schema}");
let backend = match PostgresStoreBackend::connect(&dsn) {
Ok(b) => b,
Err(e) => {
eprintln!("l_2 t14: connect failed ({e}) — skipping");
return;
}
};
if backend.ping().await.is_err() {
eprintln!("l_2 t14: Postgres unreachable — skipping");
return;
}
let table = "t14_legacy_widgets";
let qualified = format!("{schema}.{table}");
exec(&backend, &format!("CREATE SCHEMA IF NOT EXISTS {schema}")).await;
exec(&backend, &format!("DROP TABLE IF EXISTS {qualified}")).await;
exec(
&backend,
&format!("CREATE TABLE {qualified} (tid uuid, label text)"),
)
.await;
let unqualified_resolves: Option<String> =
sqlx::query_scalar(&format!("SELECT to_regclass('\"{table}\"')::text"))
.fetch_one(backend.pool())
.await
.unwrap_or(None);
if unqualified_resolves.is_none() {
eprintln!(
"l_2 t14: search_path option not honoured here — skipping \
(covered by the direct-connection lane)"
);
exec(&backend, &format!("DROP SCHEMA IF EXISTS {schema} CASCADE")).await;
return;
}
let uuid = "83d078e1-b372-42ba-9572-ff8dc521386e";
backend
.insert(
&mut sc(&backend),
table,
&[("tid".into(), text(uuid)), ("label".into(), text("probe"))],
)
.await
.expect("v1.36.5 — persist resolves a table outside current_schema()");
let rows = backend
.query(&mut sc(&backend), table, &format!("tid = '{uuid}'"), &nb())
.await
.expect(
"v1.36.5 — retrieve resolves a uuid column outside current_schema()",
);
assert_eq!(rows.len(), 1, "the row in the legacy schema round-trips");
assert_eq!(rows[0].get("label"), Some(&serde_json::json!("probe")));
exec(&backend, &format!("DROP TABLE IF EXISTS {qualified}")).await;
exec(&backend, &format!("DROP SCHEMA IF EXISTS {schema} CASCADE")).await;
}
#[tokio::test]
async fn t15_coherent_session_miss_then_hit() {
let backend = pg_or_skip!();
let table = "coherent_2";
fresh_table(&backend, table, "id integer, label text").await;
let inserted = backend
.insert(
&mut sc(&backend),
table,
&[
("id".into(), SqlValue::Integer(1)),
("label".into(), text("miss")),
],
)
.await
.expect("v1.32.0 — the miss-path insert resolves + writes in one txn");
assert_eq!(inserted, 1);
let rows = backend
.query(&mut sc(&backend), table, "id = 1", &nb())
.await
.expect("v1.32.0 — the hit-path query needs no transaction");
assert_eq!(
rows.len(),
1,
"the miss-path write is visible to the hit-path read"
);
assert_eq!(rows[0].get("label"), Some(&serde_json::json!("miss")));
let updated = backend
.mutate(&mut sc(&backend), table, "id = 1", &[("label".into(), text("hit"))], &nb())
.await
.expect("v1.32.0 — the hit-path mutate");
assert_eq!(updated, 1);
let rows = backend.query(&mut sc(&backend), table, "id = 1", &nb()).await.expect("query");
assert_eq!(rows[0].get("label"), Some(&serde_json::json!("hit")));
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t16_d9_write_self_heals_without_double_writing() {
let backend = pg_or_skip!();
let table = "selfheal";
fresh_table(&backend, table, "id integer").await;
backend
.insert(&mut sc(&backend), table, &[("id".into(), SqlValue::Integer(1))])
.await
.expect("v1.32.0 — the seed insert");
exec(
&backend,
&format!("ALTER TABLE {table} ALTER COLUMN id TYPE text USING id::text"),
)
.await;
let inserted = backend
.insert(&mut sc(&backend), table, &[("id".into(), text("3"))])
.await
.expect("v1.32.0 (D9) — the write SELF-HEALS after the drift");
assert_eq!(inserted, 1, "the self-healed insert reports one row");
let rows = backend.query(&mut sc(&backend), table, "", &nb()).await.expect("count");
assert_eq!(
rows.len(),
2,
"v1.32.0 — exactly one row was added by the self-healed write; \
a double-write would leave three"
);
drop_table(&backend, table).await;
}
#[tokio::test]
async fn t17_d8_deploy_verification_verified_and_missing() {
let backend = pg_or_skip!();
let dsn = std::env::var("AXON_TEST_DATABASE_URL").unwrap();
let real_table = "real_2";
fresh_table(&backend, real_table, "id integer").await;
let registry = StoreRegistry::build(&[
IRAxonStore {
node_type: "axonstore",
source_line: 0,
source_column: 0,
name: real_table.to_string(),
backend: "postgresql".to_string(),
connection: dsn.clone(),
confidence_floor: None,
isolation: String::new(),
on_breach: String::new(),
capability: String::new(),
class: String::new(),
column_schema: None,
resource_ref: String::new(),
},
IRAxonStore {
node_type: "axonstore",
source_line: 0,
source_column: 0,
name: "ghost_table".to_string(),
backend: "postgresql".to_string(),
connection: dsn.clone(),
confidence_floor: None,
isolation: String::new(),
on_breach: String::new(),
capability: String::new(),
class: String::new(),
column_schema: None,
resource_ref: String::new(),
},
])
.expect("registry build");
let report = registry.verify_postgres_schemas().await;
assert!(
report.verified.contains(&real_table.to_string()),
"v1.32.0 — the real table verified at deploy"
);
assert_eq!(
report.missing.len(),
1,
"v1.32.0 — the ghost table is a fatal `missing` entry"
);
assert_eq!(report.missing[0].0, "ghost_table");
assert!(
report.has_fatal(),
"v1.32.0 (D8) — a missing table on a reachable store FAILS the deploy"
);
let diag = &report.missing[0].1;
assert!(
diag.contains("database:"),
"v1.32.0 (D6) — the missing diagnostic must surface the \
physical-database context, got: {diag}"
);
assert!(
diag.contains("***") || !diag.contains('@'),
"v1.32.0 (D6) — when the DSN carries credentials they MUST be \
masked (`***`), got: {diag}"
);
assert!(
diag.contains("pg_catalog"),
"v1.32.0 (D6) — the actionable hint (pg_catalog scan, so \
`search_path` is not the culprit) must survive composition \
with the masked DSN, got: {diag}"
);
drop_table(&backend, real_table).await;
}