Index /admin/audit's action filter, and answer the migration-runner question - #168
Conversation
…uestion Two open rows from SECOND-PASS.md section 4b, both settled by measurement rather than by argument. Adds audit_log_action_pattern_idx on `action text_pattern_ops`, which is what /admin/audit's LIKE-prefix filter needs under this deployment's en_US.utf8 collation. Measuring first changed the case for it: at the page's real query shape (ORDER BY id DESC LIMIT 100 — the page passes no limit, so queryAuditLog falls back to AUDIT_PAGE_SIZE), any prefix with recent matches is already answered in under 0.2ms by a backward audit_log_pkey scan and never touches the new index. The whole cost is the tail: a prefix with few or no recent rows falls back to a full seq scan — 2.3ms at 40k rows, 26ms at 500k, 52ms at 1M, 80ms at 2M, against a flat 0.08-0.09ms indexed. The filter is a free-text box, so a typo is a zero-match prefix, which is the worst case: a full scan for a page that returns nothing. audit_log is never purged. Single-column, not composite: btree deduplication keeps it at 304kB (14MB at 2M), while (action, id DESC) was measured and rejected at 82MB for no gain, since a distinct id per row defeats the dedup. The related row — whether migrations need a runner that can apply statements outside a transaction, so CREATE INDEX CONCURRENTLY becomes available — is answered no, deliberately, and the trigger condition is recorded in docs/ops.md instead of machinery being built. A plain CREATE INDEX holds its SHARE lock for 33ms at current scale; revisit at ~1M rows, where it is 816ms. The two answers resolve each other: adding this index now, while the build is 33ms, is what keeps the runner unnecessary. src/db/migrate.ts is unchanged. Migration generated via npm run db:generate; no applied migration touched.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe audit log now has a ChangesAudit action index
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ops.md`:
- Around line 76-78: Update the database inspection command in the operations
documentation to use fly postgres connect with the application name and database
name, removing the invalid -c SQL usage. Instruct users to execute SELECT
count(*) FROM audit_log; after connecting at the psql prompt.
- Around line 88-90: Update the failed CONCURRENTLY index recovery guidance in
the “Whichever route” section of docs/ops.md to document DROP INDEX CONCURRENTLY
followed by retrying CREATE INDEX CONCURRENTLY, or alternatively REINDEX INDEX
CONCURRENTLY. State that each command must run outside a transaction block and
avoid recommending plain DROP INDEX.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b11d1ecb-e9a3-45dc-9a4e-49b9fd46ab47
⛔ Files ignored due to path filters (3)
docs/design-sweep/SECOND-PASS.mdis excluded by!docs/design-sweep/**drizzle/meta/0011_snapshot.jsonis excluded by!drizzle/meta/**drizzle/meta/_journal.jsonis excluded by!drizzle/meta/**
📒 Files selected for processing (4)
docs/ops.mddrizzle/0011_cold_gargoyle.sqlsrc/db/schema.tssrc/services/audit.ts
…recovery fly postgres connect has no flag for passing SQL — -c is the app config path, as this same file already says at the connection-headroom check. Use the interactive form and run the count at the psql prompt. Recovery from a failed CONCURRENTLY build was written as plain DROP INDEX / REINDEX, both of which take ACCESS EXCLUSIVE — the lock the whole section exists to avoid. Name the concurrent forms and the outside-a-transaction requirement.
…-index # Conflicts: # docs/design-sweep/SECOND-PASS.md
Closes the two index-related rows in
docs/design-sweep/SECOND-PASS.mdsection 4b. Both were "is it worth it yet" questions rather than bugs, so both were measured before deciding.1. The action filter gets its index
queryAuditLogmatchesactionwith a LIKE prefix, and under this deployment'sen_US.utf8collation a plain btree can't serveLIKE 'x%'withouttext_pattern_ops— EXPLAIN puts it inFilter, notIndex Cond.audit_log_action_target_id_idxdoes not cover it, despite looking like it should.Measuring changed the case for it. The row assumed a slow seq scan; that isn't what most filters do. At the page's real query shape —
ORDER BY id DESC LIMIT 100, since/admin/auditpasses nolimitandqueryAuditLogfalls back toAUDIT_PAGE_SIZE— any prefix with recent matches is already answered in under 0.2 ms by a backwardaudit_log_pkeyscan and never touches the new index.The entire cost is the tail:
audit_logrowsWhat decided it: the filter is a free-text box, so a typo (
teir.) is a zero-match prefix — the worst case, a full scan for a page returning nothing. Andsrc/jobs/purge.tscovers sessions, OAuth transactions and outbox only, soaudit_loggrows without bound.Single-column, not composite. Btree deduplication keeps it at 304 kB (14 MB at 2M).
(action, id DESC)was measured and rejected: no gain, 82 MB at 2M, because a distinctidper row defeats the dedup.Two hazards that would have silently made the index useless were tested rather than assumed — escaped underscores (
queryAuditLogescapes_, and 25 of the 49 action literals contain one) and bind-parameter generic plans. Both clear; details in the new section 4c.2. No migration runner — deliberately
Whether to build a runner that can apply statements outside a transaction, making
CREATE INDEX CONCURRENTLYavailable. Answer: not yet. The trigger condition is recorded indocs/ops.mdinstead of machinery being built.audit_logrowsCREATE INDEXRevisit at ~1M rows. Above it, the recommendation is applying such a migration out-of-band, not the
COMMIT;-in-the-file hack, which makes migration files carry runner semantics and requires hand-editing a generated migration.docs/ops.mdalso records the failure mode to design for — a failedCONCURRENTLYbuild leaves an INVALID index in a release step nobody watches — with the recovery query.The two answers resolve each other: adding this index now, while the build is 33 ms, is what keeps the runner unnecessary. Deferring it lands the build in the ~1 s regime that would have justified the machinery.
src/db/migrate.tsandfly.tomlare unchanged. Nothing about how migrations are applied changed.Notes for review
npm run db:generatefromsrc/db/schema.ts. No applied migration touched;drizzle/meta/_journal.jsonis a pure append and0011_snapshot.json'sprevIdchains to0010.src/services/audit.tsis a comment-only change:logAuditIfChanged's docblock said nothing indexed the LIKE filter, which is now false.en_US.utf8seeded to a production-shaped action distribution — not from the Fly database. The curve shape and plan choices are the durable part; absolute milliseconds will differ. That's why the trigger indocs/ops.mdis a row count you can check withfly pg connect, not a latency threshold.Verification
CI is currently unreliable, so all five gates were run locally:
End-to-end: the real chain was applied with
npx tsx src/db/migrate.tsto a scratch DB,\d audit_logshows"audit_log_action_pattern_idx" btree (action text_pattern_ops), and EXPLAIN confirmsIndex Scan ... Index Condfor the zero-match case rather than a seq scan.Summary by CodeRabbit
Performance Improvements
Documentation