From 58e8068958b4a9cc9565ee4a6fc77b031d73b1b3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Jul 2026 12:04:45 -0400 Subject: [PATCH] #21355: Refactor trigger migrations (#22608) --- .../0058_denormalization_triggers.py | 24 +++++++++- netbox/dcim/signals.py | 7 +-- .../0093_denormalization_triggers.py | 24 +++++++++- netbox/utilities/migration.py | 48 ++++--------------- .../0057_denormalization_triggers.py | 24 +++++++++- .../0021_denormalization_triggers.py | 24 +++++++++- 6 files changed, 99 insertions(+), 52 deletions(-) diff --git a/netbox/circuits/migrations/0058_denormalization_triggers.py b/netbox/circuits/migrations/0058_denormalization_triggers.py index db0b9ffb3..830f9e9da 100644 --- a/netbox/circuits/migrations/0058_denormalization_triggers.py +++ b/netbox/circuits/migrations/0058_denormalization_triggers.py @@ -4,7 +4,7 @@ of the Python `post_save` handler formerly registered in netbox.denormalized. """ from django.db import migrations -from utilities.migration import cached_scope_triggers +from utilities.migration import InstallDenormalizationTrigger class Migration(migrations.Migration): @@ -15,4 +15,24 @@ class Migration(migrations.Migration): ('dcim', '0238_ltree_paths'), ] - operations = cached_scope_triggers('circuits_circuittermination') + operations = [ + InstallDenormalizationTrigger( + dependent_table='circuits_circuittermination', + source_table='dcim_site', + fk_column='_site_id', + mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'}, + ), + InstallDenormalizationTrigger( + dependent_table='circuits_circuittermination', + source_table='dcim_location', + fk_column='_location_id', + mappings={'_site_id': 'site_id'}, + related_mappings=( + { + 'table': 'dcim_site', + 'source_fk': 'site_id', + 'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'}, + }, + ), + ), + ] diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 29feab929..af79c032f 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -34,9 +34,7 @@ from .utils import create_cablepaths, rebuild_paths def handle_location_site_change(instance, created, **kwargs): """ Cascade a Location's Site assignment down to the Racks, Devices, and PowerPanels it contains - (and to descendant Locations). The denormalized cache columns on cable terminations and device - components are maintained by PostgreSQL triggers, which fire on these Site/Location/Rack/Device - column writes. + (and to descendant Locations). """ if not created: instance.get_descendants().update(site=instance.site) @@ -49,8 +47,7 @@ def handle_location_site_change(instance, created, **kwargs): @receiver(post_save, sender=Rack) def handle_rack_site_change(instance, created, **kwargs): """ - Cascade a Rack's Site/Location assignment down to the Devices it contains. The denormalized cache - columns on those devices' components are maintained by PostgreSQL triggers. + Cascade a Rack's Site/Location assignment down to the Devices it contains. """ if not created: Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) diff --git a/netbox/ipam/migrations/0093_denormalization_triggers.py b/netbox/ipam/migrations/0093_denormalization_triggers.py index 902f7f294..2f95ede8b 100644 --- a/netbox/ipam/migrations/0093_denormalization_triggers.py +++ b/netbox/ipam/migrations/0093_denormalization_triggers.py @@ -4,7 +4,7 @@ Python `post_save` handler formerly registered in netbox.denormalized (and dcim. """ from django.db import migrations -from utilities.migration import cached_scope_triggers +from utilities.migration import InstallDenormalizationTrigger class Migration(migrations.Migration): @@ -15,4 +15,24 @@ class Migration(migrations.Migration): ('dcim', '0238_ltree_paths'), ] - operations = cached_scope_triggers('ipam_prefix') + operations = [ + InstallDenormalizationTrigger( + dependent_table='ipam_prefix', + source_table='dcim_site', + fk_column='_site_id', + mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'}, + ), + InstallDenormalizationTrigger( + dependent_table='ipam_prefix', + source_table='dcim_location', + fk_column='_location_id', + mappings={'_site_id': 'site_id'}, + related_mappings=( + { + 'table': 'dcim_site', + 'source_fk': 'site_id', + 'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'}, + }, + ), + ), + ] diff --git a/netbox/utilities/migration.py b/netbox/utilities/migration.py index 296655930..d936c8e5b 100644 --- a/netbox/utilities/migration.py +++ b/netbox/utilities/migration.py @@ -4,7 +4,6 @@ from netbox.config import ConfigItem __all__ = ( 'InstallDenormalizationTrigger', - 'cached_scope_triggers', 'custom_deconstruct', ) @@ -68,6 +67,15 @@ class InstallDenormalizationTrigger(migrations.operations.base.Operation): newly created source row has no dependents yet) and it does not recurse: the dependent tables carry no triggers of their own. + Example: refresh a CircuitTermination's cached region/sitegroup when its Site's region or group changes:: + + InstallDenormalizationTrigger( + dependent_table='circuits_circuittermination', + source_table='dcim_site', + fk_column='_site_id', + mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'}, + ) + Note: this is a row-level trigger, so a bulk source update of N rows fires it N times. A statement-level trigger with transition tables would batch this, but PostgreSQL forbids transition tables on a trigger with an `UPDATE OF ` list, and dropping that column list would fire the trigger on every source @@ -139,41 +147,3 @@ class InstallDenormalizationTrigger(migrations.operations.base.Operation): def describe(self): return f'Install denormalization trigger on {self.source_table} updating {self.dependent_table}' - - -# Site/region/site-group lookup shared by every CachedScopeMixin-style dependent (see cached_scope_triggers). -SITE_SCOPE_RELATED_MAPPINGS = ( - { - 'table': 'dcim_site', - 'source_fk': 'site_id', - 'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'}, - }, -) - - -def cached_scope_triggers(dependent_table): - """ - Return the Site + Location `InstallDenormalizationTrigger` pair for a dependent table carrying the - standard cached-scope columns (_site/_location/_region/_site_group) — i.e. any CachedScopeMixin model - (Prefix, Cluster, WirelessLAN) plus CircuitTermination, which share the same denormalization shape. - - Region- and SiteGroup-scoped rows need no trigger: their cached FK is the scoped object itself and - never changes underneath them. So two triggers fully cover the cache: - - dcim_site: region/group changed -> refresh _region/_site_group on rows scoped to that site - - dcim_location: site changed -> refresh _site (and the new site's region/group) - """ - return [ - InstallDenormalizationTrigger( - dependent_table=dependent_table, - source_table='dcim_site', - fk_column='_site_id', - mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'}, - ), - InstallDenormalizationTrigger( - dependent_table=dependent_table, - source_table='dcim_location', - fk_column='_location_id', - mappings={'_site_id': 'site_id'}, - related_mappings=SITE_SCOPE_RELATED_MAPPINGS, - ), - ] diff --git a/netbox/virtualization/migrations/0057_denormalization_triggers.py b/netbox/virtualization/migrations/0057_denormalization_triggers.py index d40003a75..72bab9892 100644 --- a/netbox/virtualization/migrations/0057_denormalization_triggers.py +++ b/netbox/virtualization/migrations/0057_denormalization_triggers.py @@ -4,7 +4,7 @@ via PostgreSQL triggers instead of the Python `dcim.signals.sync_cached_scope_fi """ from django.db import migrations -from utilities.migration import cached_scope_triggers +from utilities.migration import InstallDenormalizationTrigger class Migration(migrations.Migration): @@ -15,4 +15,24 @@ class Migration(migrations.Migration): ('dcim', '0238_ltree_paths'), ] - operations = cached_scope_triggers('virtualization_cluster') + operations = [ + InstallDenormalizationTrigger( + dependent_table='virtualization_cluster', + source_table='dcim_site', + fk_column='_site_id', + mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'}, + ), + InstallDenormalizationTrigger( + dependent_table='virtualization_cluster', + source_table='dcim_location', + fk_column='_location_id', + mappings={'_site_id': 'site_id'}, + related_mappings=( + { + 'table': 'dcim_site', + 'source_fk': 'site_id', + 'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'}, + }, + ), + ), + ] diff --git a/netbox/wireless/migrations/0021_denormalization_triggers.py b/netbox/wireless/migrations/0021_denormalization_triggers.py index 97dd56379..9196ce4b2 100644 --- a/netbox/wireless/migrations/0021_denormalization_triggers.py +++ b/netbox/wireless/migrations/0021_denormalization_triggers.py @@ -5,7 +5,7 @@ handler. """ from django.db import migrations -from utilities.migration import cached_scope_triggers +from utilities.migration import InstallDenormalizationTrigger class Migration(migrations.Migration): @@ -16,4 +16,24 @@ class Migration(migrations.Migration): ('dcim', '0238_ltree_paths'), ] - operations = cached_scope_triggers('wireless_wirelesslan') + operations = [ + InstallDenormalizationTrigger( + dependent_table='wireless_wirelesslan', + source_table='dcim_site', + fk_column='_site_id', + mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'}, + ), + InstallDenormalizationTrigger( + dependent_table='wireless_wirelesslan', + source_table='dcim_location', + fk_column='_location_id', + mappings={'_site_id': 'site_id'}, + related_mappings=( + { + 'table': 'dcim_site', + 'source_fk': 'site_id', + 'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'}, + }, + ), + ), + ]