From dac0a06f4fa72441adbb9645d81bee6e96af2494 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 11:10:28 -0400 Subject: [PATCH 1/4] Introduce case-insensitive collations --- netbox/dcim/migrations/0216_ci_collations.py | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 netbox/dcim/migrations/0216_ci_collations.py diff --git a/netbox/dcim/migrations/0216_ci_collations.py b/netbox/dcim/migrations/0216_ci_collations.py new file mode 100644 index 0000000000..ee0aa637d8 --- /dev/null +++ b/netbox/dcim/migrations/0216_ci_collations.py @@ -0,0 +1,26 @@ +from django.contrib.postgres.operations import CreateCollation +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0215_rackreservation_status'), + ] + + operations = [ + # Create a case-insensitive collation + CreateCollation( + 'case_insensitive', + provider='icu', + locale='und-u-ks-level2', + deterministic=False, + ), + # Create a case-insensitive collation with natural sorting + CreateCollation( + 'ci_natural_sort', + provider='icu', + locale='und-u-kn-true-ks-level2', + deterministic=False, + ), + ] From 06052f8eaa57a277020f8dc0e8a45a5435c3985e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Oct 2025 14:12:59 -0400 Subject: [PATCH 2/4] Use case-insensitive collations on fields considered for uniqueness --- netbox/circuits/models/circuits.py | 5 ++-- netbox/circuits/models/providers.py | 13 +++++++---- netbox/circuits/models/virtual_circuits.py | 5 ++-- netbox/core/models/data.py | 3 ++- .../dcim/models/device_component_templates.py | 2 +- netbox/dcim/models/device_components.py | 2 +- netbox/dcim/models/devices.py | 23 +++++++++---------- netbox/dcim/models/modules.py | 6 +++-- netbox/dcim/models/power.py | 4 ++-- netbox/dcim/models/racks.py | 8 ++++--- netbox/dcim/models/sites.py | 7 +++--- netbox/extras/models/configs.py | 6 +++-- netbox/extras/models/customfields.py | 4 +++- netbox/extras/models/models.py | 15 ++++++++---- netbox/extras/models/notifications.py | 3 ++- netbox/extras/models/tags.py | 17 +++++++++++++- netbox/ipam/models/asns.py | 7 +----- netbox/ipam/models/services.py | 3 ++- netbox/ipam/models/vlans.py | 9 +++++--- netbox/ipam/models/vrfs.py | 5 ++-- netbox/netbox/models/__init__.py | 12 ++++++---- netbox/tenancy/models/contacts.py | 2 +- netbox/tenancy/models/tenants.py | 10 ++++---- netbox/virtualization/models/clusters.py | 2 +- .../virtualization/models/virtualmachines.py | 9 ++++---- netbox/vpn/models/crypto.py | 10 ++++---- netbox/vpn/models/l2vpn.py | 5 ++-- netbox/vpn/models/tunnels.py | 2 +- netbox/wireless/models.py | 5 ++-- 29 files changed, 123 insertions(+), 81 deletions(-) diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 643aac3bd5..9cf6b31b3c 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -41,9 +41,10 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel) ProviderAccount. Circuit port speed and commit rate are measured in Kbps. """ cid = models.CharField( - max_length=100, verbose_name=_('circuit ID'), - help_text=_('Unique circuit ID') + max_length=100, + db_collation='case_insensitive', + help_text=_('Unique circuit ID'), ) provider = models.ForeignKey( to='circuits.Provider', diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index be81caa549..ccfeefd84f 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -21,13 +21,14 @@ class Provider(ContactsMixin, PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, + db_collation='ci_natural_sort', help_text=_('Full name of the provider'), - db_collation="natural_sort" ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) asns = models.ManyToManyField( to='ipam.ASN', @@ -56,13 +57,15 @@ class ProviderAccount(ContactsMixin, PrimaryModel): related_name='accounts' ) account = models.CharField( + verbose_name=_('account ID'), max_length=100, - verbose_name=_('account ID') + db_collation='ci_natural_sort', ) name = models.CharField( verbose_name=_('name'), max_length=100, - blank=True + db_collation='ci_natural_sort', + blank=True, ) clone_fields = ('provider', ) @@ -97,7 +100,7 @@ class ProviderNetwork(PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) provider = models.ForeignKey( to='circuits.Provider', diff --git a/netbox/circuits/models/virtual_circuits.py b/netbox/circuits/models/virtual_circuits.py index c97a3560ae..8597eb65ad 100644 --- a/netbox/circuits/models/virtual_circuits.py +++ b/netbox/circuits/models/virtual_circuits.py @@ -34,9 +34,10 @@ class VirtualCircuit(PrimaryModel): A virtual connection between two or more endpoints, delivered across one or more physical circuits. """ cid = models.CharField( - max_length=100, verbose_name=_('circuit ID'), - help_text=_('Unique circuit ID') + max_length=100, + db_collation='case_insensitive', + help_text=_('Unique circuit ID'), ) provider_network = models.ForeignKey( to='circuits.ProviderNetwork', diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 52a11c58ee..70fbea51e1 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -38,7 +38,8 @@ class DataSource(JobsMixin, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) type = models.CharField( verbose_name=_('type'), diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 74e624d6c9..8490f8a7db 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -43,10 +43,10 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin): name = models.CharField( verbose_name=_('name'), max_length=64, + db_collation='ci_natural_sort', help_text=_( "{module} is accepted as a substitution for the module bay position when attached to a module type." ), - db_collation="natural_sort" ) label = models.CharField( verbose_name=_('label'), diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 9c44e04942..7e5c9cdc56 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -52,7 +52,7 @@ class ComponentModel(NetBoxModel): name = models.CharField( verbose_name=_('name'), max_length=64, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) label = models.CharField( verbose_name=_('label'), diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index be93f33b9b..d5b05779a7 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,8 +1,7 @@ import decimal -import yaml - from functools import cached_property +import yaml from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -10,7 +9,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import F, ProtectedError, prefetch_related_objects -from django.db.models.functions import Lower from django.db.models.signals import post_save from django.urls import reverse from django.utils.safestring import mark_safe @@ -25,8 +23,8 @@ from netbox.choices import ColorChoices from netbox.config import ConfigItem from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel -from netbox.models.mixins import WeightMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin +from netbox.models.mixins import WeightMixin from utilities.fields import ColorField, CounterCacheField from utilities.prefetch import get_prefetchable_fields from utilities.tracking import TrackingModelMixin @@ -34,7 +32,6 @@ from .mixins import RenderConfigMixin from .modules import Module - __all__ = ( 'Device', 'DeviceRole', @@ -83,11 +80,13 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): ) model = models.CharField( verbose_name=_('model'), - max_length=100 + max_length=100, + db_collation='case_insensitive', ) slug = models.SlugField( verbose_name=_('slug'), - max_length=100 + max_length=100, + db_collation='case_insensitive', ) default_platform = models.ForeignKey( to='dcim.Platform', @@ -525,7 +524,7 @@ class Device( max_length=64, blank=True, null=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) serial = models.CharField( max_length=50, @@ -721,11 +720,11 @@ class Meta: ordering = ('name', 'pk') # Name may be null constraints = ( models.UniqueConstraint( - Lower('name'), 'site', 'tenant', + 'name', 'site', 'tenant', name='%(app_label)s_%(class)s_unique_name_site_tenant' ), models.UniqueConstraint( - Lower('name'), 'site', + '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.") @@ -1119,7 +1118,7 @@ class VirtualChassis(PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=64, - db_collation="natural_sort" + db_collation='natural_sort', ) domain = models.CharField( verbose_name=_('domain'), @@ -1182,7 +1181,7 @@ class VirtualDeviceContext(PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=64, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) status = models.CharField( verbose_name=_('status'), diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 4376f40aa4..42163b40f9 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -31,7 +31,8 @@ class ModuleTypeProfile(PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) schema = models.JSONField( blank=True, @@ -72,7 +73,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): ) model = models.CharField( verbose_name=_('model'), - max_length=100 + max_length=100, + db_collation='ci_natural_sort', ) part_number = models.CharField( verbose_name=_('part number'), diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 284cfe8325..b1d5ab747d 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -37,7 +37,7 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) prerequisite_models = ( @@ -88,7 +88,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) status = models.CharField( verbose_name=_('status'), diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 02bce2019f..c57aceeed2 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -137,12 +137,14 @@ class RackType(RackBase): ) model = models.CharField( verbose_name=_('model'), - max_length=100 + max_length=100, + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) clone_fields = ( @@ -262,7 +264,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) facility_id = models.CharField( max_length=50, diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 7880a067fc..4dcd98d7b1 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -142,13 +142,14 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - help_text=_("Full name of the site"), - db_collation="natural_sort" + db_collation='ci_natural_sort', + help_text=_("Full name of the site") ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) status = models.CharField( verbose_name=_('status'), diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index a9d233568e..bbcd6611d6 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -35,7 +35,8 @@ class ConfigContextProfile(SyncedDataMixin, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) description = models.CharField( verbose_name=_('description'), @@ -77,7 +78,8 @@ class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLogge name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) profile = models.ForeignKey( to='extras.ConfigContextProfile', diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index b1d22ee0b4..1789f33d5b 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -94,6 +94,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): verbose_name=_('name'), max_length=50, unique=True, + db_collation='ci_natural_sort', help_text=_('Internal field name'), validators=( RegexValidator( @@ -779,7 +780,8 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel """ name = models.CharField( max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) description = models.CharField( max_length=200, diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 7361d087de..b1e09d96d0 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -59,7 +59,8 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged name = models.CharField( verbose_name=_('name'), max_length=150, - unique=True + unique=True, + db_collation='ci_natural_sort', ) description = models.CharField( verbose_name=_('description'), @@ -164,7 +165,8 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo name = models.CharField( verbose_name=_('name'), max_length=150, - unique=True + unique=True, + db_collation='ci_natural_sort', ) description = models.CharField( verbose_name=_('description'), @@ -307,7 +309,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) enabled = models.BooleanField( verbose_name=_('enabled'), @@ -468,12 +471,14 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) description = models.CharField( verbose_name=_('description'), diff --git a/netbox/extras/models/notifications.py b/netbox/extras/models/notifications.py index f813a0d290..99422839a9 100644 --- a/netbox/extras/models/notifications.py +++ b/netbox/extras/models/notifications.py @@ -125,7 +125,8 @@ class NotificationGroup(ChangeLoggedModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) description = models.CharField( verbose_name=_('description'), diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 0df76d7b3a..994fabf28a 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -2,7 +2,7 @@ from django.db import models from django.urls import reverse from django.utils.text import slugify -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _, pgettext_lazy from taggit.models import TagBase, GenericTaggedItemBase from netbox.choices import ColorChoices @@ -25,6 +25,21 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase): id = models.BigAutoField( primary_key=True ) + # Override TagBase.name to set db_collation + name = models.CharField( + verbose_name=pgettext_lazy("A tag name", "name"), + unique=True, + max_length=100, + db_collation='ci_natural_sort', + ) + # Override TagBase.slug to set db_collation + slug = models.SlugField( + verbose_name=pgettext_lazy("A tag slug", "slug"), + unique=True, + max_length=100, + allow_unicode=True, + db_collation='case_insensitive', + ) color = ColorField( verbose_name=_('color'), default=ColorChoices.COLOR_GREY diff --git a/netbox/ipam/models/asns.py b/netbox/ipam/models/asns.py index ee1a5416ec..101b8858c0 100644 --- a/netbox/ipam/models/asns.py +++ b/netbox/ipam/models/asns.py @@ -18,12 +18,7 @@ class ASNRange(OrganizationalModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" - ) - slug = models.SlugField( - verbose_name=_('slug'), - max_length=100, - unique=True + db_collation='ci_natural_sort', ) rir = models.ForeignKey( to='ipam.RIR', diff --git a/netbox/ipam/models/services.py b/netbox/ipam/models/services.py index c2c9ca4447..c03f909f60 100644 --- a/netbox/ipam/models/services.py +++ b/netbox/ipam/models/services.py @@ -50,7 +50,8 @@ class ServiceTemplate(ServiceBase, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) class Meta: diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index efa1ed39ed..0a900e0816 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -37,11 +37,12 @@ class VLANGroup(OrganizationalModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), - max_length=100 + max_length=100, + db_collation='case_insensitive', ) scope_type = models.ForeignKey( to='contenttypes.ContentType', @@ -214,7 +215,8 @@ class VLAN(PrimaryModel): ) name = models.CharField( verbose_name=_('name'), - max_length=64 + max_length=64, + db_collation='ci_natural_sort', ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -362,6 +364,7 @@ class VLANTranslationPolicy(PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, + db_collation='ci_natural_sort', ) class Meta: diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py index 6a8b8d649b..d0999c31a2 100644 --- a/netbox/ipam/models/vrfs.py +++ b/netbox/ipam/models/vrfs.py @@ -19,11 +19,12 @@ class VRF(PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='natural_sort', ) rd = models.CharField( max_length=VRF_RD_MAX_LENGTH, unique=True, + db_collation='case_insensitive', blank=True, null=True, verbose_name=_('route distinguisher'), @@ -75,8 +76,8 @@ class RouteTarget(PrimaryModel): verbose_name=_('name'), max_length=VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4) unique=True, + db_collation='ci_natural_sort', help_text=_('Route target value (formatted in accordance with RFC 4360)'), - db_collation="natural_sort" ) tenant = models.ForeignKey( to='tenancy.Tenant', diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index b067181366..ab03bfc14e 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -153,11 +153,13 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): ) name = models.CharField( verbose_name=_('name'), - max_length=100 + max_length=100, + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), - max_length=100 + max_length=100, + db_collation='case_insensitive', ) description = models.CharField( verbose_name=_('description'), @@ -202,12 +204,14 @@ class OrganizationalModel(NetBoxModel): name = models.CharField( verbose_name=_('name'), max_length=100, - unique=True + unique=True, + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) description = models.CharField( verbose_name=_('description'), diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 19ffb2b0b6..de3e8fe8cd 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -55,7 +55,7 @@ class Contact(PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='natural_sort', ) title = models.CharField( verbose_name=_('title'), diff --git a/netbox/tenancy/models/tenants.py b/netbox/tenancy/models/tenants.py index 55f0c5933c..652e5a8bb3 100644 --- a/netbox/tenancy/models/tenants.py +++ b/netbox/tenancy/models/tenants.py @@ -19,12 +19,13 @@ class TenantGroup(NestedGroupModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort' ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive' ) class Meta: @@ -41,11 +42,12 @@ class Tenant(ContactsMixin, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), - max_length=100 + max_length=100, + db_collation='case_insensitive', ) group = models.ForeignKey( to='tenancy.TenantGroup', diff --git a/netbox/virtualization/models/clusters.py b/netbox/virtualization/models/clusters.py index 9f7b97e298..b132118041 100644 --- a/netbox/virtualization/models/clusters.py +++ b/netbox/virtualization/models/clusters.py @@ -51,7 +51,7 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) type = models.ForeignKey( verbose_name=_('type'), diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index aca2a7dbd1..31de43d261 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -5,7 +5,6 @@ from django.core.validators import MinValueValidator from django.db import models from django.db.models import Q, Sum -from django.db.models.functions import Lower from django.utils.translation import gettext_lazy as _ from dcim.models import BaseInterface @@ -70,7 +69,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co name = models.CharField( verbose_name=_('name'), max_length=64, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) status = models.CharField( max_length=50, @@ -156,11 +155,11 @@ class Meta: ordering = ('name', 'pk') # Name may be non-unique constraints = ( models.UniqueConstraint( - Lower('name'), 'cluster', 'tenant', + 'name', 'cluster', 'tenant', name='%(app_label)s_%(class)s_unique_name_cluster_tenant' ), models.UniqueConstraint( - Lower('name'), 'cluster', + '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.") @@ -275,7 +274,7 @@ class ComponentModel(NetBoxModel): name = models.CharField( verbose_name=_('name'), max_length=64, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) description = models.CharField( verbose_name=_('description'), diff --git a/netbox/vpn/models/crypto.py b/netbox/vpn/models/crypto.py index 8e991b5786..b0d0f3f0ed 100644 --- a/netbox/vpn/models/crypto.py +++ b/netbox/vpn/models/crypto.py @@ -23,7 +23,7 @@ class IKEProposal(PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) authentication_method = models.CharField( verbose_name=('authentication method'), @@ -69,7 +69,7 @@ class IKEPolicy(PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) version = models.PositiveSmallIntegerField( verbose_name=_('version'), @@ -128,7 +128,7 @@ class IPSecProposal(PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) encryption_algorithm = models.CharField( verbose_name=_('encryption'), @@ -180,7 +180,7 @@ class IPSecPolicy(PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) proposals = models.ManyToManyField( to='vpn.IPSecProposal', @@ -216,7 +216,7 @@ class IPSecProfile(PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) mode = models.CharField( verbose_name=_('mode'), diff --git a/netbox/vpn/models/l2vpn.py b/netbox/vpn/models/l2vpn.py index bab7510ffb..fb7b1b13e0 100644 --- a/netbox/vpn/models/l2vpn.py +++ b/netbox/vpn/models/l2vpn.py @@ -20,12 +20,13 @@ class L2VPN(ContactsMixin, PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) type = models.CharField( verbose_name=_('type'), diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py index a250367890..1d44bf54f7 100644 --- a/netbox/vpn/models/tunnels.py +++ b/netbox/vpn/models/tunnels.py @@ -32,7 +32,7 @@ class Tunnel(ContactsMixin, PrimaryModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) status = models.CharField( verbose_name=_('status'), diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 11f9e06eb7..517f3e8197 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -53,12 +53,13 @@ class WirelessLANGroup(NestedGroupModel): verbose_name=_('name'), max_length=100, unique=True, - db_collation="natural_sort" + db_collation='ci_natural_sort', ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, - unique=True + unique=True, + db_collation='case_insensitive', ) class Meta: From a34553325e3cc5df2b4f49a29064a80f2cc2bd5d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Oct 2025 15:03:46 -0400 Subject: [PATCH 3/4] Add migrations to remove indexes and alter field collations --- .../circuits/migrations/0053_ci_collations.py | 97 ++++++ netbox/core/migrations/0020_ci_collations.py | 30 ++ netbox/dcim/migrations/0217_ci_collations.py | 311 ++++++++++++++++++ .../extras/migrations/0134_ci_collations.py | 114 +++++++ netbox/ipam/migrations/0083_ci_collations.py | 100 ++++++ .../tenancy/migrations/0021_ci_collations.py | 70 ++++ .../migrations/0049_ci_collations.py | 92 ++++++ netbox/vpn/migrations/0010_ci_collations.py | 84 +++++ .../wireless/migrations/0016_ci_collations.py | 36 ++ 9 files changed, 934 insertions(+) create mode 100644 netbox/circuits/migrations/0053_ci_collations.py create mode 100644 netbox/core/migrations/0020_ci_collations.py create mode 100644 netbox/dcim/migrations/0217_ci_collations.py create mode 100644 netbox/extras/migrations/0134_ci_collations.py create mode 100644 netbox/ipam/migrations/0083_ci_collations.py create mode 100644 netbox/tenancy/migrations/0021_ci_collations.py create mode 100644 netbox/virtualization/migrations/0049_ci_collations.py create mode 100644 netbox/vpn/migrations/0010_ci_collations.py create mode 100644 netbox/wireless/migrations/0016_ci_collations.py diff --git a/netbox/circuits/migrations/0053_ci_collations.py b/netbox/circuits/migrations/0053_ci_collations.py new file mode 100644 index 0000000000..c847c12e3c --- /dev/null +++ b/netbox/circuits/migrations/0053_ci_collations.py @@ -0,0 +1,97 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'circuits_circuitgroup_name_ec8ac1e5_like', + 'circuits_circuitgroup_slug_61ca866b_like', + 'circuits_circuittype_name_8256ea9a_like', + 'circuits_circuittype_slug_9b4b3cf9_like', + 'circuits_provider_name_8f2514f5_like', + 'circuits_provider_slug_c3c0aa10_like', + 'circuits_virtualcircuittype_name_5184db16_like', + 'circuits_virtualcircuittype_slug_75d5c661_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0052_extend_circuit_abs_distance_upper_limit'), + ('dcim', '0216_ci_collations'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='circuit', + name='cid', + field=models.CharField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='circuitgroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='circuitgroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='circuittype', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='circuittype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='provider', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='provider', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='provideraccount', + name='account', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='provideraccount', + name='name', + field=models.CharField(blank=True, db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='providernetwork', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='virtualcircuit', + name='cid', + field=models.CharField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='virtualcircuittype', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='virtualcircuittype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + ] diff --git a/netbox/core/migrations/0020_ci_collations.py b/netbox/core/migrations/0020_ci_collations.py new file mode 100644 index 0000000000..d1ce9b0fd8 --- /dev/null +++ b/netbox/core/migrations/0020_ci_collations.py @@ -0,0 +1,30 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'core_datasource_name_17788499_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_configrevision_active'), + ('dcim', '0216_ci_collations'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='datasource', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + ] diff --git a/netbox/dcim/migrations/0217_ci_collations.py b/netbox/dcim/migrations/0217_ci_collations.py new file mode 100644 index 0000000000..97401cd380 --- /dev/null +++ b/netbox/dcim/migrations/0217_ci_collations.py @@ -0,0 +1,311 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'dcim_devicerole_slug_7952643b_like', + 'dcim_devicetype_slug_448745bd_like', + 'dcim_inventoryitemrole_name_4c8cfe6d_like', + 'dcim_inventoryitemrole_slug_3556c227_like', + 'dcim_location_slug_352c5472_like', + 'dcim_manufacturer_name_841fcd92_like', + 'dcim_manufacturer_slug_00430749_like', + 'dcim_moduletypeprofile_name_1709c36e_like', + 'dcim_platform_slug_b0908ae4_like', + 'dcim_rackrole_name_9077cfcc_like', + 'dcim_rackrole_slug_40bbcd3a_like', + 'dcim_racktype_slug_6bbb384a_like', + 'dcim_region_slug_ff078a66_like', + 'dcim_site_name_8fe66c76_like', + 'dcim_site_slug_4412c762_like', + 'dcim_sitegroup_slug_a11d2b04_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0216_ci_collations'), + ('extras', '0134_ci_collations'), + ('ipam', '0083_ci_collations'), + ('tenancy', '0021_ci_collations'), + ('virtualization', '0048_populate_mac_addresses'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.RemoveConstraint( + model_name='device', + name='dcim_device_unique_name_site_tenant', + ), + migrations.RemoveConstraint( + model_name='device', + name='dcim_device_unique_name_site', + ), + migrations.AlterField( + model_name='consoleport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='consoleporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='consoleserverport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='consoleserverporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(blank=True, db_collation='ci_natural_sort', max_length=64, null=True), + ), + migrations.AlterField( + model_name='devicebay', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='devicebaytemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='devicerole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='devicerole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='devicetype', + name='model', + field=models.CharField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='devicetype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='frontport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='interface', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='inventoryitem', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='inventoryitemrole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='inventoryitemrole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='inventoryitemtemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='location', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='location', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='manufacturer', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='manufacturer', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='modulebay', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='modulebaytemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='moduletype', + name='model', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='moduletypeprofile', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='platform', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='platform', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='powerfeed', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='poweroutlet', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='powerpanel', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='powerport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='rack', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='rackrole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='rackrole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='racktype', + name='model', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='racktype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='rearport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='rearporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='region', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='region', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='site', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='site', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='sitegroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='sitegroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='virtualdevicecontext', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint( + models.F('name'), models.F('site'), models.F('tenant'), name='dcim_device_unique_name_site_tenant' + ), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint( + models.F('name'), + models.F('site'), + condition=models.Q(('tenant__isnull', True)), + name='dcim_device_unique_name_site', + violation_error_message='Device name must be unique per site.', + ), + ), + ] diff --git a/netbox/extras/migrations/0134_ci_collations.py b/netbox/extras/migrations/0134_ci_collations.py new file mode 100644 index 0000000000..f990ad25e3 --- /dev/null +++ b/netbox/extras/migrations/0134_ci_collations.py @@ -0,0 +1,114 @@ +import django.core.validators +import re +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'extras_configcontext_name_4bbfe25d_like', + 'extras_configcontextprofile_name_070de83b_like', + 'extras_customfield_name_2fe72707_like', + 'extras_customfieldchoiceset_name_963e63ea_like', + 'extras_customlink_name_daed2d18_like', + 'extras_eventrule_name_899453c6_like', + 'extras_notificationgroup_name_70b0a3f9_like', + 'extras_savedfilter_name_8a4bbd09_like', + 'extras_savedfilter_slug_4f93a959_like', + 'extras_tag_name_9550b3d9_like', + 'extras_tag_slug_aaa5b7e9_like', + 'extras_webhook_name_82cf60b5_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0133_make_cf_minmax_decimal'), + ('dcim', '0216_ci_collations'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='configcontext', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='configcontextprofile', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='customfield', + name='name', + field=models.CharField( + db_collation='ci_natural_sort', + max_length=50, + unique=True, + validators=[ + django.core.validators.RegexValidator( + flags=re.RegexFlag['IGNORECASE'], + message='Only alphanumeric characters and underscores are allowed.', + regex='^[a-z0-9_]+$', + ), + django.core.validators.RegexValidator( + flags=re.RegexFlag['IGNORECASE'], + inverse_match=True, + message='Double underscores are not permitted in custom field names.', + regex='__', + ), + ], + ), + ), + migrations.AlterField( + model_name='customfieldchoiceset', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='customlink', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='eventrule', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=150, unique=True), + ), + migrations.AlterField( + model_name='notificationgroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='savedfilter', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='savedfilter', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tag', + name='slug', + field=models.SlugField(allow_unicode=True, db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='webhook', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=150, unique=True), + ), + ] diff --git a/netbox/ipam/migrations/0083_ci_collations.py b/netbox/ipam/migrations/0083_ci_collations.py new file mode 100644 index 0000000000..0c2652c512 --- /dev/null +++ b/netbox/ipam/migrations/0083_ci_collations.py @@ -0,0 +1,100 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'ipam_asnrange_name_c7585e73_like', + 'ipam_asnrange_slug_c8a7d8a1_like', + 'ipam_rir_name_64a71982_like', + 'ipam_rir_slug_ff1a369a_like', + 'ipam_role_name_13784849_like', + 'ipam_role_slug_309ca14c_like', + 'ipam_routetarget_name_212be79f_like', + 'ipam_servicetemplate_name_1a2f3410_like', + 'ipam_vlangroup_slug_40abcf6b_like', + 'ipam_vlantranslationpolicy_name_17e0a007_like', + 'ipam_vrf_rd_0ac1bde1_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0082_add_prefix_network_containment_indexes'), + ('dcim', '0216_ci_collations'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='asnrange', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='asnrange', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='rir', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='rir', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='role', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='role', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='routetarget', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=21, unique=True), + ), + migrations.AlterField( + model_name='servicetemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='vlan', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='vlangroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='vlangroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='vlantranslationpolicy', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='vrf', + name='rd', + field=models.CharField(blank=True, db_collation='case_insensitive', max_length=21, null=True, unique=True), + ), + ] diff --git a/netbox/tenancy/migrations/0021_ci_collations.py b/netbox/tenancy/migrations/0021_ci_collations.py new file mode 100644 index 0000000000..11af281342 --- /dev/null +++ b/netbox/tenancy/migrations/0021_ci_collations.py @@ -0,0 +1,70 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'tenancy_contactgroup_slug_5b0f3e75_like', + 'tenancy_contactrole_name_44b01a1f_like', + 'tenancy_contactrole_slug_c5837d7d_like', + 'tenancy_tenant_slug_0716575e_like', + 'tenancy_tenantgroup_name_53363199_like', + 'tenancy_tenantgroup_slug_e2af1cb6_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0020_remove_contactgroupmembership'), + ('dcim', '0216_ci_collations'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='contactgroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='contactgroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='contactrole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='contactrole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tenant', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='tenant', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='tenantgroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tenantgroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + ] diff --git a/netbox/virtualization/migrations/0049_ci_collations.py b/netbox/virtualization/migrations/0049_ci_collations.py new file mode 100644 index 0000000000..1eed7802b0 --- /dev/null +++ b/netbox/virtualization/migrations/0049_ci_collations.py @@ -0,0 +1,92 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'virtualization_clustergroup_name_4fcd26b4_like', + 'virtualization_clustergroup_slug_57ca1d23_like', + 'virtualization_clustertype_name_ea854d3d_like', + 'virtualization_clustertype_slug_8ee4d0e0_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0217_ci_collations'), + ('extras', '0134_ci_collations'), + ('ipam', '0083_ci_collations'), + ('tenancy', '0021_ci_collations'), + ('virtualization', '0048_populate_mac_addresses'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.RemoveConstraint( + model_name='virtualmachine', + name='virtualization_virtualmachine_unique_name_cluster_tenant', + ), + migrations.RemoveConstraint( + model_name='virtualmachine', + name='virtualization_virtualmachine_unique_name_cluster', + ), + migrations.AlterField( + model_name='cluster', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='clustergroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='clustergroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='clustertype', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='clustertype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='virtualdisk', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='virtualmachine', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AddConstraint( + model_name='virtualmachine', + constraint=models.UniqueConstraint( + models.F('name'), + models.F('cluster'), + models.F('tenant'), + name='virtualization_virtualmachine_unique_name_cluster_tenant', + ), + ), + migrations.AddConstraint( + model_name='virtualmachine', + constraint=models.UniqueConstraint( + models.F('name'), + models.F('cluster'), + condition=models.Q(('tenant__isnull', True)), + name='virtualization_virtualmachine_unique_name_cluster', + violation_error_message='Virtual machine name must be unique per cluster.', + ), + ), + ] diff --git a/netbox/vpn/migrations/0010_ci_collations.py b/netbox/vpn/migrations/0010_ci_collations.py new file mode 100644 index 0000000000..3863abcdbd --- /dev/null +++ b/netbox/vpn/migrations/0010_ci_collations.py @@ -0,0 +1,84 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'vpn_ikepolicy_name_5124aa3b_like', + 'vpn_ikeproposal_name_254623b7_like', + 'vpn_ipsecpolicy_name_cf28a1aa_like', + 'vpn_ipsecprofile_name_3ac63c72_like', + 'vpn_ipsecproposal_name_2fb98e2b_like', + 'vpn_l2vpn_name_8824eda5_like', + 'vpn_l2vpn_slug_76b5a174_like', + 'vpn_tunnel_name_f060beab_like', + 'vpn_tunnelgroup_name_9f6ebf92_like', + 'vpn_tunnelgroup_slug_9e614d62_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0216_ci_collations'), + ('vpn', '0009_remove_redundant_indexes'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='ikepolicy', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='ikeproposal', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='ipsecpolicy', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='ipsecprofile', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='ipsecproposal', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='l2vpn', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='l2vpn', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tunnel', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tunnelgroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='tunnelgroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + ] diff --git a/netbox/wireless/migrations/0016_ci_collations.py b/netbox/wireless/migrations/0016_ci_collations.py new file mode 100644 index 0000000000..a84034ab73 --- /dev/null +++ b/netbox/wireless/migrations/0016_ci_collations.py @@ -0,0 +1,36 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'wireless_wirelesslangroup_name_2ffd60c8_like', + 'wireless_wirelesslangroup_slug_f5d59831_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0216_ci_collations'), + ('wireless', '0015_extend_wireless_link_abs_distance_upper_limit'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='wirelesslangroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='wirelesslangroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + ] From 4f2f61c90db8f8fa90a984efa3590567965854cd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Oct 2025 15:25:45 -0400 Subject: [PATCH 4/4] Reindex migrations --- .../circuits/migrations/0053_ci_collations.py | 2 +- netbox/core/migrations/0020_ci_collations.py | 2 +- netbox/dcim/migrations/0216_ci_collations.py | 26 -- netbox/dcim/migrations/0217_ci_collations.py | 319 +----------------- netbox/dcim/migrations/0218_ci_collations.py | 311 +++++++++++++++++ .../extras/migrations/0134_ci_collations.py | 2 +- netbox/ipam/migrations/0083_ci_collations.py | 2 +- .../tenancy/migrations/0021_ci_collations.py | 2 +- netbox/vpn/migrations/0010_ci_collations.py | 2 +- .../wireless/migrations/0016_ci_collations.py | 2 +- 10 files changed, 335 insertions(+), 335 deletions(-) delete mode 100644 netbox/dcim/migrations/0216_ci_collations.py create mode 100644 netbox/dcim/migrations/0218_ci_collations.py diff --git a/netbox/circuits/migrations/0053_ci_collations.py b/netbox/circuits/migrations/0053_ci_collations.py index c847c12e3c..9c65ca7508 100644 --- a/netbox/circuits/migrations/0053_ci_collations.py +++ b/netbox/circuits/migrations/0053_ci_collations.py @@ -21,7 +21,7 @@ class Migration(migrations.Migration): dependencies = [ ('circuits', '0052_extend_circuit_abs_distance_upper_limit'), - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ] operations = [ diff --git a/netbox/core/migrations/0020_ci_collations.py b/netbox/core/migrations/0020_ci_collations.py index d1ce9b0fd8..ba39f21f18 100644 --- a/netbox/core/migrations/0020_ci_collations.py +++ b/netbox/core/migrations/0020_ci_collations.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): dependencies = [ ('core', '0019_configrevision_active'), - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ] operations = [ diff --git a/netbox/dcim/migrations/0216_ci_collations.py b/netbox/dcim/migrations/0216_ci_collations.py deleted file mode 100644 index ee0aa637d8..0000000000 --- a/netbox/dcim/migrations/0216_ci_collations.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.contrib.postgres.operations import CreateCollation -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0215_rackreservation_status'), - ] - - operations = [ - # Create a case-insensitive collation - CreateCollation( - 'case_insensitive', - provider='icu', - locale='und-u-ks-level2', - deterministic=False, - ), - # Create a case-insensitive collation with natural sorting - CreateCollation( - 'ci_natural_sort', - provider='icu', - locale='und-u-kn-true-ks-level2', - deterministic=False, - ), - ] diff --git a/netbox/dcim/migrations/0217_ci_collations.py b/netbox/dcim/migrations/0217_ci_collations.py index 97401cd380..6fb52e627f 100644 --- a/netbox/dcim/migrations/0217_ci_collations.py +++ b/netbox/dcim/migrations/0217_ci_collations.py @@ -1,311 +1,26 @@ -from django.db import migrations, models - -PATTERN_OPS_INDEXES = [ - 'dcim_devicerole_slug_7952643b_like', - 'dcim_devicetype_slug_448745bd_like', - 'dcim_inventoryitemrole_name_4c8cfe6d_like', - 'dcim_inventoryitemrole_slug_3556c227_like', - 'dcim_location_slug_352c5472_like', - 'dcim_manufacturer_name_841fcd92_like', - 'dcim_manufacturer_slug_00430749_like', - 'dcim_moduletypeprofile_name_1709c36e_like', - 'dcim_platform_slug_b0908ae4_like', - 'dcim_rackrole_name_9077cfcc_like', - 'dcim_rackrole_slug_40bbcd3a_like', - 'dcim_racktype_slug_6bbb384a_like', - 'dcim_region_slug_ff078a66_like', - 'dcim_site_name_8fe66c76_like', - 'dcim_site_slug_4412c762_like', - 'dcim_sitegroup_slug_a11d2b04_like', -] - - -def remove_indexes(apps, schema_editor): - for idx in PATTERN_OPS_INDEXES: - schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') +from django.contrib.postgres.operations import CreateCollation +from django.db import migrations class Migration(migrations.Migration): + dependencies = [ - ('dcim', '0216_ci_collations'), - ('extras', '0134_ci_collations'), - ('ipam', '0083_ci_collations'), - ('tenancy', '0021_ci_collations'), - ('virtualization', '0048_populate_mac_addresses'), + ('dcim', '0216_poweroutlettemplate_color'), ] operations = [ - migrations.RunPython( - code=remove_indexes, - reverse_code=migrations.RunPython.noop, - ), - migrations.RemoveConstraint( - model_name='device', - name='dcim_device_unique_name_site_tenant', - ), - migrations.RemoveConstraint( - model_name='device', - name='dcim_device_unique_name_site', - ), - migrations.AlterField( - model_name='consoleport', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='consoleporttemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='consoleserverport', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='consoleserverporttemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='device', - name='name', - field=models.CharField(blank=True, db_collation='ci_natural_sort', max_length=64, null=True), - ), - migrations.AlterField( - model_name='devicebay', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='devicebaytemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='devicerole', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='devicerole', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='devicetype', - name='model', - field=models.CharField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='devicetype', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='frontport', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='frontporttemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='interface', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='inventoryitem', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='inventoryitemrole', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='inventoryitemrole', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='inventoryitemtemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='location', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='location', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='manufacturer', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='manufacturer', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='modulebay', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='modulebaytemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='moduletype', - name='model', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='moduletypeprofile', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='platform', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='platform', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='powerfeed', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='poweroutlet', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='poweroutlettemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='powerpanel', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='powerport', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='rack', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='rackrole', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='rackrole', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='racktype', - name='model', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='racktype', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='rearport', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='rearporttemplate', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AlterField( - model_name='region', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='region', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='site', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='site', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), - ), - migrations.AlterField( - model_name='sitegroup', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=100), - ), - migrations.AlterField( - model_name='sitegroup', - name='slug', - field=models.SlugField(db_collation='case_insensitive', max_length=100), - ), - migrations.AlterField( - model_name='virtualdevicecontext', - name='name', - field=models.CharField(db_collation='ci_natural_sort', max_length=64), - ), - migrations.AddConstraint( - model_name='device', - constraint=models.UniqueConstraint( - models.F('name'), models.F('site'), models.F('tenant'), name='dcim_device_unique_name_site_tenant' - ), - ), - migrations.AddConstraint( - model_name='device', - constraint=models.UniqueConstraint( - models.F('name'), - models.F('site'), - condition=models.Q(('tenant__isnull', True)), - name='dcim_device_unique_name_site', - violation_error_message='Device name must be unique per site.', - ), + # Create a case-insensitive collation + CreateCollation( + 'case_insensitive', + provider='icu', + locale='und-u-ks-level2', + deterministic=False, + ), + # Create a case-insensitive collation with natural sorting + CreateCollation( + 'ci_natural_sort', + provider='icu', + locale='und-u-kn-true-ks-level2', + deterministic=False, ), ] diff --git a/netbox/dcim/migrations/0218_ci_collations.py b/netbox/dcim/migrations/0218_ci_collations.py new file mode 100644 index 0000000000..b24c6cc6d2 --- /dev/null +++ b/netbox/dcim/migrations/0218_ci_collations.py @@ -0,0 +1,311 @@ +from django.db import migrations, models + +PATTERN_OPS_INDEXES = [ + 'dcim_devicerole_slug_7952643b_like', + 'dcim_devicetype_slug_448745bd_like', + 'dcim_inventoryitemrole_name_4c8cfe6d_like', + 'dcim_inventoryitemrole_slug_3556c227_like', + 'dcim_location_slug_352c5472_like', + 'dcim_manufacturer_name_841fcd92_like', + 'dcim_manufacturer_slug_00430749_like', + 'dcim_moduletypeprofile_name_1709c36e_like', + 'dcim_platform_slug_b0908ae4_like', + 'dcim_rackrole_name_9077cfcc_like', + 'dcim_rackrole_slug_40bbcd3a_like', + 'dcim_racktype_slug_6bbb384a_like', + 'dcim_region_slug_ff078a66_like', + 'dcim_site_name_8fe66c76_like', + 'dcim_site_slug_4412c762_like', + 'dcim_sitegroup_slug_a11d2b04_like', +] + + +def remove_indexes(apps, schema_editor): + for idx in PATTERN_OPS_INDEXES: + schema_editor.execute(f'DROP INDEX IF EXISTS {idx}') + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0217_ci_collations'), + ('extras', '0134_ci_collations'), + ('ipam', '0083_ci_collations'), + ('tenancy', '0021_ci_collations'), + ('virtualization', '0048_populate_mac_addresses'), + ] + + operations = [ + migrations.RunPython( + code=remove_indexes, + reverse_code=migrations.RunPython.noop, + ), + migrations.RemoveConstraint( + model_name='device', + name='dcim_device_unique_name_site_tenant', + ), + migrations.RemoveConstraint( + model_name='device', + name='dcim_device_unique_name_site', + ), + migrations.AlterField( + model_name='consoleport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='consoleporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='consoleserverport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='consoleserverporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(blank=True, db_collation='ci_natural_sort', max_length=64, null=True), + ), + migrations.AlterField( + model_name='devicebay', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='devicebaytemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='devicerole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='devicerole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='devicetype', + name='model', + field=models.CharField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='devicetype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='frontport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='interface', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='inventoryitem', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='inventoryitemrole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='inventoryitemrole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='inventoryitemtemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='location', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='location', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='manufacturer', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='manufacturer', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='modulebay', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='modulebaytemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='moduletype', + name='model', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='moduletypeprofile', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='platform', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='platform', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='powerfeed', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='poweroutlet', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='powerpanel', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='powerport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='rack', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='rackrole', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='rackrole', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='racktype', + name='model', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='racktype', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='rearport', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='rearporttemplate', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AlterField( + model_name='region', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='region', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='site', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='site', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100, unique=True), + ), + migrations.AlterField( + model_name='sitegroup', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=100), + ), + migrations.AlterField( + model_name='sitegroup', + name='slug', + field=models.SlugField(db_collation='case_insensitive', max_length=100), + ), + migrations.AlterField( + model_name='virtualdevicecontext', + name='name', + field=models.CharField(db_collation='ci_natural_sort', max_length=64), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint( + models.F('name'), models.F('site'), models.F('tenant'), name='dcim_device_unique_name_site_tenant' + ), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint( + models.F('name'), + models.F('site'), + condition=models.Q(('tenant__isnull', True)), + name='dcim_device_unique_name_site', + violation_error_message='Device name must be unique per site.', + ), + ), + ] diff --git a/netbox/extras/migrations/0134_ci_collations.py b/netbox/extras/migrations/0134_ci_collations.py index f990ad25e3..8da9545a1b 100644 --- a/netbox/extras/migrations/0134_ci_collations.py +++ b/netbox/extras/migrations/0134_ci_collations.py @@ -26,7 +26,7 @@ def remove_indexes(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ ('extras', '0133_make_cf_minmax_decimal'), - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ] operations = [ diff --git a/netbox/ipam/migrations/0083_ci_collations.py b/netbox/ipam/migrations/0083_ci_collations.py index 0c2652c512..7bba154713 100644 --- a/netbox/ipam/migrations/0083_ci_collations.py +++ b/netbox/ipam/migrations/0083_ci_collations.py @@ -24,7 +24,7 @@ class Migration(migrations.Migration): dependencies = [ ('ipam', '0082_add_prefix_network_containment_indexes'), - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ] operations = [ diff --git a/netbox/tenancy/migrations/0021_ci_collations.py b/netbox/tenancy/migrations/0021_ci_collations.py index 11af281342..2378572a55 100644 --- a/netbox/tenancy/migrations/0021_ci_collations.py +++ b/netbox/tenancy/migrations/0021_ci_collations.py @@ -19,7 +19,7 @@ class Migration(migrations.Migration): dependencies = [ ('tenancy', '0020_remove_contactgroupmembership'), - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ] operations = [ diff --git a/netbox/vpn/migrations/0010_ci_collations.py b/netbox/vpn/migrations/0010_ci_collations.py index 3863abcdbd..6772b6afcb 100644 --- a/netbox/vpn/migrations/0010_ci_collations.py +++ b/netbox/vpn/migrations/0010_ci_collations.py @@ -22,7 +22,7 @@ def remove_indexes(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ('vpn', '0009_remove_redundant_indexes'), ] diff --git a/netbox/wireless/migrations/0016_ci_collations.py b/netbox/wireless/migrations/0016_ci_collations.py index a84034ab73..84e5e4ff30 100644 --- a/netbox/wireless/migrations/0016_ci_collations.py +++ b/netbox/wireless/migrations/0016_ci_collations.py @@ -14,7 +14,7 @@ def remove_indexes(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0216_ci_collations'), + ('dcim', '0217_ci_collations'), ('wireless', '0015_extend_wireless_link_abs_distance_upper_limit'), ]