closure_tree 5.0.0 → 5.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +39 -49
- data/lib/closure_tree/numeric_deterministic_ordering.rb +7 -7
- data/lib/closure_tree/support_attributes.rb +8 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/migration_generator.rb +30 -0
- data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +16 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df0555778d166d7f54acabde46e6118f2990c6c5
|
4
|
+
data.tar.gz: 837a50c2039d612eac26a9d4bf4ec106729991b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 223f1243db513bc4754d6894f971799a0b998642841f5fb536b7023e9b4b8437ce3cca6b9e750477534b04674c46e49e00544022a2b1186f4d1cd4e691c10c9d
|
7
|
+
data.tar.gz: 5ca1595f2c52d857902d51020cc354b035272a210c5e3d6748071a86840beecc65ae996d6001ca5d4883c51175a6d30aa9b60aa34f4e429c55942676cba7ee73
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
### 5.1.0
|
4
|
+
|
5
|
+
* [Abdelkader Boudih](https://github.com/seuros) added a database generator
|
6
|
+
for the hierarchies table. Thanks!
|
7
|
+
* [Jason Weathered](https://github.com/jasoncodes) fixed [issue #117](https://github.com/mceachen/closure_tree/pull/117)
|
8
|
+
with the preordered traversal code that assumed the primary key column was called `id`. Thanks!
|
9
|
+
|
3
10
|
### 5.0.0
|
4
11
|
|
5
12
|
#### Breaking API changes
|
data/README.md
CHANGED
@@ -27,7 +27,7 @@ closure_tree has some great features:
|
|
27
27
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
28
28
|
* __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/mceachen/with_advisory_lock))
|
29
29
|
* __Support for Rails 3.2, 4.0, and 4.1__
|
30
|
-
* __Support for Ruby 1.9, 2.1, and jRuby 1.6.
|
30
|
+
* __Support for Ruby 1.9, 2.1, and jRuby 1.6.13__
|
31
31
|
* Support for reparenting children (and all their descendants)
|
32
32
|
* Support for [single-table inheritance (STI)](#sti) within the hierarchy
|
33
33
|
* ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
|
@@ -56,16 +56,24 @@ for a description of different tree storage algorithms.
|
|
56
56
|
|
57
57
|
Note that closure_tree only supports Rails 3.2 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
|
58
58
|
|
59
|
-
1. Add
|
59
|
+
1. Add `gem 'closure_tree'` to your Gemfile
|
60
60
|
|
61
|
-
2. Run
|
61
|
+
2. Run `bundle install`
|
62
62
|
|
63
|
-
3. Add
|
64
|
-
Make sure you add ```acts_as_tree``` *after any ```attr_accessible``` and ```self.table_name =```
|
65
|
-
lines in your model.
|
66
|
-
Please review the [available options](#available-options) you can provide.
|
63
|
+
3. Add `acts_as_tree` to your hierarchical model:
|
67
64
|
|
68
|
-
|
65
|
+
```ruby
|
66
|
+
class Tag < ActiveRecord::Base
|
67
|
+
acts_as_tree
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
Make sure you check out the [large number options](#available-options) that `acts_as_tree` accepts.
|
72
|
+
|
73
|
+
Make sure you add `acts_as_tree` **after** `attr_accessible` and
|
74
|
+
`self.table_name =` lines in your model.
|
75
|
+
|
76
|
+
4. Add a migration to add a `parent_id` column to the hierarchical model.
|
69
77
|
You may want to also [add a column for deterministic ordering of children](#sort_order), but that's optional.
|
70
78
|
|
71
79
|
```ruby
|
@@ -76,40 +84,22 @@ Note that closure_tree only supports Rails 3.2 and later, and has test coverage
|
|
76
84
|
end
|
77
85
|
```
|
78
86
|
|
79
|
-
|
87
|
+
The column must be nullable. Root nodes have a `NULL` `parent_id`.
|
80
88
|
|
81
|
-
5.
|
82
|
-
|
83
|
-
"_hierarchies". Note that by calling ```acts_as_tree```, a "virtual model" (in this case, ```TagHierarchy```)
|
84
|
-
will be added automatically, so you don't need to create it.
|
89
|
+
5. Run `rails g closure_tree:migration tag` (and replace `tag` with your model name)
|
90
|
+
to create the closure tree table for your model.
|
85
91
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
create_table :tag_hierarchies, :id => false do |t|
|
90
|
-
t.integer :ancestor_id, :null => false # ID of the parent/grandparent/great-grandparent/... tag
|
91
|
-
t.integer :descendant_id, :null => false # ID of the target tag
|
92
|
-
t.integer :generations, :null => false # Number of generations between the ancestor and the descendant. Parent/child = 1, for example.
|
93
|
-
end
|
94
|
-
|
95
|
-
# For "all progeny of…" and leaf selects:
|
96
|
-
add_index :tag_hierarchies, [:ancestor_id, :descendant_id, :generations],
|
97
|
-
:unique => true, :name => "tag_anc_desc_udx"
|
98
|
-
|
99
|
-
# For "all ancestors of…" selects,
|
100
|
-
add_index :tag_hierarchies, [:descendant_id],
|
101
|
-
:name => "tag_desc_idx"
|
102
|
-
end
|
103
|
-
end
|
104
|
-
```
|
92
|
+
By default the table name will be the model's table name, followed by
|
93
|
+
"_hierarchies". Note that by calling ```acts_as_tree```, a "virtual model" (in this case, ```TagHierarchy```)
|
94
|
+
will be created dynamically. You don't need to create it.
|
105
95
|
|
106
|
-
6. Run
|
96
|
+
6. Run `rake db:migrate`
|
107
97
|
|
108
98
|
7. If you're migrating from another system where your model already has a
|
109
|
-
|
110
|
-
|
99
|
+
`parent_id` column, run `Tag.rebuild!` and your
|
100
|
+
`tag_hierarchies` table will be truncated and rebuilt.
|
111
101
|
|
112
|
-
If you're starting from scratch you don't need to call
|
102
|
+
If you're starting from scratch you don't need to call `rebuild!`.
|
113
103
|
|
114
104
|
## Usage
|
115
105
|
|
@@ -118,26 +108,26 @@ Note that closure_tree only supports Rails 3.2 and later, and has test coverage
|
|
118
108
|
Create a root node:
|
119
109
|
|
120
110
|
```ruby
|
121
|
-
grandparent = Tag.create(:
|
111
|
+
grandparent = Tag.create(name: 'Grandparent')
|
122
112
|
```
|
123
113
|
|
124
114
|
Child nodes are created by appending to the children collection:
|
125
115
|
|
126
116
|
```ruby
|
127
|
-
parent = grandparent.children.create(:
|
117
|
+
parent = grandparent.children.create(name: 'Parent')
|
128
118
|
```
|
129
119
|
|
130
120
|
Or by appending to the children collection:
|
131
121
|
|
132
122
|
```ruby
|
133
|
-
child2 = Tag.new(:
|
123
|
+
child2 = Tag.new(name: 'Second Child')
|
134
124
|
parent.children << child2
|
135
125
|
```
|
136
126
|
|
137
127
|
Or by calling the "add_child" method:
|
138
128
|
|
139
129
|
```ruby
|
140
|
-
child3 = Tag.new(:
|
130
|
+
child3 = Tag.new(name: 'Third Child')
|
141
131
|
parent.add_child child3
|
142
132
|
```
|
143
133
|
|
@@ -153,22 +143,22 @@ child1.ancestry_path
|
|
153
143
|
|
154
144
|
### find_or_create_by_path
|
155
145
|
|
156
|
-
You can
|
146
|
+
You can `find` as well as `find_or_create` by "ancestry paths".
|
157
147
|
|
158
148
|
If you provide an array of strings to these methods, they reference the `name` column in your
|
159
|
-
model, which can be overridden with the `:name_column` option provided to
|
149
|
+
model, which can be overridden with the `:name_column` option provided to `acts_as_tree`.
|
160
150
|
|
161
151
|
```ruby
|
162
|
-
child = Tag.find_or_create_by_path([
|
152
|
+
child = Tag.find_or_create_by_path(%w[grandparent parent child])
|
163
153
|
```
|
164
154
|
|
165
155
|
As of v5.0.0, `find_or_create_by_path` can also take an array of attribute hashes:
|
166
156
|
|
167
157
|
```ruby
|
168
158
|
child = Tag.find_or_create_by_path([
|
169
|
-
{name:
|
170
|
-
{name:
|
171
|
-
{name:
|
159
|
+
{name: 'Grandparent', title: 'Sr.'},
|
160
|
+
{name: 'Parent', title: 'Mrs.'},
|
161
|
+
{name: 'Child', title: 'Jr.'}
|
172
162
|
])
|
173
163
|
```
|
174
164
|
|
@@ -189,8 +179,8 @@ Nodes can be moved around to other parents, and closure_tree moves the node's de
|
|
189
179
|
new parent for you:
|
190
180
|
|
191
181
|
```ruby
|
192
|
-
d = Tag.find_or_create_by_path %w
|
193
|
-
h = Tag.find_or_create_by_path %w
|
182
|
+
d = Tag.find_or_create_by_path %w[a b c d]
|
183
|
+
h = Tag.find_or_create_by_path %w[e f g h]
|
194
184
|
e = h.root
|
195
185
|
d.add_child(e) # "d.children << e" would work too, of course
|
196
186
|
h.ancestry_path
|
@@ -486,7 +476,7 @@ the spec ```tag_spec.rb```:
|
|
486
476
|
Tag.rebuild! # <- required if you use fixtures
|
487
477
|
end
|
488
478
|
```
|
489
|
-
|
479
|
+
`
|
490
480
|
**However, if you're just starting with Rails, may I humbly suggest you adopt a factory library**,
|
491
481
|
rather than using fixtures? [Lots of people have written about this already](https://www.google.com/search?q=fixtures+versus+factories).
|
492
482
|
|
@@ -38,14 +38,14 @@ module ClosureTree
|
|
38
38
|
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
39
39
|
ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
|
40
40
|
JOIN #{_ct.quoted_table_name} anc
|
41
|
-
ON anc.
|
41
|
+
ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
|
42
42
|
JOIN #{_ct.quoted_hierarchy_table_name} depths
|
43
|
-
ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.
|
43
|
+
ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.#{_ct.quoted_id_column_name}
|
44
44
|
SQL
|
45
45
|
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
|
46
46
|
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.generations)"
|
47
47
|
order_by = "sum(#{node_score})"
|
48
|
-
self_and_descendants.joins(join_sql).group("#{_ct.quoted_table_name}.
|
48
|
+
self_and_descendants.joins(join_sql).group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}").reorder(order_by)
|
49
49
|
end
|
50
50
|
|
51
51
|
module ClassMethods
|
@@ -58,19 +58,19 @@ module ClosureTree
|
|
58
58
|
SQL
|
59
59
|
join_sql = <<-SQL.strip_heredoc
|
60
60
|
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
61
|
-
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.
|
61
|
+
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
|
62
62
|
JOIN #{_ct.quoted_table_name} anc
|
63
|
-
ON anc.
|
63
|
+
ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
|
64
64
|
JOIN (
|
65
65
|
SELECT descendant_id, max(generations) AS max_depth
|
66
66
|
FROM #{_ct.quoted_hierarchy_table_name}
|
67
67
|
GROUP BY 1
|
68
|
-
) AS depths ON depths.descendant_id = anc.
|
68
|
+
) AS depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
|
69
69
|
SQL
|
70
70
|
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
|
71
71
|
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.max_depth)"
|
72
72
|
order_by = "sum(#{node_score})"
|
73
|
-
joins(join_sql).group("#{_ct.quoted_table_name}.
|
73
|
+
joins(join_sql).group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}").reorder(order_by)
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
@@ -21,6 +21,14 @@ module ClosureTree
|
|
21
21
|
options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
|
22
22
|
end
|
23
23
|
|
24
|
+
def primary_key_column
|
25
|
+
model_class.columns.detect { |ea| ea.name == model_class.primary_key }
|
26
|
+
end
|
27
|
+
|
28
|
+
def primary_key_type
|
29
|
+
primary_key_column.type
|
30
|
+
end
|
31
|
+
|
24
32
|
def parent_column_name
|
25
33
|
options[:parent_column_name]
|
26
34
|
end
|
data/lib/closure_tree/version.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rails/generators/named_base'
|
2
|
+
require 'rails/generators/active_record/migration'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module ClosureTree
|
6
|
+
module Generators # :nodoc:
|
7
|
+
class MigrationGenerator < ::Rails::Generators::NamedBase # :nodoc:
|
8
|
+
include ActiveRecord::Generators::Migration
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :ct, :hierarchy_table_name, :primary_key_type
|
12
|
+
|
13
|
+
def self.default_generator_root
|
14
|
+
File.dirname(__FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_migration_file
|
18
|
+
migration_template 'create_hierarchies_table.rb.erb', "db/migrate/create_#{singular_table_name}_hierarchies.rb"
|
19
|
+
end
|
20
|
+
|
21
|
+
def migration_class_name
|
22
|
+
"Create#{ct.hierarchy_class_name}".gsub(/\W/, '')
|
23
|
+
end
|
24
|
+
|
25
|
+
def ct
|
26
|
+
@ct ||= class_name.constantize._ct
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :<%= hierarchy_table_name %>, id: false do |t|
|
4
|
+
t.<%= primary_key_type %> :ancestor_id, null: false
|
5
|
+
t.<%= primary_key_type %> :descendant_id, null: false
|
6
|
+
t.integer :generations, null: false
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :<%= hierarchy_table_name %>, [:ancestor_id, :descendant_id, :generations],
|
10
|
+
unique: true,
|
11
|
+
name: "anc_desc_idx"
|
12
|
+
|
13
|
+
add_index :<%= hierarchy_table_name -%>, [:descendant_id],
|
14
|
+
name: "desc_idx"
|
15
|
+
end
|
16
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew McEachen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -203,6 +203,8 @@ files:
|
|
203
203
|
- lib/closure_tree/support_flags.rb
|
204
204
|
- lib/closure_tree/test/matcher.rb
|
205
205
|
- lib/closure_tree/version.rb
|
206
|
+
- lib/generators/closure_tree/migration_generator.rb
|
207
|
+
- lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb
|
206
208
|
- mktree.rb
|
207
209
|
- spec/cache_invalidation_spec.rb
|
208
210
|
- spec/cuisine_type_spec.rb
|