with_recursive_tree 0.2.0 → 0.3.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 +4 -4
- data/README.md +3 -3
- data/lib/with_recursive_tree/backport.rb +1 -1
- data/lib/with_recursive_tree/version.rb +1 -1
- data/lib/with_recursive_tree.rb +88 -90
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 108a3060c431b14be0274538083b1251da56c3c5404ff96628007c849a86b94c
|
4
|
+
data.tar.gz: 6d3619942217a168e4eb8bfc04533b5aa78edd6e0bf428eac9cd628007a47582
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be39718928d3296a0dcbd9e2d149a6d8c7dcb3fece985620b433a4e45f5f83a4bdcc3e87f8b6840b2b6194d6428b476ba260a49c874754454622bc88811d4b06
|
7
|
+
data.tar.gz: 7e66c836eced475a35d398a4bceb47cd1ca441178b823b5be7c82d3a47534dd9e8ff1b9fd3842e72c1c288233c4a2c40b0703c7bdaad2e75e0273d551268ebfb
|
data/README.md
CHANGED
@@ -18,7 +18,7 @@ $ bundle
|
|
18
18
|
|
19
19
|
## Usage
|
20
20
|
|
21
|
-
First, your model needs a reference to the its parent.
|
21
|
+
First, your model needs a reference to the its parent. Typically, this is a `parent_id` column in your table. Once you have that reference, you can add `with_recursive_tree` to your model:
|
22
22
|
|
23
23
|
```ruby
|
24
24
|
class Category < ApplicationRecord
|
@@ -31,7 +31,7 @@ By doing this, with_recursive_tree will add 2 associations:
|
|
31
31
|
* `parent`: the parent of the node
|
32
32
|
* `children`: the children of this node
|
33
33
|
|
34
|
-
To build these associations, with_recursive_tree will use the `id` and the `parent_id` columns as the primary and
|
34
|
+
To build these associations, with_recursive_tree will use the `id` and the `parent_id` columns as the primary and foreign 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
35
|
|
36
36
|
```ruby
|
37
37
|
class Category < ApplicationRecord
|
@@ -156,7 +156,7 @@ with_recursive_tree is compatible with:
|
|
156
156
|
|
157
157
|
## Benchmarks
|
158
158
|
|
159
|
-
You can run some [benchmarks](/benchmarks/benchmark.rb) to compare with_recursive_tree
|
159
|
+
You can run some [benchmarks](/benchmarks/benchmark.rb) to compare with_recursive_tree against [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).
|
160
160
|
|
161
161
|
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..
|
162
162
|
|
data/lib/with_recursive_tree.rb
CHANGED
@@ -1,31 +1,10 @@
|
|
1
|
-
require "active_support/concern"
|
2
|
-
|
3
1
|
require "with_recursive_tree/version"
|
4
2
|
|
5
3
|
module WithRecursiveTree
|
6
|
-
|
7
|
-
|
8
|
-
included do
|
9
|
-
scope :bfs, -> {
|
10
|
-
if defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
11
|
-
order :depth, with_recursive_tree_order
|
12
|
-
else
|
13
|
-
order :depth
|
14
|
-
end
|
15
|
-
}
|
16
|
-
scope :dfs, -> do
|
17
|
-
if defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
18
|
-
order with_recursive_tree_order, :path
|
19
|
-
elsif defined?(ActiveRecord::ConnectionAdapters::PostgreSQL)
|
20
|
-
order :path
|
21
|
-
elsif defined?(ActiveRecord::ConnectionAdapters::SQLite3)
|
22
|
-
self
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
class_methods do
|
4
|
+
module ClassMethods
|
28
5
|
def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, order: nil)
|
6
|
+
include InstanceMethods
|
7
|
+
|
29
8
|
belongs_to :parent, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true
|
30
9
|
|
31
10
|
has_many :children, -> { order order }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent
|
@@ -33,92 +12,111 @@ module WithRecursiveTree
|
|
33
12
|
define_singleton_method(:with_recursive_tree_primary_key) { primary_key }
|
34
13
|
define_singleton_method(:with_recursive_tree_foreign_key) { foreign_key }
|
35
14
|
define_singleton_method(:with_recursive_tree_order) { order || primary_key }
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
15
|
+
define_singleton_method(:with_recursive_tree_order_column) do
|
16
|
+
if with_recursive_tree_order.is_a?(Hash)
|
17
|
+
with_recursive_tree_order.keys.first
|
18
|
+
else
|
19
|
+
with_recursive_tree_order.to_s.split(" ").first
|
20
|
+
end
|
21
|
+
end
|
41
22
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
23
|
+
scope :bfs, -> {
|
24
|
+
if defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
25
|
+
order(:depth, with_recursive_tree_order)
|
26
|
+
else
|
27
|
+
order(:depth)
|
28
|
+
end
|
29
|
+
}
|
30
|
+
scope :dfs, -> do
|
31
|
+
if defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
32
|
+
order(with_recursive_tree_order, :path)
|
33
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::PostgreSQL)
|
34
|
+
order(:path)
|
35
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::SQLite3)
|
36
|
+
self
|
37
|
+
end
|
47
38
|
end
|
39
|
+
scope :roots, -> { where with_recursive_tree_foreign_key => nil }
|
48
40
|
end
|
49
41
|
end
|
50
42
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def descendants
|
56
|
-
self_and_descendants.excluding self
|
57
|
-
end
|
58
|
-
|
59
|
-
def leaf?
|
60
|
-
children.none?
|
61
|
-
end
|
43
|
+
module InstanceMethods
|
44
|
+
def ancestors
|
45
|
+
self_and_ancestors.excluding self
|
46
|
+
end
|
62
47
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
alias_method :level, :depth
|
48
|
+
def descendants
|
49
|
+
self_and_descendants.excluding self
|
50
|
+
end
|
67
51
|
|
68
|
-
|
69
|
-
|
70
|
-
|
52
|
+
def leaf?
|
53
|
+
children.none?
|
54
|
+
end
|
71
55
|
|
72
|
-
|
73
|
-
|
74
|
-
|
56
|
+
def depth
|
57
|
+
attributes["depth"] || ancestors.count
|
58
|
+
end
|
59
|
+
alias_method :level, :depth
|
75
60
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)),
|
80
|
-
self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} = tree.#{self.class.with_recursive_tree_foreign_key}")
|
81
|
-
]
|
82
|
-
).select("*").from("tree AS #{self.class.table_name}")
|
83
|
-
end
|
61
|
+
def root
|
62
|
+
self_and_ancestors.find_by self.class.with_recursive_tree_foreign_key => nil
|
63
|
+
end
|
84
64
|
|
85
|
-
|
86
|
-
|
87
|
-
"ARRAY[#{self.class.with_recursive_tree_order_column}]::text[]"
|
88
|
-
elsif defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
89
|
-
"CAST(CONCAT('/', #{self.class.with_recursive_tree_primary_key}, '/') AS CHAR(512))"
|
90
|
-
elsif defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
|
91
|
-
"'/' || #{self.class.with_recursive_tree_primary_key} || '/'"
|
65
|
+
def root?
|
66
|
+
parent.blank?
|
92
67
|
end
|
93
68
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
69
|
+
def self_and_ancestors
|
70
|
+
self.class.with_recursive(
|
71
|
+
tree: [
|
72
|
+
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)),
|
73
|
+
self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} = tree.#{self.class.with_recursive_tree_foreign_key}")
|
74
|
+
]
|
75
|
+
).select("*").from("tree AS #{self.class.table_name}")
|
100
76
|
end
|
101
77
|
|
102
|
-
|
78
|
+
def self_and_descendants
|
79
|
+
anchor_path = if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
80
|
+
"ARRAY[#{self.class.with_recursive_tree_order_column}]::text[]"
|
81
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
82
|
+
"CAST(CONCAT('/', #{self.class.with_recursive_tree_primary_key}, '/') AS CHAR(512))"
|
83
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
|
84
|
+
"'/' || #{self.class.with_recursive_tree_primary_key} || '/'"
|
85
|
+
end
|
86
|
+
|
87
|
+
recursive_path = if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
88
|
+
"tree.path || #{self.class.table_name}.#{self.class.with_recursive_tree_order_column}::text"
|
89
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
90
|
+
"CONCAT(tree.path, #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key}, '/')"
|
91
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::SQLite3)
|
92
|
+
"tree.path || #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} || '/'"
|
93
|
+
end
|
103
94
|
|
104
|
-
|
105
|
-
|
95
|
+
recursive_query = self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key} = tree.#{self.class.with_recursive_tree_primary_key}").select("#{self.class.table_name}.*, #{recursive_path} AS path, depth + 1 AS depth")
|
96
|
+
|
97
|
+
unless defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
98
|
+
recursive_query = recursive_query.order(self.class.with_recursive_tree_order)
|
99
|
+
end
|
100
|
+
|
101
|
+
self.class.with_recursive(
|
102
|
+
tree: [
|
103
|
+
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)).select("*, #{anchor_path} AS path, 0 AS depth"),
|
104
|
+
Arel.sql(recursive_query.to_sql)
|
105
|
+
]
|
106
|
+
).select("*").from("tree AS #{self.class.table_name}")
|
106
107
|
end
|
107
108
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
Arel.sql(recursive_query.to_sql)
|
112
|
-
]
|
113
|
-
).select("*").from("tree AS #{self.class.table_name}")
|
114
|
-
end
|
109
|
+
def self_and_siblings
|
110
|
+
root? ? self.class.roots : parent.children
|
111
|
+
end
|
115
112
|
|
116
|
-
|
117
|
-
|
113
|
+
def siblings
|
114
|
+
self_and_siblings.excluding self
|
115
|
+
end
|
118
116
|
end
|
119
117
|
|
120
|
-
def
|
121
|
-
|
118
|
+
def self.included(mod)
|
119
|
+
mod.extend ClassMethods
|
122
120
|
end
|
123
121
|
end
|
124
122
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: with_recursive_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Patricio Mac Adden
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -77,7 +77,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
77
|
requirements:
|
78
78
|
- - ">="
|
79
79
|
- !ruby/object:Gem::Version
|
80
|
-
version: 3.
|
80
|
+
version: '3.0'
|
81
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
82
|
requirements:
|
83
83
|
- - ">="
|