diff --git a/docs/release-notes/version-4.7.md b/docs/release-notes/version-4.7.md index 4b585eaa8..6914471cf 100644 --- a/docs/release-notes/version-4.7.md +++ b/docs/release-notes/version-4.7.md @@ -25,10 +25,6 @@ Plugins which maintain their own `ltree` models via the `InstallLtreeTriggers` migration operation are affected in the same way, and their tables are not touched by the migrations above. Where such a database was restored from a dump, the plugin's cascade triggers are missing entirely; where it was upgraded in place, they carry the old definition and will be lost by its next dump. Either way, applying `InstallLtreeTriggers` again from a new plugin migration reinstalls them: as of this release the operation drops each trigger before recreating it, so it is safe to re-run. -### Bug Fixes - -* [#23130](https://github.com/netbox-community/netbox/issues/23130) - Ensure the triggers which cascade hierarchical paths to descendants can be restored from a `pg_dump` - --- ## v4.7.0 (2026-09-02) diff --git a/netbox/dcim/migrations/0251_fix_ltree_cascade_triggers.py b/netbox/dcim/migrations/0251_fix_ltree_cascade_triggers.py index a3de01af5..b9b6bbedd 100644 --- a/netbox/dcim/migrations/0251_fix_ltree_cascade_triggers.py +++ b/netbox/dcim/migrations/0251_fix_ltree_cascade_triggers.py @@ -11,13 +11,19 @@ are absent) and one upgraded in place (they exist with the old definition, which fail its own next restore). InstallLtreeTriggers drops before creating, so this applies cleanly in either state. -This reinstalls triggers only. It does not touch table data, so it does not incur the -table-wide lock that 0242's path backfill did, and it does not repair path/sort_path -values which went stale while the triggers were missing; see the v4.7.1 release notes for -detection and repair. +This reinstalls triggers only, so it takes ACCESS EXCLUSIVE on each table for the DDL +itself and performs no table scan. Note that this is a stronger lock than the ROW +EXCLUSIVE held by 0242's backfill, and it blocks readers as well as writers: it is brief, +but on a busy table it queues behind any long-running query and holds everything behind +it for that query's duration. -Reversing this migration is a no-op: the triggers it replaces belong to 0242_ltree_paths, -which recreates them (from the corrected template) when reversed in turn. +It does not repair path/sort_path values which went stale while the triggers were +missing; see the v4.7.1 release notes for detection and repair. + +Reversing this migration is a no-op. Reversing 0242_ltree_paths in turn drops these +triggers rather than recreating them, which is that migration's business; what matters +here is that undoing a corrective reinstall has no target state of its own, since the +definition it replaced is the broken one. """ from django.db import migrations diff --git a/netbox/utilities/tests/test_ltree.py b/netbox/utilities/tests/test_ltree.py index 53f19bc5d..bd3095b7a 100644 --- a/netbox/utilities/tests/test_ltree.py +++ b/netbox/utilities/tests/test_ltree.py @@ -1102,12 +1102,19 @@ class CascadeTriggerDefinitionTests(TestCase): expected - set(definitions), set(), msg='these core ltree tables have no cascade trigger installed', ) + # Assert on the cast rather than on PostgreSQL's exact rendering of the clause: + # the parenthesization pg_get_triggerdef() emits is an implementation detail. for table in sorted(expected): + definition = definitions[table] self.assertIn( - '(old.path)::text IS DISTINCT FROM (new.path)::text', definitions[table], + '::text IS DISTINCT FROM', definition, msg=f'{table}: the cascade trigger compares ltree values directly, so it ' f'will not survive a pg_dump restore (see #23130)', ) + self.assertNotRegex( + definition, r'old\.path\s+IS DISTINCT FROM\s+new\.path', + msg=f'{table}: the cascade trigger compares path without a cast to text', + ) class LtreeTriggerSqlTests(SimpleTestCase):