Skip to content

Commit 94120ef

Browse files
authored
fix(db): index decision_ledger_anchors on (row_hash, status) (#9830)
`anchorBackendsMissingForRowHash` (#9489) runs `SELECT DISTINCT backend FROM decision_ledger_anchors WHERE row_hash = ? AND status = 'ok' AND backend IN (...)` once per row being re-anchored, but the table had no index on row_hash — so each call was a full table scan that grows with the anchor ledger. Add migration 0203 creating `decision_ledger_anchors_row_hash_status (row_hash, status)` — row_hash first (the equality predicate), status included so the `status = 'ok'` filter is served from the index rather than a row fetch. Mirrors the decision_ledger_record_id index added in 0198. The migration contains nothing else. (Issue named 0202; that number is now taken, so this is the next contiguous 0203.) A new test asserts the index exists after replaying migrations and that the real query SEARCHes via it (not SCAN); the existing anchorBackendsMissingForRowHash behaviour cases confirm results are unchanged. Closes #9652
1 parent 4b1f5e2 commit 94120ef

2 files changed

Lines changed: 24 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- #9489: anchorBackendsMissingForRowHash queries
2+
-- `SELECT DISTINCT backend FROM decision_ledger_anchors WHERE row_hash = ? AND status = 'ok' AND backend IN (...)`
3+
-- once per row being re-anchored; without an index on (row_hash, status) that is a full table scan per call.
4+
-- Lead with row_hash (the equality predicate) and include status so the `status = 'ok'` filter is served from
5+
-- the index rather than a row fetch. Sibling precedent: decision_ledger_record_id in migrations/0198.
6+
CREATE INDEX IF NOT EXISTS decision_ledger_anchors_row_hash_status ON decision_ledger_anchors (row_hash, status);

test/unit/ledger-anchor-persistence.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,22 @@ describe("anchorBackendsMissingForRowHash (#9489)", () => {
240240
expect(await anchorBackendsMissingForRowHash(env, "hash-a", [])).toEqual([]);
241241
expect(spy).not.toHaveBeenCalled();
242242
});
243+
244+
it("is served by the (row_hash, status) index rather than a full table scan (#9652)", async () => {
245+
const env = createTestEnv();
246+
const idx = await env.DB.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name = ?")
247+
.bind("decision_ledger_anchors_row_hash_status")
248+
.first<{ name: string }>();
249+
expect(idx?.name).toBe("decision_ledger_anchors_row_hash_status");
250+
251+
// The exact predicate anchorBackendsMissingForRowHash runs — must SEARCH via the new index, not SCAN.
252+
const plan = await env.DB.prepare(
253+
"EXPLAIN QUERY PLAN SELECT DISTINCT backend FROM decision_ledger_anchors WHERE row_hash = ? AND status = 'ok' AND backend IN ('rekor', 'git')",
254+
)
255+
.bind("hash-a")
256+
.all<{ detail: string }>();
257+
const detail = (plan.results ?? []).map((row) => row.detail).join(" ");
258+
expect(detail).toContain("decision_ledger_anchors_row_hash_status");
259+
expect(detail).not.toContain("SCAN decision_ledger_anchors ");
260+
});
243261
});

0 commit comments

Comments
 (0)