Skip to content

Commit 54526fd

Browse files
Merge pull request #15 from florinpatrascu/more_about_node_id
Updated code to support custom primary keys in CTE
2 parents ab4e7b0 + 5c52738 commit 54526fd

17 files changed

+513
-29
lines changed

.formatter.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Used by "mix format"
22
[
3+
import_deps: [:ecto, :ecto_sql],
34
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
45
locals_without_parens: [add: :*],
56
import_deps: [:ecto]

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ erl_crash.dump
2323
closure_table-*.tar
2424
.vale.ini
2525
.favorites.json
26+
.envrc

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
# CHANGELOG
22

3+
## 2.0.5
4+
5+
**Highlights**
6+
7+
- Updated code to support custom primary keys in CTE. You are no longer required to name your primary keys `id`. Although `id` remains the default, you now have the option to use your own keys and data types. Example:
8+
9+
```elixir
10+
use CTE,
11+
repo: Repo,
12+
nodes: Tag,
13+
paths: TagTreePath,
14+
options: %{
15+
node: %{primary_key: :name, type: :string},
16+
paths: %{
17+
ancestor: [type: :string],
18+
descendant: [type: :string]
19+
}
20+
}
21+
```
22+
23+
**Improvements**
24+
25+
- Added support for nodes with custom primary keys
26+
- Updated formatter configuration
27+
- Modified Ecto queries to use dynamic fields instead of hardcoded 'id'
28+
- Introduced new test cases for products and tags with custom IDs
29+
- Adjusted existing tests to accommodate changes
30+
- Updated migration files to include new tables and extensions
31+
32+
**Fixes**
33+
34+
- Finally crushed a sneaky bug that had been hiding out for way too long. The `depth` was stealthily hanging out in the descendants list and, on occasion, masquerading as one of the descendants if its value matched one of the nodes ids.
35+
336
## 2.0.0
437

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

README.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,43 @@ Warning:
1616

1717
## Quick start
1818

19-
The current implementation is depending on Ecto ~> 3.1; using [Ecto.SubQuery](https://hexdocs.pm/ecto/Ecto.SubQuery.html)!
19+
The current implementation is depending on Ecto >= 3.1; using [Ecto.SubQuery](https://hexdocs.pm/ecto/Ecto.SubQuery.html)!
2020

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

23-
1. a table containing the nodes, with `id` as the primary key. This is the default for now, but it will be configurable in the near future.
24-
2. a table name where the tree paths will be stored.
23+
1. a table containing the nodes, with `id` as the primary key. With this new version, you are no longer required to name your primary keys `id`. Although `id` remains the default, you now have the option to use your own keys and data types. Example:
24+
25+
```elixir
26+
# For more details and an example of designing and implementing simple
27+
# hierarchical structures of tags, see test/custom_node_id_test.exs (excerpt below)
28+
use CTE,
29+
repo: Repo,
30+
nodes: Tag,
31+
paths: TagTreePath,
32+
options: %{
33+
node: %{primary_key: :name, type: :string},
34+
paths: %{
35+
ancestor: [type: :string],
36+
descendant: [type: :string]
37+
}
38+
}
39+
```
40+
41+
2. a table name for storing the tree paths.
2542
3. the name of the Ecto.Repo, defined by your app
2643

44+
Using a structure like the one above and just a few functions from this library, you will be able to create highly efficient hierarchies such as these. Their management will be quick, accurate, and straightforward:
45+
46+
food
47+
├── vegetable
48+
├── fruit
49+
│ ├── apple
50+
│ ├── orange
51+
│ └── berry
52+
│ └── tomato
53+
└── meat
54+
└── burger
55+
2756
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.
2857

2958
For example, given you have the following Schemas for comments:
@@ -193,7 +222,7 @@ end
193222
## License
194223

195224
```txt
196-
Copyright 2023 Florin T.PATRASCU & the Contributors
225+
Copyright 2024 Florin T.PATRASCU & the Contributors
197226
198227
Licensed under the Apache License, Version 2.0 (the "License");
199228
you may not use this file except in compliance with the License.

lib/cte.ex

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,25 @@ defmodule CTE do
4747
nodes: nodes | nil,
4848
paths: paths | nil,
4949
repo: repo | nil,
50-
name: name | nil
50+
name: name | nil,
51+
options: map() | nil
5152
}
52-
defstruct [:nodes, :paths, :adapter, :repo, :name]
53+
defstruct [:nodes, :paths, :adapter, :repo, :name, :options]
5354

5455
defmacro __using__(opts) do
5556
quote bind_quoted: [opts: opts] do
5657
@opts %CTE{
5758
nodes: Keyword.get(opts, :nodes, []),
5859
paths: Keyword.get(opts, :paths, []),
59-
repo: Keyword.get(opts, :repo, nil)
60+
repo: Keyword.get(opts, :repo, nil),
61+
options:
62+
Keyword.get(opts, :options, %{
63+
node: %{primary_key: :id, type: :integer},
64+
paths: %{
65+
ancestor: [type: :integer],
66+
descendant: [type: :integer]
67+
}
68+
})
6069
}
6170

6271
def insert(leaf, ancestor, opts \\ [])

lib/cte/ecto.ex

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ defmodule CTE.Ecto do
5757
"""
5858

5959
def tree(leaf, opts, config) do
60-
%CTE{paths: paths, nodes: nodes, repo: repo} = config
60+
%CTE{paths: paths, nodes: nodes, repo: repo, options: options} = config
61+
%{primary_key: pk} = options.node
6162

6263
descendants_opts = [itself: true] ++ Keyword.take(opts, [:depth])
6364
descendants = _descendants(leaf, descendants_opts, config)
@@ -74,14 +75,14 @@ defmodule CTE.Ecto do
7475

7576
unique_descendants =
7677
subtree
77-
|> List.flatten()
78+
|> Enum.map(fn [_ancestor, descendant, _depth] -> descendant end)
7879
|> Enum.uniq()
7980

80-
query = from n in nodes, where: n.id in ^unique_descendants
81+
query = from n in nodes, where: field(n, ^pk) in ^unique_descendants
8182

8283
some_nodes =
8384
repo.all(query)
84-
|> Enum.reduce(%{}, fn node, acc -> Map.put(acc, node.id, node) end)
85+
|> Enum.reduce(%{}, fn node, acc -> Map.put(acc, Map.get(node, pk), node) end)
8586

8687
{:ok, %{paths: subtree, nodes: some_nodes}}
8788
end
@@ -92,14 +93,20 @@ defmodule CTE.Ecto do
9293

9394
@doc false
9495
defp _insert(leaf, ancestor, config) do
95-
%CTE{paths: paths, repo: repo} = config
96+
%CTE{paths: paths, repo: repo, options: options} = config
97+
%{descendant: [type: descendant_type]} = options.paths
9698

9799
descendants =
98100
from p in paths,
99101
where: p.descendant == ^ancestor,
100-
select: %{ancestor: p.ancestor, descendant: type(^leaf, :integer), depth: p.depth + 1}
102+
select: %{
103+
ancestor: p.ancestor,
104+
descendant: type(^leaf, ^descendant_type),
105+
depth: p.depth + 1
106+
}
101107

102108
new_records = repo.all(descendants) ++ [%{ancestor: leaf, descendant: leaf, depth: 0}]
109+
103110
descendants = Enum.map(new_records, fn r -> [r.ancestor, r.descendant] end)
104111

105112
case repo.insert_all(paths, new_records, on_conflict: :nothing) do
@@ -113,13 +120,15 @@ defmodule CTE.Ecto do
113120

114121
@doc false
115122
defp _descendants(ancestor, opts, config) do
116-
%CTE{paths: paths, nodes: nodes, repo: repo} = config
123+
%CTE{paths: paths, nodes: nodes, repo: repo, options: options} = config
124+
%{primary_key: pk} = options.node
125+
# %{descendant: [type: descendant_type]} = options.paths
117126

118127
query =
119128
from n in nodes,
120129
join: p in ^paths,
121130
as: :tree,
122-
on: n.id == p.descendant,
131+
on: field(n, ^pk) == p.descendant,
123132
where: p.ancestor == ^ancestor,
124133
order_by: [asc: p.depth]
125134

@@ -133,13 +142,14 @@ defmodule CTE.Ecto do
133142

134143
@doc false
135144
defp _ancestors(descendant, opts, config) do
136-
%CTE{paths: paths, nodes: nodes, repo: repo} = config
145+
%CTE{paths: paths, nodes: nodes, repo: repo, options: options} = config
146+
%{primary_key: pk} = options.node
137147

138148
query =
139149
from n in nodes,
140150
join: p in ^paths,
141151
as: :tree,
142-
on: n.id == p.ancestor,
152+
on: field(n, ^pk) == p.ancestor,
143153
where: p.descendant == ^descendant,
144154
order_by: [desc: p.depth]
145155

@@ -193,11 +203,14 @@ defmodule CTE.Ecto do
193203
######################################
194204
# Utils
195205
######################################
196-
defp selected(query, opts, _config) do
206+
defp selected(query, opts, config) do
207+
%CTE{options: options} = config
208+
%{primary_key: pk} = options.node
209+
197210
if Keyword.get(opts, :nodes, false) do
198211
from(n in query)
199212
else
200-
from n in query, select: n.id
213+
from n in query, select: field(n, ^pk)
201214
end
202215
end
203216

@@ -226,8 +239,8 @@ defmodule CTE.Ecto do
226239
end
227240

228241
defp prune(query, descendants, opts, _config) do
229-
if Keyword.get(opts, :depth) do
230-
from t in query, where: t.descendant in ^descendants
242+
if depth = Keyword.get(opts, :depth) do
243+
from t in query, where: t.descendant in ^descendants and t.depth <= ^max(depth, 0)
231244
else
232245
query
233246
end

lib/cte/utils.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ defmodule CTE.Utils do
8888
def print_tree(tree, id, opts \\ [])
8989

9090
def print_tree(%{paths: paths, nodes: nodes}, id, opts) do
91-
user_callback = Keyword.get(opts, :callback, fn id, _nodes -> {id, "info..."} end)
91+
user_callback =
92+
Keyword.get(opts, :callback, fn id, _nodes -> {id, "#{inspect(id)} => more..."} end)
93+
9294
direct_children = direct_children(paths)
9395

9496
callback = fn

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule CTE.MixProject do
22
use Mix.Project
33

4-
@version "2.0.0"
4+
@version "2.0.5"
55
@url_docs "https://hexdocs.pm/closure_table"
66
@url_github "https://github.com/florinpatrascu/closure_table"
77

test/cte_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule CTE.Ecto.Test do
1212
├── (8) I’m sold! And I’ll use its Elixir implementation! <3
1313
└── (9) w⦿‿⦿t!
1414
"""
15-
use CTE.DataCase
15+
use CTE.DataCase, async: false
1616
import ExUnit.CaptureIO
1717

1818
@moduletag :ecto

0 commit comments

Comments
 (0)