Skip to content

Index /admin/audit's action filter, and answer the migration-runner question - #168

Merged
guarzo merged 3 commits into
mainfrom
worktree-audit-action-index
Aug 7, 2026
Merged

Index /admin/audit's action filter, and answer the migration-runner question#168
guarzo merged 3 commits into
mainfrom
worktree-audit-action-index

Conversation

@guarzo

@guarzo guarzo commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes the two index-related rows in docs/design-sweep/SECOND-PASS.md section 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

queryAuditLog matches action with a LIKE prefix, and under this deployment's en_US.utf8 collation a plain btree can't serve LIKE 'x%' without text_pattern_ops — EXPLAIN puts it in Filter, not Index Cond. audit_log_action_target_id_idx does 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/audit passes no limit and queryAuditLog falls back to AUDIT_PAGE_SIZE — any prefix with recent matches is already answered in under 0.2 ms by a backward audit_log_pkey scan and never touches the new index.

The entire cost is the tail:

audit_log rows zero-match prefix, no index with index
40k (current) 2.3 ms 0.083 ms
500k 26 ms 0.077 ms
1M 52 ms 0.078 ms
2M 80 ms 0.090 ms

What 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. And src/jobs/purge.ts covers sessions, OAuth transactions and outbox only, so audit_log grows 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 distinct id per row defeats the dedup.

Two hazards that would have silently made the index useless were tested rather than assumed — escaped underscores (queryAuditLog escapes _, 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 CONCURRENTLY available. Answer: not yet. The trigger condition is recorded in docs/ops.md instead of machinery being built.

audit_log rows plain CREATE INDEX index size
40k (current) 33 ms 304 kB
500k 367 ms 3.4 MB
1M 816 ms 6.8 MB
2M 1.58 s 14 MB

Revisit 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.md also records the failure mode to design for — a failed CONCURRENTLY build 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.ts and fly.toml are unchanged. Nothing about how migrations are applied changed.

Notes for review

  • Migration generated via npm run db:generate from src/db/schema.ts. No applied migration touched; drizzle/meta/_journal.json is a pure append and 0011_snapshot.json's prevId chains to 0010.
  • src/services/audit.ts is a comment-only change: logAuditIfChanged's docblock said nothing indexed the LIKE filter, which is now false.
  • Numbers come from a local Postgres 16.11 on en_US.utf8 seeded 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 in docs/ops.md is a row count you can check with fly pg connect, not a latency threshold.

Verification

CI is currently unreliable, so all five gates were run locally:

npm run format:check → All matched files use Prettier code style!
npm run typecheck    → tsc --noEmit, no diagnostics
npm run lint         → eslint ., no diagnostics
npm test             → Test Files 80 passed (80) | Tests 1217 passed (1217)
npm run build        → ✓ Compiled successfully; static pages (7/7)

End-to-end: the real chain was applied with npx tsx src/db/migrate.ts to a scratch DB, \d audit_log shows "audit_log_action_pattern_idx" btree (action text_pattern_ops), and EXPLAIN confirms Index Scan ... Index Cond for the zero-match case rather than a seq scan.

Summary by CodeRabbit

  • Performance Improvements

    • Improved performance for audit log searches that filter actions by prefix.
    • Added a dedicated database index to support faster prefix-based filtering.
  • Documentation

    • Added operational guidance for applying and monitoring audit log index migrations.
    • Documented expected migration behavior, performance considerations, row-count checks, and recovery steps for failed index builds.

…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.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5ab6b4c1-3434-4ea3-8bf8-b0b84cc3d012

📥 Commits

Reviewing files that changed from the base of the PR and between b600445 and afa7ce8.

⛔ Files ignored due to path filters (1)
  • docs/design-sweep/SECOND-PASS.md is excluded by !docs/design-sweep/**
📒 Files selected for processing (1)
  • docs/ops.md

📝 Walkthrough

Walkthrough

The audit log now has a text_pattern_ops index for action prefix queries. Schema and service comments describe index coverage. Operational documentation records locking behavior, measured build times, row-count checks, and recovery commands.

Changes

Audit action index

Layer / File(s) Summary
Define and document the action index
src/db/schema.ts, drizzle/0011_cold_gargoyle.sql, src/services/audit.ts
Added audit_log_action_pattern_idx on audit_log.action with text_pattern_ops in src/db/schema.ts:265-283 and drizzle/0011_cold_gargoyle.sql:1. Updated the audit service comment in src/services/audit.ts:48-49.
Document index migration operations
docs/ops.md
Added measured build times and locking details in docs/ops.md:196-217, row-count commands in docs/ops.md:243-246, and concurrent recovery commands in docs/ops.md:291-296.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • guarzo/authGD#163: Both changes update audit-log index coverage and related service documentation.

Poem

An action index takes its place,
Prefix queries gain a trace.
Locks and timings now are clear,
Recovery commands persevere.
Audit searches run their course.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes the audit action filter but does not use the required Conventional Commit format. Rename it to a format such as perf(audit): add index for action prefix filtering. I cannot assess the migration-runner wording as the required summary.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly covers the changes, rationale, verification, deployment implications, and known limitations, despite omitting the template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4c543da and e2ede03.

⛔ Files ignored due to path filters (3)
  • docs/design-sweep/SECOND-PASS.md is excluded by !docs/design-sweep/**
  • drizzle/meta/0011_snapshot.json is excluded by !drizzle/meta/**
  • drizzle/meta/_journal.json is excluded by !drizzle/meta/**
📒 Files selected for processing (4)
  • docs/ops.md
  • drizzle/0011_cold_gargoyle.sql
  • src/db/schema.ts
  • src/services/audit.ts

Comment thread docs/ops.md Outdated
Comment thread docs/ops.md Outdated
guarzo added 2 commits August 6, 2026 19:36
…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
@guarzo
guarzo merged commit a4a292b into main Aug 7, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant