ソースを参照

Closes #20547: Consolidate unique constraints comprising nullable fields (#22549)

Jeremy Stretch 1 週間 前
コミット
18fc16df8e

+ 204 - 0
netbox/dcim/migrations/0241_consolidate_unique_constraints.py

@@ -0,0 +1,204 @@
+import django.db.models.functions.text
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('dcim', '0240_device__config_context_data'),
+        ('extras', '0139_alter_customfieldchoiceset_extra_choices'),
+        ('tenancy', '0025_ltree_paths'),
+        ('users', '0016_default_ordering_indexes'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='devicerole',
+            name='dcim_devicerole_parent_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='devicerole',
+            name='dcim_devicerole_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='devicerole',
+            name='dcim_devicerole_parent_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='devicerole',
+            name='dcim_devicerole_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='location',
+            name='dcim_location_parent_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='location',
+            name='dcim_location_parent_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='location',
+            name='dcim_location_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='location',
+            name='dcim_location_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='platform',
+            name='dcim_platform_manufacturer_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='platform',
+            name='dcim_platform_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='platform',
+            name='dcim_platform_manufacturer_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='platform',
+            name='dcim_platform_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='region',
+            name='dcim_region_parent_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='region',
+            name='dcim_region_parent_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='region',
+            name='dcim_region_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='region',
+            name='dcim_region_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='sitegroup',
+            name='dcim_sitegroup_parent_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='sitegroup',
+            name='dcim_sitegroup_parent_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='sitegroup',
+            name='dcim_sitegroup_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='sitegroup',
+            name='dcim_sitegroup_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='device',
+            name='dcim_device_unique_name_site_tenant',
+        ),
+        migrations.RemoveConstraint(
+            model_name='device',
+            name='dcim_device_unique_name_site',
+        ),
+        migrations.AddConstraint(
+            model_name='devicerole',
+            constraint=models.UniqueConstraint(
+                fields=('parent', 'name'),
+                name='dcim_devicerole_parent_name',
+                nulls_distinct=False,
+                violation_error_message='A device role with this name already exists.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='devicerole',
+            constraint=models.UniqueConstraint(
+                fields=('parent', 'slug'),
+                name='dcim_devicerole_parent_slug',
+                nulls_distinct=False,
+                violation_error_message='A device role with this slug already exists.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='location',
+            constraint=models.UniqueConstraint(
+                fields=('site', 'parent', 'name'),
+                name='dcim_location_parent_name',
+                nulls_distinct=False,
+                violation_error_message='A location with this name already exists within the specified site.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='location',
+            constraint=models.UniqueConstraint(
+                fields=('site', 'parent', 'slug'),
+                name='dcim_location_parent_slug',
+                nulls_distinct=False,
+                violation_error_message='A location with this slug already exists within the specified site.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='platform',
+            constraint=models.UniqueConstraint(
+                fields=('manufacturer', 'name'),
+                name='dcim_platform_manufacturer_name',
+                nulls_distinct=False,
+                violation_error_message='Platform name must be unique.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='platform',
+            constraint=models.UniqueConstraint(
+                fields=('manufacturer', 'slug'),
+                name='dcim_platform_manufacturer_slug',
+                nulls_distinct=False,
+                violation_error_message='Platform slug must be unique.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='region',
+            constraint=models.UniqueConstraint(
+                fields=('parent', 'name'),
+                name='dcim_region_parent_name',
+                nulls_distinct=False,
+                violation_error_message='A region with this name already exists.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='region',
+            constraint=models.UniqueConstraint(
+                fields=('parent', 'slug'),
+                name='dcim_region_parent_slug',
+                nulls_distinct=False,
+                violation_error_message='A region with this slug already exists.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='sitegroup',
+            constraint=models.UniqueConstraint(
+                fields=('parent', 'name'),
+                name='dcim_sitegroup_parent_name',
+                nulls_distinct=False,
+                violation_error_message='A site group with this name already exists.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='sitegroup',
+            constraint=models.UniqueConstraint(
+                fields=('parent', 'slug'),
+                name='dcim_sitegroup_parent_slug',
+                nulls_distinct=False,
+                violation_error_message='A site group with this slug already exists.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='device',
+            constraint=models.UniqueConstraint(
+                django.db.models.functions.text.Lower('name'),
+                models.F('site'),
+                models.F('tenant'),
+                condition=models.Q(('name__isnull', False)),
+                name='dcim_device_unique_name_site_tenant',
+                nulls_distinct=False,
+                violation_error_message='Device name must be unique per site and tenant.',
+            ),
+        ),
+    ]

+ 12 - 31
netbox/dcim/models/devices.py

@@ -420,23 +420,15 @@ class DeviceRole(NestedLtreeGroupModel):
         constraints = (
             models.UniqueConstraint(
                 fields=('parent', 'name'),
-                name='%(app_label)s_%(class)s_parent_name'
-            ),
-            models.UniqueConstraint(
-                fields=('name',),
-                name='%(app_label)s_%(class)s_name',
-                condition=Q(parent__isnull=True),
-                violation_error_message=_("A top-level device role with this name already exists.")
+                name='%(app_label)s_%(class)s_parent_name',
+                nulls_distinct=False,
+                violation_error_message=_("A device role with this name already exists.")
             ),
             models.UniqueConstraint(
                 fields=('parent', 'slug'),
-                name='%(app_label)s_%(class)s_parent_slug'
-            ),
-            models.UniqueConstraint(
-                fields=('slug',),
-                name='%(app_label)s_%(class)s_slug',
-                condition=Q(parent__isnull=True),
-                violation_error_message=_("A top-level device role with this slug already exists.")
+                name='%(app_label)s_%(class)s_parent_slug',
+                nulls_distinct=False,
+                violation_error_message=_("A device role with this slug already exists.")
             ),
         )
         verbose_name = _('device role')
@@ -478,21 +470,13 @@ class Platform(NestedLtreeGroupModel):
             models.UniqueConstraint(
                 fields=('manufacturer', 'name'),
                 name='%(app_label)s_%(class)s_manufacturer_name',
-            ),
-            models.UniqueConstraint(
-                fields=('name',),
-                name='%(app_label)s_%(class)s_name',
-                condition=Q(manufacturer__isnull=True),
+                nulls_distinct=False,
                 violation_error_message=_("Platform name must be unique.")
             ),
             models.UniqueConstraint(
                 fields=('manufacturer', 'slug'),
                 name='%(app_label)s_%(class)s_manufacturer_slug',
-            ),
-            models.UniqueConstraint(
-                fields=('slug',),
-                name='%(app_label)s_%(class)s_slug',
-                condition=Q(manufacturer__isnull=True),
+                nulls_distinct=False,
                 violation_error_message=_("Platform slug must be unique.")
             ),
         )
@@ -764,13 +748,10 @@ class Device(
         constraints = (
             models.UniqueConstraint(
                 Lower('name'), 'site', 'tenant',
-                name='%(app_label)s_%(class)s_unique_name_site_tenant'
-            ),
-            models.UniqueConstraint(
-                Lower('name'), 'site',
-                name='%(app_label)s_%(class)s_unique_name_site',
-                condition=Q(tenant__isnull=True),
-                violation_error_message=_("Device name must be unique per site.")
+                name='%(app_label)s_%(class)s_unique_name_site_tenant',
+                condition=Q(name__isnull=False),
+                nulls_distinct=False,
+                violation_error_message=_("Device name must be unique per site and tenant.")
             ),
             models.UniqueConstraint(
                 fields=('rack', 'position', 'face'),

+ 16 - 40
netbox/dcim/models/sites.py

@@ -53,23 +53,15 @@ class Region(ContactsMixin, NestedLtreeGroupModel):
         constraints = (
             models.UniqueConstraint(
                 fields=('parent', 'name'),
-                name='%(app_label)s_%(class)s_parent_name'
-            ),
-            models.UniqueConstraint(
-                fields=('name',),
-                name='%(app_label)s_%(class)s_name',
-                condition=Q(parent__isnull=True),
-                violation_error_message=_("A top-level region with this name already exists.")
+                name='%(app_label)s_%(class)s_parent_name',
+                nulls_distinct=False,
+                violation_error_message=_("A region with this name already exists.")
             ),
             models.UniqueConstraint(
                 fields=('parent', 'slug'),
-                name='%(app_label)s_%(class)s_parent_slug'
-            ),
-            models.UniqueConstraint(
-                fields=('slug',),
-                name='%(app_label)s_%(class)s_slug',
-                condition=Q(parent__isnull=True),
-                violation_error_message=_("A top-level region with this slug already exists.")
+                name='%(app_label)s_%(class)s_parent_slug',
+                nulls_distinct=False,
+                violation_error_message=_("A region with this slug already exists.")
             ),
         )
         verbose_name = _('region')
@@ -114,23 +106,15 @@ class SiteGroup(ContactsMixin, NestedLtreeGroupModel):
         constraints = (
             models.UniqueConstraint(
                 fields=('parent', 'name'),
-                name='%(app_label)s_%(class)s_parent_name'
-            ),
-            models.UniqueConstraint(
-                fields=('name',),
-                name='%(app_label)s_%(class)s_name',
-                condition=Q(parent__isnull=True),
-                violation_error_message=_("A top-level site group with this name already exists.")
+                name='%(app_label)s_%(class)s_parent_name',
+                nulls_distinct=False,
+                violation_error_message=_("A site group with this name already exists.")
             ),
             models.UniqueConstraint(
                 fields=('parent', 'slug'),
-                name='%(app_label)s_%(class)s_parent_slug'
-            ),
-            models.UniqueConstraint(
-                fields=('slug',),
-                name='%(app_label)s_%(class)s_slug',
-                condition=Q(parent__isnull=True),
-                violation_error_message=_("A top-level site group with this slug already exists.")
+                name='%(app_label)s_%(class)s_parent_slug',
+                nulls_distinct=False,
+                violation_error_message=_("A site group with this slug already exists.")
             ),
         )
         verbose_name = _('site group')
@@ -341,22 +325,14 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedLtreeGroupModel):
         constraints = (
             models.UniqueConstraint(
                 fields=('site', 'parent', 'name'),
-                name='%(app_label)s_%(class)s_parent_name'
-            ),
-            models.UniqueConstraint(
-                fields=('site', 'name'),
-                name='%(app_label)s_%(class)s_name',
-                condition=Q(parent__isnull=True),
+                name='%(app_label)s_%(class)s_parent_name',
+                nulls_distinct=False,
                 violation_error_message=_("A location with this name already exists within the specified site.")
             ),
             models.UniqueConstraint(
                 fields=('site', 'parent', 'slug'),
-                name='%(app_label)s_%(class)s_parent_slug'
-            ),
-            models.UniqueConstraint(
-                fields=('site', 'slug'),
-                name='%(app_label)s_%(class)s_slug',
-                condition=Q(parent__isnull=True),
+                name='%(app_label)s_%(class)s_parent_slug',
+                nulls_distinct=False,
                 violation_error_message=_("A location with this slug already exists within the specified site.")
             ),
         )

+ 46 - 0
netbox/tenancy/migrations/0026_consolidate_unique_constraints.py

@@ -0,0 +1,46 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('extras', '0139_alter_customfieldchoiceset_extra_choices'),
+        ('tenancy', '0025_ltree_paths'),
+        ('users', '0016_default_ordering_indexes'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='tenant',
+            name='tenancy_tenant_unique_group_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='tenant',
+            name='tenancy_tenant_unique_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='tenant',
+            name='tenancy_tenant_unique_group_slug',
+        ),
+        migrations.RemoveConstraint(
+            model_name='tenant',
+            name='tenancy_tenant_unique_slug',
+        ),
+        migrations.AddConstraint(
+            model_name='tenant',
+            constraint=models.UniqueConstraint(
+                fields=('group', 'name'),
+                name='tenancy_tenant_unique_group_name',
+                nulls_distinct=False,
+                violation_error_message='Tenant name must be unique per group.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='tenant',
+            constraint=models.UniqueConstraint(
+                fields=('group', 'slug'),
+                name='tenancy_tenant_unique_group_slug',
+                nulls_distinct=False,
+                violation_error_message='Tenant slug must be unique per group.',
+            ),
+        ),
+    ]

+ 2 - 11
netbox/tenancy/models/tenants.py

@@ -1,6 +1,5 @@
 from django.contrib.postgres.indexes import GistIndex
 from django.db import models
-from django.db.models import Q
 from django.utils.translation import gettext_lazy as _
 
 from netbox.models import NestedLtreeGroupModel, PrimaryModel
@@ -71,23 +70,15 @@ class Tenant(ContactsMixin, PrimaryModel):
             models.UniqueConstraint(
                 fields=('group', 'name'),
                 name='%(app_label)s_%(class)s_unique_group_name',
+                nulls_distinct=False,
                 violation_error_message=_("Tenant name must be unique per group.")
             ),
-            models.UniqueConstraint(
-                fields=('name',),
-                name='%(app_label)s_%(class)s_unique_name',
-                condition=Q(group__isnull=True)
-            ),
             models.UniqueConstraint(
                 fields=('group', 'slug'),
                 name='%(app_label)s_%(class)s_unique_group_slug',
+                nulls_distinct=False,
                 violation_error_message=_("Tenant slug must be unique per group.")
             ),
-            models.UniqueConstraint(
-                fields=('slug',),
-                name='%(app_label)s_%(class)s_unique_slug',
-                condition=Q(group__isnull=True)
-            ),
         )
         verbose_name = _('tenant')
         verbose_name_plural = _('tenants')

+ 56 - 0
netbox/virtualization/migrations/0059_consolidate_unique_constraints.py

@@ -0,0 +1,56 @@
+import django.db.models.functions.text
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('dcim', '0241_consolidate_unique_constraints'),
+        ('extras', '0139_alter_customfieldchoiceset_extra_choices'),
+        ('ipam', '0093_denormalization_triggers'),
+        ('tenancy', '0026_consolidate_unique_constraints'),
+        ('users', '0016_default_ordering_indexes'),
+        ('virtualization', '0058_virtualmachine__config_context_data'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='virtualmachine',
+            name='virtualization_virtualmachine_unique_name_cluster_tenant',
+        ),
+        migrations.RemoveConstraint(
+            model_name='virtualmachine',
+            name='virtualization_virtualmachine_unique_name_cluster',
+        ),
+        migrations.RemoveConstraint(
+            model_name='virtualmachine',
+            name='virtualization_virtualmachine_unique_name_device_tenant',
+        ),
+        migrations.RemoveConstraint(
+            model_name='virtualmachine',
+            name='virtualization_virtualmachine_unique_name_device',
+        ),
+        migrations.AddConstraint(
+            model_name='virtualmachine',
+            constraint=models.UniqueConstraint(
+                django.db.models.functions.text.Lower('name'),
+                models.F('cluster'),
+                models.F('tenant'),
+                condition=models.Q(('cluster__isnull', False)),
+                name='virtualization_virtualmachine_unique_name_cluster_tenant',
+                nulls_distinct=False,
+                violation_error_message='Virtual machine name must be unique per cluster and tenant.',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='virtualmachine',
+            constraint=models.UniqueConstraint(
+                django.db.models.functions.text.Lower('name'),
+                models.F('device'),
+                models.F('tenant'),
+                condition=models.Q(('cluster__isnull', True), ('device__isnull', False)),
+                name='virtualization_virtualmachine_unique_name_device_tenant',
+                nulls_distinct=False,
+                violation_error_message='Virtual machine name must be unique per device and tenant.',
+            ),
+        ),
+    ]

+ 3 - 12
netbox/virtualization/models/virtualmachines.py

@@ -264,26 +264,17 @@ class VirtualMachine(
             models.UniqueConstraint(
                 Lower('name'), 'cluster', 'tenant',
                 name='%(app_label)s_%(class)s_unique_name_cluster_tenant',
+                condition=Q(cluster__isnull=False),
+                nulls_distinct=False,
                 violation_error_message=_('Virtual machine name must be unique per cluster and tenant.')
             ),
-            models.UniqueConstraint(
-                Lower('name'), 'cluster',
-                name='%(app_label)s_%(class)s_unique_name_cluster',
-                condition=Q(tenant__isnull=True),
-                violation_error_message=_('Virtual machine name must be unique per cluster.')
-            ),
             models.UniqueConstraint(
                 Lower('name'), 'device', 'tenant',
                 name='%(app_label)s_%(class)s_unique_name_device_tenant',
                 condition=Q(cluster__isnull=True, device__isnull=False),
+                nulls_distinct=False,
                 violation_error_message=_('Virtual machine name must be unique per device and tenant.')
             ),
-            models.UniqueConstraint(
-                Lower('name'), 'device',
-                name='%(app_label)s_%(class)s_unique_name_device',
-                condition=Q(cluster__isnull=True, device__isnull=False, tenant__isnull=True),
-                violation_error_message=_('Virtual machine name must be unique per device.')
-            ),
         )
         verbose_name = _('virtual machine')
         verbose_name_plural = _('virtual machines')

+ 21 - 0
netbox/vpn/migrations/0013_consolidate_unique_constraints.py

@@ -0,0 +1,21 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('extras', '0139_alter_customfieldchoiceset_extra_choices'),
+        ('tenancy', '0026_consolidate_unique_constraints'),
+        ('users', '0016_default_ordering_indexes'),
+        ('vpn', '0012_default_ordering_indexes'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='tunnel',
+            name='vpn_tunnel_group_name',
+        ),
+        migrations.RemoveConstraint(
+            model_name='tunnel',
+            name='vpn_tunnel_name',
+        ),
+    ]

+ 0 - 12
netbox/vpn/models/tunnels.py

@@ -1,7 +1,6 @@
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.core.exceptions import ValidationError
 from django.db import models
-from django.db.models import Q
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
@@ -78,17 +77,6 @@ class Tunnel(ContactsMixin, PrimaryModel):
 
     class Meta:
         ordering = ('name',)
-        constraints = (
-            models.UniqueConstraint(
-                fields=('group', 'name'),
-                name='%(app_label)s_%(class)s_group_name'
-            ),
-            models.UniqueConstraint(
-                fields=('name',),
-                name='%(app_label)s_%(class)s_name',
-                condition=Q(group__isnull=True)
-            ),
-        )
         verbose_name = _('tunnel')
         verbose_name_plural = _('tunnels')
 

+ 16 - 0
netbox/wireless/migrations/0022_wirelesslangroup_drop_unique_constraint.py

@@ -0,0 +1,16 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('extras', '0139_alter_customfieldchoiceset_extra_choices'),
+        ('users', '0016_default_ordering_indexes'),
+        ('wireless', '0021_denormalization_triggers'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='wirelesslangroup',
+            name='wireless_wirelesslangroup_unique_parent_name',
+        ),
+    ]

+ 0 - 6
netbox/wireless/models.py

@@ -70,12 +70,6 @@ class WirelessLANGroup(NestedLtreeGroupModel):
             GistIndex(fields=['path'], name='wireless_lan_grp_path_gist'),
             models.Index(fields=['sort_path'], name='wireless_lan_grp_sort_idx'),
         )
-        constraints = (
-            models.UniqueConstraint(
-                fields=('parent', 'name'),
-                name='%(app_label)s_%(class)s_unique_parent_name'
-            ),
-        )
         verbose_name = _('wireless LAN group')
         verbose_name_plural = _('wireless LAN groups')