From c853bfd355ccea04af8e596e2737d3732eb53d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A6l=E0=B9=8F=D0=B3=E0=B9=80=E0=B8=A0?= Date: Sat, 9 Sep 2023 19:31:56 -0400 Subject: [PATCH 1/6] Version 2 This version is introducing major breaking changes. 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" .. --- .iex.exs | 24 -- CHANGELOG.md | 17 + README.md | 40 +- examples/ct_ecto/config/dev.exs | 2 +- examples/ct_ecto/config/test.exs | 2 +- examples/ct_ecto/lib/ct.ex | 10 - examples/ct_ecto/lib/ct/application.ex | 3 +- examples/ct_ecto/lib/ct/my_cte.ex | 2 - examples/ct_ecto/lib/ct/tree_path.ex | 2 + examples/ct_ecto/test/ct_test.exs | 2 +- examples/ct_ecto/test/support/data_case.ex | 1 - examples/ct_memory/.formatter.exs | 4 - examples/ct_memory/.gitignore | 24 -- examples/ct_memory/README.md | 20 - examples/ct_memory/lib/ctm.ex | 11 - examples/ct_memory/lib/ctm/application.ex | 11 - examples/ct_memory/mix.exs | 26 -- examples/ct_memory/mix.lock | 4 - examples/ct_memory/test/ctm_test.exs | 9 - examples/ct_memory/test/test_helper.exs | 1 - lib/cte.ex | 185 ++------- lib/cte/adapter.ex | 79 ---- lib/cte/adapter/ecto.ex | 459 --------------------- lib/cte/adapter/memory.ex | 276 ------------- lib/cte/ecto.ex | 396 ++++++++++++++++++ mix.exs | 4 +- test/cte_memory_ascii_print_test.exs | 56 --- test/cte_memory_test.exs | 309 -------------- test/{cte_ecto_test.exs => cte_test.exs} | 16 +- test/support/in_memory.ex | 72 ---- 30 files changed, 458 insertions(+), 1609 deletions(-) delete mode 100644 examples/ct_memory/.formatter.exs delete mode 100644 examples/ct_memory/.gitignore delete mode 100644 examples/ct_memory/README.md delete mode 100644 examples/ct_memory/lib/ctm.ex delete mode 100644 examples/ct_memory/lib/ctm/application.ex delete mode 100644 examples/ct_memory/mix.exs delete mode 100644 examples/ct_memory/mix.lock delete mode 100644 examples/ct_memory/test/ctm_test.exs delete mode 100644 examples/ct_memory/test/test_helper.exs delete mode 100644 lib/cte/adapter.ex delete mode 100644 lib/cte/adapter/ecto.ex delete mode 100644 lib/cte/adapter/memory.ex create mode 100644 lib/cte/ecto.ex delete mode 100644 test/cte_memory_ascii_print_test.exs delete mode 100644 test/cte_memory_test.exs rename test/{cte_ecto_test.exs => cte_test.exs} (97%) delete mode 100644 test/support/in_memory.ex 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/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..c38702f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ [![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 :) @@ -13,39 +11,9 @@ Throughout the various examples and tests, we will refer to the hierarchies depi ## Quick start -To start, you can simply use one `Adapter` from the ones provided, same way you'd use the Ecto's own Repo: - -```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 -``` - -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. - -Add the `CTM` module to your main supervision tree: +### TODO -```elixir -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 -``` +```` And then using `iex -S mix`, for quickly experimenting with the CTE API, let's find the descendants of comment #1: @@ -62,7 +30,7 @@ Is Closure Table better than the Nested Sets? └── It depends. Do you need referential integrity? └── Yeah. -``` +```` Please check the docs for more details and return from more updates! @@ -82,7 +50,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/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/ecto.ex b/lib/cte/ecto.ex new file mode 100644 index 0000000..b29c771 --- /dev/null +++ b/lib/cte/ecto.ex @@ -0,0 +1,396 @@ +defmodule CTE.Ecto do + @moduledoc """ + 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, + 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 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() + + 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) + + {: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} -> + # 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 + + 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, _} -> {:ok, %{deleted: deleted, updated: 0}} + e -> e + end + end +end diff --git a/mix.exs b/mix.exs index 3a61037..6668f51 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.13", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), 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 97% rename from test/cte_ecto_test.exs rename to test/cte_test.exs index 6fa12f9..7ae3b95 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 From 73e0ca35d8b1e78a2d29373fe0fba1c2f13e7d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A6l=E0=B9=8F=D0=B3=E0=B9=80=E0=B8=A0?= Date: Sat, 9 Sep 2023 20:41:38 -0400 Subject: [PATCH 2/6] cleanup remnants - support for supervision, etc --- lib/cte/application.ex | 13 ------------ lib/cte/registry.ex | 48 ------------------------------------------ lib/cte/supervisor.ex | 38 --------------------------------- mix.exs | 3 +-- mix.lock | 2 +- 5 files changed, 2 insertions(+), 102 deletions(-) delete mode 100644 lib/cte/application.ex delete mode 100644 lib/cte/registry.ex delete mode 100644 lib/cte/supervisor.ex 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/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 6668f51..0131c7d 100644 --- a/mix.exs +++ b/mix.exs @@ -37,8 +37,7 @@ 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 diff --git a/mix.lock b/mix.lock index c2bc57d..f927bd5 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "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"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.1.1", "eee6fc570d77ad6851c7bc08de420a47fd1e449ef5ccfa6a77ef68b72e7e51ad", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f82262b54dee533467021723892e15c3267349849f1f737526523ecba4e6baae"}, "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"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, From 97a5f2a7b616aa2016d52e0b8b2a47d5b03d340b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A6l=E0=B9=8F=D0=B3=E0=B9=80=E0=B8=A0?= Date: Sun, 10 Sep 2023 10:44:10 -0400 Subject: [PATCH 3/6] better returns from the core functions - we return exactly what Ecto returns from functions such as insert_all, update_all and delete_all - updated documentation --- README.md | 150 +++++++++++++++++++++++++++++++++++++++++----- lib/cte/ecto.ex | 132 +++------------------------------------- test/cte_test.exs | 12 ++-- 3 files changed, 149 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index c38702f..f9eea44 100644 --- a/README.md +++ b/README.md @@ -11,26 +11,148 @@ Throughout the various examples and tests, we will refer to the hierarchies depi ## Quick start -### TODO +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: -And then using `iex -S mix`, for quickly experimenting with the CTE API, let's find the descendants of comment #1: +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 -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})) + defmodule CT.Comment do + use Ecto.Schema + import Ecto.Changeset -Is Closure Table better than the Nested Sets? -└── It depends. Do you need referential integrity? - └── Yeah. + @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 +``` + +we can define the following module: + +```elixir + + defmodule CT.MyCTE do + use CTE, + repo: CT.Repo, + nodes: CT.Comment, + paths: CT.TreePath + end +``` -```` +We add our CTE Repo to the app's main supervision tree, like this: + +```elixir + 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 +``` + +restart your app.a-main + +Then using `iex -S mix`, we can start experimenting. Examples: + +```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], + ... + ] + }} +``` + +if you want to visualize a tree, you can do that too: + +```elixir +iex» CTE.Utils.print_tree(tree, 1, callback: &({&2[&1], &2[&1].text})) +``` + +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! diff --git a/lib/cte/ecto.ex b/lib/cte/ecto.ex index b29c771..3ea4fb1 100644 --- a/lib/cte/ecto.ex +++ b/lib/cte/ecto.ex @@ -1,122 +1,5 @@ defmodule CTE.Ecto do @moduledoc """ - 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, - 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 provided will accept the following options: - `:limit`, to limit the total number of nodes returned, when finding the ancestors or the descendants for nodes @@ -189,12 +72,12 @@ defmodule CTE.Ecto do |> prune(descendants, opts, config) |> repo.all() - authors = + unique_descendants = subtree |> List.flatten() |> Enum.uniq() - query = from n in nodes, where: n.id in ^authors + query = from n in nodes, where: n.id in ^unique_descendants some_nodes = repo.all(query) @@ -221,7 +104,6 @@ defmodule CTE.Ecto do case repo.insert_all(paths, new_records, on_conflict: :nothing) do {_nr, _r} -> - # l when l == nr <- length(new_records) do {:ok, descendants} e -> @@ -300,9 +182,9 @@ defmodule CTE.Ecto do } repo.transaction(fn -> - {deleted, _} = repo.delete_all(query_delete) + deleted = repo.delete_all(query_delete) inserts = repo.all(query_insert) - {inserted, _} = repo.insert_all(paths, inserts) + inserted = repo.insert_all(paths, inserts) %{deleted: deleted, inserted: inserted} end) @@ -372,8 +254,8 @@ defmodule CTE.Ecto do ] repo.transaction(fn -> - {deleted, _} = repo.delete_all(query_delete_leaf) - {updated, _} = repo.update_all(query_move_leafs_kids_up, []) + deleted = repo.delete_all(query_delete_leaf) + updated = repo.update_all(query_move_leafs_kids_up, []) %{deleted: deleted, updated: updated} end) @@ -389,7 +271,7 @@ defmodule CTE.Ecto do on: p.descendant == sub.descendant case repo.delete_all(query) do - {deleted, _} -> {:ok, %{deleted: deleted, updated: 0}} + {_deleted, _} = d -> {:ok, %{deleted: d, updated: {0, nil}}} e -> e end end diff --git a/test/cte_test.exs b/test/cte_test.exs index 7ae3b95..0c7a69a 100644 --- a/test/cte_test.exs +++ b/test/cte_test.exs @@ -180,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, %{deleted: 4, updated: 0}} = 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) @@ -188,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, %{deleted: 11, updated: 0}} == 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, %{deleted: 3, updated: 0}} == 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, %{deleted: 26, updated: 0}} == CH.delete(1, limit: 0) + assert {:ok, %{deleted: {26, _}, updated: {0, _}}} = CH.delete(1, limit: 0) assert {:ok, []} == CH.descendants(1, itself: true) end @@ -220,7 +220,7 @@ defmodule CTE.Ecto.Test do assert {:ok, [1, 4]} == CH.ancestors(6) assert {:ok, [8, 9]} == CH.descendants(6) - assert {:ok, %{inserted: 9, deleted: 6}} = 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)) @@ -316,7 +316,7 @@ defmodule CTE.Ecto.Test do print_io = fn -> # limit: 1 - assert {:ok, %{deleted: 4, updated: 2}} == 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 From 40e150cda35a0e2683cd84df37d0b4913798aa5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A6l=E0=B9=8F=D0=B3=E0=B9=80=E0=B8=A0?= Date: Sun, 10 Sep 2023 11:08:56 -0400 Subject: [PATCH 4/6] Update README.md fix a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9eea44..c01a6a2 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ We add our CTE Repo to the app's main supervision tree, like this: end ``` -restart your app.a-main +restart your application. Then using `iex -S mix`, we can start experimenting. Examples: From 254d03ee880ab3d34549841ab2fb802070b50fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A6l=E0=B9=8F=D0=B3=E0=B9=80=E0=B8=A0?= Date: Mon, 11 Sep 2023 17:14:52 -0400 Subject: [PATCH 5/6] use an eye_drops iteration that works on mac os ventura as well --- mix.exs | 3 ++- mix.lock | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 0131c7d..6da47be 100644 --- a/mix.exs +++ b/mix.exs @@ -58,7 +58,8 @@ defmodule CTE.MixProject do {:dialyxir, "~> 1.4.1", 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: "68ba926", only: [:dev, :test], runtime: false}, # Documentation dependencies # Run me like this: `mix docs` diff --git a/mix.lock b/mix.lock index f927bd5..3db919b 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "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", []}, + "eye_drops": {:git, "https://github.com/florinpatrascu/eye_drops.git", "68ba926d5c76db1fb652796e79b4b72170929659", [ref: "68ba926"]}, "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"}, From aafc6aa0add726f788726f1fa4483c2eba7bd996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A6l=E0=B9=8F=D0=B3=E0=B9=80=E0=B8=A0?= Date: Sun, 21 Jul 2024 12:13:20 -0400 Subject: [PATCH 6/6] HEX pre-release 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. --- .github/workflows/ci.yml | 12 ++++-------- .tool-versions | 4 ++-- README.md | 5 +++++ config/config.exs | 2 +- mix.exs | 25 +++++++++---------------- mix.lock | 37 ++++++++++++++++++------------------- 6 files changed, 39 insertions(+), 46 deletions(-) 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/.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/README.md b/README.md index c01a6a2..893ecc4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ Throughout the various examples and tests, we will refer to the hierarchies depi ![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 The current implementation is depending on Ecto ~> 3.1; using [Ecto.SubQuery](https://hexdocs.pm/ecto/Ecto.SubQuery.html)! 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/mix.exs b/mix.exs index 6da47be..1cf08e5 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule CTE.MixProject do [ app: :closure_table, version: @version, - elixir: "~> 1.13", + elixir: "~> 1.15", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), @@ -44,26 +44,25 @@ defmodule CTE.MixProject do # 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", ref: "68ba926", only: [:dev, :test], runtime: false}, + 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 3db919b..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", "68ba926d5c76db1fb652796e79b4b72170929659", [ref: "68ba926"]}, - "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.1", "eee6fc570d77ad6851c7bc08de420a47fd1e449ef5ccfa6a77ef68b72e7e51ad", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f82262b54dee533467021723892e15c3267349849f1f737526523ecba4e6baae"}, - "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"}, }