Metadata-Version: 2.4
Name: django-postgres-objects
Version: 1.0.0
Summary: Declare PostgreSQL functions and other non-table objects as classes, and let makemigrations manage them.
Author-email: DjanQuiltDB Project <djanquiltdb@portal42.net>
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/djanquiltdb/django-postgres-objects
Project-URL: Documentation, https://django-postgres-objects.readthedocs.io/
Project-URL: Source, https://github.com/djanquiltdb/django-postgres-objects
Project-URL: Issues, https://github.com/djanquiltdb/django-postgres-objects/issues
Project-URL: Changelog, https://github.com/djanquiltdb/django-postgres-objects/blob/master/CHANGELOG.rst
Keywords: django,database,postgresql,migrations,functions
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Framework :: Django
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Topic :: Database
Classifier: Typing :: Typed
Requires-Python: >=3.14
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: django<7.0,>=6.0
Provides-Extra: lint
Requires-Dist: ruff==0.15.21; extra == "lint"
Provides-Extra: test
Requires-Dist: coverage; extra == "test"
Requires-Dist: psycopg[binary]>=3.0.0; extra == "test"
Requires-Dist: dj-database-url; extra == "test"
Provides-Extra: docs
Requires-Dist: sphinx<10,>=8; extra == "docs"
Requires-Dist: sphinx-rtd-theme<4,>=3.1; extra == "docs"
Provides-Extra: dev
Requires-Dist: django-postgres-objects[docs,lint,test]; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: tox>=4.21; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

=======================
django-postgres-objects
=======================

.. image:: https://img.shields.io/pypi/v/django-postgres-objects.svg
    :target: https://pypi.org/project/django-postgres-objects/
    :alt: PyPI

.. image:: https://img.shields.io/pypi/pyversions/django-postgres-objects.svg
    :target: https://pypi.org/project/django-postgres-objects/
    :alt: Supported Python versions

.. image:: https://github.com/djanquiltdb/django-postgres-objects/actions/workflows/ci.yml/badge.svg
    :target: https://github.com/djanquiltdb/django-postgres-objects/actions/workflows/ci.yml
    :alt: CI

.. image:: https://readthedocs.org/projects/django-postgres-objects/badge/?version=latest
    :target: https://django-postgres-objects.readthedocs.io/
    :alt: Documentation

.. image:: https://img.shields.io/pypi/l/django-postgres-objects.svg
    :target: https://github.com/djanquiltdb/django-postgres-objects/blob/master/LICENSE
    :alt: BSD-3-Clause licence

PostgreSQL objects (i.e. non-tables) have no representation in Django's migration state, so the usual way to manage
these in Django migrations is through hand-written ``RunSQL``. That works, but nothing notices when the declaration and
the database drift apart, and ``makemigrations --check`` will never tell you.

This package lets you declare such an object as a class, the same way a model is a class, have ``makemigrations`` write
the operations for you, and have ``migrate`` perform the operations for you.

Full documentation is at https://django-postgres-objects.readthedocs.io/.

.. code-block:: python

    # example/db_functions.py
    from postgres_objects import Function


    class AllUppercase(Function):
        arguments = 'input TEXT'
        returns = 'TEXT'
        volatility = 'IMMUTABLE'
        strict = True
        parallel = 'SAFE'
        body = """
            BEGIN
                RETURN UPPER(input);
            END;
        """

Views are declared the same way, in a module of their own::

    # example/db_views.py
    from postgres_objects import View


    class UppercasedCakes(View):
        sql = 'SELECT id, name_uppercased FROM example_cake'

A view masking an existing model (e.g. a subset of its columns, a row condition or an aggregate) can declare the
queryset instead of the SQL, and then its columns are declared exactly once: the compiled SELECT goes into the
migration, and the declaration exposes ``.objects``, a generated unmanaged model for reading the view back::

    from django.db.models import Count

    from example.models import Cake
    from postgres_objects import MaterializedView


    class CakeCounts(MaterializedView):
        unique_index = ('name',)

        def queryset():
            return Cake.objects.values('name').annotate(cakes=Count('id'))

    CakeCounts.objects.filter(cakes__gt=3)

A materialized view stores its rows, and no declaration can say when they have gone stale, so repopulating one is a call
you make from wherever you know the data has moved::

    CakeCounts.refresh()

Point a setting at the module each kind lives in, relative to each app::

    POSTGRES_OBJECTS = {
        'FUNCTIONS_MODULE_PATH': 'db_functions',
        'VIEWS_MODULE_PATH': 'db_views',
    }

``manage.py makemigrations`` now writes the operations for whatever you added, changed or removed. Function migrations
are written *before* that app's model migrations and removals *after* them, so a model migration adding a generated
column can rely on the function its expression calls, and a function is only dropped once nothing refers to it any more.
Views are placed the other way round, since a view reads from tables rather than being read by them.

The declaration is callable, so the same class serves the migration that creates the function and the queries that call
it:

.. code-block:: python

    from django.db.models import F

    from example.db_functions import AllUppercase

    Cake.objects.annotate(uppercased=AllUppercase(F('name')))

    # or as a generated column
    from postgres_objects import GeneratedField


    class Cake(models.Model):
        name = models.CharField(max_length=128)
        name_uppercased = GeneratedField(
            expression=AllUppercase(F('name')),
            output_field=models.TextField(),
            db_persist=True,
        )

``postgres_objects.GeneratedField`` is Django's ``GeneratedField`` plus one promise: when a declared function the column
depends on changes what it computes, the migration that alters the function is followed by one that recalculates the
stored values, which PostgreSQL does not do on its own. Django's plain ``GeneratedField`` works too, but its stored
values are then left as the old body computed them.

Installation
------------

::

    pip install django-postgres-objects

Add it to ``INSTALLED_APPS``::

    INSTALLED_APPS = (
        ...
        'postgres_objects',
        ...
    )

Then name the module each kind of declaration lives in, relative to each app::

    POSTGRES_OBJECTS = {
        'FUNCTIONS_MODULE_PATH': 'db_functions',
        'VIEWS_MODULE_PATH': 'db_views',
    }

Leave one path undefined to use only the other kind. A path may be dotted, so ``'db.functions'`` works too. See the
`installation docs <https://django-postgres-objects.readthedocs.io/en/latest/modules/installation.html>`_ for the full
set of options, including how to coexist with a library that ships its own ``makemigrations``.

Relationship to DjanQuiltDB
---------------------------

While this library was written in conjunction with `DjanQuiltDB <https://github.com/djanquiltdb/djanquiltdb>`_ and the
libraries were designed to work together seamlessly in projects where both are installed, both libraries are written to
be used as standalone libraries as well. For using django-postgres-objects, it is not necessary to use or be familiar
with DjanQuiltDB.

If you wish to use DjanQuiltDB features for objects managed through django-postgres-objects, you can install the
``djanquiltdb[postgres-objects]`` extra. For more information, please refer to the DjanQuiltDB documentation.

Scope limitations
-----------------

A major type of PostgreSQL object that this library is intentionally not covering is triggers, since there is already a
very mature library available for this in `django-pgtrigger <https://github.com/AmbitionEng/django-pgtrigger>`_.

Requirements
------------

* Python 3.14
* Django 6.0 or 6.1
* PostgreSQL 17 or 18
