ancestry 4.3.3 → 5.0.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 +59 -1
- data/README.md +43 -40
- data/lib/ancestry/class_methods.rb +96 -68
- data/lib/ancestry/exceptions.rb +3 -1
- data/lib/ancestry/has_ancestry.rb +74 -48
- data/lib/ancestry/instance_methods.rb +103 -87
- data/lib/ancestry/locales/en.yml +0 -1
- data/lib/ancestry/materialized_path.rb +79 -38
- data/lib/ancestry/materialized_path2.rb +36 -15
- data/lib/ancestry/materialized_path_pg.rb +26 -11
- data/lib/ancestry/version.rb +3 -1
- data/lib/ancestry.rb +3 -1
- metadata +18 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84b6f22caf8e4b43a3807fe036cdb098c8e141c0cd75efefb6c3fb5994155557
|
|
4
|
+
data.tar.gz: d12e54734dca578ca1860dc6ad90f385e55741049dfee37f32f52daaf2fea419
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d001a3bfe2418e202534ff6b143a74f22382da63a7cdc61cc68da8330fa7b9b8adadc267bcc96dc2aad30d698f46b74ebcf6c31de839a53a62da14fa5fd8e4b
|
|
7
|
+
data.tar.gz: 4db5a4ebc441a9f2c267eb1854c51e4015ae60bc6f4fca168b3e97264e184e1b93b7cf002ce508b259ef17649c078644a34223586147eab3504e8d0d736b3c87
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,60 @@
|
|
|
3
3
|
Doing our best at supporting [SemVer](http://semver.org/) with
|
|
4
4
|
a nice looking [Changelog](http://keepachangelog.com).
|
|
5
5
|
|
|
6
|
+
## Version [5.0.0] <sub><sup>2026-02-08</sub></sup>
|
|
7
|
+
|
|
8
|
+
* Fix: `siblings` now excludes self [#710](https://github.com/stefankroes/ancestry/pull/710) (thx @chikamichi)
|
|
9
|
+
* Introduce `orphan_strategy: :none` [#658](https://github.com/stefankroes/ancestry/pull/658)
|
|
10
|
+
* Introduce `rebuild_counter_cache!` to reset counter caches. [#663](https://github.com/stefankroes/ancestry/pull/663) [#668](https://github.com/stefankroes/ancestry/pull/668) (thx @RongRongTeng)
|
|
11
|
+
* Introduce `in_subtree_of?` instance method [#680](https://github.com/stefankroes/ancestry/pull/680) (thx @instrumentl)
|
|
12
|
+
* Optimize `has_siblings?` to use `exists?` [#693](https://github.com/stefankroes/ancestry/pull/693) (thx @a5-stable)
|
|
13
|
+
* Fix: humanise model name in error messages [#700](https://github.com/stefankroes/ancestry/pull/700) (thx @labeebklatif)
|
|
14
|
+
* Fix: touch with sql update strategy
|
|
15
|
+
* Introduce `update_strategy: :sql` hooks for extension developers
|
|
16
|
+
* Added support for virtual depth column
|
|
17
|
+
* Documentation fixes [#664](https://github.com/stefankroes/ancestry/pull/664) [#667](https://github.com/stefankroes/ancestry/pull/667) (thx @motokikando, @onerinas)
|
|
18
|
+
* Introduce `build_cache_depth_sql!`, a sql alternative to `build_cache_depth` [#654](https://github.com/stefankroes/ancestry/pull/654)
|
|
19
|
+
* Drop `ancestry_primary_key_format` [#649](https://github.com/stefankroes/ancestry/pull/649)
|
|
20
|
+
* When destroying orphans, going from leafs up to node [#635](https://github.com/stefankroes/ancestry/pull/635) (thx @brendon)
|
|
21
|
+
* Changed config setters to class readers [#633](https://github.com/stefankroes/ancestry/pull/633) (thx @kshurov)
|
|
22
|
+
* Split apply_orphan_strategy into multiple methods [#632](https://github.com/stefankroes/ancestry/pull/633) [#633](https://github.com/stefankroes/ancestry/pull/617)
|
|
23
|
+
* Ruby 3.4 support
|
|
24
|
+
* Rails 8.0 support
|
|
25
|
+
|
|
26
|
+
#### Notable features
|
|
27
|
+
|
|
28
|
+
Depth scopes now work without `cache_depth`. But please only use this in background
|
|
29
|
+
jobs. If you need to do this in the ui, please use `cache_depth`.
|
|
30
|
+
|
|
31
|
+
`build_cache_depth_sql!` is quicker than `build_cache_depth!` (1 query instead of N+1 queries).
|
|
32
|
+
|
|
33
|
+
#### Deprecations
|
|
34
|
+
|
|
35
|
+
- Option `:depth_cache_column` is going away.
|
|
36
|
+
Please use a single key instead: `cache_depth: :depth_cach_column_name`.
|
|
37
|
+
`cache_depth: true` still defaults to `ancestry_depth`.
|
|
38
|
+
|
|
39
|
+
#### Breaking Changes
|
|
40
|
+
|
|
41
|
+
* `siblings` no longer returns self. This is a bug fix, but does change behavior.
|
|
42
|
+
* Dropped support for Rails < 6.1
|
|
43
|
+
* Renamed internal methods to follow Rails conventions: `*_before_save` methods renamed to `*_before_last_save`
|
|
44
|
+
(e.g., `child_ancestry_before_save` => `child_ancestry_before_last_save`)
|
|
45
|
+
* Options are no longer set via class methods. Using `has_ancestry` is now the only way to enable these features.
|
|
46
|
+
These are all not part of the public API.
|
|
47
|
+
* These are now class level read only accessors
|
|
48
|
+
- `ancestry_base_class` (introduced 1.1, removed by #633)
|
|
49
|
+
- `ancestry_column` (introduced 1.2, removed by #633)
|
|
50
|
+
- `ancestry_delimiter` (introduced 4.3.0, removed by #633)
|
|
51
|
+
- `depth_cache_column` (introduced 4.3.0, removed by #654)
|
|
52
|
+
* These no longer have any accessors
|
|
53
|
+
- `ancestry_format` (introduced 4.3.0, removed by #654)
|
|
54
|
+
- `orphan_strategy` (introduced 1.1, removed by #617)
|
|
55
|
+
- `ancestry_primary_key_format` (introduced 4.3.0, removed by #649)
|
|
56
|
+
- `touch_ancestors` (introduced 2.1, removed by TODO)
|
|
57
|
+
* These are seen as internal and may go away:
|
|
58
|
+
- `apply_orphan_strategy` Please use `orphan_strategy: :none` and a custom `before_destory` instead.
|
|
59
|
+
|
|
6
60
|
## Version [4.3.3] <sub><sup>2023-04-01</sub></sup>
|
|
7
61
|
|
|
8
62
|
* Fix: sort_by_ancesty with custom ancestry_column [#656](https://github.com/stefankroes/ancestry/pull/656) (thx @mitsuru)
|
|
@@ -312,7 +366,11 @@ Missed 2 commits (which are feature adds)
|
|
|
312
366
|
* Validations
|
|
313
367
|
|
|
314
368
|
|
|
315
|
-
[HEAD]: https://github.com/stefankroes/ancestry/compare/
|
|
369
|
+
[HEAD]: https://github.com/stefankroes/ancestry/compare/v5.0.0...HEAD
|
|
370
|
+
[5.0.0]: https://github.com/stefankroes/ancestry/compare/v4.3.3...v5.0.0
|
|
371
|
+
[4.3.3]: https://github.com/stefankroes/ancestry/compare/v4.3.2...v4.3.3
|
|
372
|
+
[4.3.2]: https://github.com/stefankroes/ancestry/compare/v4.3.1...v4.3.2
|
|
373
|
+
[4.3.1]: https://github.com/stefankroes/ancestry/compare/v4.3.0...v4.3.1
|
|
316
374
|
[4.3.0]: https://github.com/stefankroes/ancestry/compare/v4.2.0...v4.3.0
|
|
317
375
|
[4.2.0]: https://github.com/stefankroes/ancestry/compare/v4.1.0...v4.2.0
|
|
318
376
|
[4.1.0]: https://github.com/stefankroes/ancestry/compare/v4.0.0...v4.1.0
|
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
|
|
|
@@ -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
|
|
@@ -104,7 +108,7 @@ module Ancestry
|
|
|
104
108
|
|
|
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
|
-
def sort_by_ancestry(nodes
|
|
111
|
+
def sort_by_ancestry(nodes)
|
|
108
112
|
arranged = nodes if nodes.is_a?(Hash)
|
|
109
113
|
|
|
110
114
|
unless arranged
|
|
@@ -124,48 +128,43 @@ module Ancestry
|
|
|
124
128
|
# compromised tree integrity is unlikely without explicitly setting cyclic parents or invalid ancestry and circumventing validation
|
|
125
129
|
# just in case, raise an AncestryIntegrityException if issues are detected
|
|
126
130
|
# specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages
|
|
127
|
-
def check_ancestry_integrity!
|
|
131
|
+
def check_ancestry_integrity!(options = {})
|
|
128
132
|
parents = {}
|
|
129
133
|
exceptions = [] if options[:report] == :list
|
|
130
134
|
|
|
131
135
|
unscoped_where do |scope|
|
|
132
136
|
# For each node ...
|
|
133
137
|
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
|
|
138
|
+
# ... check validity of ancestry column
|
|
139
|
+
if !node.sane_ancestor_ids?
|
|
140
|
+
raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.invalid_ancestry_column",
|
|
141
|
+
:node_id => node.id,
|
|
142
|
+
:ancestry_column => node.read_attribute(node.class.ancestry_column))
|
|
143
|
+
end
|
|
144
|
+
# ... check that all ancestors exist
|
|
145
|
+
node.ancestor_ids.each do |ancestor_id|
|
|
146
|
+
unless exists?(ancestor_id)
|
|
147
|
+
raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.reference_nonexistent_node",
|
|
148
|
+
:node_id => node.id,
|
|
149
|
+
:ancestor_id => ancestor_id)
|
|
161
150
|
end
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
end
|
|
152
|
+
# ... check that all node parents are consistent with values observed earlier
|
|
153
|
+
node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
|
|
154
|
+
parents[node_id] = parent_id unless parents.key?(node_id)
|
|
155
|
+
unless parents[node_id] == parent_id
|
|
156
|
+
raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.conflicting_parent_id",
|
|
157
|
+
:node_id => node_id,
|
|
158
|
+
:parent_id => parent_id || 'nil',
|
|
159
|
+
:expected => parents[node_id] || 'nil')
|
|
167
160
|
end
|
|
168
161
|
end
|
|
162
|
+
rescue Ancestry::AncestryIntegrityException => e
|
|
163
|
+
case options[:report]
|
|
164
|
+
when :list then exceptions << e
|
|
165
|
+
when :echo then puts e
|
|
166
|
+
else raise e
|
|
167
|
+
end
|
|
169
168
|
end
|
|
170
169
|
end
|
|
171
170
|
exceptions if options[:report] == :list
|
|
@@ -175,7 +174,7 @@ module Ancestry
|
|
|
175
174
|
def restore_ancestry_integrity!
|
|
176
175
|
parent_ids = {}
|
|
177
176
|
# Wrap the whole thing in a transaction ...
|
|
178
|
-
|
|
177
|
+
ancestry_base_class.transaction do
|
|
179
178
|
unscoped_where do |scope|
|
|
180
179
|
# For each node ...
|
|
181
180
|
scope.find_each do |node|
|
|
@@ -212,7 +211,7 @@ module Ancestry
|
|
|
212
211
|
end
|
|
213
212
|
|
|
214
213
|
# Build ancestry from parent ids for migration purposes
|
|
215
|
-
def build_ancestry_from_parent_ids!
|
|
214
|
+
def build_ancestry_from_parent_ids!(column = :parent_id, parent_id = nil, ancestor_ids = [])
|
|
216
215
|
unscoped_where do |scope|
|
|
217
216
|
scope.where(column => parent_id).find_each do |node|
|
|
218
217
|
node.without_ancestry_callbacks do
|
|
@@ -225,9 +224,9 @@ module Ancestry
|
|
|
225
224
|
|
|
226
225
|
# Rebuild depth cache if it got corrupted or if depth caching was just turned on
|
|
227
226
|
def rebuild_depth_cache!
|
|
228
|
-
raise
|
|
227
|
+
raise(Ancestry::AncestryException, I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to?(:depth_cache_column)
|
|
229
228
|
|
|
230
|
-
|
|
229
|
+
ancestry_base_class.transaction do
|
|
231
230
|
unscoped_where do |scope|
|
|
232
231
|
scope.find_each do |node|
|
|
233
232
|
node.update_attribute depth_cache_column, node.depth
|
|
@@ -236,8 +235,37 @@ module Ancestry
|
|
|
236
235
|
end
|
|
237
236
|
end
|
|
238
237
|
|
|
238
|
+
# NOTE: this is temporarily kept separate from rebuild_depth_cache!
|
|
239
|
+
# this will become the implementation of rebuild_depth_cache!
|
|
240
|
+
def rebuild_depth_cache_sql!
|
|
241
|
+
update_all("#{depth_cache_column} = #{ancestry_depth_sql}")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def rebuild_counter_cache!
|
|
245
|
+
if %w(mysql mysql2).include?(connection.adapter_name.downcase)
|
|
246
|
+
connection.execute %{
|
|
247
|
+
UPDATE #{table_name} AS dest
|
|
248
|
+
LEFT JOIN (
|
|
249
|
+
SELECT #{table_name}.#{primary_key}, COUNT(*) AS child_count
|
|
250
|
+
FROM #{table_name}
|
|
251
|
+
JOIN #{table_name} children ON children.#{ancestry_column} = (#{child_ancestry_sql})
|
|
252
|
+
GROUP BY #{table_name}.#{primary_key}
|
|
253
|
+
) src USING(#{primary_key})
|
|
254
|
+
SET dest.#{counter_cache_column} = COALESCE(src.child_count, 0)
|
|
255
|
+
}
|
|
256
|
+
else
|
|
257
|
+
update_all %{
|
|
258
|
+
#{counter_cache_column} = (
|
|
259
|
+
SELECT COUNT(*)
|
|
260
|
+
FROM #{table_name} children
|
|
261
|
+
WHERE children.#{ancestry_column} = (#{child_ancestry_sql})
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
239
267
|
def unscoped_where
|
|
240
|
-
yield
|
|
268
|
+
yield ancestry_base_class.default_scoped.unscope(:where)
|
|
241
269
|
end
|
|
242
270
|
|
|
243
271
|
ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
|