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

This commit is contained in:
Jeremy Stretch 2026-06-26 12:51:09 -04:00 committed by GitHub
parent 4daa1a0165
commit 18fc16df8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 376 additions and 112 deletions

View File

@ -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.',
),
),
]

View File

@ -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'),

View File

@ -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.")
),
)

View File

@ -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.',
),
),
]

View File

@ -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')

View File

@ -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.',
),
),
]

View File

@ -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')

View File

@ -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',
),
]

View File

@ -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')

View File

@ -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',
),
]

View File

@ -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')