///|
fn classify_refresh(words : ArrayView[String]) -> Command {
let concurrent = words.get(3) == Some("CONCURRENTLY")
let start = if concurrent { 4 } else { 3 }
let end = match relation_name_end(words, start) {
Some(end) => end
None => return Unsupported
}
let no_data = match words[end:] {
[] | ["WITH", "DATA"] => false
["WITH", "NO", "DATA"] => true
_ => return Unsupported
}
Refresh(concurrent~, no_data~)
}
///|
fn refresh_findings(
concurrent : Bool,
no_data : Bool,
span : Span,
diagnostics : Array[Diagnostic],
) -> Unit {
diagnostics.push(
finding(
"ML170",
Warning,
span,
if concurrent {
"Concurrent materialized-view refresh takes EXCLUSIVE locks while allowing ordinary SELECT"
} else {
"Materialized-view refresh takes ACCESS EXCLUSIVE locks and blocks readers"
},
"Locks are held until transaction end; only one refresh can run per view. Review MAINTAIN privilege, backing-query cost and temporary space. lock_timeout bounds individual lock waits, not refresh duration; result ordering is not preserved by contract.",
),
)
if concurrent {
diagnostics.push(
finding(
"ML171",
Warning,
span,
"Concurrent refresh requires a populated view and a qualifying UNIQUE index",
"Verify catalog state: at least one UNIQUE index must cover all rows with plain column keys, without expressions or a WHERE predicate. An unpopulated view cannot be refreshed concurrently. These preconditions are not established by offline SQL review.",
),
)
}
if no_data {
diagnostics.push(
finding(
"ML172",
Warning,
span,
"WITH NO DATA leaves the materialized view unscannable",
"Readers fail until a successful non-concurrent WITH DATA refresh repopulates the view. Plan the outage and follow-up refresh; this statement does not retain the old query results.",
),
)
}
}