use saya_connectors::SqlReferences;
use saya_types::{ClaimId, ClaimStatus};
use super::receipt::{RecallReceipt, SuppliedClaim, SuppliedContract};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OverrideFinding {
pub claim_id: ClaimId,
pub kind: &'static str,
pub claimed_value: String,
pub observed_columns: Vec<String>,
}
pub(crate) const HANDLED_KIND: &str = "default_time_column";
pub(crate) fn detect_overrides(
receipt: &RecallReceipt,
refs: Option<&SqlReferences>,
) -> Vec<OverrideFinding> {
let Some(refs) = refs else {
return Vec::new();
};
if refs.partial {
return Vec::new();
}
let Some((object, columns)) = single_object_table(refs) else {
return Vec::new();
};
let mut findings = Vec::new();
for contract in &receipt.supplied {
if !object_matches(object, contract, &receipt.supplied) {
continue;
}
for claim in &contract.claims {
if let Some(finding) = contradicted_default_time_column(claim, columns) {
findings.push(finding);
}
}
}
findings
}
fn single_object_table(refs: &SqlReferences) -> Option<(&[String], &[String])> {
if refs.objects.len() == 1 {
Some((refs.objects[0].as_slice(), refs.columns.as_slice()))
} else {
None
}
}
fn object_matches(
object: &[String],
contract: &SuppliedContract,
supplied: &[SuppliedContract],
) -> bool {
let matches: Vec<&SuppliedContract> = supplied
.iter()
.filter(|c| suffix_aligns(object, &c.object))
.collect();
matches.len() == 1 && matches[0].object == contract.object
}
fn suffix_aligns(object: &[String], qualified: &str) -> bool {
let parts = name_parts(qualified);
if object.len() > parts.len() {
return false;
}
let offset = parts.len() - object.len();
object
.iter()
.zip(parts[offset..].iter())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
fn name_parts(qualified: &str) -> Vec<String> {
qualified.split('.').map(String::from).collect()
}
fn contradicted_default_time_column(
claim: &SuppliedClaim,
columns: &[String],
) -> Option<OverrideFinding> {
if claim.status != ClaimStatus::Confirmed {
return None;
}
if claim.kind != HANDLED_KIND {
return None;
}
let claimed = claim.value.as_str();
if claimed.is_empty() {
return None;
}
let claimed_referenced = columns.iter().any(|c| c.eq_ignore_ascii_case(claimed));
if claimed_referenced {
return None;
}
let mut observed: Vec<String> = columns
.iter()
.filter(|c| !c.eq_ignore_ascii_case(claimed) && is_time_named(c))
.cloned()
.collect();
if observed.is_empty() {
return None;
}
observed.sort();
observed.dedup();
Some(OverrideFinding {
claim_id: claim.claim_id.clone(),
kind: claim.kind,
claimed_value: claim.value.clone(),
observed_columns: observed,
})
}
fn is_time_named(column: &str) -> bool {
const EXACT: &[&str] = &["date", "time", "timestamp"];
const SUFFIX: &[&str] = &["_date", "_time", "_timestamp", "_at", "_ts", "_dt"];
let lower = column.to_ascii_lowercase();
if EXACT.contains(&lower.as_str()) {
return true;
}
SUFFIX.iter().any(|s| lower.ends_with(s))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::RecallOutcomeKind;
use saya_connectors::sql_references;
use saya_types::{ClaimId, ClaimStatus, SqlDialect};
const D: SqlDialect = SqlDialect::Postgres;
fn rental_return_date_receipt() -> RecallReceipt {
one_contract_receipt(ClaimStatus::Confirmed, "return_date")
}
fn one_contract_receipt(status: ClaimStatus, value: &str) -> RecallReceipt {
RecallReceipt {
kind: RecallOutcomeKind::Ran {
store_unavailable: false,
},
supplied: vec![SuppliedContract {
profile: "pagila".into(),
object: "pagila.public.rental".into(),
schema_state: "current",
claims: vec![SuppliedClaim {
claim_id: ClaimId::parse("c-rental-time").unwrap(),
kind: "default_time_column",
value: value.into(),
column: None,
status,
}],
}],
dropped_by_bounds: 0,
}
}
fn refs_for(sql: &str) -> Option<SqlReferences> {
sql_references(sql, D)
}
#[test]
fn confirmed_time_column_overridden_by_a_different_time_column() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT rental_date FROM rental WHERE rental_date > '2024-01-01'");
let findings = detect_overrides(&receipt, refs.as_ref());
assert_eq!(findings.len(), 1);
let f = &findings[0];
assert_eq!(f.claim_id, ClaimId::parse("c-rental-time").unwrap());
assert_eq!(f.kind, "default_time_column");
assert_eq!(f.claimed_value, "return_date");
assert_eq!(f.observed_columns, vec!["rental_date".to_string()]);
}
const LIVE_OVERRIDE_SQL: &str = "SELECT TO_CHAR(rental_date, 'YYYY-MM') AS month, \
COUNT(*) AS rental_count FROM pagila.public.rental \
WHERE rental_date >= '2022-01-01' AND rental_date < '2023-01-01' \
GROUP BY TO_CHAR(rental_date, 'YYYY-MM') ORDER BY month";
const LIVE_COMPLIANT_SQL: &str = "SELECT date_trunc('month', return_date) AS month, \
COUNT(*) AS rental_count FROM pagila.public.rental \
WHERE return_date >= '2022-01-01' AND return_date < '2023-01-01' \
GROUP BY date_trunc('month', return_date) ORDER BY month";
#[test]
fn the_real_generated_override_sql_is_detected() {
let findings = detect_overrides(
&rental_return_date_receipt(),
refs_for(LIVE_OVERRIDE_SQL).as_ref(),
);
assert_eq!(
findings.len(),
1,
"the SQL from the live override must produce exactly one finding: {findings:?}"
);
assert_eq!(findings[0].claimed_value, "return_date");
assert!(
findings[0]
.observed_columns
.contains(&"rental_date".to_string()),
"the finding must name the column actually referenced: {findings:?}"
);
}
#[test]
fn the_real_compliant_sql_is_not_flagged() {
let findings = detect_overrides(
&rental_return_date_receipt(),
refs_for(LIVE_COMPLIANT_SQL).as_ref(),
);
assert!(
findings.is_empty(),
"honouring the claim must never be reported as an override: {findings:?}"
);
}
#[test]
fn claimed_column_used_is_no_override() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT return_date FROM rental WHERE return_date > '2024-01-01'");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn non_time_column_does_not_contradict() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT amount FROM rental");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn candidate_claim_is_not_overridable() {
let receipt = one_contract_receipt(ClaimStatus::Candidate, "return_date");
let refs = refs_for("SELECT rental_date FROM rental WHERE rental_date > '2024-01-01'");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
let confirmed = rental_return_date_receipt();
assert_eq!(
detect_overrides(&confirmed, refs.as_ref()).len(),
1,
"the same SQL must fire for a confirmed claim, proving the status guard is the gate"
);
}
#[test]
fn unparseable_sql_yields_no_findings() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT FROM WHERE");
assert!(refs.is_none(), "fixture must be unparseable");
assert!(detect_overrides(&receipt, None).is_empty());
}
#[test]
fn partial_column_list_yields_no_findings() {
let receipt = rental_return_date_receipt();
let refs = SqlReferences {
objects: vec![vec!["rental".to_string()]],
columns: vec!["rental_date".to_string()],
partial: true,
};
assert!(refs.partial, "fixture must be partial");
assert!(
detect_overrides(&receipt, Some(&refs)).is_empty(),
"under partial, the claimed column's absence is not reliable → no finding"
);
}
#[test]
fn different_object_is_no_override() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT rental_date FROM film");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn same_inputs_same_findings_same_order() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT rental_date FROM rental WHERE rental_date > '2024-01-01'");
let a = detect_overrides(&receipt, refs.as_ref());
let b = detect_overrides(&receipt, refs.as_ref());
assert_eq!(a, b);
let refs2 = refs_for("SELECT rental_date FROM rental");
assert_eq!(
detect_overrides(&receipt, refs2.as_ref()),
detect_overrides(&receipt, refs2.as_ref())
);
}
#[test]
fn only_default_time_column_is_handled() {
let receipt = RecallReceipt {
kind: RecallOutcomeKind::Ran {
store_unavailable: false,
},
supplied: vec![SuppliedContract {
profile: "pagila".into(),
object: "pagila.public.rental".into(),
schema_state: "current",
claims: vec![SuppliedClaim {
claim_id: ClaimId::parse("c-alias").unwrap(),
kind: "table_alias",
value: "r".into(),
column: None,
status: ClaimStatus::Confirmed,
}],
}],
dropped_by_bounds: 0,
};
let refs = refs_for("SELECT * FROM rental AS x");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn multi_table_statement_yields_no_findings() {
let receipt = rental_return_date_receipt();
let refs = refs_for(
"SELECT rental_date FROM rental JOIN payment ON rental.payment_id = payment.id",
);
assert!(refs.as_ref().map(|r| r.objects.len()).unwrap_or(0) > 1);
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn ambiguous_bare_object_matches_neither() {
let receipt = RecallReceipt {
kind: RecallOutcomeKind::Ran {
store_unavailable: false,
},
supplied: vec![
SuppliedContract {
profile: "pagila".into(),
object: "pagila.public.rental".into(),
schema_state: "current",
claims: vec![SuppliedClaim {
claim_id: ClaimId::parse("c-a").unwrap(),
kind: "default_time_column",
value: "return_date".into(),
column: None,
status: ClaimStatus::Confirmed,
}],
},
SuppliedContract {
profile: "archive".into(),
object: "archive.public.rental".into(),
schema_state: "current",
claims: vec![SuppliedClaim {
claim_id: ClaimId::parse("c-b").unwrap(),
kind: "default_time_column",
value: "return_date".into(),
column: None,
status: ClaimStatus::Confirmed,
}],
},
],
dropped_by_bounds: 0,
};
let refs = refs_for("SELECT rental_date FROM rental");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn disagreeing_schema_qualifier_is_no_match() {
let receipt = rental_return_date_receipt();
let refs = refs_for("SELECT rental_date FROM warehouse.rental");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
#[test]
fn case_fold_keeps_claimed_column_match() {
let receipt = one_contract_receipt(ClaimStatus::Confirmed, "Return_Date");
let refs = refs_for("SELECT return_date FROM rental WHERE return_date > '2024-01-01'");
assert!(detect_overrides(&receipt, refs.as_ref()).is_empty());
}
}