perf(audit-logs): index organization-level rows and read them as two keyset parts

Merged
WA
1d ago
+31598-19

Summary

Fixes a regression from #9419 in the organization audit-log listing.

  • #9419 reads a page's ids as one keyset scan per org workspace plus one organization-level branch (workspace_id IS NULL and the org named in metadata or as the resource). No index served that branch in order, so unless the organization had a page of org-level rows near the top, every listing walked every workspace-less audit row in the table, scattered across the heap. Listings got slower and long reads on the replica began hitting recovery-conflict cancellations
  • Migration 0444 adds audit_log_org_level_created_at_id_idx, a partial index on (metadata->>'organizationId', date_trunc('milliseconds', created_at), id) WHERE workspace_id IS NULL AND metadata->>'organizationId' IS NOT NULL. It's built concurrently with the replay-safe invalid-index handling from 0442, and it only holds workspace-less rows that name an organization
  • The org-level branch is split into two disjoint keyset parts: rows naming the org in metadata (read backward through the new index, stopping after a page) and organization-resource rows that don't (read through audit_log_resource_idx, a handful per org). The index alone isn't enough: under the single OR the planner bitmap-ORs both indexes and sorts every org-level row of the organization
  • buildOrgScopeCondition (single-record read, data drain) is unchanged and shares the same two definitions of an org-level row

Type of Change

  • Bug fix
  • Other: performance

Testing

  • New list-plans.integration.ts: many workspace-less rows from other organizations, then EXPLAINs the SQL queryAuditLogs sends (current members and departed, with and without a cursor). It asserts the metadata part reads the new index and nothing seeks workspace_id IS NULL, and that the index serves it as a backward scan with the cursor in the index condition. Fails on the previous query
  • list-scope.integration.ts adds metadata-only, resource-only and both-matching rows (appear once) and a resource row naming another org, and pages across all parts at several page sizes against the single-predicate reference
  • Autopsy: #9419's tests compared results only, on a fixture of a few dozen rows where walking every workspace-less row costs nothing; nothing asserted the org-level branch's plan
  • Migration replayed with the index missing, valid and invalid; integration suites pass in migrate and push modes
  • bun run lint, type-check, check:audits, check-test-types, check:migrations, drizzle-kit generate (no changes)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)