closure_tree 5.0.0 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|