mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(archive): reject a partial scope-age index in M4 shape validation
`pragma_index_info` reports an index's key columns but not whether it carries a `WHERE` predicate, so a partial index with the exact expected columns passed `scope_age_index_is_correct` and M4 wrote its marker over it. That index cannot serve the unrestricted scope-age range scan the Phase-2 prune query needs — SQLite falls back to the primary-key autoindex — so the marker would certify a missing access path. Probe `pragma_index_list`'s `partial` flag inside the same `BEGIN IMMEDIATE` transaction and treat a partial named age index like any other wrong shape: drop and rebuild it non-partial (an index carries no data, so a rebuild is safe). A mutation-sensitive test precreates a partial index with correct table and ordered columns and asserts M4 replaces it with a non-partial index before committing the marker. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -139,8 +139,9 @@ fn table_shape_matches(
|
||||
|
||||
/// Whether the scope-age index exists on `archived_event_scopes` with exactly
|
||||
/// the expected key columns in order. False when the index is absent, sits on
|
||||
/// the wrong table, or indexes the wrong columns — all of which M4 repairs by
|
||||
/// dropping and recreating it (an index carries no data, so a rebuild is safe).
|
||||
/// the wrong table, indexes the wrong columns, or is partial (carries a `WHERE`
|
||||
/// predicate) — all of which M4 repairs by dropping and recreating it (an index
|
||||
/// carries no data, so a rebuild is safe).
|
||||
pub(super) fn scope_age_index_is_correct(conn: &Connection) -> Result<bool, String> {
|
||||
let table: Option<String> = conn
|
||||
.query_row(
|
||||
@@ -154,6 +155,27 @@ pub(super) fn scope_age_index_is_correct(conn: &Connection) -> Result<bool, Stri
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// A partial index — one carrying a `WHERE` predicate — can hold the exact
|
||||
// expected key columns yet still fail to serve the unrestricted scope-age
|
||||
// range scan the prune query needs (SQLite falls back to the PK autoindex).
|
||||
// `pragma_index_info` reports key columns but NOT partiality, so probe
|
||||
// `pragma_index_list`'s `partial` flag explicitly and treat a partial index
|
||||
// like any other wrong shape (drop + rebuild non-partial). Absent from the
|
||||
// list (should not happen after the table check above) fails closed.
|
||||
let is_partial = conn
|
||||
.query_row(
|
||||
"SELECT partial FROM pragma_index_list('archived_event_scopes') WHERE name = ?1",
|
||||
params![SCOPE_AGE_INDEX_NAME],
|
||||
|r| r.get::<_, i64>(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|e| format!("shape check: scope-age index partiality: {e}"))?
|
||||
.map(|p| p != 0)
|
||||
.unwrap_or(true);
|
||||
if is_partial {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&format!(
|
||||
"SELECT name FROM pragma_index_info('{SCOPE_AGE_INDEX_NAME}') ORDER BY seqno"
|
||||
|
||||
@@ -466,6 +466,50 @@ fn test_m4_wrong_index_order_dropped_and_rebuilt() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_m4_partial_age_index_dropped_and_rebuilt_non_partial() {
|
||||
let db = NamedTempFile::new().unwrap();
|
||||
build_pre_m4_db(db.path());
|
||||
// Precreate the scope-age index with the expected NAME, right table, and
|
||||
// EXACT key order — but a `WHERE` predicate making it PARTIAL. It holds the
|
||||
// right columns, so `pragma_index_info` (key columns only) cannot tell it
|
||||
// apart from the required index; only the partiality probe catches it. A
|
||||
// partial index cannot serve the unrestricted prune-age range scan, so M4
|
||||
// must treat it like any other wrong shape and rebuild it non-partial.
|
||||
{
|
||||
let conn = Connection::open(db.path()).unwrap();
|
||||
conn.execute_batch(&format!(
|
||||
"CREATE INDEX {SCOPE_AGE_INDEX_NAME}
|
||||
ON archived_event_scopes
|
||||
(identity_pubkey, relay_url, scope_type, scope_value, archived_at, id)
|
||||
WHERE archived_at > 1000;"
|
||||
))
|
||||
.unwrap();
|
||||
// The validator must reject the partial index BEFORE M4 runs — this is
|
||||
// the exact check that fails without the partiality probe.
|
||||
assert!(
|
||||
!scope_age_index_is_correct(&conn).unwrap(),
|
||||
"a partial index with correct columns must not be certified"
|
||||
);
|
||||
}
|
||||
// M4 rebuilds the partial index into a non-partial one, then the marker lands.
|
||||
let conn = fresh(&db);
|
||||
assert_eq!(m4_marker_count(&conn), 1, "M4 completes after the rebuild");
|
||||
assert!(
|
||||
scope_age_index_is_correct(&conn).unwrap(),
|
||||
"the index was rebuilt non-partial with the correct key order"
|
||||
);
|
||||
// Prove the rebuilt index carries no `WHERE` predicate.
|
||||
let partial: i64 = conn
|
||||
.query_row(
|
||||
"SELECT partial FROM pragma_index_list('archived_event_scopes') WHERE name = ?1",
|
||||
params![SCOPE_AGE_INDEX_NAME],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(partial, 0, "the rebuilt index must not be partial");
|
||||
}
|
||||
|
||||
// ── Concurrency ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user