AncestryEx (ancestry_ex v0.1.0)

Copy Markdown View Source

Organise the records of an Ecto schema as a tree (or a forest of trees) using a materialized path column, like Ruby's ancestry gem.

Every node stores the ids of all its ancestors in a single string column ("/1/5/12/"), so every navigation (parent, ancestors, children, descendants, siblings, depth...) is a single indexed query, and moving a subtree is a single UPDATE.

Setup

defmodule MyApp.Catalog.Category do
  use Ecto.Schema
  use AncestryEx, orphan_strategy: :rootify, cache_depth: true

  import Ecto.Changeset

  schema "categories" do
    field :name, :string
    ancestry_fields()
    timestamps()
  end

  def changeset(category, attrs) do
    category
    |> cast(attrs, [:name])
    |> validate_required([:name])
    |> AncestryEx.Changeset.cast_parent()
  end
end

mix ancestry_ex.gen.migration adds the columns to an existing table, and mix ancestry_ex.gen.schema / mix ancestry_ex.gen.context generate a new schema (and context) from scratch.

Options

  • :ancestry_column - name of the column, defaults to :ancestry.
  • :ancestry_format - :materialized_path2 (default) or :materialized_path. See AncestryEx.Format.
  • :orphan_strategy - what happens to the descendants of a deleted node (see AncestryEx.Changeset.handle_orphans/2):
    • :destroy (default) - delete them.
    • :rootify - the children become roots.
    • :adopt - the children are attached to the parent of the deleted node.
    • :restrict - refuse to delete a node with children.
    • :none - do nothing.
  • :cache_depth - true to keep the depth of every node in an integer column, :virtual to use a PostgreSQL generated column instead. Defaults to false; depth scopes still work, computing it in SQL.
  • :depth_cache_column - defaults to :ancestry_depth.
  • :counter_cache - true (column :children_count) or a column name to keep the number of children of every node.
  • :touch - when true, ancestors get their autoupdated timestamps (updated_at) bumped whenever a descendant is saved or deleted.

Generated functions

use AncestryEx defines these functions in the schema. Query functions take an optional queryable as first argument and return an Ecto.Query.

QueryPure (no database)
roots/0,1root?/1, has_parent?/1
parent/1,2parent_id/1, parent_of?/2
root/1,2root_id/1, root_of?/2
ancestors/1,2ancestor_ids/1, ancestor_of?/2
path/1,2path_ids/1
children/1,2child_of?/2, child_ancestry/1
siblings/1,2sibling_of?/2
descendants/1,2descendant_of?/2
subtree/1,2in_subtree_of?/2
indirects/1,2indirect_of?/2
leaves/0,1depth/1
before_depth/1,2, to_depth/1,2, at_depth/1,2, from_depth/1,2, after_depth/1,2
ordered_by_ancestry/0,1

All of them can be overridden. The pure functions are also available directly in this module (AncestryEx.parent_id(node)), together with the tree helpers arrange/1, arrange_serializable/2 and sort_by_ancestry/1.

Summary

Functions

Ids of the ancestors of node, root first.

Returns true if ancestor is an ancestor of node.

Defines the ancestry fields inside a schema block.

Arranges a flat list of nodes into a nested tree.

Arranges nodes like arrange/1 and converts each node into a map with a :children key, ready to be encoded as JSON.

Ancestry value of the children of node. The node must be persisted.

Returns true if node is a child of parent.

Depth of node in its tree, roots have depth 0.

Returns true if node is a descendant of ancestor.

Returns true if node has a parent.

Returns true if node is ancestor or one of its descendants.

Returns true if node is a descendant of ancestor but not one of its children.

Id of the parent of node, nil for a root.

Returns true if parent is the parent of node.

Ids of the ancestors of node plus its own id, root first.

Returns true if node is a root.

Id of the root of the tree node belongs to (its own id for a root).

Returns true if root is the root of the tree node belongs to.

Returns true if a and b are different nodes sharing the same parent.

Sorts nodes so that every parent comes before its children (pre-order), keeping the relative order of siblings.

Functions

ancestor_ids(node)

Ids of the ancestors of node, root first.

ancestor_of?(ancestor, node)

Returns true if ancestor is an ancestor of node.

ancestry_fields(opts \\ [])

(macro)

Defines the ancestry fields inside a schema block.

  • the ancestry column (:string, defaulting to the root value),
  • the depth cache column when :cache_depth is enabled,
  • the counter cache column when :counter_cache is enabled,
  • a virtual :parent_id field used by AncestryEx.Changeset.cast_parent/1.

Pass parent_id: false to skip the virtual field, for example when the table still has a real parent_id column mapped in the schema.

arrange(nodes)

Arranges a flat list of nodes into a nested tree.

Returns a list of {node, children} tuples where children has the same shape. Nodes whose parent is not in the list become top-level entries and siblings keep the order they have in the input.

Category.subtree(category)
|> Category.ordered_by_ancestry()
|> Repo.all()
|> AncestryEx.arrange()
#=> [{%Category{name: "Root"}, [{%Category{name: "Child"}, []}]}]

arrange_serializable(nodes, fun \\ &Tree.default_serializer/2)

Arranges nodes like arrange/1 and converts each node into a map with a :children key, ready to be encoded as JSON.

A function (node, children) -> term can be given to build each entry.

child_ancestry(node)

Ancestry value of the children of node. The node must be persisted.

child_of?(node, parent)

Returns true if node is a child of parent.

depth(node)

Depth of node in its tree, roots have depth 0.

descendant_of?(node, ancestor)

Returns true if node is a descendant of ancestor.

has_parent?(node)

Returns true if node has a parent.

in_subtree_of?(node, ancestor)

Returns true if node is ancestor or one of its descendants.

indirect_of?(node, ancestor)

Returns true if node is a descendant of ancestor but not one of its children.

parent_id(node)

Id of the parent of node, nil for a root.

parent_of?(parent, node)

Returns true if parent is the parent of node.

path_ids(node)

Ids of the ancestors of node plus its own id, root first.

root?(node)

Returns true if node is a root.

root_id(node)

Id of the root of the tree node belongs to (its own id for a root).

root_of?(root, node)

Returns true if root is the root of the tree node belongs to.

sibling_of?(a, b)

Returns true if a and b are different nodes sharing the same parent.

sort_by_ancestry(nodes)

Sorts nodes so that every parent comes before its children (pre-order), keeping the relative order of siblings.