Application-level constraint validation for Django backends that don't support conditional/unique constraints.
Some Django database backends (e.g., django-ydb-backend) lack support for conditional/unique constraints, causing migrations to fail when models define UniqueConstraint. This library provides transparent application-level validation that works with any Django backend.
Solution: Automatic constraint interception during app startup, with validation via Django's pre_save signal system.
- Constraints are discovered and converted during Django app initialization (
apps.pyready method) - Django's
UniqueConstraintandunique_togetherdefinitions are automatically converted to application-level validators (UniversalConstraint) - Original model definitions remain unchanged
pre_savesignal intercepts all model saves before database write- Constraint validation occurs via additional SELECT queries
- Validation respects Django's database routing system
All constraints are handled at the application level only. The library provides app-level validation via Django signals, while leaving the original constraint definitions in your models unchanged.
Database Backend Responsibility: How constraints are handled at the database level depends entirely on the database backend being used:
- Some backends may skip unsupported constraints during migrations (no error)
- Some backends may add supported constraints to the database schema
- Some backends may raise errors for unsupported constraint types
This is now the responsibility of the individual database backend, not this library. The library focuses purely on providing reliable application-level validation that works consistently across all backends.
- Additional Queries: 1-2 SELECT queries per save operation for constraint validation
- Race Condition Protection: Optional
select_for_update()adds database locking overhead - Memory Overhead: Minimal (constraint metadata stored per model class)
pip install django-universal-constraintsCritical: universal_constraints must be placed LAST in INSTALLED_APPS, after all applications that define models:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
# ... your apps with models
'myapp',
'anotherapp',
# Must be last:
'universal_constraints',
]UNIVERSAL_CONSTRAINTS = {
'database_alias': {
'EXCLUDE_APPS': ['admin', 'auth', 'contenttypes', 'sessions'],
'RACE_CONDITION_PROTECTION': True, # Default: True
'LOG_LEVEL': 'INFO',
}
}After adding universal_constraints to INSTALLED_APPS and configuring your database settings, the auto-discovery system automatically runs during Django startup. No additional setup is required - your existing model constraints will be automatically converted to application-level validation.
from universal_constraints.validators import add_universal_constraint
add_universal_constraint(
User,
fields=['username'],
condition=Q(is_active=True),
name='unique_active_username'
) # Adds a UniversalConstraint for the User model. If Users have is_active=True, then usernames must be uniqueDATABASES = {
'postgres_db': {
'ENGINE': 'django.db.backends.postgresql',
},
'ydb_database': {
'ENGINE': 'ydb_backend.backend',
}
}
UNIVERSAL_CONSTRAINTS = {
'postgres_db': {
'RACE_CONDITION_PROTECTION': False,
},
'ydb_database': {
'RACE_CONDITION_PROTECTION': True,
}
}- High Concurrency: Multiple processes/threads modifying same constraint fields
- Critical Data Integrity: When constraint violations must be prevented
- Uses
select_for_update()to create database row locks - Prevents race conditions across different processes/transactions
- Blocks concurrent validation until transaction completes
- Additional Overhead: Database locking adds latency
- Recommendation: Enable for critical constraints, disable for high-throughput scenarios
- Fallback: Gracefully degrades to non-protected validation if locking fails
UNIVERSAL_CONSTRAINTS = {
'default': {
'RACE_CONDITION_PROTECTION': True, # Enable for critical data
}
}Supports common Django field lookups:
exact,isnull,in,gt,gte,lt,lte- Limitation: Complex lookups fall back to "assume condition applies"
- Behavior: Conservative approach prevents false negatives
- Application-level: 1-2 additional SELECT queries per save
- Database-level: Zero query overhead, handled by database engine
- Trade-off: Compatibility vs performance
python manage.py discover_constraints
python manage.py discover_constraints --format=json- ✅ SQLite (
django.db.backends.sqlite3) - ✅ PostgreSQL (
django.db.backends.postgresql) - ✅ MySQL (
django.db.backends.mysql) - ✅ YDB (
django-ydb-backend) - ✅ Any Django-compatible backend
Run the test suite:
uv sync
uv run tests/runtests.py- "No such table" errors: Ensure
universal_constraintsis last inINSTALLED_APPS - Constraints not validated: Check database is configured in
UNIVERSAL_CONSTRAINTS - Migration failures: May occur with backends that don't support conditional constraints
LOGGING = {
'version': 1,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'loggers': {
'universal_constraints': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}