diff --git a/migrations/0203_decision_ledger_anchors_row_hash_index.sql b/migrations/0203_decision_ledger_anchors_row_hash_index.sql new file mode 100644 index 000000000..3dc33007c --- /dev/null +++ b/migrations/0203_decision_ledger_anchors_row_hash_index.sql @@ -0,0 +1,6 @@ +-- #9489: anchorBackendsMissingForRowHash queries +-- `SELECT DISTINCT backend FROM decision_ledger_anchors WHERE row_hash = ? AND status = 'ok' AND backend IN (...)` +-- once per row being re-anchored; without an index on (row_hash, status) that is a full table scan per call. +-- Lead with row_hash (the equality predicate) and include status so the `status = 'ok'` filter is served from +-- the index rather than a row fetch. Sibling precedent: decision_ledger_record_id in migrations/0198. +CREATE INDEX IF NOT EXISTS decision_ledger_anchors_row_hash_status ON decision_ledger_anchors (row_hash, status); diff --git a/test/unit/ledger-anchor-persistence.test.ts b/test/unit/ledger-anchor-persistence.test.ts index 5d4cbb828..014f2a7dd 100644 --- a/test/unit/ledger-anchor-persistence.test.ts +++ b/test/unit/ledger-anchor-persistence.test.ts @@ -240,4 +240,22 @@ describe("anchorBackendsMissingForRowHash (#9489)", () => { expect(await anchorBackendsMissingForRowHash(env, "hash-a", [])).toEqual([]); expect(spy).not.toHaveBeenCalled(); }); + + it("is served by the (row_hash, status) index rather than a full table scan (#9652)", async () => { + const env = createTestEnv(); + const idx = await env.DB.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name = ?") + .bind("decision_ledger_anchors_row_hash_status") + .first<{ name: string }>(); + expect(idx?.name).toBe("decision_ledger_anchors_row_hash_status"); + + // The exact predicate anchorBackendsMissingForRowHash runs — must SEARCH via the new index, not SCAN. + const plan = await env.DB.prepare( + "EXPLAIN QUERY PLAN SELECT DISTINCT backend FROM decision_ledger_anchors WHERE row_hash = ? AND status = 'ok' AND backend IN ('rekor', 'git')", + ) + .bind("hash-a") + .all<{ detail: string }>(); + const detail = (plan.results ?? []).map((row) => row.detail).join(" "); + expect(detail).toContain("decision_ledger_anchors_row_hash_status"); + expect(detail).not.toContain("SCAN decision_ledger_anchors "); + }); });