django-universal-constraints


Namedjango-universal-constraints JSON
Version 1.1.1 PyPI version JSON
download
home_pageNone
SummaryUniversal conditional/unique constraint support for Django - makes unique constraints work with ANY database backend
upload_time2025-08-07 23:11:07
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords backend compatibility constraints database django unique validation
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # 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"
}
        
Elapsed time: 1.67472s