with_recursive_tree 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +161 -0
- data/Rakefile +3 -0
- data/lib/with_recursive_tree/version.rb +3 -0
- data/lib/with_recursive_tree.rb +83 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c1ccdf38306a7ddff768b00b73f3d3ff58bf094f7524e55b63e0502f4d757dd
|
4
|
+
data.tar.gz: 5572ccbc0023f7d7226b41f24ffa8513084dc2e8780075f48904dac3aa510a96
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0a9003bb5a943e7a4d48abed8db293a292dd21dfae943ab1c4899673920fa1c943b5f949d4caf9351c763b3f62e0a14bbfca0ff3e9dfa8fe7daa8e9f4ea6462e
|
7
|
+
data.tar.gz: 378da103d4c78855cc5c2acef7557e19a27eb2c3f11a4c63d26fa1e29dadff551d433d84060b0f7ef5bd2b711808d97b5c071e80e8f1ff5b61801ddca7a8bd38
|
data/README.md
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# with_recursive_tree
|
2
|
+
|
3
|
+
Tree structures for ActiveRecord using CTE (Common Table Expressions). Traverse the whole tree with just one query.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "with_recursive_tree"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
First, your model needs a reference to the its parent. Tipically, this is a `parent_id` column in your table. Once you have that reference, you can add `with_recursive_tree` to your model:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Category < ApplicationRecord
|
25
|
+
with_recursive_tree
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
By doing this, with_recursive_tree will add 2 associations:
|
30
|
+
|
31
|
+
* `parent`: the parent of the node
|
32
|
+
* `children`: the children of this node
|
33
|
+
|
34
|
+
To build these associations, with_recursive_tree will use the `id` and the `parent_id` columns as the primary and foreing keys, respectively. If you want to specify different primary and foreign keys, you can do that by passing the `primary_key` and `foreign_key` options. For example, for a categories table whose primary key is `category_id` and the parent record id is `parent_category_id`, you would set it up as follows:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class Category < ApplicationRecord
|
38
|
+
with_recursive_tree foreign_key: :parent_category_id, primary_key: :category_id
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Lastly, you can specify how to sort each node's `children` by passing the `order` option to `with_recursive_tree`. If no `order` option is set, it will default to `id`. This option is useful especially when you need to traverse the tree in a specific order. For example:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class Category < ApplicationRecord
|
46
|
+
with_recursive_tree order: :name
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### Class methods
|
51
|
+
|
52
|
+
| Method | Description |
|
53
|
+
|--------|-------------|
|
54
|
+
| `::roots` | Returns all roots (nodes without parent). |
|
55
|
+
|
56
|
+
### Instance methods
|
57
|
+
|
58
|
+
| Method | Description |
|
59
|
+
|--------|-------------|
|
60
|
+
| `#ancestors` | Returns all ancestors of the node. |
|
61
|
+
| `#descendants` | Returns all descendants of the node (subtree). |
|
62
|
+
| `#leaf?` | Returns whether the node is a leaf (has no children). |
|
63
|
+
| `#depth` | Returns the depth of the current node. |
|
64
|
+
| `#root` | Returns the root node of the current node's tree. |
|
65
|
+
| `#root?` | Returns whether the node is a root (has no parent). |
|
66
|
+
| `#self_and_ancestors` | Returns the node and all its ancestors. |
|
67
|
+
| `#self_and_descendants` | Returns the node and all its descendants (subtree). |
|
68
|
+
| `#self_and_siblings` | Returns the current node and all its siblings. |
|
69
|
+
| `#siblings` | Returns the current node's siblings. |
|
70
|
+
|
71
|
+
### Tree traversing
|
72
|
+
|
73
|
+
You can traverse the tree using `#descendants` or `#self_and_descendants` in combination with the `#bfs` (breadth-first search) and `#dfs` (depth-first search, pre-order) scopes.
|
74
|
+
|
75
|
+
For example, given the following tree:
|
76
|
+
|
77
|
+

|
78
|
+
|
79
|
+
and the following class:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class Node < ApplicationRecord
|
83
|
+
with_recursive_tree order: :name
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
You can do:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
root = Node.roots.first
|
91
|
+
|
92
|
+
puts root.self_and_descendants.bfs.map { |node| "#{"-" * node.depth}#{node.name}" }
|
93
|
+
```
|
94
|
+
|
95
|
+
and you will get:
|
96
|
+
|
97
|
+
```
|
98
|
+
A
|
99
|
+
-B
|
100
|
+
-L
|
101
|
+
--C
|
102
|
+
--H
|
103
|
+
--M
|
104
|
+
--N
|
105
|
+
---D
|
106
|
+
---I
|
107
|
+
---O
|
108
|
+
---P
|
109
|
+
---Q
|
110
|
+
---R
|
111
|
+
----E
|
112
|
+
----F
|
113
|
+
----G
|
114
|
+
----J
|
115
|
+
-----K
|
116
|
+
```
|
117
|
+
|
118
|
+
Similarly, you can do the same with `#dfs`:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
puts root.self_and_descendants.dfs.map { |node| "#{"-" * node.depth}#{node.name}" }
|
122
|
+
```
|
123
|
+
|
124
|
+
and you will get:
|
125
|
+
|
126
|
+
```
|
127
|
+
A
|
128
|
+
-B
|
129
|
+
--C
|
130
|
+
---D
|
131
|
+
----E
|
132
|
+
----F
|
133
|
+
----G
|
134
|
+
--H
|
135
|
+
---I
|
136
|
+
----J
|
137
|
+
-----K
|
138
|
+
-L
|
139
|
+
--M
|
140
|
+
--N
|
141
|
+
---O
|
142
|
+
---P
|
143
|
+
---Q
|
144
|
+
---R
|
145
|
+
```
|
146
|
+
|
147
|
+
## Benchmarks
|
148
|
+
|
149
|
+
You can run some [benchmarks](/benchmarks/benchmark.rb) to compare with_recursive_tree agains [acts_as_tree](https://github.com/amerine/acts_as_tree), [ancestry](https://github.com/stefankroes/ancestry/) and [closure_tree](https://github.com/ClosureTree/closure_tree).
|
150
|
+
|
151
|
+
Spoiler: benchmarks are always basic cases so you mustn't trust them as if they were the word of god, but they are useful tools for development/testing and setting a baseline performance requirement..
|
152
|
+
|
153
|
+
In any case, you must weight the trade-offs between what you need to accomplish and performance.
|
154
|
+
|
155
|
+
## Contributing
|
156
|
+
|
157
|
+
Fork the repo, add your feature, create a PR.
|
158
|
+
|
159
|
+
## License
|
160
|
+
|
161
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
require "with_recursive_tree/version"
|
4
|
+
|
5
|
+
module WithRecursiveTree
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
scope :bfs, -> { order :depth }
|
10
|
+
scope :dfs, -> { self }
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, order: nil)
|
15
|
+
belongs_to :parent, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true
|
16
|
+
|
17
|
+
has_many :children, -> { order order }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent
|
18
|
+
|
19
|
+
define_singleton_method(:with_recursive_tree_primary_key) { primary_key }
|
20
|
+
define_singleton_method(:with_recursive_tree_foreign_key) { foreign_key }
|
21
|
+
define_singleton_method(:with_recursive_tree_order) { order || primary_key }
|
22
|
+
end
|
23
|
+
|
24
|
+
def roots
|
25
|
+
where with_recursive_tree_foreign_key => nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def ancestors
|
30
|
+
self_and_ancestors.excluding self
|
31
|
+
end
|
32
|
+
|
33
|
+
def descendants
|
34
|
+
self_and_descendants.excluding self
|
35
|
+
end
|
36
|
+
|
37
|
+
def leaf?
|
38
|
+
children.none?
|
39
|
+
end
|
40
|
+
|
41
|
+
def depth
|
42
|
+
attributes["depth"] || ancestors.count
|
43
|
+
end
|
44
|
+
alias_method :level, :depth
|
45
|
+
|
46
|
+
def root
|
47
|
+
self_and_ancestors.find_by self.class.with_recursive_tree_foreign_key => nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def root?
|
51
|
+
parent.blank?
|
52
|
+
end
|
53
|
+
|
54
|
+
def self_and_ancestors
|
55
|
+
self.class.with(search_tree: self.class.with_recursive(
|
56
|
+
search_tree: [
|
57
|
+
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)),
|
58
|
+
self.class.joins("JOIN search_tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} = search_tree.#{self.class.with_recursive_tree_foreign_key}")
|
59
|
+
]
|
60
|
+
).select("*").from("search_tree")).from("search_tree AS #{self.class.table_name}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def self_and_descendants
|
64
|
+
self.class.with(search_tree: self.class.with_recursive(
|
65
|
+
search_tree: [
|
66
|
+
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)).select("*, '/' || #{self.class.with_recursive_tree_primary_key} || '/' AS path, 0 AS depth"),
|
67
|
+
Arel.sql(self.class.joins("JOIN search_tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key} = search_tree.#{self.class.with_recursive_tree_primary_key}").select("#{self.class.table_name}.*, search_tree.path || #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} || '/' AS path, depth + 1 AS depth").order(self.class.with_recursive_tree_order).to_sql)
|
68
|
+
]
|
69
|
+
).select("*").from("search_tree")).from("search_tree AS #{self.class.table_name}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def self_and_siblings
|
73
|
+
root? ? self.class.roots : parent.children
|
74
|
+
end
|
75
|
+
|
76
|
+
def siblings
|
77
|
+
self_and_siblings.excluding self
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ActiveSupport.on_load :active_record do
|
82
|
+
include WithRecursiveTree
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: with_recursive_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patricio Mac Adden
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '7.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '7.2'
|
55
|
+
description: Tree structures for ActiveRecord
|
56
|
+
email:
|
57
|
+
- patriciomacadden@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- lib/with_recursive_tree.rb
|
65
|
+
- lib/with_recursive_tree/version.rb
|
66
|
+
homepage: https://github.com/sinaptia/with_recursive_tree
|
67
|
+
licenses: []
|
68
|
+
metadata:
|
69
|
+
homepage_uri: https://github.com/sinaptia/with_recursive_tree
|
70
|
+
source_code_uri: https://github.com/sinaptia/with_recursive_tree
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.5.11
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Tree structures for ActiveRecord
|
90
|
+
test_files: []
|