Django username uniqueness depends on database collation
0 reputation · 03 Apr 2023, 03:44 UTC
0 reputation · 03 Apr 2023, 03:44 UTC
The goal is consistent case-insensitive uniqueness for Django's USERNAME_FIELD across PostgreSQL and MySQL, so usernames differing only by case are treated as duplicates on either backend.
Django enforces uniqueness through a database-level unique constraint. PostgreSQL's default collation is case-sensitive, while MySQL's default collation is typically case-insensitive. Django documents the discrepancy but does not supply automatic cross-backend normalization.
Yes – Django’s enforcement of uniqueness for USERNAME_FIELD follows the database collation. PostgreSQL’s default collation is case‑sensitive, while MySQL’s default collation is typically case‑insensitive, so the same model behaves differently on the two backends.
The uniqueness check is performed by a database‑level unique constraint (or index). Django’s UniqueValidator merely mirrors that constraint; it does not add its own case‑insensitive logic. Therefore, if the column collation treats 'User' and 'user' as distinct values, PostgreSQL will allow both, whereas MySQL will reject the second insert.
UniqueValidator runs a case‑sensitive filter(username=value) query.citext extension or a nondeterministic ICU collation for true case‑insensitive columns; MySQL offers case‑insensitive collations such as utf8mb4_0900_ai_ci.lower(username) yields case‑insensitive uniqueness on both backends, but Django’s migration autodetector does not detect it, so it must be added via RunSQL.save() or clean()) together with a regular unique constraint works identically on PostgreSQL and MySQL, at the cost of losing the original casing.CREATE EXTENSION citext; on PostgreSQL or choose a case‑insensitive collation when creating the MySQL table, alter the column to that type and keep Django’s default unique=True. This requires no extra indexes or application logic.# migrations/0002_add_username_ci_index.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('your_app', '0001_initial'),
]
operations = [
migrations.RunSQL(
sql="""
CREATE UNIQUE INDEX username_ci_idx
ON auth_user (lower(username));
""",
reverse_sql="""
DROP INDEX username_ci_idx;
""",
)
]
Keep the model’s username = models.CharField(unique=True) so Django’s ORM still generates the usual constraint; the functional index adds the case‑insensitive guard.
# models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
def clean(self):
self.username = self.username.lower()
super().clean()
def save(self, *args, **kwargs):
self.username = self.username.lower()
super().save(*args, **kwargs)
Run migrations as usual; the unique constraint will now treat 'User' and 'user' as duplicates because both are stored as 'user'.
User.objects.create(username='Tester') followed by User.objects.create(username='tester').IntegrityError on the second create, confirming case‑insensitive uniqueness.If you cannot change the collation or install citext, please confirm whether you are willing to store usernames in lower case (losing original casing). This determines whether the functional‑index route or the lower‑casing‑at‑save route is appropriate for your project.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.