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 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: []