with_recursive_tree 0.3.0 → 0.4.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 +61 -20
- data/lib/with_recursive_tree/version.rb +1 -1
- data/lib/with_recursive_tree.rb +70 -8
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 135e72700b84ad5ba2129e6b2e6afe8ab4ada838d83f696085e1e0fb0c33bb86
|
|
4
|
+
data.tar.gz: bf922dd155ba9cf2cea2833fce5ebf2cec06a68043eb0b5866259363065f22df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49af47e2ede3b23bb0063ca1ee6082580c0e84b71d7974b2d5fbb5d7aacdfbb5510b49a49f2242bb6377956a73ad42435065dc95a7d11ce6f37796ca354b12c6
|
|
7
|
+
data.tar.gz: 7def7190b8c6540a1e655ddadf63be9611239e45c8ea046766a6be9dacc360a55aaf880c37daa521f72b1cdf8ad43fa95721dc129fb5ab5521094574dbd8de99
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# with_recursive_tree
|
|
2
2
|
|
|
3
|
-
Tree structures for ActiveRecord using
|
|
3
|
+
Tree structures for ActiveRecord using CTEs (Common Table Expressions). Traverse an entire tree with just one query.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ $ bundle
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
-
First, your model needs a reference to
|
|
21
|
+
First, your model needs a reference to 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
|
|
@@ -28,10 +28,10 @@ end
|
|
|
28
28
|
|
|
29
29
|
By doing this, with_recursive_tree will add 2 associations:
|
|
30
30
|
|
|
31
|
-
* `parent`: the parent
|
|
32
|
-
* `children`: the children
|
|
31
|
+
* `parent`: the node's parent
|
|
32
|
+
* `children`: the node's children
|
|
33
33
|
|
|
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
|
|
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 specify them 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
|
|
@@ -39,6 +39,27 @@ class Category < ApplicationRecord
|
|
|
39
39
|
end
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
For polymorphic associations where a node can have different types of parents, you can use the `foreign_key_type` option to specify a column that stores the parent model's class name:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class Comment < ApplicationRecord
|
|
46
|
+
with_recursive_tree foreign_key: :parent_id, foreign_key_type: :parent_type
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This allows nodes to belong to different parent model types while maintaining proper tree structure constraints.
|
|
51
|
+
|
|
52
|
+
When using `foreign_key_type`, a node is considered a root if it meets any of these conditions:
|
|
53
|
+
|
|
54
|
+
1. Both `foreign_key` and `foreign_key_type` are `nil` (traditional root)
|
|
55
|
+
2. `foreign_key` is `nil` and `foreign_key_type` matches the model's class name
|
|
56
|
+
3. `foreign_key` is not `nil` but `foreign_key_type` is different from the model's class name (belongs to a different model type)
|
|
57
|
+
|
|
58
|
+
The `foreign_key_type` value is automatically managed through a `before_save` callback:
|
|
59
|
+
|
|
60
|
+
* When setting a parent (`foreign_key`), the `foreign_key_type` is automatically set to the model's class name if it's blank
|
|
61
|
+
* When clearing a parent (`foreign_key` becomes `nil`), the `foreign_key_type` is set to `nil` unless it's explicitly set to the model's class name
|
|
62
|
+
|
|
42
63
|
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
64
|
|
|
44
65
|
```ruby
|
|
@@ -49,24 +70,24 @@ end
|
|
|
49
70
|
|
|
50
71
|
### Class methods
|
|
51
72
|
|
|
52
|
-
| Method
|
|
53
|
-
|
|
73
|
+
| Method | Description |
|
|
74
|
+
| --------- | ----------------------------------------- |
|
|
54
75
|
| `::roots` | Returns all roots (nodes without parent). |
|
|
55
76
|
|
|
56
77
|
### Instance methods
|
|
57
78
|
|
|
58
|
-
| Method
|
|
59
|
-
|
|
60
|
-
| `#ancestors`
|
|
61
|
-
| `#descendants`
|
|
62
|
-
| `#leaf?`
|
|
63
|
-
| `#depth`
|
|
64
|
-
| `#root`
|
|
65
|
-
| `#root?`
|
|
66
|
-
| `#self_and_ancestors`
|
|
67
|
-
| `#self_and_descendants` | Returns the node and all its descendants (subtree).
|
|
68
|
-
| `#self_and_siblings`
|
|
69
|
-
| `#siblings`
|
|
79
|
+
| Method | Description |
|
|
80
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
81
|
+
| `#ancestors` | Returns all ancestors of the node. |
|
|
82
|
+
| `#descendants` | Returns all descendants of the node (subtree). |
|
|
83
|
+
| `#leaf?` | Returns whether the node is a leaf (has no children). |
|
|
84
|
+
| `#depth` | Returns the depth of the current node. |
|
|
85
|
+
| `#root` | Returns the root node of the current node's tree. |
|
|
86
|
+
| `#root?` | Returns whether the node is a root (has no parent). |
|
|
87
|
+
| `#self_and_ancestors` | Returns the node and all its ancestors. |
|
|
88
|
+
| `#self_and_descendants` | Returns the node and all its descendants (subtree). |
|
|
89
|
+
| `#self_and_siblings` | Returns the current node and all its siblings. |
|
|
90
|
+
| `#siblings` | Returns the current node's siblings. |
|
|
70
91
|
|
|
71
92
|
### Tree traversing
|
|
72
93
|
|
|
@@ -74,7 +95,27 @@ You can traverse the tree using `#descendants` or `#self_and_descendants` in com
|
|
|
74
95
|
|
|
75
96
|
For example, given the following tree:
|
|
76
97
|
|
|
77
|
-
|
|
98
|
+
```mermaid
|
|
99
|
+
flowchart TD
|
|
100
|
+
A
|
|
101
|
+
A --> B
|
|
102
|
+
A --> L
|
|
103
|
+
B --> C
|
|
104
|
+
B --> H
|
|
105
|
+
C --> D
|
|
106
|
+
D --> E
|
|
107
|
+
D --> F
|
|
108
|
+
D --> G
|
|
109
|
+
H --> I
|
|
110
|
+
I --> J
|
|
111
|
+
J --> K
|
|
112
|
+
L --> M
|
|
113
|
+
L --> N
|
|
114
|
+
N --> O
|
|
115
|
+
N --> P
|
|
116
|
+
N --> Q
|
|
117
|
+
N --> R
|
|
118
|
+
```
|
|
78
119
|
|
|
79
120
|
and the following class:
|
|
80
121
|
|
data/lib/with_recursive_tree.rb
CHANGED
|
@@ -2,15 +2,18 @@ require "with_recursive_tree/version"
|
|
|
2
2
|
|
|
3
3
|
module WithRecursiveTree
|
|
4
4
|
module ClassMethods
|
|
5
|
-
def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, order: nil)
|
|
5
|
+
def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, foreign_key_type: nil, order: nil)
|
|
6
6
|
include InstanceMethods
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
scope_condition = foreign_key_type.present? ? -> { where foreign_key_type => self.class.name } : -> { self }
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
belongs_to :parent, scope_condition, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true
|
|
11
|
+
|
|
12
|
+
has_many :children, -> { scope_condition.call.order(order) }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent
|
|
11
13
|
|
|
12
14
|
define_singleton_method(:with_recursive_tree_primary_key) { primary_key }
|
|
13
15
|
define_singleton_method(:with_recursive_tree_foreign_key) { foreign_key }
|
|
16
|
+
define_singleton_method(:with_recursive_tree_foreign_key_type) { foreign_key_type }
|
|
14
17
|
define_singleton_method(:with_recursive_tree_order) { order || primary_key }
|
|
15
18
|
define_singleton_method(:with_recursive_tree_order_column) do
|
|
16
19
|
if with_recursive_tree_order.is_a?(Hash)
|
|
@@ -20,6 +23,20 @@ module WithRecursiveTree
|
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
|
|
26
|
+
if foreign_key_type.present?
|
|
27
|
+
before_save do
|
|
28
|
+
if send(:"#{foreign_key}_changed?")
|
|
29
|
+
if send(foreign_key).present?
|
|
30
|
+
# When setting a parent, the foreign_key_type is automatically set to the model's class name if it's blank
|
|
31
|
+
send(:"#{foreign_key_type}=", self.class.name) if send(foreign_key_type).blank?
|
|
32
|
+
elsif send(foreign_key).nil?
|
|
33
|
+
# When clearing parent, the foreign_key_type is to nil unless it's explicitly set to the model's class name
|
|
34
|
+
send(:"#{foreign_key_type}=", nil) unless send(foreign_key_type) == self.class.name
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
23
40
|
scope :bfs, -> {
|
|
24
41
|
if defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
|
25
42
|
order(:depth, with_recursive_tree_order)
|
|
@@ -36,7 +53,22 @@ module WithRecursiveTree
|
|
|
36
53
|
self
|
|
37
54
|
end
|
|
38
55
|
end
|
|
39
|
-
scope :roots, -> {
|
|
56
|
+
scope :roots, -> {
|
|
57
|
+
if with_recursive_tree_foreign_key_type.present?
|
|
58
|
+
# Root conditions with foreign_key_type:
|
|
59
|
+
# 1. foreign_key is nil AND foreign_key_type is nil
|
|
60
|
+
# 2. foreign_key is nil AND foreign_key_type matches the model's class name
|
|
61
|
+
# 3. foreign_key is not nil AND foreign_key_type is different from model's class name
|
|
62
|
+
where(with_recursive_tree_foreign_key => nil)
|
|
63
|
+
.where(with_recursive_tree_foreign_key_type => [nil, name])
|
|
64
|
+
.or(
|
|
65
|
+
where.not(with_recursive_tree_foreign_key => nil)
|
|
66
|
+
.where.not(with_recursive_tree_foreign_key_type => name)
|
|
67
|
+
)
|
|
68
|
+
else
|
|
69
|
+
where with_recursive_tree_foreign_key => nil
|
|
70
|
+
end
|
|
71
|
+
}
|
|
40
72
|
end
|
|
41
73
|
end
|
|
42
74
|
|
|
@@ -59,18 +91,46 @@ module WithRecursiveTree
|
|
|
59
91
|
alias_method :level, :depth
|
|
60
92
|
|
|
61
93
|
def root
|
|
62
|
-
|
|
94
|
+
return self if root?
|
|
95
|
+
|
|
96
|
+
if self.class.with_recursive_tree_foreign_key_type.present?
|
|
97
|
+
# For foreign_key_type, find the first ancestor that satisfies root conditions
|
|
98
|
+
self_and_ancestors.where(
|
|
99
|
+
self.class.with_recursive_tree_foreign_key => nil,
|
|
100
|
+
self.class.with_recursive_tree_foreign_key_type => [nil, self.class.name]
|
|
101
|
+
).or(
|
|
102
|
+
self_and_ancestors.where.not(self.class.with_recursive_tree_foreign_key => nil)
|
|
103
|
+
.where.not(self.class.with_recursive_tree_foreign_key_type => self.class.name)
|
|
104
|
+
).first
|
|
105
|
+
else
|
|
106
|
+
self_and_ancestors.find_by self.class.with_recursive_tree_foreign_key => nil
|
|
107
|
+
end
|
|
63
108
|
end
|
|
64
109
|
|
|
65
110
|
def root?
|
|
66
|
-
|
|
111
|
+
foreign_key_value = send(self.class.with_recursive_tree_foreign_key)
|
|
112
|
+
|
|
113
|
+
if self.class.with_recursive_tree_foreign_key_type.present?
|
|
114
|
+
foreign_key_type_value = send(self.class.with_recursive_tree_foreign_key_type)
|
|
115
|
+
|
|
116
|
+
# Root conditions with foreign_key_type:
|
|
117
|
+
# 1. foreign_key is nil AND foreign_key_type is nil
|
|
118
|
+
# 2. foreign_key is nil AND foreign_key_type matches the model's class name
|
|
119
|
+
# 3. foreign_key is not nil AND foreign_key_type is different from model's class name
|
|
120
|
+
(foreign_key_value.nil? && [nil, self.class.name].include?(foreign_key_type_value)) ||
|
|
121
|
+
(foreign_key_value.present? && foreign_key_type_value != self.class.name)
|
|
122
|
+
else
|
|
123
|
+
foreign_key_value.nil?
|
|
124
|
+
end
|
|
67
125
|
end
|
|
68
126
|
|
|
69
127
|
def self_and_ancestors
|
|
128
|
+
scope_condition = self.class.with_recursive_tree_foreign_key_type.present? ? {"tree.#{self.class.with_recursive_tree_foreign_key_type}" => self.class.name} : nil
|
|
129
|
+
|
|
70
130
|
self.class.with_recursive(
|
|
71
131
|
tree: [
|
|
72
132
|
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}")
|
|
133
|
+
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}").where(scope_condition)
|
|
74
134
|
]
|
|
75
135
|
).select("*").from("tree AS #{self.class.table_name}")
|
|
76
136
|
end
|
|
@@ -92,7 +152,9 @@ module WithRecursiveTree
|
|
|
92
152
|
"tree.path || #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} || '/'"
|
|
93
153
|
end
|
|
94
154
|
|
|
95
|
-
|
|
155
|
+
scope_condition = self.class.with_recursive_tree_foreign_key_type.present? ? {"#{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key_type}" => self.class.name} : nil
|
|
156
|
+
|
|
157
|
+
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").where scope_condition
|
|
96
158
|
|
|
97
159
|
unless defined?(ActiveRecord::ConnectionAdapters::MySQL)
|
|
98
160
|
recursive_query = recursive_query.order(self.class.with_recursive_tree_order)
|
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.4.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-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|