Renames that quietly unhook a policy predicate
Postgres tracks column renames faithfully wherever the column is referenced by ID — views, defaults, constraints. Policies are the exception that bites: their expressions persist as deparsed text, and a rename leaves that text pointing at a name that no longer exists.
Outcomes split by luck. Queries reaching the broken predicate may error loudly with undefined-column complaints, or — when a compatible column happens to exist — evaluate silently against the wrong thing. Both appear in real incidents; the quiet variant costs weeks. Catalog snapshots catch the pair mechanically: RowShield diffs each scan against the previous, surfacing the policy-text divergence beside the column change. Monitoring behaviour, not a named rule — stated plainly here as always.
RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.
Why policies differ from views
Views store parsed query trees, so Postgres rewires them across renames automatically. Policy expressions predate that machinery: qual and with_check live in pg_policies as text. Rename underneath and the text stays frozen mid-sentence, grammatically plausible and semantically orphaned.
Supabase’s SQL editor makes renames one click, AI migrations emit them freely during refactors, and nothing in the flow warns that five policies elsewhere just went stale — which is why the failure keeps recurring despite being thoroughly understood. The recurrence has an operational shape: renames ship as routine refactors, reviewed as routine refactors, merged alongside unrelated changes. Reviewers diff application code rather than catalog text, so stale predicates pass review unnoticed and surface later as errors nobody connects to the merge that caused them.
Finding orphaned predicates immediately after a rename
Resolve every policy expression against current columns; the cheap version catches the common case in seconds. Run it immediately after the rename completes, before anything else ships: catching orphaned text in the same working session costs one edit, while catching it next sprint means reconstructing which deploy carried the change:
SELECT policyname, tablename, qual FROM pg_catalog.pg_policies WHERE schemaname = 'public' AND tablename = 'projects' AND qual LIKE '%user_id%'; -- the name you just renamed away
Fixing forward, then proving it
Rewrite affected policies explicitly rather than hoping the planner forgives the stale name, then prove the rewrite behaviourally: query once as the anon key and once as an authenticated user, confirming the first returns nothing while the second returns only owned rows. A clean policy-update message alone proves nothing about reachability — catalog truth and wire truth agreeing is the finish line:
ALTER POLICY "owners_read_projects" ON public.projects USING ((SELECT auth.uid()) = owner_id);
What the alert looks like
A risky rename under monitoring arrives as a paired transition: policy text diverging in one scan window beside the column change that caused it. The pairing converts the classic mystery — intermittent undefined-column errors days after an innocent-looking merge — into a two-line story with both halves named.
Silence carries meaning here too. When a rename lands and no policy movement follows it in the diff, you have learned something: either nothing referenced the old name, or the references moved in the same deploy. Both readings beat wondering, which was the entire pre-monitoring alternative.
Making renames boring
Two habits defuse the class permanently. Procedurally, prefer expand-over-destructive: add the new column, backfill, switch readers, drop later — four reversible steps instead of one irreversible gamble, each independently verifiable.
Structurally, let scheduled drift alerts stand guard. A rename lands, the next scan reports the policy text diverging from reality, and the repair happens in minutes instead of after the first confused customer report. The pair appearing together in one diff window is the tell that turns mystery into chore.
Frequently asked
- Do policies break automatically on rename?
- Their stored expressions reference names, so yes — they stop resolving against the renamed column. Views self-heal; policies do not, because their conditions are stored as text rather than parse trees. Nothing prompts you otherwise: the migration completes cleanly, the dashboard shows the policy present, and the breakage hides until a query touches the stale reference. Treat any rename touching a governed table as a policy event requiring immediate re-verification.
- Will we at least get an error?
- Often, as undefined-column failures from queries that reach the broken predicate. Commands nobody exercises can stay broken silently for weeks — silence is not safety here. The quiet variant is nastier: when a compatible column happens to exist nearby, the comparison evaluates against the wrong data and authorization decisions change shape without any error anywhere. Audit paths matter more than error logs for this failure class.
- Can monitoring catch this automatically?
- Drift diffing flags the policy-text divergence in the same window as the column change, exposing the causal pair. No named rule targets renames specifically, and this page prefers saying so. Reading the pair together tells remediation from regression: a deliberate fix shows policy text changing on purpose, while an accident shows text left frozen mid-sentence. Either way the evidence arrives mechanically rather than through incident archaeology.
- Are there tools that refuse bad renames?
- Migration linters can flag DROP patterns, but resolving whether stored policy text still parses requires catalog evaluation — which is exactly what post-change scanning provides. The practical loop stays the same regardless of tooling: rename lands, next scan compares snapshots, divergence surfaces with both states attached, and the repair happens in minutes instead of after the first confused customer report.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit