Skip to content

fix(admin): make database VACUUM actually run - #388

Merged
EraPartner merged 3 commits into
mainfrom
claude/project-thread-l61da4
Oct 29, 2026
Merged

EraPartner merged 3 commits into
mainfrom
claude/project-thread-l61da4

Conversation

@EraPartner

@EraPartner EraPartner commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Requested by EraPartner · project thread

Summary

"Vacuum all tables" (and per-table vacuum) on /admin/db did nothing. The backend pool runs as the least-privilege app role, which owns no tables. PostgreSQL then skips each table with WARNING: permission denied to vacuum "<t>", skipping it and the statement succeeds, so the route returned { vacuumed: "all" } and the page toasted "Vacuum completed". The 42501 handler never fired. The page also showed only last_autovacuum/last_autoanalyze, so even a working manual run would not change what you saw.

Reproduced on a local PostgreSQL 16 cluster (owner role + DML-only app role): both VACUUM ANALYZE and VACUUM ANALYZE t as the app role exit 0 with the skip warning, and last_vacuum stays NULL. PostgreSQL 18 uses the same warn-and-skip path, gated by MAINTAIN.

Changes (ADR-262, option chosen by Tor in the thread):

  • config/postgres/app-role-grants.sql.tpl: GRANT MAINTAIN ON ALL TABLES IN SCHEMA public + matching default privilege.
  • roleBootstrap.ts: the per-relation expansion of ALL TABLES grants now takes privileges from the template statement and applies only those valid for the relation kind (matview: SELECT, MAINTAIN; view/foreign table: no MAINTAIN). Existing installs get the grant on next launch.
  • routes/admin.ts: checks has_table_privilege(relid, 'MAINTAIN') first; any requested table without it → 403, nothing runs. "All" names every public table explicitly (a bare VACUUM also tries system catalogs).
  • Stats: adds last_vacuum, last_analyze (contract, openapi, generated types); the page shows the later of manual and automatic.

Type of change

  • fix — bug fix

Checklist

  • Lint clean (lint:backend, frontend lint 0 errors, lint:dead)
  • Types check (frontend, backend, packages)
  • Conventional Commit title
  • Affected docs/ pages updated (docs/features/database-maintenance.md, docs/api/admin.md, docs/guides/native-macos-runtime.md, docs/reference/api-endpoint-matrix.md)
  • No new i18n strings

Architecture & data

  • New ADR: docs/adr/262-app-role-maintain-for-admin-vacuum.md
  • API change, non-breaking: two new nullable fields on DbTableStat; 403 now actually returned
  • No schema change (grant change only, applied by the role bootstrap)

Verification

  • Local: adminValidation.test.ts 10/10 (new: 403 for missing MAINTAIN, explicit table list for "all"); frontend DbMaintenancePage, msw contracts, admin pages 175/175.
  • CI on 61b6fe8: Test (Backend) green, which runs the DB suites on PostgreSQL 18 (roleBootstrap.db.test.ts MAINTAIN assertions, aiAuditRowContracts.db.test.ts vacuum → last_vacuum set). Not run locally (PG18 unavailable here).

Residual risk & follow-ups

  • MAINTAIN also allows ANALYZE/CLUSTER/REINDEX/REFRESH MATERIALIZED VIEW/LOCK TABLE on public tables; no DDL or data rights added.
  • Not checked on the Mac.

🤖 Generated with Claude Code

https://claude.ai/code/session_018QJtaKQ6oXhXgrVsDF6tTG

The backend runs VACUUM as the least-privilege app role, which owns no
tables. PostgreSQL skipped every table with only a warning, so "Vacuum
all tables" and per-table vacuum did nothing while the page reported
success.

- Grant MAINTAIN on public tables (and future ones) to the app role.
- Check MAINTAIN before vacuuming and return 403 instead of a false
  success; name every public table explicitly for "all".
- Expose last_vacuum/last_analyze and show the later of manual and
  automatic runs, so a manual vacuum is visible on the page.
- ADR-262.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QJtaKQ6oXhXgrVsDF6tTG
@EraPartner EraPartner self-assigned this Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Coverage Report for backend

Status Category Percentage Covered / Total
🟢 Lines 91.06% (🎯 80%) 23181 / 25455
🟢 Statements 89.33% (🎯 78%) 24947 / 27926
🟢 Functions 91.45% (🎯 76%) 5010 / 5478
🟢 Branches 81.6% (🎯 68%) 17489 / 21431
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/node-backend/src/database/roleBootstrap.ts 81.48% 75% 85.71% 81.13% 105, 218-221, 300-303, 312-315, 335-338, 354-357, 385-393, 419-428, 440-443
apps/node-backend/src/database/rows/admin.ts 100% 100% 100% 100%
apps/node-backend/src/routes/admin.ts 94.02% 74.13% 96.87% 93.84% 101-102, 119-124, 432-439
Generated in workflow #1964 for commit 6be9fcc by the Vitest Coverage Report Action

@EraPartner
EraPartner marked this pull request as ready for review October 29, 2026 19:53
@EraPartner
EraPartner enabled auto-merge (squash) October 29, 2026 19:53

EraPartner commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner Author

Test (Frontend 1/2) failed on 6be9fcc in src/pages/__tests__/TransactionsPage.integration.test.tsx:769: a waitFor on the includeBalance request param hit its 4 s timeout. I don't think this PR caused it. The diff doesn't touch the Transactions page or its API, the same shard passed on 61b6fe8, and the file passes 30/30 three times in a row locally on 6be9fcc. No fix exists yet. I tried to re-run the failed jobs, but that returned 403 (the integration has no permission), so the re-run needs a maintainer to click "Re-run failed jobs".


Generated by Claude Code

@EraPartner
EraPartner merged commit be31315 into main Oct 29, 2026
17 of 19 checks passed
@EraPartner
EraPartner deleted the claude/project-thread-l61da4 branch October 29, 2026 21:21
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.

2 participants