Fixes #23130: Ensure ltree cascade triggers can be restored from a pg_dump - #23137
Fixes #23130: Ensure ltree cascade triggers can be restored from a pg_dump#23137jnovinger wants to merge 3 commits into
Conversation
…_dump The AFTER triggers which cascade a hierarchical object's path to its descendants gated themselves on `OLD.path IS DISTINCT FROM NEW.path`. `IS DISTINCT FROM` is SQL grammar rather than a schema-qualifiable operator: it expands to the operand type's `=` operator, resolved from search_path at CREATE TRIGGER time. The ltree extension installs into `public`, so a CREATE TRIGGER replayed by pg_restore -- which sets search_path to the empty string and schema-qualifies every name it can -- could not resolve `ltree = ltree` and failed. Because psql does not stop on error by default, restoring a v4.7.0 dump reported success while silently omitting all 11 cascade triggers. Renaming or moving a group object then left its descendants' path and sort_path stale, with no error surfaced. Comparing the paths as text resolves `pg_catalog.text =` instead, which is always available. The comparison is equivalent because ltree's text I/O is byte-preserving and ltree_eq is a memcmp over the same bytes. Also reinstalls the triggers on existing databases, which carry either the old definition (upgraded in place) or no cascade trigger at all (restored from a dump), and makes InstallLtreeTriggers idempotent so it can be re-run. Adds -v ON_ERROR_STOP=1 to the documented restore procedure, which is what allowed the failure to go unnoticed.
|
@claude /review Only report on issues of potential concern; we don't need commentary on anything that doesn't require further consideration. |
|
Claude finished @jnovinger's task in 5m 44s —— View job Review: Fixes #23130 — restore-safe ltree cascade triggers
The core fix is sound. I verified the table coverage against 1.
|
|
@claude /review Only report on issues of potential concern; we don't need commentary on anything that doesn't require further consideration. Thanks, that was a useful pass. Changes are in
Fixed.
Fixed, and I took the stronger option: a rebuild now skips tables with nothing to repair rather than rewriting every row of every table.
Fixed by refusing rather than threading it through. A model which maintains
Added, between
Corrected. The docstring now says ACCESS EXCLUSIVE for the DDL with no table scan, and notes it is stronger than the backfill's ROW EXCLUSIVE and blocks readers.
Corrected. The justification was wrong; the conclusion was not.
Fixed as suggested: asserts
I dropped the bug-fix bullet, which is the part that conflicts at release time. I'm keeping the operator warning deliberately: a database restored from a v4.7.0 dump may already be silently corrupt, and that is upgrade-time guidance a changelog bullet would not carry. Happy to move it into CI is green on 3.12, 3.13 and 3.14. Locally, 79 tests across |
|
Claude finished @jnovinger's task in 3m 48s —— View job Review: Fixes #23130 — restore-safe ltree cascade triggers (round 2)
Prior round: #1, #4, #5, #6, #7 are resolved as described — I re-read each. #2 is resolved, and the skip logic is sound for the #23130 scenario specifically: the BEFORE trigger has no Two concerns, both in 1.
|
- Assert on the presence of the `::text` cast rather than PostgreSQL's exact rendering of the WHEN clause, which is an implementation detail of its deparser and would need revisiting on a future major release. - Correct 0251's docstring on locking: reinstalling a trigger takes ACCESS EXCLUSIVE, which is stronger than the ROW EXCLUSIVE held by 0242's backfill and blocks readers too. It is brief and scans nothing, but on a busy table it queues behind any long-running query. - Correct 0251's docstring on reversal: reversing 0242 drops these triggers rather than recreating them. The no-op reverse is still right, since undoing a corrective reinstall has no target state of its own. - Drop the release note's bug-fix bullet, which is generated in aggregate at release time and would conflict there. The operator warning stays: it is upgrade-time guidance that a changelog entry would not convey.
Reinstalling the cascade triggers corrects every subsequent write, but it does not repair path and sort_path values which went stale while those triggers were missing. Repairing them meant calling populate_paths_sql(), which returns a SQL string for use in a migration rather than something an operator can run, or rebuild_sort_paths(), which covers only half the problem: a stale path misplaces an object in the hierarchy, while a stale sort_path only misorders a list. The command wraps populate_paths_sql() for every core hierarchical model, or for those named on the command line. It deliberately does nothing else. Detection lives in the v4.7.1 release notes, as queries an operator can run against a replica without installing anything, which is both a better home for it and avoids re-deriving the path label width and sort_path separator that mptt_to_ltree already owns. Plugin models are excluded. populate_paths_sql() reads the name column by name while InstallLtreeTriggers accepts any column, so a plugin maintaining sort_path from another column cannot be rebuilt correctly here and needs its own repair path. Relates to #23130.
6259b5b to
eb57625
Compare
|
Closes: #23130
The bug
WHEN (OLD.path IS DISTINCT FROM NEW.path).IS DISTINCT FROMis grammar, not a schema-qualifiable operator: it expands to the operand type's=, resolved fromsearch_pathatCREATE TRIGGERtime.ltreeinstalls intopublic, sopg_restore(emptysearch_path) cannot resolveltree = ltreeand theCREATE TRIGGERfails.psqldoes not stop on error by default, so restoring a v4.7.0 dump reported success while omitting all 11 cascade triggers. Descendants then went stale on the next rename or reparent.The fix
pg_catalog.text =, always available.sort_pathis already text.dcim.0251,tenancy.0027,wireless.0024reinstall the triggers on existing databases, which carry either the old definition (upgraded in place) or none at all (restored from a dump).InstallLtreeTriggersnow drops before creating, so it converges from either state instead of failing with42710. Also unbreaks re-running it for plugins, which themptt_to_ltreedocstring points at.-v ON_ERROR_STOP=1. That omission is what made this silent, and it changes behavior: a dump which appeared to restore fine will now abort on its first error.Stale data
pathandsort_pathinstead. They take no locks, so they can be run against a replica.rebuild_ltree_pathsdoes the repair:populate_paths_sql()returns a string for migrations, andrebuild_sort_paths()covers half the problem. It wraps that helper and does nothing else.The commits
f6c4d69e0andec75e72c7close ltree cascade triggers fail to restore from a pg_dump (operator does not exist: ltree = ltree) #23130 on their own.eb57625a9is the management command, which is a debatable addition in a patch release. Drop that one commit and only the release note's pointer to it goes with it.On
::textrather than qualifying the operatorltree's text I/O is byte-preserving (memcpyin and out,ltree_eqis amemcmp), so two values are equal iff their text renderings are.OPERATOR("schema"."=")or a function-body check.::textfits better: it hardcodes no schema, so it survivesltreeliving outsidepublic. Open to the body-guard alternative if you'd rather not lean on that.Reproduced against the published v4.7 demo dump, which needs regenerating once this ships.