Metadata-Version: 2.5
Name: django-postgres-multirange
Version: 0.1.0
Summary: PostgreSQL multirange fields for Django, modeled on django.contrib.postgres range fields.
Project-URL: Homepage, https://github.com/omarish/django-postgres-multirange
Project-URL: Documentation, https://github.com/omarish/django-postgres-multirange#readme
Project-URL: Repository, https://github.com/omarish/django-postgres-multirange
Project-URL: Issues, https://github.com/omarish/django-postgres-multirange/issues
Project-URL: Changelog, https://github.com/omarish/django-postgres-multirange/blob/master/CHANGELOG.md
Author-email: Omar Bohsali <omar.bohsali@gmail.com>
Maintainer-email: Omar Bohsali <omar.bohsali@gmail.com>
License: BSD-3-Clause
License-File: LICENSE
Keywords: django,multirange,postgres,postgresql,range
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: django>=5.2
Requires-Dist: psycopg>=3.1
Provides-Extra: binary
Requires-Dist: psycopg[binary]>=3.1; extra == 'binary'
Provides-Extra: dev
Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
Requires-Dist: pytest-django>=4.9; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: release
Requires-Dist: build>=1.2; extra == 'release'
Requires-Dist: twine>=5.0; extra == 'release'
Description-Content-Type: text/markdown

# django-postgres-multirange

[![PyPI](https://img.shields.io/pypi/v/django-postgres-multirange.svg)](https://pypi.org/project/django-postgres-multirange/)
[![Python versions](https://img.shields.io/pypi/pyversions/django-postgres-multirange.svg)](https://pypi.org/project/django-postgres-multirange/)
[![License](https://img.shields.io/pypi/l/django-postgres-multirange.svg)](https://github.com/omarish/django-postgres-multirange/blob/master/LICENSE)
[![Tests](https://github.com/omarish/django-postgres-multirange/actions/workflows/tests.yml/badge.svg)](https://github.com/omarish/django-postgres-multirange/actions/workflows/tests.yml)

PostgreSQL [multirange](https://www.postgresql.org/docs/17/rangetypes.html) fields for Django, built on the same primitives as `django.contrib.postgres` range fields.

A multirange is an ordered set of non-overlapping ranges — the native way to store things like available hours, blocked-out dates, or VLAN id spans in a single column.

Django [declined](https://code.djangoproject.com/ticket/33238) to add these types to `contrib.postgres`. This package fills that gap.

## Requirements

- Python 3.10+
- Django 5.2+
- [psycopg](https://www.psycopg.org/psycopg3/) 3
- PostgreSQL 14+ (developed against PostgreSQL 17)

## Install

```bash
pip install django-postgres-multirange
# or, with the binary psycopg build:
pip install "django-postgres-multirange[binary]"
```

Add `django.contrib.postgres` to `INSTALLED_APPS`. Adding `django_postgres_multirange` is optional; the fields work without it.

## Fields

Each range field Django already ships has a matching multirange:

| Field | PostgreSQL type | Element |
| --- | --- | --- |
| `IntegerMultiRangeField` | `int4multirange` | `int4` / `IntegerRangeField` |
| `BigIntegerMultiRangeField` | `int8multirange` | `int8` / `BigIntegerRangeField` |
| `DecimalMultiRangeField` | `nummultirange` | `numeric` / `DecimalRangeField` |
| `DateTimeMultiRangeField` | `tstzmultirange` | `timestamptz` / `DateTimeRangeField` |
| `DateMultiRangeField` | `datemultirange` | `date` / `DateRangeField` |

Python values are [`psycopg.types.multirange.Multirange`](https://www.psycopg.org/psycopg3/docs/basic/pgtypes.html#multirange-adaptation) objects — a mutable sequence of `Range`. Lists of ranges or `(lower, upper)` pairs are accepted on assignment, the same way `RangeField` accepts a pair.

```python
from django.contrib.postgres.indexes import GistIndex
from django.db import models
from django_postgres_multirange import IntegerMultiRangeField, Multirange
from psycopg.types.range import Range


class Schedule(models.Model):
    hours = IntegerMultiRangeField()

    class Meta:
        indexes = [GistIndex(fields=["hours"])]


Schedule.objects.create(hours=[(9, 12), (13, 17)])
row = Schedule.objects.get()
# PostgreSQL normalizes on write.
assert row.hours == Multirange([Range(9, 12), Range(13, 17)])
```

Continuous fields (`DecimalMultiRangeField`, `DateTimeMultiRangeField`) accept Django's `default_bounds` (`"[)"` by default) for pair input.

Empty (`{}` / `Multirange()`) is distinct from `NULL`.

## Lookups

The lookups match `RangeField`, plus two that only make sense on a multirange:

| Lookup | Operator / function |
| --- | --- |
| `contains` | `@>` element, range, or multirange |
| `contained_by` | `<@` |
| `overlap` | `&&` |
| `fully_lt` | `<<` |
| `fully_gt` | `>>` |
| `not_lt` | `&>` |
| `not_gt` | `&<` |
| `adjacent_to` | `-|-` |
| `startswith` / `endswith` | `lower()` / `upper()` of the whole multirange |
| `isempty` | `isempty()` |
| `lower_inc`, `lower_inf`, `upper_inc`, `upper_inf` | bound tests |
| `range_merge` | smallest covering range |
| `len` | number of ranges |

```python
Schedule.objects.filter(hours__contains=10)
Schedule.objects.filter(hours__overlap=(12, 14))
Schedule.objects.filter(hours__contains=[(9, 10), (14, 15)])
Schedule.objects.filter(hours__range_merge__contains=16)
Schedule.objects.filter(hours__len=2)
```

GiST indexes and `ExclusionConstraint` work the same way they do for range fields (`&&`, `@>`, `<@`, …).

## Development

```bash
make up     # Postgres 17 on localhost:54317
make test
```

See [CONTRIBUTING.md](https://github.com/omarish/django-postgres-multirange/blob/master/CONTRIBUTING.md) for the full workflow.

## Roadmap

Not in this release:

- `tsmultirange` (Django has no `tsrange` counterpart)
- union / intersection / difference / `unnest` database functions
- `range_agg` / `range_intersect_agg`
- user-defined range types
- a non-JSON admin widget

## License

[BSD-3-Clause](https://github.com/omarish/django-postgres-multirange/blob/master/LICENSE)

Questions or issues: [omar.bohsali@gmail.com](mailto:omar.bohsali@gmail.com)
