django-postgres-extra

repository·master·Indexed 20 days ago

https://github.com/sectorlabs/django-postgres-extra

A library that extends the Django ORM to expose advanced PostgreSQL features, including table partitioning, materialized views, atomic upserts, and schema management. It provides specialized tools such as PostgresManager, PostgresQuerySet, HStoreField, and conflict handling via .on_conflict() for concurrency-safe inserts. The library integrates with Django migrations through a patching framework to support PostgresPartitionedModel, PostgresViewModel, and PostgresMaterializedView.

Tokens
19.9K
Snippets
59
Records
76
Agent score
72%

What's inside django-postgres-extra

  1. Overview of django-postgres-extra features

    master

    django-postgres-extra extends the Django ORM to provide native support for advanced PostgreSQL features. Unlike smaller, single-feature packages, this library provides well-tested implementations that integrate seamlessly with Django migrations, meaning you do not need to manually modify migration files to use its fields and objects.

    Key capabilities include:

    • Conflict Handling: Atomic, concurrency-safe upserts using PostgreSQL's ON CONFLICT syntax (supports DO UPDATE and DO NOTHING, including conditional updates).
    • Table Partitioning: Support for PostgreSQL 11.x declarative table partitioning, fully integrated into Django migrations. Includes commands for automatic time-based partition creation.
    • Views & Materialized Views: Ability to define views and materialized views as standard Django models, integrated into migrations.
    • Locking: Support for explicit table-level locks.
    • Schema Management: Tools for creating and dropping PostgreSQL schemas.
    • Truncation: Support for TRUNCATE TABLE statements, including cascading deletes.
    • Advanced Indexing:
      • Partial unique indices (conditional application).
      • Case-insensitive indices.
      • Multi-field unique indices.
    • HStore Constraints: Unique and required constraints on specific hstore keys.
  2. Manage PostgreSQL schemas with PostgresSchema

    master

    The PostgresSchema class provides basic schema management functionality in PostgreSQL.

    Important Note: This module does NOT provide multi-schema support for Django models. Django does not natively support custom schemas. This module is strictly for creating/dropping schemas and executing raw SQL within a specific schema.

  3. Handle PostgreSQL conflicts with on_conflict()

    master

    The PostgresManager provides support for PostgreSQL's ON CONFLICT clause, allowing for concurrency-safe inserts. You can specify whether to update an existing row (ConflictAction.UPDATE) or do nothing if a conflict occurs (ConflictAction.NOTHING).

    Important: Standard Django methods like .create(), .update(), .get_or_create(), and .update_or_create() are not affected by .on_conflict(). You must use the specific psqlextra methods like .insert() or .insert_and_get() for the conflict logic to apply.

    from django.db import models
    from psqlextra.models import PostgresModel
    from psqlextra.query import ConflictAction
    
    class MyModel(PostgresModel):
        myfield = models.CharField(max_length=255, unique=True)
    
    # Upsert: insert or update if already exists, then fetch the object
    obj2 = (
        MyModel.objects
        .on_conflict(['myfield'], ConflictAction.UPDATE)
        .insert_and_get(myfield='beer')
    )
    
    # Do nothing: insert, or do nothing if it already exists, then fetch
    obj1 = (
        MyModel.objects
        .on_conflict(['myfield'], ConflictAction.NOTHING)
        .insert_and_get(myfield='beer')
    )
    
    # Insert only: insert or update if already exists, then fetch only the primary key
    id = (
        MyModel.objects
        .on_conflict(['myfield'], ConflictAction.UPDATE)
        .insert(myfield='beer')
    )
  4. Configure primary keys for partitioned models

    master

    PostgreSQL requires that the primary key is either the same as or includes the partitioning key. Behavior depends on your Django version:

    Django < 5.2

    • If the PK is the same as the partitioning key, standard behavior applies.
    • If the PK is different or the partitioning key is composite, an implicit composite primary key is created (not visible to Django).

    Django >= 5.2

    • No explicit PK defined: A CompositePrimaryKey is automatically created including an auto-incrementing id field and the partitioning keys.
    • Explicit CompositePrimaryKey defined: The library makes no modifications; you are responsible for ensuring partitioning keys are included in the definition.

    Warning: Manually defining a pk using CompositePrimaryKey overrides the default behavior that includes an auto-incrementing id field.

    # Custom composite primary key (overrides default auto-incrementing id)
    class MyModel(PostgresPartitionedModel):
        class PartitioningMeta:
            method = PostgresPartitioningMethod.RANGE
            key = ["timestamp"]
    
        pk = models.CompositePrimaryKey("name", "timestamp")
        name = models.TextField()
        timestamp = models.DateTimeField()
  5. What the migration patches transform

    master

    The migration patches hook into Django's MigrationAutodetector and ProjectState to transform standard Django operations into Postgres-specific ones. This ensures that partitioning and view metadata (PartitioningMeta and ViewMeta) are preserved in your migration files.

    Autodetector Transformations

    The patch intercepts MigrationAutodetector.add_operation to perform the following mappings:

    Original OperationTarget Postgres OperationCondition
    CreateModelPostgresCreatePartitionedModel (+ PostgresAddDefaultPartition)Model is PostgresPartitionedModel
    DeleteModelPostgresDeletePartitionedModelModel is PostgresPartitionedModel
    CreateModelPostgresCreateViewModelModel is PostgresViewModel
    DeleteModelPostgresDeleteViewModelModel is PostgresViewModel
    CreateModelPostgresCreateMaterializedViewModelModel is PostgresMaterializedViewModel
    DeleteModelPostgresDeleteMaterializedViewModelModel is PostgresMaterializedViewModel
    AddFieldApplyStateModel is PostgresViewModel or PostgresMaterializedViewModel
    AlterFieldApplyStateModel is PostgresViewModel or PostgresMaterializedViewModel
    RenameFieldApplyStateModel is PostgresViewModel or PostgresMaterializedViewModel
    RemoveFieldApplyStateModel is PostgresViewModel or PostgresMaterializedViewModel

    ProjectState Transformations

    The patch intercepts ProjectState.from_apps to ensure custom model states are created, allowing the migration system to track specialized metadata:

    • PostgresPartitionedModelState for PostgresPartitionedModel.
    • PostgresViewModelState for PostgresViewModel.
    • PostgresMaterializedViewModelState for PostgresMaterializedViewModel.
  6. How locking works in django-postgres-extra

    master

    The library provides support for explicit PostgreSQL table-level locks. All locks are strictly bound to the current database transaction. They are automatically released only when the transaction is either committed or rolled back.

    Important Lifecycle Note: Locks are released when the outermost transaction commits. If you are using nested transactions, the lock will persist until the top-level transaction finishes. To ensure your transaction is the outermost one and that locks are released predictably, use transaction.atomic(durable=True).

    There is no support for explicitly releasing a lock manually; you must manage the transaction lifecycle.

  7. Configure django-postgres-extra in Django settings

    master

    To use django-postgres-extra, you must perform three configuration steps in your Django settings.py:

    1. Add django.contrib.postgres and psqlextra to your INSTALLED_APPS.
    2. Set the database ENGINE to psqlextra.backend.

    If you are already using a custom database backend, instead of changing the ENGINE directly, set the POSTGRES_EXTRA_DB_BACKEND_BASE environment variable (or setting) to your custom backend path.

    INSTALLED_APPS = [
        ...
        "django.contrib.postgres",
        "psqlextra",
    ]
    
    DATABASES = {
        "default": {
            ...
            "ENGINE": "psqlextra.backend",
        },
    }
  8. Configure the Partitioning Manager

    master

    To use the partitioning management command, you must define a PostgresPartitioningManager instance in your project and register its import path in your Django settings using the PSQLEXTRA_PARTITIONING_MANAGER key.

    # myapp/partitioning.py
    from psqlextra.partitioning import PostgresPartitioningManager
    
    manager = PostgresPartitioningManager(...)
    
    # myapp/settings.py
    PSQLEXTRA_PARTITIONING_MANAGER = 'myapp.partitioning.manager'
  9. How to declare a partitioned model

    master

    To use PostgreSQL Declarative Table Partitioning, inherit your model from PostgresPartitionedModel and define a nested PartitioningMeta class. The PartitioningMeta class must specify a method and a key.

    Available partitioning methods via psqlextra.types.PostgresPartitioningMethod:

    • RANGE: PARTITION BY RANGE
    • LIST: PARTITION BY LIST
    • HASH: PARTITION BY HASH
    from django.db import models
    from psqlextra.types import PostgresPartitioningMethod
    from psqlextra.models import PostgresPartitionedModel
    
    class MyModel(PostgresPartitionedModel):
        class PartitioningMeta:
            method = PostgresPartitioningMethod.RANGE
            key = ["timestamp"]
    
        name = models.TextField()
        timestamp = models.DateTimeField()
  10. Use a custom named PostgresManager

    master

    If you do not want to override the default objects manager, you can assign PostgresManager to a custom attribute name. Note that if you do this, the psqlextra features will only be available through that custom name, and calling them on MyModel.objects will result in an error.

    from django.db import models
    from psqlextra.manager import PostgresManager
    
    class MyModel(models.Model):
        # custom manager name
        beer = PostgresManager()
        myfield = models.CharField(max_length=255)
    
    # Correct usage:
    MyModel.beer.upsert(..)
    
    # Incorrect usage (will error):
    MyModel.objects.upsert(..)