{"id":13507203,"url":"https://github.com/asiniy/ecto_materialized_path","last_synced_at":"2025-07-28T05:31:42.047Z","repository":{"id":44102435,"uuid":"95670698","full_name":"asiniy/ecto_materialized_path","owner":"asiniy","description":"Tree structure \u0026 hierarchy for ecto models","archived":false,"fork":false,"pushed_at":"2021-10-08T15:28:44.000Z","size":48,"stargazers_count":64,"open_issues_count":4,"forks_count":18,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-15T16:16:13.985Z","etag":null,"topics":["ancestry","ecto","elixir"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/asiniy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-06-28T13:14:50.000Z","updated_at":"2025-02-21T03:25:12.000Z","dependencies_parsed_at":"2022-08-13T04:50:10.073Z","dependency_job_id":null,"html_url":"https://github.com/asiniy/ecto_materialized_path","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/asiniy/ecto_materialized_path","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asiniy%2Fecto_materialized_path","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asiniy%2Fecto_materialized_path/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asiniy%2Fecto_materialized_path/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asiniy%2Fecto_materialized_path/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/asiniy","download_url":"https://codeload.github.com/asiniy/ecto_materialized_path/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asiniy%2Fecto_materialized_path/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265707612,"owners_count":23814834,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ancestry","ecto","elixir"],"created_at":"2024-08-01T02:00:27.400Z","updated_at":"2025-07-28T05:31:41.782Z","avatar_url":"https://github.com/asiniy.png","language":"Elixir","funding_links":[],"categories":["Algorithms and Data structures"],"sub_categories":[],"readme":"# `ecto_materialized_path`\n\n[![Build Status](https://travis-ci.org/asiniy/ecto_materialized_path.svg?branch=master)](https://travis-ci.org/asiniy/ecto_materialized_path)\n![badge](https://img.shields.io/hexpm/v/ecto_materialized_path.svg)\n\nAllows you to store and organize your Ecto records in a tree structure (or an hierarchy). It uses a single database column, using the materialized path pattern. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants, depth) and all of them can be fetched in a single SQL query.\n\n## Installation\n\n`mix.exs`\n\n```elixir\ndef deps do\n  [{:ecto_materialized_path, \"~\u003e x.x.x\"}]\nend\n```\n\n\n## Getting started\n\n`use EctoMaterializedPath` in your schema. It takes 2 arguments:\n\n  * `column_name` (default: `\"path\"`): the name of the database column which stores hierarchy data;\n  * `namespace` (default: `nil`): you can namespace your functions if you have some naming conflicts. [Details](#namespace)\n\n``` elixir\ndefmodule Comment do\n  use MyApp.Web, :model\n\n  use EctoMaterializedPath\n\n  schema \"comments\" do\n    field :path, EctoMaterializedPath.Path, default: [] # default is important here\n  end\nend\n```\n\nWrite a migration for this functionality\n\n``` elixir\ndefmodule MyApp.AddMaterializedPathToComments do\n  use Ecto.Migration\n\n  def change do\n    alter table(:comments) do\n      add :path, {:array, :integer}, null: false\n    end\n  end\nend\n```\n\n## How does it work?\n\n`ecto_materialized_path` stores node position as the tree of its ancestors, i.e.\n\n``` elixir\n%Comment{ path: [] } # no ancestors =\u003e is root\n%Comment{ path: [1] } # this comment is a child of comment with id == 1\n%Comment{ path: [1, 3] } # this comment is a child of the comment with id == 3, which in its turn is the child of the comment with id == 1\n```\n\nOnly postgresql `\u003e 9.x` supports array as the stored field, so that `ecto_materialized_path` is compatible with postgresql only.\n\n## Assigning functions\n\nAre usable when you need to assign some schema as a child of another schema\n\n#### `build_child/1`\n\n``` elixir\ncomment = %Comment{ id: 17, path: [89] }\nComment.build_child(comment)\n# =\u003e %Comment{ id: nil, path: [17, 89] }\n```\n\n#### `make_child_of/2`\n\nTakes a struct (or changeset) and parent struct; returns changeset with correct path.\n\n``` elixir\ncomment = %Comment{ id: 17, path: [] } # or comment |\u003e Ecto.Changeset.change(%{})\nparent_comment = %Comment{ id: 11, path: [14, 28] }\nComment.make_child_of(comment, parent_comment)\n# =\u003e Ecto.Changeset\u003cchanges: %{ path: [14, 28, 11] }, ...\u003e\n```\n\n## Fetching functions\n\n#### `parent/1`\n\nReturns an `Ecto.Query` to find parent for a node\n\n```\ncomment = %Comment{ path: [14, 17, 18] }\nComment.parent(comment) # =\u003e Ecto.Query to find node with id == 18\n\nroot_comment = %Comment{ path: [] }\nComment.root(root_comment) # =\u003e Ecto.Query which will return nothing\n```\n\n#### `parent_id/1`\n\nReturns a parent node id. It'll return nil for root node\n\n```\ncomment = %Comment{ path: [14, 17, 18] }\nComment.parent_id(comment) # =\u003e 18\n\nroot_comment = %Comment{ path: [] }\nComment.root(root_comment) # =\u003e nil\n```\n\n#### `root/1`\n\nTakes a node as an argument and returns `Ecto.Query` to find its root - even if node is a root itself :(\n\n``` elixir\ncomment = %Comment{ path: [15, 16, 17] }\nComment.root(comment) # =\u003e Ecto.Query for id=15\n\nroot_comment = %Comment{ path: [] }\nComment.root(root_comment) # =\u003e Ecto.Query to find self\n```\n\n#### `root_id/1`\n\nReturns the node's root id. For the root node, it shows own id.\n\n``` elixir\ncomment = %Comment{ path: [15, 16, 17] }\nComment.root(comment) # =\u003e 15\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.root(root_comment) # =\u003e 2\n```\n\n#### `root?/1`\n\nReturns true if node is a root, false otherwise\n\n``` elixir\ncomment = %Comment{ path: [15, 16, 17] }\nComment.root?(comment) # =\u003e false\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.root?(root_comment) # =\u003e true\n```\n\n#### `ancestor_ids/1`\n\nReturns node list of ancestor ids. Function works absolutely the same as `node.path`, but exists for convenience.\n\n``` elixir\ncomment = %Comment{ path: [15, 16, 17] }\nComment.ancestor_ids(comment) # =\u003e [15, 16, 17]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestor_ids(root_comment) # =\u003e []\n```\n\n#### `ancestors/1`\n\nReturns `Ecto.Query` to find node ancestors.\n\n``` elixir\ncomment = %Comment{ path: [15, 16, 17] }\nComment.ancestors(comment) # =\u003e Ecto.Query to find nodes with ids in [15, 16, 17]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestors(root_comment) # =\u003e Ecto.Query which will return nothing\n```\n\n#### `path_ids/1`\n\nReturns a list of path ids, starting with the root id and ending with the node's own id.\n\n``` elixir\ncomment = %Comment{ id: 18, path: [15, 16, 17] }\nComment.path_ids(comment) # =\u003e [15, 16, 17, 18]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.path_ids(root_comment) # =\u003e [2]\n```\n\n#### `path/1`\n\nReturns an `Ecto.Query` which looks for the path ids, starting with the root id and ending with the node's own id.\n\n``` elixir\ncomment = %Comment{ id: 18, path: [15, 16, 17] }\nComment.path(comment) # =\u003e Ecto.Query to find nodes with ids: [15, 16, 17, 18]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestor_ids(root_comment) # =\u003e Ecto.Query to find nodes with id == 2\n```\n\n#### `children/1`\n\nReturns an `Ecto.Query` which searches for the node children.\n\n``` elixir\ncomment = %Comment{ id: 18, path: [15, 16, 17] }\nComment.path(comment) # =\u003e Ecto.Query to find nodes with path equals to: [15, 16, 17, 18]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestor_ids(root_comment) # =\u003e Ecto.Query to find nodes with path equals to: [2]\n```\n\n#### `siblings/1`\n\nReturns an `Ecto.Query` which searches for the node siblings.\n\n``` elixir\ncomment = %Comment{ id: 18, path: [15, 16, 17] }\nComment.path(comment) # =\u003e Ecto.Query to find nodes with path: [15, 16, 17]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestor_ids(root_comment) # =\u003e Ecto.Query to find nodes with path: []\n```\n\n#### `descendants/1`\n\nReturns an `Ecto.Query` which searches for the node descendants.\n\n``` elixir\ncomment = %Comment{ id: 18, path: [15, 16, 17] }\nComment.path(comment) # =\u003e Ecto.Query to find nodes with path containing: [15, 16, 17, 18]\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestor_ids(root_comment) # =\u003e Ecto.Query to find nodes with path containing: [2]\n```\n\n#### `subtree/1`\n\nReturns an `Ecto.Query` which searches for the node \u0026 its descendants.\n\n``` elixir\ncomment = %Comment{ id: 18, path: [15, 16, 17] }\nComment.path(comment) # =\u003e Ecto.Query to find node \u0026 its descendants\n\nroot_comment = %Comment{ id: 2, path: [] }\nComment.ancestor_ids(root_comment) # =\u003e Ecto.Query to find node \u0026 its descendants\n```\n\n#### `depth/1`\n\nYou can get depth level of the node in the tree\n\n``` elixir\n%Comment{ path: [] } |\u003e Comment.depth() # =\u003e 0 for root\n%Comment{ path: [15, 47] } |\u003e Comment.depth() # =\u003e 2\n```\n\n#### `where_depth/2`\n\nYou can specify a query to search for nodes with some level of depth. It uses `CARDINALITY()` postgres function internally, so ensure your postgres version is at least `9.4`.\n\n``` elixir\nComment.where_depth(Comment, is_bigger_than: 2) # =\u003e Find all nodes with more than 2 levels deep\nComment.where_depth(Comment, is_equal_to: 0) # =\u003e Roots only\n# is_bigger_than_or_equal_to\n# is_smaller_than_or_equal_to\n# is_smaller_than\n\n# You can pass query instead of schema, like:\nquery = Ecto.Query.from(q in Comment, ...)\nquery |\u003e Comment.where_depth(is_equal_to: 1)\n```\n\n## Arrangement\n\nYou can build a tree from the flat list of nested objects by using `arrange/1`. This function will return a tree of nested nodes which are looking like `{ object, list_of_children_tuples_like_me }`. For example:\n\n``` elixir\ncomment_1 = %Comment{ id: 1 }\n  comment_3 = %Comment{ id: 3, path: [1] }\n    comment_8 = %Comment{ id: 8, path: [1, 3] }\n      comment_9 = %Comment{ id: 9, path: [1, 3, 8] }\n  comment_4 = %Comment{ id: 4, path: [1] }\n  comment_5 = %Comment{ id: 5, path: [1] }\ncomment_2 = %Comment{ id: 2 }\n  comment_6 = %Comment{ id: 6, path: [2] }\n    comment_7 = %Comment{ id: 7, path: [2, 6] }\n\nlist = [comment_1, comment_2, comment_3, comment_4, comment_5, comment_6, comment_7, comment_8, comment_9]\nComment.arrange(list)\n# =\u003e\n# [\n#   {comment_1, [\n#     {comment_3, [\n#       {comment_8, [\n#         {comment_9, []}\n#       ]}\n#     ]},\n#     {comment_4, []},\n#     {comment_5, []}\n#   ]},\n#   {comment_2, [\n#     {comment_6, [\n#       {comment_7, []}\n#     ]}\n#   ]}\n# ]\n```\n\n`arrange/1`:\n* Saves the order of nodes\n* Raises exception if it doesn't arrange all nodes from tree to the list.\n\n## Namespace\n\nYou can namespace all your functions on a module, it's very suitable when schema belongs to a couple of trees or in case of function name conflicts. Just do:\n\n``` elixir\nuse EctoMaterializedPath,\n  namespace: \"brutalist\"\n```\n\nAnd you will have all functions namespaced:\n\n``` elixir\nComment.brutalist_root(comment)\nComment.brutalist_root?(comment)\n# et.c.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasiniy%2Fecto_materialized_path","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasiniy%2Fecto_materialized_path","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasiniy%2Fecto_materialized_path/lists"}