use super::{CheckContext, query_error_check};
use crate::doctor::{Stat, check::Check};
const NAME: &str = "pg_checksums";
const FAILURES_MIN_VERSION_NUM: i64 = 120000;
const SETTING_QUERY: &str = "
SELECT
current_setting('data_checksums') AS data_checksums,
current_setting('server_version_num')::bigint AS server_version_num
";
const FAILURES_QUERY: &str = "
SELECT
coalesce(sum(checksum_failures), 0)::bigint AS checksum_failures,
max(checksum_last_failure)::text AS checksum_last_failure
FROM pg_stat_database
";
#[derive(Debug, Clone)]
struct Checksums {
setting: String,
failures: Option<i64>,
last_failure: Option<String>,
}
fn grade(c: &Checksums) -> Check {
let enabled = c.setting == "on";
let failures = c.failures.unwrap_or(0);
let base = if failures > 0 {
let mut reason = match &c.last_failure {
Some(when) => format!(
"postgres has detected {failures} data-page checksum failure(s), most recently at {when} — this cluster has corrupt pages on disk"
),
None => format!(
"postgres has detected {failures} data-page checksum failure(s) — this cluster has corrupt pages on disk"
),
};
if !enabled {
reason.push_str(&format!(
"; data_checksums is now {:?}, so further corruption goes unnoticed",
c.setting
));
}
Check::fail(NAME, format!("{failures} checksum failures"), reason)
} else {
match c.setting.as_str() {
"on" => Check::pass(NAME, "data checksums enabled"),
"off" => Check::fail(
NAME,
"data checksums disabled",
"postgres cannot detect corrupted pages on this cluster; enable with pg_checksums -e while the cluster is shut down (or initdb --data-checksums for a new one)",
),
other => Check::warning(
NAME,
format!("data checksums {other}"),
format!(
"postgres reports data_checksums as {other:?}, which is neither on nor off; checksum protection is not confirmed"
),
),
}
};
let mut check = base
.with_detail("data_checksums", c.setting.clone())
.with_stat(
Stat::gauge("enabled", if enabled { 1.0 } else { 0.0 })
.help("Whether postgres data checksums are enabled"),
);
if let Some(failures) = c.failures {
check = check.with_detail("checksum_failures", failures).with_stat(
Stat::counter("failures", failures as f64)
.help("Postgres data-page checksum failures since the statistics were reset"),
);
}
if let Some(when) = &c.last_failure {
check = check.with_detail("checksum_last_failure", when.clone());
}
check
}
pub async fn run(ctx: CheckContext) -> Check {
let Some(client) = ctx.db.as_deref() else {
return Check::skip(
NAME,
"no DB connection",
"can't read postgres settings; db_connect reports the outage",
);
};
let row = match client.query_one(SETTING_QUERY, &[]).await {
Ok(row) => row,
Err(err) => return query_error_check(NAME, &err),
};
let setting = match row.try_get::<_, String>("data_checksums") {
Ok(setting) => setting,
Err(err) => return Check::broken(NAME, "row decode failed", err.to_string()),
};
let version_num: i64 = row.try_get("server_version_num").unwrap_or_default();
let mut checksums = Checksums {
setting,
failures: None,
last_failure: None,
};
if version_num >= FAILURES_MIN_VERSION_NUM {
match client.query_one(FAILURES_QUERY, &[]).await {
Ok(row) => {
checksums.failures = row.try_get("checksum_failures").ok();
checksums.last_failure = row.try_get("checksum_last_failure").ok();
}
Err(err) => return query_error_check(NAME, &err),
}
}
grade(&checksums)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::doctor::{
check::CheckStatus,
checks::test_support::{central_ctx, facility_ctx},
};
fn state(setting: &str, failures: Option<i64>) -> Checksums {
Checksums {
setting: setting.into(),
failures,
last_failure: None,
}
}
#[test]
fn checksums_on_and_clean_passes() {
let check = grade(&state("on", Some(0)));
assert!(matches!(check.status, CheckStatus::Pass));
assert_eq!(check.details["data_checksums"], "on");
assert_eq!(check.details["checksum_failures"], 0);
}
#[test]
fn checksums_off_fails_and_says_how_to_fix() {
match grade(&state("off", Some(0))).status {
CheckStatus::Fail(reason) => assert!(
reason.contains("pg_checksums"),
"expected the remedy in the reason, got {reason:?}"
),
other => panic!("expected a failure for disabled checksums, got {other:?}"),
}
}
#[test]
fn detected_failures_fail_with_the_count_and_time() {
let c = Checksums {
setting: "on".into(),
failures: Some(3),
last_failure: Some("2026-07-30 04:05:06+00".into()),
};
let check = grade(&c);
match &check.status {
CheckStatus::Fail(reason) => {
assert!(reason.contains('3'), "expected the count, got {reason:?}");
assert!(
reason.contains("2026-07-30"),
"expected the last-failure time, got {reason:?}"
);
}
other => panic!("expected a failure for detected corruption, got {other:?}"),
}
assert_eq!(check.summary, "3 checksum failures");
assert_eq!(check.details["checksum_failures"], 3);
assert_eq!(
check.details["checksum_last_failure"],
"2026-07-30 04:05:06+00"
);
}
#[test]
fn failures_outrank_the_setting() {
match grade(&state("off", Some(1))).status {
CheckStatus::Fail(reason) => {
assert!(
reason.contains("corrupt pages"),
"expected the corruption to lead, got {reason:?}"
);
assert!(
reason.contains("goes unnoticed"),
"expected the disabled state noted too, got {reason:?}"
);
}
other => panic!("expected a failure, got {other:?}"),
}
}
#[test]
fn unexpected_setting_warns_and_quotes_it() {
match grade(&state("inprogress", Some(0))).status {
CheckStatus::Warning(reason) => assert!(
reason.contains("inprogress"),
"expected the raw value in the reason, got {reason:?}"
),
other => panic!("expected a warning for an unrecognised value, got {other:?}"),
}
}
#[test]
fn untracked_failures_are_not_reported_as_zero() {
let check = grade(&state("on", None));
assert!(matches!(check.status, CheckStatus::Pass));
assert!(!check.details.contains_key("checksum_failures"));
assert!(check.stats.iter().all(|s| s.name != "failures"));
}
#[test]
fn metrics_cover_the_state_and_the_count() {
let check = grade(&state("on", Some(2)));
let enabled = check.stats.iter().find(|s| s.name == "enabled").unwrap();
assert_eq!(enabled.value, 1.0);
let failures = check.stats.iter().find(|s| s.name == "failures").unwrap();
assert_eq!(failures.value, 2.0);
assert_eq!(failures.kind, crate::doctor::stat::StatKind::Counter);
let off = grade(&state("off", Some(0)));
let enabled = off.stats.iter().find(|s| s.name == "enabled").unwrap();
assert_eq!(enabled.value, 0.0);
}
#[tokio::test]
async fn no_db_skips() {
let check = run(facility_ctx()).await;
assert!(matches!(check.status, CheckStatus::Skip(_)));
}
#[tokio::test]
async fn reads_the_state_from_a_live_server() {
let Some(ctx) = central_ctx().await else {
return;
};
let check = run(ctx).await;
assert!(
matches!(
check.status,
CheckStatus::Pass | CheckStatus::Warning(_) | CheckStatus::Fail(_)
),
"expected a graded outcome from a live server, got {:?}",
check.status
);
assert!(check.details.contains_key("data_checksums"));
assert!(check.details.contains_key("checksum_failures"));
}
}