# Django Universal Constraints
Application-level constraint validation for Django backends that don't support conditional/unique constraints.
## Problem & Solution
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.
## Technical Architecture
### App Startup Integration
- Constraints are discovered and converted during Django app initialization (`apps.py` ready method)
- Django's `UniqueConstraint` and `unique_together` definitions are automatically converted to application-level validators (`UniversalConstraint`)
- Original model definitions remain unchanged
### Signal-Based Validation
- `pre_save` signal intercepts all model saves before database write
- Constraint validation occurs via additional SELECT queries
- Validation respects Django's database routing system
### Database Constraint Handling
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.
### Performance Characteristics
- **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)
## Installation
```bash
pip install django-universal-constraints
```
## Configuration
### Required: INSTALLED_APPS
**Critical**: `universal_constraints` must be placed LAST in `INSTALLED_APPS`, after all applications that define models:
```python
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
# ... your apps with models
'myapp',
'anotherapp',
# Must be last:
'universal_constraints',
]
```
### Optional: Per-Database Settings
```python
UNIVERSAL_CONSTRAINTS = {
'database_alias': {
'EXCLUDE_APPS': ['admin', 'auth', 'contenttypes', 'sessions'],
'RACE_CONDITION_PROTECTION': True, # Default: True
'LOG_LEVEL': 'INFO',
}
}
```
## Usage (Simple)
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.
## Usage (Advanced)
### Programmatic Constraint Addition
```python
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 unique
```
### Multi-Database Configuration
```python
DATABASES = {
'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,
}
}
```
## Race Condition Protection
### When to Enable
- **High Concurrency**: Multiple processes/threads modifying same constraint fields
- **Critical Data Integrity**: When constraint violations must be prevented
### How It Works
- Uses `select_for_update()` to create database row locks
- Prevents race conditions across different processes/transactions
- Blocks concurrent validation until transaction completes
### Performance Impact
- **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
```python
UNIVERSAL_CONSTRAINTS = {
'default': {
'RACE_CONDITION_PROTECTION': True, # Enable for critical data
}
}
```
## Implementation Limitations
### Q-Object Evaluation
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
### Performance vs Native Constraints
- **Application-level**: 1-2 additional SELECT queries per save
- **Database-level**: Zero query overhead, handled by database engine
- **Trade-off**: Compatibility vs performance
## Management Commands
### Constraint Discovery
```bash
python manage.py discover_constraints
python manage.py discover_constraints --format=json
```
## Supported Backends
- ✅ SQLite (`django.db.backends.sqlite3`)
- ✅ PostgreSQL (`django.db.backends.postgresql`)
- ✅ MySQL (`django.db.backends.mysql`)
- ✅ YDB (`django-ydb-backend`)
- ✅ Any Django-compatible backend
## Testing (and development)
Run the test suite:
```bash
uv sync
uv run tests/runtests.py
```
## Troubleshooting
### Common Issues
- **"No such table" errors**: Ensure `universal_constraints` is last in `INSTALLED_APPS`
- **Constraints not validated**: Check database is configured in `UNIVERSAL_CONSTRAINTS`
- **Migration failures**: May occur with backends that don't support conditional constraints
### Debug Logging
```python
LOGGING = {
'version': 1,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'loggers': {
'universal_constraints': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
```
Raw data
{
"_id": null,
"home_page": null,
"name": "django-universal-constraints",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "backend, compatibility, constraints, database, django, unique, validation",
"author": null,
"author_email": "J2K Studio <dev@j2k.studio>",
"download_url": "https://files.pythonhosted.org/packages/4b/e3/9d80515f559cd756bb21380300c386d140585943481707156133135b7c85/django_universal_constraints-1.1.1.tar.gz",
"platform": null,
"description": "# Django Universal Constraints\n\nApplication-level constraint validation for Django backends that don't support conditional/unique constraints.\n\n## Problem & Solution\n\nSome 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.\n\n**Solution**: Automatic constraint interception during app startup, with validation via Django's `pre_save` signal system.\n\n## Technical Architecture\n\n### App Startup Integration\n- Constraints are discovered and converted during Django app initialization (`apps.py` ready method)\n- Django's `UniqueConstraint` and `unique_together` definitions are automatically converted to application-level validators (`UniversalConstraint`)\n- Original model definitions remain unchanged\n\n### Signal-Based Validation\n- `pre_save` signal intercepts all model saves before database write\n- Constraint validation occurs via additional SELECT queries\n- Validation respects Django's database routing system\n\n### Database Constraint Handling\nAll 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.\n\n**Database Backend Responsibility**: How constraints are handled at the database level depends entirely on the database backend being used:\n- Some backends may skip unsupported constraints during migrations (no error)\n- Some backends may add supported constraints to the database schema\n- Some backends may raise errors for unsupported constraint types\n\nThis 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.\n\n### Performance Characteristics\n- **Additional Queries**: 1-2 SELECT queries per save operation for constraint validation\n- **Race Condition Protection**: Optional `select_for_update()` adds database locking overhead\n- **Memory Overhead**: Minimal (constraint metadata stored per model class)\n\n## Installation\n\n```bash\npip install django-universal-constraints\n```\n\n## Configuration\n\n### Required: INSTALLED_APPS\n**Critical**: `universal_constraints` must be placed LAST in `INSTALLED_APPS`, after all applications that define models:\n\n```python\nINSTALLED_APPS = [\n 'django.contrib.admin',\n 'django.contrib.auth',\n # ... your apps with models\n 'myapp',\n 'anotherapp',\n # Must be last:\n 'universal_constraints',\n]\n```\n\n### Optional: Per-Database Settings\n```python\nUNIVERSAL_CONSTRAINTS = {\n 'database_alias': {\n 'EXCLUDE_APPS': ['admin', 'auth', 'contenttypes', 'sessions'],\n 'RACE_CONDITION_PROTECTION': True, # Default: True\n 'LOG_LEVEL': 'INFO',\n }\n}\n```\n\n## Usage (Simple)\nAfter 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.\n\n## Usage (Advanced)\n\n### Programmatic Constraint Addition\n```python\nfrom universal_constraints.validators import add_universal_constraint\n\nadd_universal_constraint(\n User,\n fields=['username'],\n condition=Q(is_active=True),\n name='unique_active_username'\n) # Adds a UniversalConstraint for the User model. If Users have is_active=True, then usernames must be unique\n```\n\n### Multi-Database Configuration\n```python\nDATABASES = {\n 'postgres_db': {\n 'ENGINE': 'django.db.backends.postgresql',\n },\n 'ydb_database': {\n 'ENGINE': 'ydb_backend.backend',\n }\n}\n\nUNIVERSAL_CONSTRAINTS = {\n 'postgres_db': {\n 'RACE_CONDITION_PROTECTION': False,\n },\n 'ydb_database': {\n 'RACE_CONDITION_PROTECTION': True,\n }\n}\n```\n\n## Race Condition Protection\n\n### When to Enable\n- **High Concurrency**: Multiple processes/threads modifying same constraint fields\n- **Critical Data Integrity**: When constraint violations must be prevented\n\n### How It Works\n- Uses `select_for_update()` to create database row locks\n- Prevents race conditions across different processes/transactions\n- Blocks concurrent validation until transaction completes\n\n### Performance Impact\n- **Additional Overhead**: Database locking adds latency\n- **Recommendation**: Enable for critical constraints, disable for high-throughput scenarios\n- **Fallback**: Gracefully degrades to non-protected validation if locking fails\n\n```python\nUNIVERSAL_CONSTRAINTS = {\n 'default': {\n 'RACE_CONDITION_PROTECTION': True, # Enable for critical data\n }\n}\n```\n\n## Implementation Limitations\n\n### Q-Object Evaluation\nSupports common Django field lookups:\n- `exact`, `isnull`, `in`, `gt`, `gte`, `lt`, `lte`\n- **Limitation**: Complex lookups fall back to \"assume condition applies\"\n- **Behavior**: Conservative approach prevents false negatives\n\n### Performance vs Native Constraints\n- **Application-level**: 1-2 additional SELECT queries per save\n- **Database-level**: Zero query overhead, handled by database engine\n- **Trade-off**: Compatibility vs performance\n\n## Management Commands\n\n### Constraint Discovery\n```bash\npython manage.py discover_constraints\npython manage.py discover_constraints --format=json\n```\n\n## Supported Backends\n\n- \u2705 SQLite (`django.db.backends.sqlite3`)\n- \u2705 PostgreSQL (`django.db.backends.postgresql`)\n- \u2705 MySQL (`django.db.backends.mysql`)\n- \u2705 YDB (`django-ydb-backend`)\n- \u2705 Any Django-compatible backend\n\n## Testing (and development)\n\nRun the test suite:\n\n```bash\nuv sync\nuv run tests/runtests.py\n```\n\n## Troubleshooting\n\n### Common Issues\n- **\"No such table\" errors**: Ensure `universal_constraints` is last in `INSTALLED_APPS`\n- **Constraints not validated**: Check database is configured in `UNIVERSAL_CONSTRAINTS`\n- **Migration failures**: May occur with backends that don't support conditional constraints\n\n### Debug Logging\n```python\nLOGGING = {\n 'version': 1,\n 'handlers': {\n 'console': {'class': 'logging.StreamHandler'},\n },\n 'loggers': {\n 'universal_constraints': {\n 'handlers': ['console'],\n 'level': 'DEBUG',\n },\n },\n}\n```\n",
"bugtrack_url": null,
"license": null,
"summary": "Universal conditional/unique constraint support for Django - makes unique constraints work with ANY database backend",
"version": "1.1.1",
"project_urls": {
"Bug Tracker": "https://github.com/j2k-studio/django-universal-constraints/issues",
"Documentation": "https://github.com/j2k-studio/django-universal-constraints",
"Homepage": "https://github.com/j2k-studio/django-universal-constraints",
"Repository": "https://github.com/j2k-studio/django-universal-constraints"
},
"split_keywords": [
"backend",
" compatibility",
" constraints",
" database",
" django",
" unique",
" validation"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "86e3b0433e6c06cb155f794563c41b972ceaf06d1f781aae23e3036ccd954d7c",
"md5": "49b291cdd3f51f2ab6499f153e29215c",
"sha256": "44a6fff11355b84126b86c813a67d827327a5cdc22e382d175c0dee4468c895e"
},
"downloads": -1,
"filename": "django_universal_constraints-1.1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "49b291cdd3f51f2ab6499f153e29215c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 17729,
"upload_time": "2025-08-07T23:11:05",
"upload_time_iso_8601": "2025-08-07T23:11:05.710391Z",
"url": "https://files.pythonhosted.org/packages/86/e3/b0433e6c06cb155f794563c41b972ceaf06d1f781aae23e3036ccd954d7c/django_universal_constraints-1.1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "4be39d80515f559cd756bb21380300c386d140585943481707156133135b7c85",
"md5": "457ade4156228af3fb5128c6221ed78d",
"sha256": "66746c03c1467e0f22fe8ae13d096e6ada4aedd6526de82ac6c3a71fb680519e"
},
"downloads": -1,
"filename": "django_universal_constraints-1.1.1.tar.gz",
"has_sig": false,
"md5_digest": "457ade4156228af3fb5128c6221ed78d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 70092,
"upload_time": "2025-08-07T23:11:07",
"upload_time_iso_8601": "2025-08-07T23:11:07.054872Z",
"url": "https://files.pythonhosted.org/packages/4b/e3/9d80515f559cd756bb21380300c386d140585943481707156133135b7c85/django_universal_constraints-1.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-07 23:11:07",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "j2k-studio",
"github_project": "django-universal-constraints",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "django-universal-constraints"
}