diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d22290a..47345fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,16 +15,12 @@ jobs: matrix: include: - pair: - elixir: "1.11" - otp: "22.3" + elixir: "1.15" + otp: "26.2" postgres: "12.13-alpine" - pair: - elixir: "1.12" - otp: "22.3" - postgres: "12.13-alpine" - - pair: - elixir: "1.14" - otp: "25.2" + elixir: "1.17" + otp: "26.2" postgres: "15.1-alpine" lint: lint diff --git a/.iex.exs b/.iex.exs index 609296a..e96df33 100644 --- a/.iex.exs +++ b/.iex.exs @@ -3,27 +3,3 @@ try do rescue Code.LoadError -> :rescued end - -defmodule CTT do - use CTE, - otp_app: :cte, - adapter: CTE.Adapter.Memory, - nodes: %{ - 1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"}, - 2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"}, - 3 => %{id: 3, author: "Olie", comment: "Yeah."} - }, - paths: [[1, 1, 0], [1, 2, 1], [1, 3, 2], [2, 2, 0], [2, 3, 1], [3, 3, 0]] -end - -Mix.shell().info([ - :green, - """ - A CTT module was defined for you. Start is and use it like this: - - iex> CTT.start_link() - iex> {:ok, tree} = CTT.tree(1) - iex> CTE.Utils.print_tree(tree, 1) - iex> CTE.Utils.print_tree(tree,1, callback: &({&2[&1].author <> ":", &2[&1].comment})) - """ -]) diff --git a/.tool-versions b/.tool-versions index 9105cea..46144b9 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ - erlang 25.1.2 - elixir 1.14.2-otp-25 +erlang 26.2.4 +elixir 1.15.8-otp-26 diff --git a/CHANGELOG.md b/CHANGELOG.md index 027f591..1feb7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # CHANGELOG +## 2.0.0 + +This version is introducing major breaking changes. We drop the concept of a CT Adapter and focus on using Ecto, for the core functions. The (in)memory adapter is gone. + +Also important: we're going "process-less", simple, streamlined, efficient and maybe a tad fast(er) + +This is very much a work in progress, with a list of immediate todos as follow: + +- code cleanup and update the documentation +- allow the user to define her own: + - primary key; name and maybe type + - foreign key; name and maybe type - optional + - callbacks (l8r) +- telemetry and better logging +- mix tasks for generating CT migrations +- support for "plugins" .. + ## 1.1.5 - dependencies update diff --git a/README.md b/README.md index 9fd5d6a..893ecc4 100644 --- a/README.md +++ b/README.md @@ -3,65 +3,160 @@ [![Hex.pm](https://img.shields.io/hexpm/dt/closure_table.svg?maxAge=2592000)](https://hex.pm/packages/closure_table) [![Hexdocs.pm](https://img.shields.io/badge/api-hexdocs-brightgreen.svg)](https://hexdocs.pm/closure_table) -> this library provides two adapters: an in-memory one, and one for using the closure-table solution with Ecto; for your testing and development convenience. - The Closure Table solution is a simple and elegant way of storing hierarchies. It involves storing all paths through a tree, not just those with a direct parent-child relationship. You may want to chose this model, over the [Nested Sets model](https://en.wikipedia.org/wiki/Nested_set_model), should you need referential integrity and to assign nodes to multiple trees. Throughout the various examples and tests, we will refer to the hierarchies depicted below, where we're modeling a hypothetical forum-like discussion between [Rolie, Olie and Polie](https://www.youtube.com/watch?v=LTkmaE_QWMQ), and their debate around the usefulness of this implementation :) ![Closure Table](assets/closure_table.png) +Warning: + +> This version is introducing major breaking changes. We drop the concept of a CT Adapter and focus on using Ecto, for the core functions. The (in)memory adapter is gone. + + ## Quick start -To start, you can simply use one `Adapter` from the ones provided, same way you'd use the Ecto's own Repo: +The current implementation is depending on Ecto ~> 3.1; using [Ecto.SubQuery](https://hexdocs.pm/ecto/Ecto.SubQuery.html)! + +For this implementation to work you'll have to provide two tables, and the name of the Repo used by your application: + +1. a table name containing the nodes. Having the `id`, as a the primary key. This is the default for now - configurable in the near future +2. a table name where the tree paths will be stores. +3. the name of the Ecto.Repo, defined by your app + +In a future version we will provide you with a convenient migration template to help you starting, but for now you must supply these tables. + +For example, given you have the following Schemas for comments: ```elixir -defmodule CTT do - use CTE, - otp_app: :cte, - adapter: CTE.Adapter.Memory, - nodes: %{ - 1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"}, - 2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"}, - 3 => %{id: 3, author: "Olie", comment: "Yeah."} - }, - paths: [[1, 1, 0], [1, 2, 1], [1, 3, 2], [2, 2, 0], [2, 3, 1], [3, 3, 0]] -end + defmodule CT.Comment do + use Ecto.Schema + import Ecto.Changeset + + @timestamps_opts [type: :utc_datetime] + + schema "comments" do + field :text, :string + belongs_to :author, CT.Author + + timestamps() + end + end +``` + +and a table used for storing the parent-child relationships + +```elixir + + defmodule CT.TreePath do + use Ecto.Schema + import Ecto.Changeset + alias CT.Comment + + @primary_key false + + schema "tree_paths" do + belongs_to :parent_comment, Comment, foreign_key: :ancestor + belongs_to :comment, Comment, foreign_key: :descendant + field :depth, :integer, default: 0 + end + end ``` -With the configuration above, the `:nodes` attribute is a map containing the comments our interlocutors made; these are "nodes", in CTE's parlance. When using the `CTE.Adapter.Ecto` implementation, the `:nodes` attribute will be a Schema (or a table name! In our initial implementation, the nodes definitions must have at least the `:id`, as one of their properties. This caveat will be lifted in a later implementation. The `:paths` attribute represents the parent-child relationship between the comments. +we can define the following module: + +```elixir + + defmodule CT.MyCTE do + use CTE, + repo: CT.Repo, + nodes: CT.Comment, + paths: CT.TreePath + end +``` -Add the `CTM` module to your main supervision tree: +We add our CTE Repo to the app's main supervision tree, like this: ```elixir -defmodule CTM.Application do - @moduledoc false + defmodule CT.Application do + use Application + + def start(_type, _args) do + children = [ + CT.Repo, + ] + + opts = [strategy: :one_for_one, name: CT.Supervisor] + Supervisor.start_link(children, opts) + end + end +``` - use Application +restart your application. - def start(_type, _args) do - opts = [strategy: :one_for_one, name: CTM.Supervisor] +Then using `iex -S mix`, we can start experimenting. Examples: - Supervisor.start_link([CTM], opts) - end -end +```elixir + iex» CT.MyCTE.ancestors(9) + {:ok, [1, 4, 6]} + + iex» {:ok, tree} = CT.MyCTE.tree(1) + {:ok, + %{ + nodes: %{ + 6 => %CT.Comment{ + __meta__: #Ecto.Schema.Metadata<:loaded, "comments">, + author: #Ecto.Association.NotLoaded, + author_id: 2, + id: 6, + inserted_at: ~U[2019-07-21 01:10:35Z], + text: "Everything is easier, than with the Nested Sets.", + updated_at: ~U[2019-07-21 01:10:35Z] + }, + 8 => %CT.Comment{ + __meta__: #Ecto.Schema.Metadata<:loaded, "comments">, + author: #Ecto.Association.NotLoaded, + author_id: 1, + id: 8, + inserted_at: ~U[2019-07-21 01:10:35Z], + text: "I’m sold! And I’ll use its Elixir implementation! <3", + updated_at: ~U[2019-07-21 01:10:35Z] + }, + ... + + }, + paths: [ + [1, 1, 0], + [1, 2, 1], + [2, 2, 0], + [1, 3, 2], + [2, 3, 1], + [3, 3, 0], + [1, 7, 3], + [2, 7, 2], + ... + ] + }} ``` -And then using `iex -S mix`, for quickly experimenting with the CTE API, let's find the descendants of comment #1: +if you want to visualize a tree, you can do that too: ```elixir -iex» CTM.descendants(1) -{:ok, [3, 2]} -iex> {:ok, tree} = CTT.tree(1) -... -iex> CTE.Utils.print_tree(tree, 1) -... -iex» CTE.Utils.print_tree(tree,1, callback: &({&2[&1].author <> ":", &2[&1].comment})) +iex» CTE.Utils.print_tree(tree, 1, callback: &({&2[&1], &2[&1].text})) +``` -Is Closure Table better than the Nested Sets? -└── It depends. Do you need referential integrity? - └── Yeah. +and you may see this: +```txt +Is Closure Table better than the Nested Sets? +├── It depends. Do you need referential integrity? +│ └── Yeah +│ └── Closure Table *has* referential integrity? +└── Querying the data it's easier. + ├── What about inserting nodes? + └── Everything is easier, than with the Nested Sets. + ├── I'm sold! And I'll use its Elixir implementation! <3 + └── w⦿‿⦿t! ``` Please check the docs for more details and return from more updates! @@ -82,7 +177,7 @@ by adding `closure_table` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:closure_table, "~> 1.1"} + {:closure_table, "~> 2.0"} ] end ``` diff --git a/config/config.exs b/config/config.exs index 69d7c50..242682a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,4 +1,4 @@ -import Mix.Config +import Config config :mix_test_watch, clear: true diff --git a/examples/ct_ecto/config/dev.exs b/examples/ct_ecto/config/dev.exs index 8360e7a..a2ca78f 100644 --- a/examples/ct_ecto/config/dev.exs +++ b/examples/ct_ecto/config/dev.exs @@ -1,7 +1,7 @@ import Config config :ct, CT.Repo, - database: "ct_ecto_dev", + database: "ct_dev", username: "postgres", password: "postgres", hostname: "localhost", diff --git a/examples/ct_ecto/config/test.exs b/examples/ct_ecto/config/test.exs index b988b83..1b007a4 100644 --- a/examples/ct_ecto/config/test.exs +++ b/examples/ct_ecto/config/test.exs @@ -5,7 +5,7 @@ config :logger, :console, format: "[$level] $message\n" config :logger, :level, :error config :ct, CT.Repo, - database: "ct_ecto_test", + database: "ct_test", username: "postgres", password: "postgres", hostname: "localhost", diff --git a/examples/ct_ecto/lib/ct.ex b/examples/ct_ecto/lib/ct.ex index 7a708cc..d82c957 100644 --- a/examples/ct_ecto/lib/ct.ex +++ b/examples/ct_ecto/lib/ct.ex @@ -17,11 +17,6 @@ defmodule CT do else e -> {:error, e} end - - # Ecto.Multi.new() - # |> Multi.insert(:comment, cs) - # |> Multi.run(:path, &insert_node/2) - # |> Repo.transaction() end @spec reply(Comment.t(), Comment.t()) :: {:ok, Comment.t()} | {:error, any()} @@ -39,9 +34,4 @@ defmodule CT do end def tree(comment), do: MyCTE.tree(comment.id) - - # defp insert_node(_repo, %{comment: comment}) do - # MyCTE.insert(comment.id, comment.id) - # {:ok, [[comment.id, comment.id]]} - # end end diff --git a/examples/ct_ecto/lib/ct/application.ex b/examples/ct_ecto/lib/ct/application.ex index 081f9e2..7989d36 100644 --- a/examples/ct_ecto/lib/ct/application.ex +++ b/examples/ct_ecto/lib/ct/application.ex @@ -7,8 +7,7 @@ defmodule CT.Application do def start(_type, _args) do children = [ - CT.Repo, - CT.MyCTE + CT.Repo ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/examples/ct_ecto/lib/ct/my_cte.ex b/examples/ct_ecto/lib/ct/my_cte.ex index 3bf97c7..507eaeb 100644 --- a/examples/ct_ecto/lib/ct/my_cte.ex +++ b/examples/ct_ecto/lib/ct/my_cte.ex @@ -3,8 +3,6 @@ defmodule CT.MyCTE do Comments hierarchy """ use CTE, - otp_app: :ct, - adapter: CTE.Adapter.Ecto, repo: CT.Repo, nodes: CT.Comment, paths: CT.TreePath diff --git a/examples/ct_ecto/lib/ct/tree_path.ex b/examples/ct_ecto/lib/ct/tree_path.ex index 1c23af4..f091e6d 100644 --- a/examples/ct_ecto/lib/ct/tree_path.ex +++ b/examples/ct_ecto/lib/ct/tree_path.ex @@ -1,6 +1,8 @@ defmodule CT.TreePath do use Ecto.Schema + import Ecto.Changeset + alias CT.Comment @primary_key false diff --git a/examples/ct_ecto/test/ct_test.exs b/examples/ct_ecto/test/ct_test.exs index 21198b4..f514154 100644 --- a/examples/ct_ecto/test/ct_test.exs +++ b/examples/ct_ecto/test/ct_test.exs @@ -1,5 +1,5 @@ defmodule CTTest do - use CT.DataCase, async: false + use CT.DataCase describe "Forum" do setup do diff --git a/examples/ct_ecto/test/support/data_case.ex b/examples/ct_ecto/test/support/data_case.ex index 7d62365..e794e59 100644 --- a/examples/ct_ecto/test/support/data_case.ex +++ b/examples/ct_ecto/test/support/data_case.ex @@ -29,7 +29,6 @@ defmodule CT.DataCase do :ok = Ecto.Adapters.SQL.Sandbox.checkout(CT.Repo) unless tags[:async] do - # adapter = CT.MyCTE.__adapter__() Ecto.Adapters.SQL.Sandbox.mode(CT.Repo, {:shared, self()}) # Ecto.Adapters.SQL.Sandbox.allow(CT.Repo, self(), adapter) end diff --git a/examples/ct_memory/.formatter.exs b/examples/ct_memory/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/examples/ct_memory/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/examples/ct_memory/.gitignore b/examples/ct_memory/.gitignore deleted file mode 100644 index f8f6050..0000000 --- a/examples/ct_memory/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -ct_memory-*.tar - diff --git a/examples/ct_memory/README.md b/examples/ct_memory/README.md deleted file mode 100644 index 0ddd062..0000000 --- a/examples/ct_memory/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CTM - -**TODO: Add description** - -## Installation - -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `ct_memory` to your list of dependencies in `mix.exs`: - -```elixir -def deps do - [ - {:ct_memory, "~> 0.1.1"} - ] -end -``` - -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at [https://hexdocs.pm/ct_memory](https://hexdocs.pm/ct_memory). diff --git a/examples/ct_memory/lib/ctm.ex b/examples/ct_memory/lib/ctm.ex deleted file mode 100644 index ae2b5e5..0000000 --- a/examples/ct_memory/lib/ctm.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule CTM do - use CTE, - otp_app: :ct_empty, - adapter: CTE.Adapter.Memory, - nodes: %{ - 1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"}, - 2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"}, - 3 => %{id: 3, author: "Polie", comment: "Yeah."} - }, - paths: [[1, 1, 0], [1, 2, 1], [1, 3, 2], [2, 2, 0], [2, 3, 1], [3, 3, 0]] -end diff --git a/examples/ct_memory/lib/ctm/application.ex b/examples/ct_memory/lib/ctm/application.ex deleted file mode 100644 index 3b9e34f..0000000 --- a/examples/ct_memory/lib/ctm/application.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule CTM.Application do - @moduledoc false - - use Application - - def start(_type, _args) do - opts = [strategy: :one_for_one, name: CTM.Supervisor] - - Supervisor.start_link([CTM], opts) - end -end diff --git a/examples/ct_memory/mix.exs b/examples/ct_memory/mix.exs deleted file mode 100644 index 351792e..0000000 --- a/examples/ct_memory/mix.exs +++ /dev/null @@ -1,26 +0,0 @@ -defmodule CTM.MixProject do - use Mix.Project - - def project do - [ - app: :ct_memory, - version: "0.1.0", - elixir: "~> 1.9", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger], - mod: {CTM.Application, []} - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [{:closure_table, ">= 0.0.0", path: "../../"}] - end -end diff --git a/examples/ct_memory/mix.lock b/examples/ct_memory/mix.lock deleted file mode 100644 index c9a1164..0000000 --- a/examples/ct_memory/mix.lock +++ /dev/null @@ -1,4 +0,0 @@ -%{ - "eye_drops": {:hex, :eye_drops, "1.3.0", "9382901bac71a6c681765880b4482afed4db1c6a037823c64579dbb343c62fc3", [:mix], [{:fs, "~> 2.12.0", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"}, - "fs": {:hex, :fs, "2.12.0", "ad631efacc9a5683c8eaa1b274e24fa64a1b8eb30747e9595b93bec7e492e25e", [:rebar3], [], "hexpm"}, -} diff --git a/examples/ct_memory/test/ctm_test.exs b/examples/ct_memory/test/ctm_test.exs deleted file mode 100644 index e9cf5a0..0000000 --- a/examples/ct_memory/test/ctm_test.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule CTMTest do - use ExUnit.Case - - describe "Closure Table, in Memory" do - test "example descendants" do - assert {:ok, [2, 3]} = CTM.descendants(1) - end - end -end diff --git a/examples/ct_memory/test/test_helper.exs b/examples/ct_memory/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/examples/ct_memory/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/lib/cte.ex b/lib/cte.ex index 8c17c51..1edf237 100644 --- a/lib/cte.ex +++ b/lib/cte.ex @@ -13,95 +13,24 @@ defmodule CTE do and deleting nodes, moving entire sub-trees or print them as a digraph (.dot) file. - ### Quick example. - - For this example we're using the in-[Memory Adapter](CTE.Adapter.Memory.html#content). - This `Adapter` is useful for prototyping or with data structures - that can easily fit in memory; their persistence being taken care - of by other components. For more involved use cases, CTE integrates - with Ecto using a simple API. - - When used from a module, the CTE expects the: `:otp_app` and - `:adapter` attributes, to be defined. The `:otp_app` should point - to an OTP application that might provide additional configuration. - Equally so are the: `:nodes` and the `:paths` attributes. The `:nodes` - attribute, in the case of the [Memory Adapter](CTE.Adapter.Memory.html#content), - is a Map defining your nodes while the: `:paths` attribute, is a list - containing the tree path between the nodes - a list of lists. For example: - - defmodule CTM do - use CTE, - otp_app: :ct_empty, - adapter: CTE.Adapter.Memory, - nodes: %{ - 1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"}, - 2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"}, - 3 => %{id: 3, author: "Olie", comment: "Yeah."} - }, - paths: [[1, 1, 0], [1, 2, 1], [1, 3, 2], [2, 2, 0], [2, 3, 1], [3, 3, 0]] - end - - - When using the `CTE.Adapter.Ecto`, the: `:nodes` attribute, - will be a Schema i.e. `Post`, `TreePath`, etc! In our initial - implementation, the nodes definitions must have at least the - `:id`, as one of their properties. This caveat will be lifted - in a later implementation. - - Add the `CTM` module to your main supervision tree: - - defmodule CTM.Application do - @moduledoc false + Options available to most of the functions: - use Application + - `:limit`, to limit the total number of nodes returned, when finding the ancestors or the descendants for nodes + - `:itself`, accepting a boolean value. When `true`, the node used for finding its neighbors are returned as part of the results. Default: true + - `:nodes`, accepting a boolean value. When `true`, the results are containing additional information about the nodes. Default: false - def start(_type, _args) do - opts = [strategy: :one_for_one, name: CTM.Supervisor] - Supervisor.start_link([CTM], opts) - end - end - - Using `iex -S mix`, for quickly experimenting with the CTE API: - - - find the descendants of comment #1 - - ```elixir - iex» CTM.descendants(1) - {:ok, [3, 2]} - ``` + ### Quick example. - - find the ancestors + In this example the: `:nodes` attribute, will be a Schema i.e. `Post`, `TreePath`, etc! + In our initial implementation, the nodes definitions must have at least the + `:id`, as one of their properties. This caveat will be lifted + in a later implementation. - ```elixir - iex» CTM.ancestors(2) - {:ok, [1]} + ...... - iex» CTM.ancestors(3) - {:ok, [1]} - ``` - - find the ancestors, with information about the node: + todo: Update the docs - ```elixir - iex» CTM.ancestors(2, nodes: true) - {:ok, - [ - %{ - author: "Olie", - comment: "Is Closure Table better than the Nested Sets?", - id: 1 - } - ]} - ``` - - move leafs/subtrees around. From being a child of comment #1, to becoming a - child of comment #2, in the following example: - - ```elixir - iex» CTM.move(3, 2) - :ok - iex» CTM.descendants(2) - {:ok, [3]} - ``` Please check the docs, the tests, and the examples folder, for more details. """ @@ -115,7 +44,6 @@ defmodule CTE do @type name :: String.t() | atom @type t :: %__MODULE__{ - adapter: any() | nil, nodes: nodes | nil, paths: paths | nil, repo: repo | nil, @@ -125,99 +53,40 @@ defmodule CTE do defmacro __using__(opts) do quote bind_quoted: [opts: opts] do - @default_adapter CTE.Adapter.Memory - @default_config [nodes: [], paths: [], adapter: @default_adapter, repo: nil] - @default_dynamic_supervisor opts[:default_dynamic_supervisor] || opts[:name] || __MODULE__ - - @otp_app Keyword.fetch!(opts, :otp_app) - @adapter Keyword.fetch!(opts, :adapter) - @opts opts - - @doc false - def config(), do: parse_config(@opts) - - @doc false - def __adapter__ do - {{:repo, _repo}, %{pid: adapter}} = CTE.Registry.lookup(get_dynamic_supervisor()) - adapter - end - - @doc false - def child_spec(opts) do - %{ - id: __MODULE__, - start: {__MODULE__, :start_link, [opts]}, - type: :supervisor - } - end - - @doc false - def start_link(opts \\ []) do - CTE.Supervisor.start_link(__MODULE__, @otp_app, @adapter, config()) - end - - @compile {:inline, get_dynamic_supervisor: 0} - - def get_dynamic_supervisor() do - Process.get({__MODULE__, :dynamic_supervisor}, @default_dynamic_supervisor) - end - - def put_dynamic_supervisor(dynamic) when is_atom(dynamic) or is_pid(dynamic) do - Process.put({__MODULE__, :dynamic_supervisor}, dynamic) || @default_dynamic_supervisor - end + @opts %CTE{ + nodes: Keyword.get(opts, :nodes, []), + paths: Keyword.get(opts, :paths, []), + repo: Keyword.get(opts, :repo, nil) + } def insert(leaf, ancestor, opts \\ []) - def insert(leaf, ancestor, opts), do: @adapter.insert(__adapter__(), leaf, ancestor, opts) + def insert(leaf, ancestor, opts), + do: CTE.Ecto.insert(leaf, ancestor, opts, @opts) def tree(leaf, opts \\ []) - - def tree(leaf, opts), do: @adapter.tree(__adapter__(), leaf, opts) + def tree(leaf, opts), do: CTE.Ecto.tree(leaf, opts, @opts) def ancestors(descendant, opts \\ []) - def ancestors(descendant, opts), do: @adapter.ancestors(__adapter__(), descendant, opts) + def ancestors(descendant, opts), + do: CTE.Ecto.ancestors(descendant, opts, @opts) def descendants(ancestor, opts \\ []) - def descendants(ancestor, opts), do: @adapter.descendants(__adapter__(), ancestor, opts) + def descendants(ancestor, opts), + do: CTE.Ecto.descendants(ancestor, opts, @opts) @doc """ when limit: 1, the default value, then delete only the leafs, else the entire subtree """ - def delete(leaf, ops \\ []) - def delete(leaf, opts), do: @adapter.delete(__adapter__(), leaf, opts) + def delete(leaf, opts \\ [limit: 1]) + def delete(leaf, opts), do: CTE.Ecto.delete(leaf, opts, @opts) def move(leaf, ancestor, opts \\ []) - def move(leaf, ancestor, opts), do: @adapter.move(__adapter__(), leaf, ancestor, opts) - - defp parse_config(config), do: CTE.parse_config(@otp_app, __MODULE__, @opts, config) + def move(leaf, ancestor, opts), + do: CTE.Ecto.move(leaf, ancestor, opts, @opts) end end - - @doc false - def parse_config(otp_app, adapter, adapter_config, dynamic_config) do - conf = - Application.get_env(otp_app, adapter, []) - |> Keyword.merge(adapter_config) - |> Keyword.merge(dynamic_config) - |> CTE.interpolate_env_vars() - - %CTE{ - nodes: Keyword.get(conf, :nodes, []), - paths: Keyword.get(conf, :paths, []), - repo: Keyword.get(conf, :repo, nil), - adapter: Keyword.get(conf, :adapter), - name: Keyword.get(conf, :name) - } - end - - @doc false - def interpolate_env_vars(config) do - Enum.map(config, fn - {key, {:system, env_var}} -> {key, System.get_env(env_var)} - {key, value} -> {key, value} - end) - end end diff --git a/lib/cte/adapter.ex b/lib/cte/adapter.ex deleted file mode 100644 index e3d843d..0000000 --- a/lib/cte/adapter.ex +++ /dev/null @@ -1,79 +0,0 @@ -defmodule CTE.Adapter do - @moduledoc ~S""" - Specification of the Closure Table implementation. - - Most of the functions implementing the `CTE.Adapter` behavior, will accept the following options: - - - `:limit`, to limit the total number of nodes returned, when finding the ancestors or the descendants for nodes - - `:itself`, accepting a boolean value. When `true`, the node used for finding its neighbors are returned as part of the results. Default: true - - `:nodes`, accepting a boolean value. When `true`, the results are containing additional information about the nodes. Default: false - - """ - @type t :: module - @type options :: Keyword.t() - - defmacro __using__(opts) do - quote bind_quoted: [opts: opts] do - use GenServer - @behaviour CTE.Adapter - - @doc """ - start the Adapter server - """ - def start_link(init_args) do - GenServer.start_link(__MODULE__, init_args) - end - - @doc """ - Initializes the adapter supervision tree by returning the children and adapter metadata. - """ - def init(repo: _repo, config: config) do - {:ok, config} - end - - defoverridable start_link: 1, init: 1 - end - end - - @doc """ - Retrieve the descendants of a node - """ - @callback descendants(pid(), ancestor :: any(), options) :: {:ok, CTE.nodes()} | {:error, any()} - - @doc """ - Retrieve the ancestors of a node - """ - @callback ancestors(pid(), descendant :: any(), options) :: {:ok, CTE.nodes()} | {:error, any()} - - @doc """ - Delete a leaf or a subtree. - hĕdzˈŭpˈ: read the docs of the implementation, in case the implementation has specific side effects - """ - @callback delete(pid(), leaf :: any()) :: :ok | {:error, any()} - - @callback delete(pid(), leaf :: any(), options) :: :ok | {:error, any()} - - @doc """ - Insert a node under an existing ancestor - """ - @callback insert(pid(), leaf :: any(), ancestor :: any(), options) :: - {:ok, CTE.t()} | {:error, any()} - - @doc """ - Move a subtree from one location to another. - - First, the subtree and its descendants are disconnected from its ancestors. And second, the subtree is inserted under the new parent (ancestor) and the subtree, including its descendants, is declared as descendants of all the new ancestors. - """ - @callback move(pid(), leaf :: any(), ancestor :: any(), options) :: :ok | {:error, any()} - - @doc """ - Calculate and return a "tree" structure containing the paths and the nodes under the given leaf/node - """ - @callback tree(pid(), leaf :: any(), options) :: {:ok, CTE.nodes()} | {:error, any()} - - @doc false - def lookup_meta(repo_name_or_pid) do - {_, meta} = CTE.Registry.lookup(repo_name_or_pid) - meta - end -end diff --git a/lib/cte/adapter/ecto.ex b/lib/cte/adapter/ecto.ex deleted file mode 100644 index d5c7243..0000000 --- a/lib/cte/adapter/ecto.ex +++ /dev/null @@ -1,459 +0,0 @@ -if match?({:module, Ecto}, Code.ensure_compiled(Ecto)) do - defmodule CTE.Adapter.Ecto do - @moduledoc """ - A CTE Adapter implementation using an existing Ecto Repo for persisting the models. - - The current implementation is depending on Ecto ~> 3.1; using [Ecto.SubQuery](https://hexdocs.pm/ecto/Ecto.SubQuery.html)! - - For this implementation to work you'll have to provide two tables, and the name of the Repo used by your application: - - 1. a table name containing the nodes. Having the `id`, as a the primary key - 2. a table name where the tree paths will be stores. - 3. the name of the Ecto.Repo, defined by your app - - In a future version we will provide you with a convenient migration template to help you starting, but for now you must supply these tables. - - For example, given you have the following Schemas for comments: - - defmodule CT.Comment do - use Ecto.Schema - import Ecto.Changeset - - @timestamps_opts [type: :utc_datetime] - - schema "comments" do - field :text, :string - belongs_to :author, CT.Author - - timestamps() - end - end - - and a table used for storing the parent-child relationships - - defmodule CT.TreePath do - use Ecto.Schema - import Ecto.Changeset - alias CT.Comment - - @primary_key false - - schema "tree_paths" do - belongs_to :parent_comment, Comment, foreign_key: :ancestor - belongs_to :comment, Comment, foreign_key: :descendant - field :depth, :integer, default: 0 - end - end - - we can define the following module: - - defmodule CT.MyCTE do - use CTE, - otp_app: :cte, - adapter: CTE.Adapter.Ecto, - repo: CT.Repo, - nodes: CT.Comment, - paths: CT.TreePath - end - - - We add our CTE Repo to the app's main supervision tree, like this: - - defmodule CT.Application do - use Application - - def start(_type, _args) do - children = [ - CT.Repo, - CT.MyCTE - ] - - opts = [strategy: :one_for_one, name: CT.Supervisor] - Supervisor.start_link(children, opts) - end - end - - restart out app and then using IEx, we can start experimenting. Examples: - - iex» CT.MyCTE.ancestors(9) - {:ok, [1, 4, 6]} - - iex» CT.MyCTE.tree(6) - {:ok, - %{ - nodes: %{ - 6 => %CT.Comment{ - __meta__: #Ecto.Schema.Metadata<:loaded, "comments">, - author: #Ecto.Association.NotLoaded, - author_id: 2, - id: 6, - inserted_at: ~U[2019-07-21 01:10:35Z], - text: "Everything is easier, than with the Nested Sets.", - updated_at: ~U[2019-07-21 01:10:35Z] - }, - 8 => %CT.Comment{ - __meta__: #Ecto.Schema.Metadata<:loaded, "comments">, - author: #Ecto.Association.NotLoaded, - author_id: 1, - id: 8, - inserted_at: ~U[2019-07-21 01:10:35Z], - text: "I’m sold! And I’ll use its Elixir implementation! <3", - updated_at: ~U[2019-07-21 01:10:35Z] - }, - 9 => %CT.Comment{ - __meta__: #Ecto.Schema.Metadata<:loaded, "comments">, - author: #Ecto.Association.NotLoaded, - author_id: 3, - id: 9, - inserted_at: ~U[2019-07-21 01:10:35Z], - text: "w⦿‿⦿t!", - updated_at: ~U[2019-07-21 01:10:35Z] - } - }, - paths: [ - [6, 6, 0], - [6, 8, 1], - [8, 8, 0], - [6, 9, 1], - [9, 9, 0] - ] - }} - - Have fun! - - - Most of the functions implementing the `CTE.Adapter` behavior, will accept the following options: - - - `:limit`, to limit the total number of nodes returned, when finding the ancestors or the descendants for nodes - - `:itself`, accepting a boolean value. When `true`, the node used for finding its neighbors are returned as part of the results. Default: true - - `:nodes`, accepting a boolean value. When `true`, the results are containing additional information about the nodes. Default: false - """ - use CTE.Adapter - - import Ecto.Query, warn: false - - @doc """ - Insert a node under an existing ancestor - """ - def insert(pid, leaf, ancestor, opts) do - GenServer.call(pid, {:insert, leaf, ancestor, opts}) - end - - @doc """ - Retrieve the descendants of a node - """ - def descendants(pid, ancestor, opts) do - GenServer.call(pid, {:descendants, ancestor, opts}) - end - - @doc """ - Retrieve the ancestors of a node - """ - def ancestors(pid, descendant, opts) do - GenServer.call(pid, {:ancestors, descendant, opts}) - end - - @doc """ - Delete a leaf or a subtree. - - To delete a leaf node set the limit option to: 1, and in this particular case - all the nodes that reference the leaf will be assigned to the leaf's immediate ancestor - - If limit is 0, then the leaf and its descendants will be deleted - """ - def delete(pid, leaf, opts \\ [limit: 1]) - - def delete(pid, leaf, opts) do - leaf? = Keyword.get(opts, :limit, 1) == 1 - GenServer.call(pid, {:delete, leaf, leaf?, opts}) - end - - @doc """ - Move a subtree from one location to another. - - First, the subtree and its descendants are disconnected from its ancestors. And second, the subtree is inserted under the new parent (ancestor) and the subtree, including its descendants, is declared as descendants of all the new ancestors. - """ - def move(pid, leaf, ancestor, opts) do - GenServer.call(pid, {:move, leaf, ancestor, opts}) - end - - @doc """ - Calculate and return a "tree" structure containing the paths and the nodes under the given leaf/node - """ - def tree(pid, leaf, opts) do - GenServer.call(pid, {:tree, leaf, opts}) - end - - ###################################### - # server callbacks - ###################################### - - @doc false - def handle_call({:delete, leaf, true, _opts}, _from, config) do - %CTE{paths: paths, repo: repo} = config - - descendants = _descendants(leaf, [itself: false], config) || [] - - query_delete_leaf = - from p in paths, - where: ^leaf in [p.ancestor, p.descendant] and p.depth >= 0, - select: %{ancestor: p.ancestor, descendant: p.descendant, depth: p.depth} - - # repo.all(query_delete_leaf) - # |> IO.inspect(label: "DELETE: ") - - query_move_leafs_kids_up = - from p in paths, - where: p.descendant in ^descendants and p.depth >= 1, - update: [ - set: [ - depth: p.depth - 1 - ] - ] - - repo.transaction(fn -> - repo.delete_all(query_delete_leaf) - repo.update_all(query_move_leafs_kids_up, []) - end) - - {:reply, :ok, config} - end - - @doc false - def handle_call({:delete, leaf, _subtree, _opts}, _from, config) do - %CTE{paths: paths, repo: repo} = config - - # DELETE FROM ancestry WHERE descendant IN (SELECT descendant FROM ancestry WHERE ancestor = 100) - sub = from p in paths, where: p.ancestor == ^leaf - - query = - from p in paths, - join: sub in subquery(sub), - on: p.descendant == sub.descendant - - repo.delete_all(query) - - {:reply, :ok, config} - end - - @doc false - def handle_call({:move, leaf, ancestor, opts}, _from, config) do - results = _move(leaf, ancestor, opts, config) - {:reply, results, config} - end - - @doc false - def handle_call({:descendants, ancestor, opts}, _from, config) do - results = _descendants(ancestor, opts, config) - {:reply, {:ok, results}, config} - end - - @doc false - def handle_call({:ancestors, descendant, opts}, _from, config) do - result = _ancestors(descendant, opts, config) - {:reply, {:ok, result}, config} - end - - def handle_call({:insert, leaf, ancestor, _opts}, _from, config) do - result = _insert(leaf, ancestor, config) - - {:reply, result, config} - end - - @doc false - def handle_call({:tree, leaf, opts}, _from, config) do - %CTE{paths: paths, nodes: nodes, repo: repo} = config - - descendants_opts = [itself: true] ++ Keyword.take(opts, [:depth]) - descendants = _descendants(leaf, descendants_opts, config) - - # subtree = Enum.filter(paths, fn [ancestor, _descendant] -> ancestor in descendants end) - query = - from p in paths, - where: p.ancestor in ^descendants, - select: [p.ancestor, p.descendant, p.depth] - - subtree = - query - |> prune(descendants, opts, config) - |> repo.all() - - authors = - subtree - |> List.flatten() - |> Enum.uniq() - - query = from n in nodes, where: n.id in ^authors - - some_nodes = - repo.all(query) - |> Enum.reduce(%{}, fn node, acc -> Map.put(acc, node.id, node) end) - - {:reply, {:ok, %{paths: subtree, nodes: some_nodes}}, config} - end - - ###################################### - # private - ###################################### - - @doc false - defp _insert(leaf, ancestor, config) do - %CTE{paths: paths, repo: repo} = config - - # SELECT t.ancestor, #{leaf}, t.depth + 1 - # FROM tree_paths AS t - # WHERE t.descendant = #{ancestor} - descendants = - from p in paths, - where: p.descendant == ^ancestor, - select: %{ancestor: p.ancestor, descendant: type(^leaf, :integer), depth: p.depth + 1} - - new_records = repo.all(descendants) ++ [%{ancestor: leaf, descendant: leaf, depth: 0}] - descendants = Enum.map(new_records, fn r -> [r.ancestor, r.descendant] end) - - case repo.insert_all(paths, new_records, on_conflict: :nothing) do - {_nr, _r} -> - # l when l == nr <- length(new_records) do - {:ok, descendants} - - e -> - {:error, e} - end - end - - @doc false - defp _descendants(ancestor, opts, config) do - %CTE{paths: paths, nodes: nodes, repo: repo} = config - - # SELECT c. * FROM comments AS c - # JOIN tree_paths AS t ON c.id = t.descendant - # WHERE t.ancestor = ^ancestor; - query = - from n in nodes, - join: p in ^paths, - as: :tree, - on: n.id == p.descendant, - where: p.ancestor == ^ancestor, - order_by: [asc: p.depth] - - query - |> selected(opts, config) - |> include_itself(opts, config) - |> depth(opts, config) - |> top(opts, config) - |> repo.all() - end - - @doc false - defp _ancestors(descendant, opts, config) do - %CTE{paths: paths, nodes: nodes, repo: repo} = config - - # SELECT c. * FROM comments AS c - # JOIN tree_paths AS t ON c.id = t.ancestor - # WHERE t.descendant = ^descendant; - query = - from n in nodes, - join: p in ^paths, - as: :tree, - on: n.id == p.ancestor, - where: p.descendant == ^descendant, - order_by: [desc: p.depth] - - query - |> selected(opts, config) - |> include_itself(opts, config) - |> depth(opts, config) - |> top(opts, config) - |> repo.all() - end - - defp _move(leaf, ancestor, _opts, config) do - %CTE{paths: paths, repo: repo} = config - - # DELETE FROM ancestry - # WHERE descendant IN (SELECT descendant FROM ancestry WHERE ancestor = ^leaf) - # AND ancestor IN (SELECT ancestor FROM ancestry WHERE descendant = ^leaf - # AND ancestor != descendant); - - q_ancestors = - from p in paths, - where: p.descendant == ^leaf, - where: p.ancestor != p.descendant - - q_descendants = - from p in paths, - where: p.ancestor == ^leaf - - query_delete = - from p in paths, - join: d in subquery(q_descendants), - on: p.descendant == d.descendant, - join: a in subquery(q_ancestors), - on: p.ancestor == a.ancestor - - # INSERT INTO ancestry (ancestor, descendant) - # SELECT super_tree.ancestor, sub_tree.descendant FROM ancestry AS super_tree - # CROSS JOIN ancestry AS sub_tree WHERE super_tree.descendant = 3 - # AND sub_tree.ancestor = 6; - query_insert = - from super_tree in paths, - cross_join: sub_tree in ^paths, - where: super_tree.descendant == ^ancestor, - where: sub_tree.ancestor == ^leaf, - select: %{ - ancestor: super_tree.ancestor, - descendant: sub_tree.descendant, - depth: super_tree.depth + sub_tree.depth + 1 - } - - repo.transaction(fn -> - repo.delete_all(query_delete) - inserts = repo.all(query_insert) - repo.insert_all(paths, inserts) - end) - end - - ###################################### - # Utils - ###################################### - defp selected(query, opts, _config) do - if Keyword.get(opts, :nodes, false) do - from(n in query) - else - from n in query, select: n.id - end - end - - defp include_itself(query, opts, _config) do - if Keyword.get(opts, :itself, false) do - query - else - from [tree: t] in query, where: t.ancestor != t.descendant - end - end - - defp top(query, opts, _config) do - if limit = Keyword.get(opts, :limit) do - from q in query, limit: ^limit - else - query - end - end - - defp depth(query, opts, _config) do - if depth = Keyword.get(opts, :depth) do - from [tree: t] in query, where: t.depth <= ^max(depth, 0) - else - query - end - end - - defp prune(query, descendants, opts, _config) do - if Keyword.get(opts, :depth) do - from t in query, where: t.descendant in ^descendants - else - query - end - end - end -end diff --git a/lib/cte/adapter/memory.ex b/lib/cte/adapter/memory.ex deleted file mode 100644 index d7096f3..0000000 --- a/lib/cte/adapter/memory.ex +++ /dev/null @@ -1,276 +0,0 @@ -defmodule CTE.Adapter.Memory do - @moduledoc """ - Basic implementation of the CTE, using the memory for persisting the models. Adapter provided as a convenient way of using CTE in tests or during the development - """ - use CTE.Adapter - - @doc false - def descendants(pid, ancestor, opts) do - GenServer.call(pid, {:descendants, ancestor, opts}) - end - - @doc false - def ancestors(pid, descendant, opts) do - GenServer.call(pid, {:ancestors, descendant, opts}) - end - - @doc false - def insert(pid, leaf, ancestor, opts) do - GenServer.call(pid, {:insert, leaf, ancestor, opts}) - end - - @doc """ - Delete a leaf or a subtree. - - To delete a leaf node set the limit option to: 1, and in this particular case - all the nodes that reference the leaf will be assigned to the leaf's immediate ancestor - - If limit is 0, then the leaf and its descendants will be deleted - """ - def delete(pid, leaf, opts \\ [limit: 1]) - - def delete(pid, leaf, opts) do - leaf? = Keyword.get(opts, :limit, 1) == 1 - GenServer.call(pid, {:delete, leaf, leaf?, opts}) - end - - @doc false - def move(pid, leaf, ancestor, opts) do - GenServer.call(pid, {:move, leaf, ancestor, opts}) - end - - @doc false - def tree(pid, leaf, opts) do - GenServer.call(pid, {:tree, leaf, opts}) - end - - @doc false - def handle_call({:tree, leaf, opts}, _from, config) do - %CTE{paths: paths, nodes: nodes} = config - - descendants_opts = [itself: true] ++ Keyword.take(opts, [:depth]) - descendants = _descendants(leaf, descendants_opts, config) - - subtree = - Enum.filter(paths, fn - [ancestor, descendant, _] -> - ancestor in descendants && descendant in descendants - end) - - nodes = - Enum.reduce(subtree, %{}, fn [ancestor, descendant, _depth], acc -> - Map.merge(acc, %{ - ancestor => Map.get(nodes, ancestor), - descendant => Map.get(nodes, descendant) - }) - end) - - {:reply, {:ok, %{paths: subtree, nodes: nodes}}, config} - end - - @doc false - def handle_call({:delete, leaf, true, _opts}, _from, %CTE{paths: paths} = config) do - leaf_parent = - case _ancestors(leaf, [itself: false], config) do - [leaf_parent | _ancestors] -> leaf_parent - _ -> nil - end - - paths = - paths - |> Enum.reduce([], fn - [_leaf, ^leaf, _], acc -> acc - [^leaf, descendant, _depth], acc -> [[leaf_parent, descendant, 1] | acc] - p, acc -> [p | acc] - end) - |> Enum.reverse() - - {:reply, :ok, %{config | paths: paths}} - end - - @doc false - def handle_call({:delete, leaf, _subtree, opts}, _from, %{paths: paths} = config) do - opts = Keyword.put(opts, :itself, true) - - descendants = _descendants(leaf, opts, config) || [] - - paths = - Enum.filter(paths, fn [_ancestor, descendant, _] -> - descendant not in descendants - end) - - {:reply, :ok, %{config | paths: paths}} - end - - @doc false - def handle_call({:move, leaf, ancestor, _opts}, _from, config) do - %CTE{paths: paths} = config - ex_ancestors = _ancestors(leaf, [itself: true], config) - - {descendants_paths, _} = descendants_collector(leaf, [itself: true], config) - descendants = Enum.map(descendants_paths, fn [_, descendant, _] -> descendant end) - - paths_with_leaf = - paths - |> Enum.filter(fn [ancestor, descendant, _] -> - ancestor in ex_ancestors and descendant in descendants and ancestor != descendant - end) - - paths_without_leaf = Enum.filter(paths, &(&1 not in paths_with_leaf)) - - {new_ancestors_paths, _} = - ancestors_collector(ancestor, [itself: true], %{config | paths: paths_without_leaf}) - - new_paths = - for [ancestor, _, super_tree_depth] <- [[leaf, leaf, -1] | new_ancestors_paths], - [_, descendant, subtree_depth] <- descendants_paths, - into: [] do - [ancestor, descendant, super_tree_depth + subtree_depth + 1] - end - |> Enum.reverse() - - {:reply, :ok, %{config | paths: paths_without_leaf ++ new_paths}} - end - - @doc false - def handle_call({:insert, leaf, ancestor, opts}, _from, config) do - case _insert(leaf, ancestor, opts, config) do - {:ok, new_paths, config} -> {:reply, {:ok, new_paths}, config} - err -> {:reply, {:error, err}, config} - end - end - - @doc false - def handle_call({:ancestors, descendant, opts}, _from, config) do - result = - _ancestors(descendant, opts, config) - |> Enum.reverse() - - {:reply, {:ok, result}, config} - end - - @doc false - def handle_call({:descendants, ancestor, opts}, _from, config) do - result = - _descendants(ancestor, opts, config) - |> Enum.reverse() - - {:reply, {:ok, result}, config} - end - - @doc false - defp _descendants(ancestor, opts, config) do - descendants_collector(ancestor, opts, config) - |> depth(opts, config) - |> selected(opts, config) - end - - @doc false - defp descendants_collector(ancestor, opts, config) do - mapper = fn paths -> Enum.map(paths, fn [_, descendant, _] -> descendant end) end - - fn path, {acc_paths, _mapper, size} = acc, itself? -> - case path do - [^ancestor, ^ancestor, _] when not itself? -> - acc - - [^ancestor, _descendant, _depth] = descendants -> - {[descendants | acc_paths], mapper, size + 1} - - _ -> - acc - end - end - |> _find_leaves(opts, config) - end - - @doc false - defp _ancestors(descendant, opts, config) do - ancestors_collector(descendant, opts, config) - |> depth(opts, config) - |> selected(opts, config) - end - - @doc false - defp ancestors_collector(descendant, opts, config) do - mapper = fn paths -> Enum.map(paths, fn [ancestor, _, _] -> ancestor end) end - - fn path, {acc_paths, _mapper, size} = acc, itself? -> - case path do - [^descendant, ^descendant, _] when not itself? -> - acc - - [_ancestor, ^descendant, _depth] = ancestors -> - {[ancestors | acc_paths], mapper, size + 1} - - _ -> - acc - end - end - |> _find_leaves(opts, config) - end - - @doc false - defp _insert(leaf, ancestor, _opts, config) do - %CTE{nodes: nodes, paths: paths} = config - - case Map.has_key?(nodes, ancestor) do - true -> - {leaf_new_ancestors, _} = ancestors_collector(ancestor, [itself: true], config) - - new_paths = - leaf_new_ancestors - |> Enum.reduce([[leaf, leaf, 0]], fn [ancestor, _, depth], acc -> - [[ancestor, leaf, depth + 1] | acc] - end) - - acc_paths = paths ++ new_paths - config = %{config | paths: acc_paths} - - {:ok, new_paths, config} - - _ -> - {:error, :no_ancestor, config} - end - end - - @doc false - defp _find_leaves(fun, opts, %CTE{paths: paths}) do - itself? = Keyword.get(opts, :itself, false) - limit = Keyword.get(opts, :limit, 0) - - {leaves_paths, mapper, _size} = - paths - |> Enum.reduce_while({[], & &1, 0}, fn path, acc -> - {_, _, sz} = dsz = fun.(path, acc, itself?) - - if limit == 0 or sz < limit, do: {:cont, dsz}, else: {:halt, dsz} - end) - - {leaves_paths, mapper} - end - - @doc false - defp depth({leaves_paths, mapper}, opts, _config) do - leaves_paths = - if depth = Keyword.get(opts, :depth) do - leaves_paths - |> Enum.filter(fn [_, _, depth_] -> depth_ <= max(depth, 0) end) - else - leaves_paths - end - - {leaves_paths, mapper} - end - - @doc false - defp selected({leaves_paths, mapper}, opts, %CTE{nodes: nodes}) do - leaves = mapper.(leaves_paths) - - if Keyword.get(opts, :nodes, false) do - Enum.map(leaves, &Map.get(nodes, &1)) - else - leaves - end - end -end diff --git a/lib/cte/application.ex b/lib/cte/application.ex deleted file mode 100644 index dd03f83..0000000 --- a/lib/cte/application.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule CTE.Application do - @moduledoc false - use Application - - def start(_type, _args) do - children = [ - CTE.Registry - ] - - opts = [strategy: :one_for_one, name: CTE.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/lib/cte/ecto.ex b/lib/cte/ecto.ex new file mode 100644 index 0000000..3ea4fb1 --- /dev/null +++ b/lib/cte/ecto.ex @@ -0,0 +1,278 @@ +defmodule CTE.Ecto do + @moduledoc """ + Most of the functions provided will accept the following options: + + - `:limit`, to limit the total number of nodes returned, when finding the ancestors or the descendants for nodes + - `:itself`, accepting a boolean value. When `true`, the node used for finding its neighbors are returned as part of the results. Default: true + - `:nodes`, accepting a boolean value. When `true`, the results are containing additional information about the nodes. Default: false + """ + import Ecto.Query, warn: false + + @doc """ + Delete a leaf or a subtree. + + To delete a leaf node set the limit option to: 1, and in this particular case + all the nodes that reference the leaf will be assigned to the leaf's immediate ancestor + + If limit is 0, then the leaf and its descendants will be deleted + """ + def delete(leaf, opts, config) do + leaf? = Keyword.get(opts, :limit, 1) == 1 + _delete(leaf, leaf?, opts, config) + end + + @doc """ + Move a subtree from one location to another. + + First, the subtree and its descendants are disconnected from its ancestors. And second, the subtree is inserted under the new parent (ancestor) and the subtree, including its descendants, is declared as descendants of all the new ancestors. + """ + def move(leaf, ancestor, opts, config) do + _move(leaf, ancestor, opts, config) + end + + @doc """ + Retrieve the descendants of a node + """ + def descendants(ancestor, opts, config) do + {:ok, _descendants(ancestor, opts, config)} + end + + @doc """ + Retrieve the ancestors of a node + """ + def ancestors(descendant, opts, config) do + {:ok, _ancestors(descendant, opts, config)} + end + + @doc """ + Insert a node under an existing ancestor + """ + + def insert(leaf, ancestor, _opts, config) do + _insert(leaf, ancestor, config) + end + + @doc """ + Calculate and return a "tree" structure containing the paths and the nodes under the given leaf/node + """ + + def tree(leaf, opts, config) do + %CTE{paths: paths, nodes: nodes, repo: repo} = config + + descendants_opts = [itself: true] ++ Keyword.take(opts, [:depth]) + descendants = _descendants(leaf, descendants_opts, config) + + query = + from p in paths, + where: p.ancestor in ^descendants, + select: [p.ancestor, p.descendant, p.depth] + + subtree = + query + |> prune(descendants, opts, config) + |> repo.all() + + unique_descendants = + subtree + |> List.flatten() + |> Enum.uniq() + + query = from n in nodes, where: n.id in ^unique_descendants + + some_nodes = + repo.all(query) + |> Enum.reduce(%{}, fn node, acc -> Map.put(acc, node.id, node) end) + + {:ok, %{paths: subtree, nodes: some_nodes}} + end + + ###################################### + # private + ###################################### + + @doc false + defp _insert(leaf, ancestor, config) do + %CTE{paths: paths, repo: repo} = config + + descendants = + from p in paths, + where: p.descendant == ^ancestor, + select: %{ancestor: p.ancestor, descendant: type(^leaf, :integer), depth: p.depth + 1} + + new_records = repo.all(descendants) ++ [%{ancestor: leaf, descendant: leaf, depth: 0}] + descendants = Enum.map(new_records, fn r -> [r.ancestor, r.descendant] end) + + case repo.insert_all(paths, new_records, on_conflict: :nothing) do + {_nr, _r} -> + {:ok, descendants} + + e -> + {:error, e} + end + end + + @doc false + defp _descendants(ancestor, opts, config) do + %CTE{paths: paths, nodes: nodes, repo: repo} = config + + query = + from n in nodes, + join: p in ^paths, + as: :tree, + on: n.id == p.descendant, + where: p.ancestor == ^ancestor, + order_by: [asc: p.depth] + + query + |> selected(opts, config) + |> include_itself(opts, config) + |> depth(opts, config) + |> top(opts, config) + |> repo.all() + end + + @doc false + defp _ancestors(descendant, opts, config) do + %CTE{paths: paths, nodes: nodes, repo: repo} = config + + query = + from n in nodes, + join: p in ^paths, + as: :tree, + on: n.id == p.ancestor, + where: p.descendant == ^descendant, + order_by: [desc: p.depth] + + query + |> selected(opts, config) + |> include_itself(opts, config) + |> depth(opts, config) + |> top(opts, config) + |> repo.all() + end + + defp _move(leaf, ancestor, _opts, config) do + %CTE{paths: paths, repo: repo} = config + + q_ancestors = + from p in paths, + where: p.descendant == ^leaf, + where: p.ancestor != p.descendant + + q_descendants = + from p in paths, + where: p.ancestor == ^leaf + + query_delete = + from p in paths, + join: d in subquery(q_descendants), + on: p.descendant == d.descendant, + join: a in subquery(q_ancestors), + on: p.ancestor == a.ancestor + + query_insert = + from super_tree in paths, + cross_join: sub_tree in ^paths, + where: super_tree.descendant == ^ancestor, + where: sub_tree.ancestor == ^leaf, + select: %{ + ancestor: super_tree.ancestor, + descendant: sub_tree.descendant, + depth: super_tree.depth + sub_tree.depth + 1 + } + + repo.transaction(fn -> + deleted = repo.delete_all(query_delete) + inserts = repo.all(query_insert) + inserted = repo.insert_all(paths, inserts) + + %{deleted: deleted, inserted: inserted} + end) + end + + ###################################### + # Utils + ###################################### + defp selected(query, opts, _config) do + if Keyword.get(opts, :nodes, false) do + from(n in query) + else + from n in query, select: n.id + end + end + + defp include_itself(query, opts, _config) do + if Keyword.get(opts, :itself, false) do + query + else + from [tree: t] in query, where: t.ancestor != t.descendant + end + end + + defp top(query, opts, _config) do + if limit = Keyword.get(opts, :limit) do + from q in query, limit: ^limit + else + query + end + end + + defp depth(query, opts, _config) do + if depth = Keyword.get(opts, :depth) do + from [tree: t] in query, where: t.depth <= ^max(depth, 0) + else + query + end + end + + defp prune(query, descendants, opts, _config) do + if Keyword.get(opts, :depth) do + from t in query, where: t.descendant in ^descendants + else + query + end + end + + @doc false + defp _delete(leaf, true, _opts, config) do + %CTE{paths: paths, repo: repo} = config + + descendants = _descendants(leaf, [itself: false], config) || [] + + query_delete_leaf = + from p in paths, + where: ^leaf in [p.ancestor, p.descendant] and p.depth >= 0, + select: %{ancestor: p.ancestor, descendant: p.descendant, depth: p.depth} + + query_move_leafs_kids_up = + from p in paths, + where: p.descendant in ^descendants and p.depth >= 1, + update: [ + set: [ + depth: p.depth - 1 + ] + ] + + repo.transaction(fn -> + deleted = repo.delete_all(query_delete_leaf) + updated = repo.update_all(query_move_leafs_kids_up, []) + + %{deleted: deleted, updated: updated} + end) + end + + @doc false + defp _delete(leaf, _subtree, _opts, %CTE{paths: paths, repo: repo}) do + sub = from p in paths, where: p.ancestor == ^leaf + + query = + from p in paths, + join: sub in subquery(sub), + on: p.descendant == sub.descendant + + case repo.delete_all(query) do + {_deleted, _} = d -> {:ok, %{deleted: d, updated: {0, nil}}} + e -> e + end + end +end diff --git a/lib/cte/registry.ex b/lib/cte/registry.ex deleted file mode 100644 index 8268250..0000000 --- a/lib/cte/registry.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule CTE.Registry do - @moduledoc false - use GenServer - - ## Public interface - - def start_link(_opts) do - GenServer.start_link(__MODULE__, :ok, name: __MODULE__) - end - - def associate(pid, value) when is_pid(pid) do - GenServer.call(__MODULE__, {:associate, pid, value}) - end - - def lookup(cte) when is_atom(cte) do - GenServer.whereis(cte) - |> Kernel.||( - raise "could not lookup #{inspect(cte)} because it was not started or it does not exist" - ) - |> lookup() - end - - def lookup(pid) when is_pid(pid) do - :ets.lookup_element(__MODULE__, pid, 3) - end - - ## Callbacks - - @impl true - def init(:ok) do - table = :ets.new(__MODULE__, [:named_table, read_concurrency: true]) - {:ok, table} - end - - @impl true - def handle_call({:associate, pid, value}, _from, table) do - ref = Process.monitor(pid) - true = :ets.insert(table, {pid, ref, value}) - {:reply, :ok, table} - end - - @impl true - def handle_info({:DOWN, ref, _type, pid, _reason}, table) do - [{^pid, ^ref, _}] = :ets.lookup(table, pid) - :ets.delete(table, pid) - {:noreply, table} - end -end diff --git a/lib/cte/supervisor.ex b/lib/cte/supervisor.ex deleted file mode 100644 index 33ea07d..0000000 --- a/lib/cte/supervisor.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule CTE.Supervisor do - @moduledoc false - use Supervisor - - def start_link(mod, otp_app, adapter, cte_config) do - name = cte_config.name || mod - Supervisor.start_link(__MODULE__, {mod, otp_app, adapter, cte_config}, name: name) - end - - def init({mod, _otp_app, adapter, cte_config}) do - # the otp_app here can be used for grabbing user definitions at runtime - args = [repo: mod] ++ [config: cte_config] - - child_spec = - %{ - id: mod, - start: {adapter, :start_link, [args]} - } - |> wrap_child_spec(args) - - Supervisor.init([child_spec], strategy: :one_for_one, max_restarts: 0) - end - - def start_child({mod, fun, args}, adapter, _meta) do - case apply(mod, fun, args) do - {:ok, pid} -> - CTE.Registry.associate(self(), {adapter, %{pid: pid}}) - {:ok, pid} - - other -> - other - end - end - - defp wrap_child_spec(%{start: start} = spec, args) do - %{spec | start: {__MODULE__, :start_child, [start | args]}} - end -end diff --git a/mix.exs b/mix.exs index 3a61037..1cf08e5 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule CTE.MixProject do use Mix.Project - @version "1.1.5" + @version "2.0.0" @url_docs "https://hexdocs.pm/closure_table" @url_github "https://github.com/florinpatrascu/closure_table" @@ -9,7 +9,7 @@ defmodule CTE.MixProject do [ app: :closure_table, version: @version, - elixir: "~> 1.11", + elixir: "~> 1.15", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), @@ -37,33 +37,32 @@ defmodule CTE.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger], - mod: {CTE.Application, []} + extra_applications: [:logger] ] end # Run "mix help deps" to learn about dependencies. defp deps do [ - # optional Ecto support - {:ecto, "~> 3.10.3", optional: true, runtime: false}, - {:ecto_sql, "~> 3.10.2", optional: true, runtime: false}, + {:ecto, "~> 3.11.2", optional: true, runtime: false}, + {:ecto_sql, "~> 3.11.3", optional: true, runtime: false}, {:postgrex, ">= 0.0.0", optional: true, runtime: false}, # dev/test/benching utilities {:benchee, ">= 0.0.0", only: :dev}, - {:mix_test_watch, "~> 1.1.0", only: [:dev, :test]}, + {:mix_test_watch, "~> 1.2.0", only: [:dev, :test]}, # Linting dependencies - {:credo, "~> 1.7.0", only: [:dev]}, - {:dialyxir, "~> 1.4.1", only: [:dev], runtime: false}, + {:credo, "~> 1.7.7", only: [:dev]}, + {:dialyxir, "~> 1.4.3", only: [:dev], runtime: false}, # mix eye_drops - {:eye_drops, github: "florinpatrascu/eye_drops", only: [:dev, :test], runtime: false}, + {:eye_drops, + github: "florinpatrascu/eye_drops", ref: "1d8c364", only: [:dev, :test], runtime: false}, # Documentation dependencies # Run me like this: `mix docs` - {:ex_doc, "~> 0.30.6", only: :dev, runtime: false} + {:ex_doc, "~> 0.34.2", only: :dev, runtime: false} ] end @@ -73,13 +72,7 @@ defmodule CTE.MixProject do defp package do %{ - files: [ - "lib", - "examples", - "assets", - "mix.exs", - "LICENSE" - ], + files: ~w(lib examples assets mix.exs LICENSE), licenses: ["Apache-2.0"], maintainers: ["Florin T.PATRASCU", "Greg Rychlewski"], links: %{ diff --git a/mix.lock b/mix.lock index c2bc57d..fa7d027 100644 --- a/mix.lock +++ b/mix.lock @@ -1,26 +1,25 @@ %{ "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, - "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, - "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, - "eye_drops": {:git, "https://github.com/florinpatrascu/eye_drops.git", "57977c51d6c49d65ff7472a98a9edb7fe86f30f7", []}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "fs": {:hex, :fs, "2.12.0", "ad631efacc9a5683c8eaa1b274e24fa64a1b8eb30747e9595b93bec7e492e25e", [:rebar3], [], "hexpm", "ff6043acf648b2b65026aeab3d32af68d2974d73c5a4c483bbdca60c0072b7e5"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, - "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, - "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "eye_drops": {:git, "https://github.com/florinpatrascu/eye_drops.git", "1d8c36493ab207c3fa7c04ca9556fa200e8ef973", [ref: "1d8c364"]}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/cte_memory_ascii_print_test.exs b/test/cte_memory_ascii_print_test.exs deleted file mode 100644 index 0e8cfd6..0000000 --- a/test/cte_memory_ascii_print_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -defmodule CTE.Memory.AsciiPrint.Test do - use ExUnit.Case, async: true - - alias CTE.InMemory, as: CTM - import ExUnit.CaptureIO - - describe "ASCII Printing" do - setup do - start_supervised(CTM) - CTM.seed() - - :ok - end - - test "valid CT in-memory adaptor" do - assert %CTE{adapter: CTE.Adapter.Memory, nodes: _nodes, paths: _paths} = CTM.config() - end - - test "print ascii tree " do - print_io = fn -> - {:ok, tree} = CTM.tree(1) - CTE.Utils.print_tree(tree, 1, callback: &{&1, "(#{&1}) #{&2[&1].comment}"}) - end - - assert capture_io(print_io) =~ """ - (1) Is Closure Table better than the Nested Sets? - ├── (2) It depends. Do you need referential integrity? - │ └── (3) Yeah. - │ └── (7) Closure Table *has* referential integrity? - └── (4) Querying the data it's easier. - ├── (5) What about inserting nodes? - └── (6) Everything is easier, than with the Nested Sets. - ├── (8) I'm sold! And I'll use its Elixir implementation! <3 - └── (9) w⦿‿⦿t! - """ - - print_io = fn -> - # limit: 1 - assert :ok == CTM.delete(3) - {:ok, tree} = CTM.tree(1) - CTE.Utils.print_tree(tree, 1, callback: &{&1, "(#{&1}) #{&2[&1].comment}"}) - end - - assert capture_io(print_io) =~ """ - (1) Is Closure Table better than the Nested Sets? - ├── (2) It depends. Do you need referential integrity? - │ └── (7) Closure Table *has* referential integrity? - └── (4) Querying the data it's easier. - ├── (5) What about inserting nodes? - └── (6) Everything is easier, than with the Nested Sets. - ├── (8) I'm sold! And I'll use its Elixir implementation! <3 - └── (9) w⦿‿⦿t! - """ - end - end -end diff --git a/test/cte_memory_test.exs b/test/cte_memory_test.exs deleted file mode 100644 index 2720d0f..0000000 --- a/test/cte_memory_test.exs +++ /dev/null @@ -1,309 +0,0 @@ -defmodule CTE.Memory.Test do - use ExUnit.Case, async: true - - alias CTE.InMemory, as: CT - - @digraph "digraph \"6) Rolie\nEverything is easier, than with the Nested Sets.\" {\n \"6) Rolie\nEverything is easier, than with the Nested Sets.\" -> \"8) Olie\nI'm sold! And I'll use its Elixir implementation! <3\"\n \"8) Olie\nI'm sold! And I'll use its Elixir implementation! <3\" -> \"8) Olie\nI'm sold! And I'll use its Elixir implementation! <3\"\n \"6) Rolie\nEverything is easier, than with the Nested Sets.\" -> \"9) Polie\nw⦿‿⦿t!\"\n \"9) Polie\nw⦿‿⦿t!\" -> \"9) Polie\nw⦿‿⦿t!\"\n}\n" - - defmodule CTEmpty do - use CTE, - otp_app: :ct_empty, - adapter: CTE.Adapter.Memory, - nodes: [], - paths: [] - end - - describe "Config" do - setup do - start_supervised(CT) - CT.seed() - end - - test "info" do - assert %CTE{adapter: CTE.Adapter.Memory, nodes: [], paths: []} == CTEmpty.config() - assert %CTE{adapter: CTE.Adapter.Memory, nodes: _nodes, paths: _paths} = CT.config() - end - end - - describe "Descendants" do - setup do - start_supervised(CT) - start_supervised(CTEmpty) - - CT.seed() - :ok - end - - test "Retrieve descendants of comment #2, including itself" do - assert {:ok, [1, 2]} == CT.descendants(1, limit: 2, itself: true) - end - - test "Retrieve descendants of comment #1, excluding itself" do - assert {:ok, [2, 3]} == CT.descendants(1, limit: 2) - assert {:ok, [2, 3, 7, 4, 5, 6, 8, 9]} == CT.descendants(1) - end - - test "Retrieve all descendants of comment #2, including itself" do - assert {:ok, [2, 3, 7]} = CT.descendants(2, itself: true) - end - - test "Retrieve descendants of comment #2, with limit" do - assert {:ok, [3, 7]} == CT.descendants(2, limit: 3) - end - - test "Retrieve descendants of comment #2, as comments" do - assert {:ok, - [ - %{ - id: 2, - comment: "It depends. Do you need referential integrity?", - author: "Rolie" - } - ]} == - CT.descendants(1, limit: 1, nodes: true) - end - - test "Retrieve descendants of comment #1, excluding itself, with depth" do - assert {:ok, [2, 3, 7, 4, 5, 6, 8, 9]} == CT.descendants(1) - assert {:ok, [2, 4]} == CT.descendants(1, depth: 1) - assert {:ok, [2, 3, 4, 5, 6]} == CT.descendants(1, depth: 2) - assert {:ok, [2, 3, 7, 4, 5, 6, 8, 9]} == CT.descendants(1, depth: 5) - assert {:ok, []} == CT.descendants(1, depth: -5) - end - end - - describe "Ancestors" do - setup do - start_supervised(CT) - start_supervised(CTEmpty) - - CT.seed() - :ok - end - - test "Retrieve ancestors of comment #6, excluding itself" do - assert {:ok, [1, 4]} == CT.ancestors(6, limit: 2) - end - - test "Retrieve ancestors of comment #6, including itself" do - assert {:ok, [1, 4, 6]} == CT.ancestors(6, itself: true) - end - - test "Retrieve ancestors of comment #6, as comments" do - assert {:ok, - [ - %{ - author: "Olie", - comment: "Is Closure Table better than the Nested Sets?", - id: 1 - }, - %{author: "Polie", comment: "Querying the data it's easier.", id: 4} - ]} == CT.ancestors(6, nodes: true) - end - - test "Retrieve ancestors of comment #6 as comments, with limit" do - assert {:ok, - [%{author: "Olie", comment: "Is Closure Table better than the Nested Sets?", id: 1}]} == - CT.ancestors(6, limit: 1, nodes: true) - end - - test "Retrieve ancestors of comment #6, including itself, with depth" do - assert {:ok, [1, 4, 6]} == CT.ancestors(6, itself: true) - assert {:ok, [4, 6]} == CT.ancestors(6, itself: true, depth: 1) - assert {:ok, [1, 4, 6]} == CT.ancestors(6, itself: true, depth: 2) - assert {:ok, [1, 4, 6]} == CT.ancestors(6, itself: true, depth: 3) - assert {:ok, [6]} == CT.ancestors(6, itself: true, depth: -3) - end - end - - describe "Tree paths operations" do - setup do - start_supervised(CT) - - CT.seed() - :ok - end - - test "insert descendant of comment #7" do - assert {:ok, - [ - [1, 281, 4], - [2, 281, 3], - [3, 281, 2], - [7, 281, 1], - [281, 281, 0] - ]} == CT.insert(281, 7) - - assert {:ok, [%{author: "Polie", comment: "Rolie is right!", id: 281}]} == - CT.descendants(7, limit: 1, nodes: true) - end - - test "delete leaf; comment #9" do - assert {:ok, [%{comment: "w⦿‿⦿t!"}]} = - CT.descendants(9, limit: 1, itself: true, nodes: true) - - assert :ok == CT.delete(9, limit: 1) - - assert {:ok, []} == CT.descendants(9, limit: 1, itself: true, nodes: true) - assert {:ok, []} == CT.descendants(9, limit: 1) - end - - test "delete subtree; comment #6 and its descendants" do - assert {:ok, [6, 8, 9]} == CT.descendants(6, itself: true) - assert :ok == CT.delete(6) - assert {:ok, []} == CT.descendants(6, itself: true) - end - - test "delete subtree w/o any leafs; comment #5 and its descendants" do - assert {:ok, [5]} == CT.descendants(5, itself: true) - assert :ok == CT.delete(5) - assert {:ok, []} == CT.descendants(5, itself: true) - end - - test "delete whole tree, from its root; comment #1" do - assert {:ok, [1, 2, 3, 7, 4, 5, 6, 8, 9]} == CT.descendants(1, itself: true) - assert :ok == CT.delete(1) - assert {:ok, []} == CT.descendants(1, itself: true) - end - - test "move subtree; comment #6, to a child of comment #3" do - assert {:ok, [1, 2]} == CT.ancestors(3) - assert {:ok, [7]} == CT.descendants(3) - - assert {:ok, [1, 4]} == CT.ancestors(6) - assert {:ok, [8, 9]} == CT.descendants(6) - - assert :ok = CT.move(6, 3) - - assert {:ok, [1, 2, 3]} == CT.ancestors(6) - assert {:ok, [1, 2, 3, 6]} == CT.ancestors(8) - assert {:ok, [1, 2, 3, 6]} == CT.ancestors(9) - - assert {:ok, [3]} == CT.ancestors(6, depth: 1) - assert {:ok, [6]} == CT.ancestors(8, depth: 1) - assert {:ok, [6]} == CT.ancestors(9, depth: 1) - - assert {:ok, [7, 6, 8, 9]} == CT.descendants(3) - end - - test "return the descendants tree of comment #4" do - assert {:ok, - %{ - nodes: %{ - 6 => %{ - author: "Rolie", - comment: "Everything is easier, than with the Nested Sets.", - id: 6 - }, - 8 => %{ - author: "Olie", - comment: "I'm sold! And I'll use its Elixir implementation! <3", - id: 8 - }, - 9 => %{author: "Polie", comment: "w⦿‿⦿t!", id: 9} - }, - paths: [ - [6, 6, 0], - [6, 8, 1], - [8, 8, 0], - [6, 9, 1], - [9, 9, 0] - ] - }} == CT.tree(6) - end - - test "return the direct descendants tree of comment #1" do - assert {:ok, - %{ - nodes: %{ - 2 => %{ - author: "Rolie", - comment: "It depends. Do you need referential integrity?", - id: 2 - }, - 3 => %{ - author: "Olie", - comment: "Yeah.", - id: 3 - } - }, - paths: [[2, 2, 0], [2, 3, 1], [3, 3, 0]] - }} = CT.tree(2, depth: 1) - end - end - - describe "Tree utils" do - setup do - start_supervised(CT) - CT.seed() - :ok - end - - test "print the tree below comment #4" do - assert {:ok, tree} = CT.tree(6) - - assert %{ - nodes: %{ - 6 => %{ - author: "Rolie", - comment: "Everything is easier, than with the Nested Sets.", - id: 6 - }, - 8 => %{ - author: "Olie", - comment: "I'm sold! And I'll use its Elixir implementation! <3", - id: 8 - }, - 9 => %{author: "Polie", comment: "w⦿‿⦿t!", id: 9} - }, - paths: [ - [6, 6, 0], - [6, 8, 1], - [8, 8, 0], - [6, 9, 1], - [9, 9, 0] - ] - } == tree - - labels = [:id, ")", " ", :author, "\n", :comment] - assert @digraph == CTE.Utils.print_dot(tree, labels: labels) - - # File.write!("polie.dot", @digraph) - # dot -Tpng polie.dot -o polie.png - # System.cmd("dot", ~w/-Tpng polie.dot -o polie.png/) - end - - test "raw tree representation, for print" do - assert {:ok, tree} = CT.tree(1) - # CTE.Utils.print_tree(tree, 1, callback: &{&1, "#{&2[&1].author}: #{&2[&1].comment}"}) - - assert [ - {0, "Olie: Is Closure Table better than the Nested Sets?"}, - {1, "Rolie: It depends. Do you need referential integrity?"}, - {2, "Olie: Yeah."}, - {3, "Rolie: Closure Table *has* referential integrity?"}, - {1, "Polie: Querying the data it's easier."}, - {2, "Olie: What about inserting nodes?"}, - {2, "Rolie: Everything is easier, than with the Nested Sets."}, - {3, "Olie: I'm sold! And I'll use its Elixir implementation! <3"}, - {3, "Polie: w⦿‿⦿t!"} - ] = - CTE.Utils.print_tree(tree, 1, - callback: &{&1, "#{&2[&1].author}: #{&2[&1].comment}"}, - raw: true - ) - - assert {:ok, tree} = CT.tree(6) - - assert [ - {0, "Rolie: Everything is easier, than with the Nested Sets."}, - {1, "Olie: I'm sold! And I'll use its Elixir implementation! <3"}, - {1, "Polie: w⦿‿⦿t!"} - ] = - CTE.Utils.print_tree(tree, 6, - callback: &{&1, "#{&2[&1].author}: #{&2[&1].comment}"}, - raw: true - ) - end - end -end diff --git a/test/cte_ecto_test.exs b/test/cte_test.exs similarity index 96% rename from test/cte_ecto_test.exs rename to test/cte_test.exs index 6fa12f9..0c7a69a 100644 --- a/test/cte_ecto_test.exs +++ b/test/cte_test.exs @@ -43,16 +43,12 @@ defmodule CTE.Ecto.Test do Comments hierarchy """ use CTE, - otp_app: :cte, - adapter: CTE.Adapter.Ecto, repo: Repo, nodes: Comment, paths: TreePath end setup_all do - start_supervised!({CH, []}) - Repo.delete_all(Comment) Repo.delete_all(Author) Repo.delete_all(TreePath) @@ -184,7 +180,7 @@ defmodule CTE.Ecto.Test do assert {:ok, [%Comment{text: "w⦿‿⦿t!"}]} = CH.descendants(9, limit: 1, itself: true, nodes: true) - assert :ok == CH.delete(9, limit: 1) + assert {:ok, %{deleted: {4, _}, updated: {0, _}}} = CH.delete(9, limit: 1) assert {:ok, []} == CH.descendants(9, limit: 1, itself: true, nodes: true) assert {:ok, []} == CH.descendants(9, limit: 1) @@ -192,19 +188,19 @@ defmodule CTE.Ecto.Test do test "delete subtree; comment #6 and its descendants" do assert {:ok, [6, 8, 9]} == CH.descendants(6, itself: true) - assert :ok == CH.delete(6, limit: 0) + assert {:ok, %{deleted: {11, _}, updated: {0, _}}} = CH.delete(6, limit: 0) assert {:ok, []} == CH.descendants(6, itself: true) end test "delete subtree w/o any leafs; comment #5 and its descendants" do assert {:ok, [5]} == CH.descendants(5, itself: true) - assert :ok == CH.delete(5) + assert {:ok, %{deleted: {3, _}, updated: {0, _}}} = CH.delete(5) assert {:ok, []} == CH.descendants(5, itself: true) end test "delete whole tree, from its root; comment #1" do assert {:ok, [1, 2, 4, 3, 5, 6, 7, 8, 9]} == CH.descendants(1, itself: true) - assert :ok == CH.delete(1, limit: 0) + assert {:ok, %{deleted: {26, _}, updated: {0, _}}} = CH.delete(1, limit: 0) assert {:ok, []} == CH.descendants(1, itself: true) end @@ -224,7 +220,7 @@ defmodule CTE.Ecto.Test do assert {:ok, [1, 4]} == CH.ancestors(6) assert {:ok, [8, 9]} == CH.descendants(6) - assert {:ok, {9, nil}} = CH.move(6, 3) + assert {:ok, %{inserted: {9, _}, deleted: {6, _}}} = CH.move(6, 3) assert {:ok, list} = CH.ancestors(6) assert MapSet.subset?(MapSet.new([1, 2, 3]), MapSet.new(list)) @@ -320,7 +316,7 @@ defmodule CTE.Ecto.Test do print_io = fn -> # limit: 1 - assert :ok == CH.delete(3) + assert {:ok, %{deleted: {4, _}, updated: {2, _}}} = CH.delete(3) {:ok, tree} = CH.tree(1) CTE.Utils.print_tree(tree, 1, callback: &{&1, "(#{&1}) #{&2[&1].text}"}) end diff --git a/test/support/in_memory.ex b/test/support/in_memory.ex deleted file mode 100644 index 1abdd1f..0000000 --- a/test/support/in_memory.ex +++ /dev/null @@ -1,72 +0,0 @@ -defmodule CTE.InMemory do - @moduledoc """ - CT implementation using the memory adapter. - - The good ol' friends Rolie, Olie and Polie, debating the usefulness of this implementation :) - You can watch them in action on: [youtube](https://www.youtube.com/watch?v=LTkmaE_QWMQ) - - After seeding the data, we'll have this graph: - (1) Is Closure Table better than the Nested Sets? - ├── (2) It depends. Do you need referential integrity? - │ └── (3) Yeah. - │ └── (7) Closure Table *has* referential integrity? - └── (4) Querying the data it's easier. - ├── (5) What about inserting nodes? - └── (6) Everything is easier, than with the Nested Sets. - ├── (8) I'm sold! And I'll use its Elixir implementation! <3 - └── (9) w⦿‿⦿t! - """ - - # %{comment_id => comment} - @comments %{ - 1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"}, - 2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"}, - 3 => %{id: 3, author: "Olie", comment: "Yeah."}, - 7 => %{id: 7, author: "Rolie", comment: "Closure Table *has* referential integrity?"}, - 4 => %{id: 4, author: "Polie", comment: "Querying the data it's easier."}, - 5 => %{id: 5, author: "Olie", comment: "What about inserting nodes?"}, - 6 => %{id: 6, author: "Rolie", comment: "Everything is easier, than with the Nested Sets."}, - 8 => %{ - id: 8, - author: "Olie", - comment: "I'm sold! And I'll use its Elixir implementation! <3" - }, - 9 => %{id: 9, author: "Polie", comment: "w⦿‿⦿t!"}, - 281 => %{author: "Polie", comment: "Rolie is right!", id: 281} - } - - # [[ancestor, descendant], [..., ...], ...] - @tree_paths [] - @insert_list [ - [1, 1], - [1, 2], - [2, 3], - [3, 7], - [1, 4], - [4, 5], - [4, 6], - [6, 8], - [6, 9] - ] - - # -1 - # --2 - # ---3 - # ----7 - # --4 - # ---5 - # ---6 - # ----8 - # ----9 - - use CTE, - otp_app: :closure_table, - adapter: CTE.Adapter.Memory, - nodes: @comments, - paths: @tree_paths - - def seed do - @insert_list - |> Enum.each(fn [ancestor, leaf] -> insert(leaf, ancestor) end) - end -end