Code Monkey home page Code Monkey logo

ecto_materialized_path's Introduction

ecto_materialized_path

Build Status badge

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

Installation

mix.exs

def deps do
  [{:ecto_materialized_path, "~> x.x.x"}]
end

Getting started

use EctoMaterializedPath in your schema. It takes 2 arguments:

  • column_name (default: "path"): the name of the database column which stores hierarchy data;
  • namespace (default: nil): you can namespace your functions if you have some naming conflicts. Details
defmodule Comment do
  use MyApp.Web, :model

  use EctoMaterializedPath

  schema "comments" do
    field :path, EctoMaterializedPath.Path, default: [] # default is important here
  end
end

Write a migration for this functionality

defmodule MyApp.AddMaterializedPathToComments do
  use Ecto.Migration

  def change do
    alter table(:comments) do
      add :path, {:array, :integer}, null: false
    end
  end
end

How does it work?

ecto_materialized_path stores node position as the tree of its ancestors, i.e.

%Comment{ path: [] } # no ancestors => is root
%Comment{ path: [1] } # this comment is a child of comment with id == 1
%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

Only postgresql > 9.x supports array as the stored field, so that ecto_materialized_path is compatible with postgresql only.

Assigning functions

Are usable when you need to assign some schema as a child of another schema

build_child/1

comment = %Comment{ id: 17, path: [89] }
Comment.build_child(comment)
# => %Comment{ id: nil, path: [17, 89] }

make_child_of/2

Takes a struct (or changeset) and parent struct; returns changeset with correct path.

comment = %Comment{ id: 17, path: [] } # or comment |> Ecto.Changeset.change(%{})
parent_comment = %Comment{ id: 11, path: [14, 28] }
Comment.make_child_of(comment, parent_comment)
# => Ecto.Changeset<changes: %{ path: [14, 28, 11] }, ...>

Fetching functions

parent/1

Returns an Ecto.Query to find parent for a node

comment = %Comment{ path: [14, 17, 18] }
Comment.parent(comment) # => Ecto.Query to find node with id == 18

root_comment = %Comment{ path: [] }
Comment.root(root_comment) # => Ecto.Query which will return nothing

parent_id/1

Returns a parent node id. It'll return nil for root node

comment = %Comment{ path: [14, 17, 18] }
Comment.parent_id(comment) # => 18

root_comment = %Comment{ path: [] }
Comment.root(root_comment) # => nil

root/1

Takes a node as an argument and returns Ecto.Query to find its root - even if node is a root itself :(

comment = %Comment{ path: [15, 16, 17] }
Comment.root(comment) # => Ecto.Query for id=15

root_comment = %Comment{ path: [] }
Comment.root(root_comment) # => Ecto.Query to find self

root_id/1

Returns the node's root id. For the root node, it shows own id.

comment = %Comment{ path: [15, 16, 17] }
Comment.root(comment) # => 15

root_comment = %Comment{ id: 2, path: [] }
Comment.root(root_comment) # => 2

root?/1

Returns true if node is a root, false otherwise

comment = %Comment{ path: [15, 16, 17] }
Comment.root?(comment) # => false

root_comment = %Comment{ id: 2, path: [] }
Comment.root?(root_comment) # => true

ancestor_ids/1

Returns node list of ancestor ids. Function works absolutely the same as node.path, but exists for convenience.

comment = %Comment{ path: [15, 16, 17] }
Comment.ancestor_ids(comment) # => [15, 16, 17]

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestor_ids(root_comment) # => []

ancestors/1

Returns Ecto.Query to find node ancestors.

comment = %Comment{ path: [15, 16, 17] }
Comment.ancestors(comment) # => Ecto.Query to find nodes with ids in [15, 16, 17]

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestors(root_comment) # => Ecto.Query which will return nothing

path_ids/1

Returns a list of path ids, starting with the root id and ending with the node's own id.

comment = %Comment{ id: 18, path: [15, 16, 17] }
Comment.path_ids(comment) # => [15, 16, 17, 18]

root_comment = %Comment{ id: 2, path: [] }
Comment.path_ids(root_comment) # => [2]

path/1

Returns an Ecto.Query which looks for the path ids, starting with the root id and ending with the node's own id.

comment = %Comment{ id: 18, path: [15, 16, 17] }
Comment.path(comment) # => Ecto.Query to find nodes with ids: [15, 16, 17, 18]

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestor_ids(root_comment) # => Ecto.Query to find nodes with id == 2

children/1

Returns an Ecto.Query which searches for the node children.

comment = %Comment{ id: 18, path: [15, 16, 17] }
Comment.path(comment) # => Ecto.Query to find nodes with path equals to: [15, 16, 17, 18]

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestor_ids(root_comment) # => Ecto.Query to find nodes with path equals to: [2]

siblings/1

Returns an Ecto.Query which searches for the node siblings.

comment = %Comment{ id: 18, path: [15, 16, 17] }
Comment.path(comment) # => Ecto.Query to find nodes with path: [15, 16, 17]

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestor_ids(root_comment) # => Ecto.Query to find nodes with path: []

descendants/1

Returns an Ecto.Query which searches for the node descendants.

comment = %Comment{ id: 18, path: [15, 16, 17] }
Comment.path(comment) # => Ecto.Query to find nodes with path containing: [15, 16, 17, 18]

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestor_ids(root_comment) # => Ecto.Query to find nodes with path containing: [2]

subtree/1

Returns an Ecto.Query which searches for the node & its descendants.

comment = %Comment{ id: 18, path: [15, 16, 17] }
Comment.path(comment) # => Ecto.Query to find node & its descendants

root_comment = %Comment{ id: 2, path: [] }
Comment.ancestor_ids(root_comment) # => Ecto.Query to find node & its descendants

depth/1

You can get depth level of the node in the tree

%Comment{ path: [] } |> Comment.depth() # => 0 for root
%Comment{ path: [15, 47] } |> Comment.depth() # => 2

where_depth/2

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

Comment.where_depth(Comment, is_bigger_than: 2) # => Find all nodes with more than 2 levels deep
Comment.where_depth(Comment, is_equal_to: 0) # => Roots only
# is_bigger_than_or_equal_to
# is_smaller_than_or_equal_to
# is_smaller_than

# You can pass query instead of schema, like:
query = Ecto.Query.from(q in Comment, ...)
query |> Comment.where_depth(is_equal_to: 1)

Arrangement

You 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:

comment_1 = %Comment{ id: 1 }
  comment_3 = %Comment{ id: 3, path: [1] }
    comment_8 = %Comment{ id: 8, path: [1, 3] }
      comment_9 = %Comment{ id: 9, path: [1, 3, 8] }
  comment_4 = %Comment{ id: 4, path: [1] }
  comment_5 = %Comment{ id: 5, path: [1] }
comment_2 = %Comment{ id: 2 }
  comment_6 = %Comment{ id: 6, path: [2] }
    comment_7 = %Comment{ id: 7, path: [2, 6] }

list = [comment_1, comment_2, comment_3, comment_4, comment_5, comment_6, comment_7, comment_8, comment_9]
Comment.arrange(list)
# =>
# [
#   {comment_1, [
#     {comment_3, [
#       {comment_8, [
#         {comment_9, []}
#       ]}
#     ]},
#     {comment_4, []},
#     {comment_5, []}
#   ]},
#   {comment_2, [
#     {comment_6, [
#       {comment_7, []}
#     ]}
#   ]}
# ]

arrange/1:

  • Saves the order of nodes
  • Raises exception if it doesn't arrange all nodes from tree to the list.

Namespace

You 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:

use EctoMaterializedPath,
  namespace: "brutalist"

And you will have all functions namespaced:

Comment.brutalist_root(comment)
Comment.brutalist_root?(comment)
# et.c.

ecto_materialized_path's People

Contributors

asiniy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

ecto_materialized_path's Issues

Post.parent() gives wrong query.

post |> Post.parent() |> Repo.one()

results in

** (Ecto.Query.CastError) deps/ecto_materialized_path/lib/ecto_materialized_path.ex:73: value 4 in where cannot be cast to type {:in, :id} in query:

from a in Post
where: a.id in ^4,
limit: 1,
select: a

(elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
(elixir) lib/enum.ex:1397: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
(elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/repo/queryable.ex:124: Ecto.Repo.Queryable.execute/5
(ecto) lib/ecto/repo/queryable.ex:37: Ecto.Repo.Queryable.all/4
(ecto) lib/ecto/repo/queryable.ex:70: Ecto.Repo.Queryable.one/4
(stdlib) erl_eval.erl:677: :erl_eval.do_apply/6
(iex) lib/iex/evaluator.ex:250: IEx.Evaluator.handle_eval/5
(iex) lib/iex/evaluator.ex:230: IEx.Evaluator.do_eval/3
(iex) lib/iex/evaluator.ex:208: IEx.Evaluator.eval/3

Running post |> Post.parent() returns

#Ecto.Query<from a in Post, where: a.id in ^4, limit: 1>

The use of the in operator seems weird to me, but it has been there for over a year, and noone has complained yet. This makes me wonder if I am doing something wrong.

`build_child/1` fails to build a valid struct

That is, the following test fails

  test "build_child/1" do
    comment = %Comment{id: 17, path: [89]}
    assert %Comment{id: nil, path: [89, 17]} = Comment.build_child(comment)
  end

with

  1) test build_child/1 (DemoTest)
     test/demo_test.exs:5
     match (=) failed
     code:  assert %Comment{id: nil, path: [89, 17]} = Comment.build_child(comment)
     right: %{__struct__: Demo.Comment, path: [89, 17]}
     stacktrace:
       test/demo_test.exs:7: (test)

xx

๐Ÿ‘

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.