ancestry 4.3.3 → 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 +161 -65
- data/README.md +56 -40
- data/lib/ancestry/class_methods.rb +105 -69
- data/lib/ancestry/exceptions.rb +3 -1
- data/lib/ancestry/has_ancestry.rb +82 -49
- data/lib/ancestry/instance_methods.rb +129 -87
- data/lib/ancestry/locales/en.yml +0 -1
- data/lib/ancestry/materialized_path.rb +117 -54
- data/lib/ancestry/materialized_path2.rb +66 -21
- data/lib/ancestry/materialized_path_pg.rb +32 -13
- data/lib/ancestry/version.rb +3 -1
- data/lib/ancestry.rb +3 -1
- metadata +18 -5
data/README.md
CHANGED
|
@@ -37,7 +37,7 @@ materialized path, closure tree table, adjacency lists, nested sets, and adjacen
|
|
|
37
37
|
- Integrity restoration
|
|
38
38
|
- Most queries use indexes on `id` or `ancestry` column. (e.g.: `LIKE '#{ancestry}/%'`)
|
|
39
39
|
|
|
40
|
-
Since a Btree index has a
|
|
40
|
+
Since a Btree index has a limitation of 2704 characters for the `ancestry` column,
|
|
41
41
|
the maximum depth of an ancestry tree is 900 items at most. If ids are 4 digits long,
|
|
42
42
|
then the max depth is 540 items.
|
|
43
43
|
|
|
@@ -46,8 +46,10 @@ When using `STI` all classes are returned from the scopes unless you specify oth
|
|
|
46
46
|
## Supported Rails versions
|
|
47
47
|
|
|
48
48
|
- Ancestry 2.x supports Rails 4.1 and earlier
|
|
49
|
-
- Ancestry 3.x supports Rails
|
|
50
|
-
- Ancestry 4.x
|
|
49
|
+
- Ancestry 3.x supports Rails 4.2 and 5.0
|
|
50
|
+
- Ancestry 4.x supports Rails 5.2 through 7.0
|
|
51
|
+
- Ancestry 5.0 supports Rails 6.0 and higher
|
|
52
|
+
Rails 5.2 with `update_strategy=ruby` is still being tested in 5.0.
|
|
51
53
|
|
|
52
54
|
# Installation
|
|
53
55
|
|
|
@@ -75,7 +77,7 @@ $ rails g migration add_[ancestry]_to_[table] ancestry:string:index
|
|
|
75
77
|
class AddAncestryToTable < ActiveRecord::Migration[6.1]
|
|
76
78
|
def change
|
|
77
79
|
change_table(:table) do |t|
|
|
78
|
-
#
|
|
80
|
+
# postgres
|
|
79
81
|
t.string "ancestry", collation: 'C', null: false
|
|
80
82
|
t.index "ancestry"
|
|
81
83
|
# mysql
|
|
@@ -86,7 +88,7 @@ class AddAncestryToTable < ActiveRecord::Migration[6.1]
|
|
|
86
88
|
end
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
There are additional options for the columns in [Ancestry Database
|
|
91
|
+
There are additional options for the columns in [Ancestry Database Column](#ancestry-database-column) and
|
|
90
92
|
an explanation for `opclass` and `collation`.
|
|
91
93
|
|
|
92
94
|
```bash
|
|
@@ -146,10 +148,10 @@ The yellow nodes are those returned by the method.
|
|
|
146
148
|
| `child_of?` |`descendant_of?` |`indirect_of?` |
|
|
147
149
|
|**siblings** |**subtree** |**path** |
|
|
148
150
|
| | | |
|
|
149
|
-
|
|
|
151
|
+
| excludes self |self..indirects |root..self |
|
|
150
152
|
|`sibling_ids` |`subtree_ids` |`path_ids` |
|
|
151
153
|
|`has_siblings?` | | |
|
|
152
|
-
|`sibling_of?(node)` |
|
|
154
|
+
|`sibling_of?(node)` |`in_subtree_of?` | |
|
|
153
155
|
|
|
154
156
|
When using `STI` all classes are returned from the scopes unless you specify otherwise using `where(:type => "ChildClass")`.
|
|
155
157
|
|
|
@@ -162,34 +164,35 @@ The `has_ancestry` method supports the following options:
|
|
|
162
164
|
:ancestry_column Column name to store ancestry
|
|
163
165
|
'ancestry' (default)
|
|
164
166
|
:ancestry_format Format for ancestry column (see Ancestry Formats section):
|
|
165
|
-
:materialized_path
|
|
166
|
-
:materialized_path2
|
|
167
|
-
:orphan_strategy
|
|
167
|
+
:materialized_path 1/2/3, root nodes ancestry=nil (default)
|
|
168
|
+
:materialized_path2 /1/2/3/, root nodes ancestry=/ (preferred)
|
|
169
|
+
:orphan_strategy How to handle children of a destroyed node:
|
|
168
170
|
:destroy All children are destroyed as well (default)
|
|
169
171
|
:rootify The children of the destroyed node become root nodes
|
|
170
172
|
:restrict An AncestryException is raised if any children exist
|
|
171
173
|
:adopt The orphan subtree is added to the parent of the deleted node
|
|
172
174
|
If the deleted node is Root, then rootify the orphan subtree
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
:primary_key_format
|
|
179
|
-
'[0-9]+'
|
|
180
|
-
'[-A-Fa-f0-9]{36}'
|
|
181
|
-
:touch
|
|
182
|
-
false
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
175
|
+
:none skip this logic. (add your own `before_destroy`)
|
|
176
|
+
:cache_depth Cache the depth of each node: (See Depth Cache section)
|
|
177
|
+
false Do not cache depth (default)
|
|
178
|
+
true Cache depth in 'ancestry_depth'
|
|
179
|
+
String Cache depth in the column referenced
|
|
180
|
+
:primary_key_format Regular expression that matches the format of the primary key:
|
|
181
|
+
'[0-9]+' integer ids (default)
|
|
182
|
+
'[-A-Fa-f0-9]{36}' UUIDs
|
|
183
|
+
:touch Touch the ancestors of a node when it changes:
|
|
184
|
+
false don't invalid nested key-based caches (default)
|
|
185
|
+
true touch all ancestors of previous and new parents
|
|
186
|
+
:counter_cache Create counter cache column accessor:
|
|
187
|
+
false don't store a counter cache (default)
|
|
188
|
+
true store counter cache in `children_count`.
|
|
189
|
+
String name of column to store counter cache.
|
|
190
|
+
:update_strategy How to update descendants nodes:
|
|
191
|
+
:ruby All descendants are updated using the ruby algorithm. (default)
|
|
192
|
+
This triggers update callbacks for each descendant node
|
|
193
|
+
:sql All descendants are updated using a single SQL statement.
|
|
194
|
+
This strategy does not trigger update callbacks for the descendants.
|
|
195
|
+
This strategy is available only for PostgreSql implementations
|
|
193
196
|
|
|
194
197
|
Legacy configuration using `acts_as_tree` is still available. Ancestry defers to `acts_as_tree` if that gem is installed.
|
|
195
198
|
|
|
@@ -302,10 +305,10 @@ Sorry, using collation or index operator classes makes this a little complicated
|
|
|
302
305
|
root of the issue is that in order to use indexes, the ancestry column needs to
|
|
303
306
|
compare strings using ascii rules.
|
|
304
307
|
|
|
305
|
-
It is well known that `LIKE '/1/2/%'` will use an index because the
|
|
308
|
+
It is well known that `LIKE '/1/2/%'` will use an index because the wildcard (i.e.: `%`)
|
|
306
309
|
is on the right hand side of the `LIKE`. While that is true for ascii strings, it is not
|
|
307
310
|
necessarily true for unicode. Since ancestry only uses ascii characters, telling the database
|
|
308
|
-
this constraint will optimize the `LIKE`
|
|
311
|
+
this constraint will optimize the `LIKE` statements.
|
|
309
312
|
|
|
310
313
|
## Collation Sorting
|
|
311
314
|
|
|
@@ -326,7 +329,7 @@ remember to drop existing indexes on the `ancestry` column and recreate them.
|
|
|
326
329
|
## ancestry_format materialized_path and nulls
|
|
327
330
|
|
|
328
331
|
If you are using the legacy `ancestry_format` of `:materialized_path`, then you need to the
|
|
329
|
-
|
|
332
|
+
column to allow `nulls`. Change the column create accordingly: `null: true`.
|
|
330
333
|
|
|
331
334
|
Chances are, you can ignore this section as you most likely want to use `:materialized_path2`.
|
|
332
335
|
|
|
@@ -370,11 +373,11 @@ You may be able to alter the database to gain some readability:
|
|
|
370
373
|
ALTER DATABASE dbname SET bytea_output to 'escape';
|
|
371
374
|
```
|
|
372
375
|
|
|
373
|
-
##
|
|
376
|
+
## MySQL Storage options
|
|
374
377
|
|
|
375
378
|
### ascii field collation
|
|
376
379
|
|
|
377
|
-
The currently suggested way to create a
|
|
380
|
+
The currently suggested way to create a MySQL field is using `'utf8mb4_bin'` collation:
|
|
378
381
|
|
|
379
382
|
```ruby
|
|
380
383
|
t.string "ancestry", collation: 'utf8mb4_bin', null: false
|
|
@@ -399,7 +402,7 @@ t.index "ancestry"
|
|
|
399
402
|
|
|
400
403
|
### ascii character set
|
|
401
404
|
|
|
402
|
-
|
|
405
|
+
MySQL supports per column character sets. Using a character set of `ascii` will
|
|
403
406
|
set this up.
|
|
404
407
|
|
|
405
408
|
```SQL
|
|
@@ -422,7 +425,7 @@ You can choose from 2 ancestry formats:
|
|
|
422
425
|
```
|
|
423
426
|
|
|
424
427
|
If you are unsure, choose `:materialized_path2`. It allows a not NULL column,
|
|
425
|
-
faster
|
|
428
|
+
faster descendant queries, has one less `OR` statement in the queries, and
|
|
426
429
|
the path can be formed easily in a database query for added benefits.
|
|
427
430
|
|
|
428
431
|
There is more discussion in [Internals](#internals) or [Migrating ancestry format](#migrate-ancestry-format)
|
|
@@ -459,7 +462,7 @@ Model.check_ancestry_integrity!
|
|
|
459
462
|
```
|
|
460
463
|
|
|
461
464
|
It is time to run your code. Most tree methods should work fine with ancestry
|
|
462
|
-
and hopefully your tests only require a few minor tweaks to get up and
|
|
465
|
+
and hopefully your tests only require a few minor tweaks to get up and running.
|
|
463
466
|
|
|
464
467
|
Once you are happy with how your app is running, remove the old `parent_id` column:
|
|
465
468
|
|
|
@@ -488,7 +491,7 @@ To add depth_caching to an existing model:
|
|
|
488
491
|
## Add column
|
|
489
492
|
|
|
490
493
|
```ruby
|
|
491
|
-
class
|
|
494
|
+
class AddDepthCacheToTable < ActiveRecord::Migration[6.1]
|
|
492
495
|
def change
|
|
493
496
|
change_table(:table) do |t|
|
|
494
497
|
t.integer "ancestry_depth", default: 0
|
|
@@ -503,7 +506,7 @@ end
|
|
|
503
506
|
# app/models/[model.rb]
|
|
504
507
|
|
|
505
508
|
class [Model] < ActiveRecord::Base
|
|
506
|
-
has_ancestry
|
|
509
|
+
has_ancestry cache_depth: true
|
|
507
510
|
end
|
|
508
511
|
```
|
|
509
512
|
|
|
@@ -516,6 +519,19 @@ Some use migrations, but that can make the migration suite fragile. The command
|
|
|
516
519
|
Model.rebuild_depth_cache!
|
|
517
520
|
```
|
|
518
521
|
|
|
522
|
+
## Depth Constraints
|
|
523
|
+
|
|
524
|
+
You can use standard Rails validations on your depth cache column to restrict the depth of your tree. For example, to ensure no nodes are deeper than 2:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
class TreeNode < ActiveRecord::Base
|
|
528
|
+
has_ancestry cache_depth: true
|
|
529
|
+
validates :ancestry_depth, numericality: { less_than_or_equal_to: 2 }
|
|
530
|
+
end
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Ancestry will automatically validate not just the node itself, but also ensure that moving a subtree does not cause any of its descendants to exceed the maximum depth.
|
|
534
|
+
|
|
519
535
|
# Running Tests
|
|
520
536
|
|
|
521
537
|
```bash
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ancestry
|
|
2
4
|
module ClassMethods
|
|
3
5
|
# Fetch tree node if necessary
|
|
4
|
-
def to_node
|
|
5
|
-
if object.is_a?(
|
|
6
|
+
def to_node(object)
|
|
7
|
+
if object.is_a?(ancestry_base_class)
|
|
6
8
|
object
|
|
7
9
|
else
|
|
8
10
|
unscoped_where { |scope| scope.find(object.try(primary_key) || object) }
|
|
@@ -10,55 +12,57 @@ module Ancestry
|
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
# Scope on relative depth options
|
|
13
|
-
def scope_depth
|
|
14
|
-
depth_options.inject(
|
|
15
|
+
def scope_depth(depth_options, depth)
|
|
16
|
+
depth_options.inject(ancestry_base_class) do |scope, option|
|
|
15
17
|
scope_name, relative_depth = option
|
|
16
18
|
if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
|
|
17
19
|
scope.send scope_name, depth + relative_depth
|
|
18
20
|
else
|
|
19
|
-
raise Ancestry::AncestryException
|
|
21
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.unknown_depth_option", scope_name: scope_name)
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
# Orphan strategy writer
|
|
25
|
-
def orphan_strategy= orphan_strategy
|
|
26
|
-
# Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed
|
|
27
|
-
if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
|
|
28
|
-
class_variable_set :@@orphan_strategy, orphan_strategy
|
|
29
|
-
else
|
|
30
|
-
raise Ancestry::AncestryException.new(I18n.t("ancestry.invalid_orphan_strategy"))
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
|
|
35
26
|
# these methods arrange an entire subtree into nested hashes for easy navigation after database retrieval
|
|
36
27
|
# the arrange method also works on a scoped class
|
|
37
28
|
# the arrange method takes ActiveRecord find options
|
|
38
29
|
# To order your hashes pass the order to the arrange method instead of to the scope
|
|
39
30
|
|
|
40
31
|
# Get all nodes and sort them into an empty hash
|
|
41
|
-
def arrange
|
|
32
|
+
def arrange(options = {})
|
|
42
33
|
if (order = options.delete(:order))
|
|
43
|
-
arrange_nodes
|
|
34
|
+
arrange_nodes(ancestry_base_class.order(order).where(options))
|
|
44
35
|
else
|
|
45
|
-
arrange_nodes
|
|
36
|
+
arrange_nodes(ancestry_base_class.where(options))
|
|
46
37
|
end
|
|
47
38
|
end
|
|
48
39
|
|
|
49
40
|
# arranges array of nodes to a hierarchical hash
|
|
50
41
|
#
|
|
51
42
|
# @param nodes [Array[Node]] nodes to be arranged
|
|
43
|
+
# @param orphan_strategy [Symbol] :rootify or :destroy (default: :rootify)
|
|
52
44
|
# @returns Hash{Node => {Node => {}, Node => {}}}
|
|
53
45
|
# If a node's parent is not included, the node will be included as if it is a top level node
|
|
54
|
-
def arrange_nodes(nodes)
|
|
46
|
+
def arrange_nodes(nodes, orphan_strategy: :rootify)
|
|
55
47
|
node_ids = Set.new(nodes.map(&:id))
|
|
56
48
|
index = Hash.new { |h, k| h[k] = {} }
|
|
57
49
|
|
|
58
50
|
nodes.each_with_object({}) do |node, arranged|
|
|
59
|
-
children = index[node.id]
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
index[node.parent_id][node] = children = index[node.id]
|
|
52
|
+
if node.parent_id.nil?
|
|
53
|
+
arranged[node] = children
|
|
54
|
+
elsif !node_ids.include?(node.parent_id)
|
|
55
|
+
case orphan_strategy
|
|
56
|
+
when :destroy
|
|
57
|
+
# All children are destroyed as well (default)
|
|
58
|
+
when :adopt
|
|
59
|
+
raise ArgumentError, "Not Implemented"
|
|
60
|
+
when :rootify
|
|
61
|
+
arranged[node] = children
|
|
62
|
+
when :restrict
|
|
63
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.cannot_delete_descendants")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
62
66
|
end
|
|
63
67
|
end
|
|
64
68
|
|
|
@@ -74,10 +78,10 @@ module Ancestry
|
|
|
74
78
|
nodes
|
|
75
79
|
end
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def arrange_serializable
|
|
81
|
+
# Arrangement to nested array for serialization
|
|
82
|
+
# You can also supply your own serialization logic using blocks
|
|
83
|
+
# also allows you to pass the order just as you can pass it to the arrange method
|
|
84
|
+
def arrange_serializable(options = {}, nodes = nil, &block)
|
|
81
85
|
nodes = arrange(options) if nodes.nil?
|
|
82
86
|
nodes.map do |parent, children|
|
|
83
87
|
if block_given?
|
|
@@ -89,13 +93,13 @@ module Ancestry
|
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
def tree_view(column, data = nil)
|
|
92
|
-
data
|
|
96
|
+
data ||= arrange
|
|
93
97
|
data.each do |parent, children|
|
|
94
98
|
if parent.depth == 0
|
|
95
99
|
puts parent[column]
|
|
96
100
|
else
|
|
97
101
|
num = parent.depth - 1
|
|
98
|
-
indent = " "*num
|
|
102
|
+
indent = " " * num
|
|
99
103
|
puts " #{"|" if parent.depth > 1}#{indent}|_ #{parent[column]}"
|
|
100
104
|
end
|
|
101
105
|
tree_view(column, children) if children
|
|
@@ -105,12 +109,16 @@ module Ancestry
|
|
|
105
109
|
# Pseudo-preordered array of nodes. Children will always follow parents,
|
|
106
110
|
# This is deterministic unless the parents are missing *and* a sort block is specified
|
|
107
111
|
def sort_by_ancestry(nodes, &block)
|
|
112
|
+
_sort_by_ancestry(nodes, ancestry_column, &block)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def _sort_by_ancestry(nodes, column, &block)
|
|
108
116
|
arranged = nodes if nodes.is_a?(Hash)
|
|
109
117
|
|
|
110
118
|
unless arranged
|
|
111
119
|
presorted_nodes = nodes.sort do |a, b|
|
|
112
|
-
rank = (a.public_send(
|
|
113
|
-
rank =
|
|
120
|
+
rank = (a.public_send(column) || ' ') <=> (b.public_send(column) || ' ')
|
|
121
|
+
rank = block.call(a, b) if rank == 0 && block
|
|
114
122
|
rank
|
|
115
123
|
end
|
|
116
124
|
|
|
@@ -124,48 +132,47 @@ module Ancestry
|
|
|
124
132
|
# compromised tree integrity is unlikely without explicitly setting cyclic parents or invalid ancestry and circumventing validation
|
|
125
133
|
# just in case, raise an AncestryIntegrityException if issues are detected
|
|
126
134
|
# specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages
|
|
127
|
-
def check_ancestry_integrity!
|
|
135
|
+
def check_ancestry_integrity!(options = {})
|
|
136
|
+
_check_ancestry_integrity!(ancestry_column, options)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def _check_ancestry_integrity!(column, options = {})
|
|
128
140
|
parents = {}
|
|
129
141
|
exceptions = [] if options[:report] == :list
|
|
130
142
|
|
|
131
143
|
unscoped_where do |scope|
|
|
132
144
|
# For each node ...
|
|
133
145
|
scope.find_each do |node|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
:node_id => node.id,
|
|
147
|
-
:ancestor_id => ancestor_id
|
|
148
|
-
))
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
# ... check that all node parents are consistent with values observed earlier
|
|
152
|
-
node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
|
|
153
|
-
parents[node_id] = parent_id unless parents.has_key? node_id
|
|
154
|
-
unless parents[node_id] == parent_id
|
|
155
|
-
raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.conflicting_parent_id",
|
|
156
|
-
:node_id => node_id,
|
|
157
|
-
:parent_id => parent_id || 'nil',
|
|
158
|
-
:expected => parents[node_id] || 'nil'
|
|
159
|
-
))
|
|
160
|
-
end
|
|
146
|
+
# ... check validity of ancestry column
|
|
147
|
+
if !node.sane_ancestor_ids?
|
|
148
|
+
raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.invalid_ancestry_column",
|
|
149
|
+
:node_id => node.id,
|
|
150
|
+
:ancestry_column => node.read_attribute(column))
|
|
151
|
+
end
|
|
152
|
+
# ... check that all ancestors exist
|
|
153
|
+
node.ancestor_ids.each do |ancestor_id|
|
|
154
|
+
unless exists?(ancestor_id)
|
|
155
|
+
raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.reference_nonexistent_node",
|
|
156
|
+
:node_id => node.id,
|
|
157
|
+
:ancestor_id => ancestor_id)
|
|
161
158
|
end
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
end
|
|
160
|
+
# ... check that all node parents are consistent with values observed earlier
|
|
161
|
+
node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
|
|
162
|
+
parents[node_id] = parent_id unless parents.key?(node_id)
|
|
163
|
+
unless parents[node_id] == parent_id
|
|
164
|
+
raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.conflicting_parent_id",
|
|
165
|
+
:node_id => node_id,
|
|
166
|
+
:parent_id => parent_id || 'nil',
|
|
167
|
+
:expected => parents[node_id] || 'nil')
|
|
167
168
|
end
|
|
168
169
|
end
|
|
170
|
+
rescue Ancestry::AncestryIntegrityException => e
|
|
171
|
+
case options[:report]
|
|
172
|
+
when :list then exceptions << e
|
|
173
|
+
when :echo then puts e
|
|
174
|
+
else raise e
|
|
175
|
+
end
|
|
169
176
|
end
|
|
170
177
|
end
|
|
171
178
|
exceptions if options[:report] == :list
|
|
@@ -175,7 +182,7 @@ module Ancestry
|
|
|
175
182
|
def restore_ancestry_integrity!
|
|
176
183
|
parent_ids = {}
|
|
177
184
|
# Wrap the whole thing in a transaction ...
|
|
178
|
-
|
|
185
|
+
ancestry_base_class.transaction do
|
|
179
186
|
unscoped_where do |scope|
|
|
180
187
|
# For each node ...
|
|
181
188
|
scope.find_each do |node|
|
|
@@ -212,7 +219,7 @@ module Ancestry
|
|
|
212
219
|
end
|
|
213
220
|
|
|
214
221
|
# Build ancestry from parent ids for migration purposes
|
|
215
|
-
def build_ancestry_from_parent_ids!
|
|
222
|
+
def build_ancestry_from_parent_ids!(column = :parent_id, parent_id = nil, ancestor_ids = [])
|
|
216
223
|
unscoped_where do |scope|
|
|
217
224
|
scope.where(column => parent_id).find_each do |node|
|
|
218
225
|
node.without_ancestry_callbacks do
|
|
@@ -225,9 +232,9 @@ module Ancestry
|
|
|
225
232
|
|
|
226
233
|
# Rebuild depth cache if it got corrupted or if depth caching was just turned on
|
|
227
234
|
def rebuild_depth_cache!
|
|
228
|
-
raise
|
|
235
|
+
raise(Ancestry::AncestryException, I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to?(:depth_cache_column)
|
|
229
236
|
|
|
230
|
-
|
|
237
|
+
ancestry_base_class.transaction do
|
|
231
238
|
unscoped_where do |scope|
|
|
232
239
|
scope.find_each do |node|
|
|
233
240
|
node.update_attribute depth_cache_column, node.depth
|
|
@@ -236,8 +243,37 @@ module Ancestry
|
|
|
236
243
|
end
|
|
237
244
|
end
|
|
238
245
|
|
|
246
|
+
# NOTE: this is temporarily kept separate from rebuild_depth_cache!
|
|
247
|
+
# this will become the implementation of rebuild_depth_cache!
|
|
248
|
+
def rebuild_depth_cache_sql!
|
|
249
|
+
update_all("#{depth_cache_column} = #{ancestry_depth_sql}")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def rebuild_counter_cache!
|
|
253
|
+
if %w(mysql mysql2).include?(connection.adapter_name.downcase)
|
|
254
|
+
connection.execute %{
|
|
255
|
+
UPDATE #{table_name} AS dest
|
|
256
|
+
LEFT JOIN (
|
|
257
|
+
SELECT #{table_name}.#{primary_key}, COUNT(*) AS child_count
|
|
258
|
+
FROM #{table_name}
|
|
259
|
+
JOIN #{table_name} children ON children.#{ancestry_column} = (#{child_ancestry_sql})
|
|
260
|
+
GROUP BY #{table_name}.#{primary_key}
|
|
261
|
+
) src USING(#{primary_key})
|
|
262
|
+
SET dest.#{counter_cache_column} = COALESCE(src.child_count, 0)
|
|
263
|
+
}
|
|
264
|
+
else
|
|
265
|
+
update_all %{
|
|
266
|
+
#{counter_cache_column} = (
|
|
267
|
+
SELECT COUNT(*)
|
|
268
|
+
FROM #{table_name} children
|
|
269
|
+
WHERE children.#{ancestry_column} = (#{child_ancestry_sql})
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
239
275
|
def unscoped_where
|
|
240
|
-
yield
|
|
276
|
+
yield ancestry_base_class.default_scoped.unscope(:where)
|
|
241
277
|
end
|
|
242
278
|
|
|
243
279
|
ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
|