Skip to main content

How to Handle Hierarchical Data and Tree Scope

Framework M provides native support for hierarchical data structures using two strategies: the Nested Set Model and the Adjacency List Model (driven by recursive CTE queries). Both strategies support Tree Scope isolation for multi-tenant or domain-level encapsulation.

This guide explains how to choose between the tree strategies, define a tree-structured DocType, configure tree scopes, and understand how the frontend automatically adjusts to each strategy.


Choosing a Tree Strategy

FeatureNested Set Model (NestedSetMixin)Adjacency List Model (AdjacencyListMixin)
Write PerformanceSlow (requires locking and shifting bounds lft/rgt for other nodes).Fast (simple inserts/updates of the parent_value column).
Read PerformanceFast (retrieves subtrees using simple BETWEEN filters without recursion).Fast (retrieves subtrees using recursive CTEs).
DB PortabilityUniversal (works on any SQL database).Requires recursive CTE support (SQLite, PostgreSQL, MySQL 8.0+, etc.).
Schema OverheadRequires lft and rgt integer columns.Requires only parent_value and is_group columns.

Defining a Tree DocType

To make a DocType behave as a tree, inherit from the appropriate mixin and configure it in the Meta section. Both mixins require is_group and parent_value fields, which are automatically supplied.

Option A: Nested Set Model

Use NestedSetMixin if you have a read-heavy tree structure (like a chart of accounts) where updates are infrequent but deep reads must be highly optimized.

from framework_m import BaseDocType
from framework_m_core.domain.mixins import NestedSetMixin

class Account(BaseDocType, NestedSetMixin):
name: str
code: str

class Meta:
table_name = "accounts"
# Optional: Isolate trees per tenant using tree_scope_fields
tree_scope_fields = ["tenant_id"]

Option B: Adjacency List Model

Use AdjacencyListMixin if you have write-heavy tree structures (like tasks, comments, or folders) where nodes are frequently inserted, moved, or deleted, and you want to avoid table-wide write locking.

from framework_m import BaseDocType
from framework_m_core.domain.mixins import AdjacencyListMixin

class TodoItem(BaseDocType, AdjacencyListMixin):
title: str
description: str | None = None
complete: bool = False

class Meta:
table_name = "todo"
tree_scope_fields = ["tenant_id"]
note

parent_value accepts a UUID indicating the ID of the parent node. If parent_value is None, the node is treated as a root node.


Understanding Tree Scope

By default, tree operations are calculated across the entire table. In multi-tenant environments, you must isolate tree boundaries per tenant to avoid cross-tenant data leaks and performance degradation.

Adding tree_scope_fields to class Meta restricts all tree manipulation checks (such as finding parent bounds, shifting gaps, checking for circular inheritance, and CTE recursion) to rows sharing identical values in the specified scope fields.

Example: Multi-Tenant Tree

If tree_scope_fields = ["tenant_id"] is set, inserting or moving a node for tenant_id="tenant_a" will only search, lock, and shift bounds (or traverse recursion paths) for nodes belonging to "tenant_a".


Shifting and Updating Trees

The framework's TreeManager handles tree modifications automatically under the hood within the active database transaction:

  1. New Nodes: If a node has a parent_value, it is inserted as a child of that parent.
  2. Moving Nodes: When a node's parent_value changes, TreeManager updates the parent pointer and automatically adjusts bounds (for Nested Set) or traverses hierarchies (for Adjacency List) cleanly.
  3. Circular Reference Protection: The system automatically validates against circular loop (ancestor) traps, raising a ValueError if you attempt to make a node a child of its own descendant.

Frontend Tree View & Sorting

The Desk's tree view components dynamically adjust their sorting and rendering behavior based on the DocType's schema:

  • Nested Set: If lft exists in the schema properties, the frontend automatically queries and sorts nodes by lft to preserve the user's manual ordering.
  • Adjacency List: If lft is absent, the frontend automatically falls back to sorting by name (or any custom orderField configured in the tree view meta).