with_recursive_tree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ ![sample tree](/assets/tree.png)
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,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ module WithRecursiveTree
2
+ VERSION = "0.1.0"
3
+ end
@@ -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: []