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).