Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.0.0 #14

Merged
merged 6 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 0 additions & 24 deletions .iex.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}))
"""
])
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
erlang 25.1.2
elixir 1.14.2-otp-25
erlang 26.2.4
elixir 1.15.8-otp-26
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
167 changes: 131 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,160 @@
[![Hex.pm](https://img.shields.io/hexpm/dt/closure_table.svg?maxAge=2592000)](https://hex.pm/packages/closure_table)
[![Hexdocs.pm](https://img.shields.io/badge/api-hexdocs-brightgreen.svg)](https://hexdocs.pm/closure_table)

> this library provides two adapters: an in-memory one, and one for using the closure-table solution with Ecto; for your testing and development convenience.

The Closure Table solution is a simple and elegant way of storing hierarchies. It involves storing all paths through a tree, not just those with a direct parent-child relationship. You may want to chose this model, over the [Nested Sets model](https://en.wikipedia.org/wiki/Nested_set_model), should you need referential integrity and to assign nodes to multiple trees.

Throughout the various examples and tests, we will refer to the hierarchies depicted below, where we're modeling a hypothetical forum-like discussion between [Rolie, Olie and Polie](https://www.youtube.com/watch?v=LTkmaE_QWMQ), and their debate around the usefulness of this implementation :)

![Closure Table](assets/closure_table.png)

Warning:

> This version is introducing major breaking changes. We drop the concept of a CT Adapter and focus on using Ecto, for the core functions. The (in)memory adapter is gone.


## Quick start

To start, you can simply use one `Adapter` from the ones provided, same way you'd use the Ecto's own Repo:
The current implementation is depending on Ecto ~> 3.1; using [Ecto.SubQuery](https://hexdocs.pm/ecto/Ecto.SubQuery.html)!

For this implementation to work you'll have to provide two tables, and the name of the Repo used by your application:

1. a table name containing the nodes. Having the `id`, as a the primary key. This is the default for now - configurable in the near future
2. a table name where the tree paths will be stores.
3. the name of the Ecto.Repo, defined by your app

In a future version we will provide you with a convenient migration template to help you starting, but for now you must supply these tables.

For example, given you have the following Schemas for comments:

```elixir
defmodule CTT do
use CTE,
otp_app: :cte,
adapter: CTE.Adapter.Memory,
nodes: %{
1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"},
2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"},
3 => %{id: 3, author: "Olie", comment: "Yeah."}
},
paths: [[1, 1, 0], [1, 2, 1], [1, 3, 2], [2, 2, 0], [2, 3, 1], [3, 3, 0]]
end
defmodule CT.Comment do
use Ecto.Schema
import Ecto.Changeset

@timestamps_opts [type: :utc_datetime]

schema "comments" do
field :text, :string
belongs_to :author, CT.Author

timestamps()
end
end
```

and a table used for storing the parent-child relationships

```elixir

defmodule CT.TreePath do
use Ecto.Schema
import Ecto.Changeset
alias CT.Comment

@primary_key false

schema "tree_paths" do
belongs_to :parent_comment, Comment, foreign_key: :ancestor
belongs_to :comment, Comment, foreign_key: :descendant
field :depth, :integer, default: 0
end
end
```

With the configuration above, the `:nodes` attribute is a map containing the comments our interlocutors made; these are "nodes", in CTE's parlance. When using the `CTE.Adapter.Ecto` implementation, the `:nodes` attribute will be a Schema (or a table name! In our initial implementation, the nodes definitions must have at least the `:id`, as one of their properties. This caveat will be lifted in a later implementation. The `:paths` attribute represents the parent-child relationship between the comments.
we can define the following module:

```elixir

defmodule CT.MyCTE do
use CTE,
repo: CT.Repo,
nodes: CT.Comment,
paths: CT.TreePath
end
```

Add the `CTM` module to your main supervision tree:
We add our CTE Repo to the app's main supervision tree, like this:

```elixir
defmodule CTM.Application do
@moduledoc false
defmodule CT.Application do
use Application

def start(_type, _args) do
children = [
CT.Repo,
]

opts = [strategy: :one_for_one, name: CT.Supervisor]
Supervisor.start_link(children, opts)
end
end
```

use Application
restart your application.

def start(_type, _args) do
opts = [strategy: :one_for_one, name: CTM.Supervisor]
Then using `iex -S mix`, we can start experimenting. Examples:

Supervisor.start_link([CTM], opts)
end
end
```elixir
iex» CT.MyCTE.ancestors(9)
{:ok, [1, 4, 6]}

iex» {:ok, tree} = CT.MyCTE.tree(1)
{:ok,
%{
nodes: %{
6 => %CT.Comment{
__meta__: #Ecto.Schema.Metadata<:loaded, "comments">,
author: #Ecto.Association.NotLoaded<association :author is not loaded>,
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<association :author is not loaded>,
author_id: 1,
id: 8,
inserted_at: ~U[2019-07-21 01:10:35Z],
text: "I’m sold! And I’ll use its Elixir implementation! <3",
updated_at: ~U[2019-07-21 01:10:35Z]
},
...

},
paths: [
[1, 1, 0],
[1, 2, 1],
[2, 2, 0],
[1, 3, 2],
[2, 3, 1],
[3, 3, 0],
[1, 7, 3],
[2, 7, 2],
...
]
}}
```

And then using `iex -S mix`, for quickly experimenting with the CTE API, let's find the descendants of comment #1:
if you want to visualize a tree, you can do that too:

```elixir
iex» CTM.descendants(1)
{:ok, [3, 2]}
iex> {:ok, tree} = CTT.tree(1)
...
iex> CTE.Utils.print_tree(tree, 1)
...
iex» CTE.Utils.print_tree(tree,1, callback: &({&2[&1].author <> ":", &2[&1].comment}))
iex» CTE.Utils.print_tree(tree, 1, callback: &({&2[&1], &2[&1].text}))
```

Is Closure Table better than the Nested Sets?
└── It depends. Do you need referential integrity?
└── Yeah.
and you may see this:

```txt
Is Closure Table better than the Nested Sets?
├── It depends. Do you need referential integrity?
│ └── Yeah
│ └── Closure Table *has* referential integrity?
└── Querying the data it's easier.
├── What about inserting nodes?
└── Everything is easier, than with the Nested Sets.
├── I'm sold! And I'll use its Elixir implementation! <3
└── w⦿‿⦿t!
```

Please check the docs for more details and return from more updates!
Expand All @@ -82,7 +177,7 @@ by adding `closure_table` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:closure_table, "~> 1.1"}
{:closure_table, "~> 2.0"}
]
end
```
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Mix.Config
import Config

config :mix_test_watch,
clear: true
Expand Down
2 changes: 1 addition & 1 deletion examples/ct_ecto/config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Config

config :ct, CT.Repo,
database: "ct_ecto_dev",
database: "ct_dev",
username: "postgres",
password: "postgres",
hostname: "localhost",
Expand Down
2 changes: 1 addition & 1 deletion examples/ct_ecto/config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 0 additions & 10 deletions examples/ct_ecto/lib/ct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand All @@ -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
3 changes: 1 addition & 2 deletions examples/ct_ecto/lib/ct/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions examples/ct_ecto/lib/ct/my_cte.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/ct_ecto/lib/ct/tree_path.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule CT.TreePath do
use Ecto.Schema

import Ecto.Changeset

alias CT.Comment

@primary_key false
Expand Down
2 changes: 1 addition & 1 deletion examples/ct_ecto/test/ct_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule CTTest do
use CT.DataCase, async: false
use CT.DataCase

describe "Forum" do
setup do
Expand Down
1 change: 0 additions & 1 deletion examples/ct_ecto/test/support/data_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions examples/ct_memory/.formatter.exs

This file was deleted.

Loading
Loading