Metadata-Version: 2.4
Name: django-ltree-xtd
Version: 0.1.0
Summary: PostgreSQL ltree-based tree models for Django with trigger-maintained paths
Author-email: Arthur Hanson <worldnomad@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/arthanson/django-ltree-xtd
Project-URL: Repository, https://github.com/arthanson/django-ltree-xtd
Project-URL: Issues, https://github.com/arthanson/django-ltree-xtd/issues
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Requires-Dist: django-pgtrigger>=4.11
Dynamic: license-file

# django-ltree-xtd

PostgreSQL ltree-based tree models for Django with trigger-maintained paths.

Provides two abstract model mixins that use PostgreSQL's native `ltree` extension for efficient hierarchical data, with database triggers (via [django-pgtrigger](https://github.com/Opus10/django-pgtrigger)) to automatically maintain tree paths.

## Features

- **Native PostgreSQL ltree** for fast hierarchical queries using GiST indexes
- **Trigger-maintained paths** — paths are computed and cascaded automatically by the database
- **Two API flavors:**
  - `LTreeTreebeardMixin` — API compatible with [django-treebeard](https://github.com/django-treebeard/django-treebeard)'s `MP_Node`
  - `LTreeMPTTMixin` — API compatible with [django-mptt](https://github.com/django-mptt/django-mptt)'s `MPTTModel`
- **PK-based paths** (e.g., `1.42.103`) — no slug collisions, no label length limits
- **No application-level path management** — the database enforces integrity

## Requirements

- Python 3.10+
- Django 4.2+
- PostgreSQL with the `ltree` extension
- django-pgtrigger 4.11+

## Installation

```bash
pip install django-ltree-xtd
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    # ...
    "ltree",
    # ...
]
```

Run migrations to create the ltree extension:

```bash
python manage.py migrate ltree
```

## Quick Start

### Using the Treebeard-compatible API

```python
from django.db import models
from ltree.models import LTreeTreebeardMixin

class Category(LTreeTreebeardMixin):
    name = models.CharField(max_length=255)

# Create tree
root = Category.add_root(name="Electronics")
laptops = root.add_child(name="Laptops")
phones = root.add_child(name="Phones")
gaming = laptops.add_child(name="Gaming Laptops")

# Navigate
root.get_children()          # [laptops, phones]
root.get_descendants()       # [laptops, phones, gaming]
gaming.get_ancestors()       # [root, laptops]
gaming.get_root()            # root

# Check relationships
gaming.is_descendant_of(root)  # True
root.is_leaf()                 # False
gaming.is_leaf()               # True

# Bulk operations
data = Category.dump_bulk()
Category.load_bulk(data)
```

### Using the MPTT-compatible API

```python
from django.db import models
from ltree.models import LTreeMPTTMixin

class Tag(LTreeMPTTMixin):
    name = models.CharField(max_length=255)

# Create tree
root = Tag.objects.create(name="Root", parent=None)
child = Tag.objects.create(name="Child", parent=root)
grandchild = Tag.objects.create(name="Grandchild", parent=child)

# Navigate (MPTT-style)
grandchild.get_ancestors()           # [root, child]
grandchild.get_ancestors(ascending=True)  # [child, root]
root.get_descendants(include_self=True)   # [root, child, grandchild]
root.get_family()                         # [root, child, grandchild]
root.get_leafnodes()                      # [grandchild]
grandchild.get_level()                    # 2

# Move nodes
grandchild.move_to(root, position="first-child")

# Boolean checks
root.is_root_node()      # True
grandchild.is_leaf_node()  # True
grandchild.is_child_node() # True

# Manager methods
Tag.objects.root_nodes()
Tag.objects.get_queryset_descendants(some_queryset)
Tag.objects.get_queryset_ancestors(some_queryset)
```

## How It Works

Both mixins provide:

- A `parent` ForeignKey to self
- An `path` LTreeField that stores the materialized path

Two PostgreSQL triggers maintain the paths:

1. **`compute_path`** — On INSERT or UPDATE of `parent_id`, computes the node's path from its parent's path + its own PK
2. **`update_descendants`** — After a path change, cascades the update to all descendants

This means moving a subtree is a single UPDATE that triggers automatic cascading — no application-level tree rebuilding needed.

## License

MIT
