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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89ffc3427a7df0f6b3adb3e3bb7311d6b8b31bc3be654672033d5fd8c2a6d57f
4
- data.tar.gz: b4de2f5af9343af5fd29d21130e7e2a0d1abf062388a3e7e7c40683f1f965563
3
+ metadata.gz: 84b6f22caf8e4b43a3807fe036cdb098c8e141c0cd75efefb6c3fb5994155557
4
+ data.tar.gz: d12e54734dca578ca1860dc6ad90f385e55741049dfee37f32f52daaf2fea419
5
5
  SHA512:
6
- metadata.gz: f064374c4c6da864aba9e90b108a2110d629999e6c794e98fc449e45077ac8ea59803b6e75a743f2d614e70af7ca6eef6644f067c115bd197235fbcf8d4a402e
7
- data.tar.gz: f3b56c40bc4b776b18f5ac0543f376c44e5bdef6b400155f16aac6913582c95f2c186a2774fcb799565c12d52cf1941a94f0632be67263e75b53ee8e3b978c99
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/v4.3.0...HEAD
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 limitaton of 2704 characters for the `ancestry` column,
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 5.0 and 4.2
50
- - Ancestry 4.x only supports rails 5.2 and higher
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
- # postgrel
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 Columnl](#ancestry-database-column) and
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
  |![siblings](/img/siblings.png) |![subtree](/img/subtree.png) |![path](/img/path.png) |
149
- | includes self |self..indirects |root..self |
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 (default) 1/2/3, root nodes ancestry=nil
166
- :materialized_path2 (preferred) /1/2/3/, root nodes ancestry=/
167
- :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
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
- :cache_depth Cache the depth of each node (See Depth Cache section)
174
- false (default)
175
-
176
- :depth_cache_column column name to store depth cache
177
- 'ancestry_depth' (default)
178
- :primary_key_format regular expression that matches the format of the primary key
179
- '[0-9]+' (default) integer ids
180
- '[-A-Fa-f0-9]{36}' UUIDs
181
- :touch Instruct Ancestry to touch the ancestors of a node when it changes
182
- false (default) don't invalide nested key-based caches
183
- :counter_cache Whether to create counter cache column accessor.
184
- false (default) don't store a counter cache
185
- true store counter cache in `children_count`.
186
- String name of column to store counter cache.
187
- :update_strategy Choose the strategy to update descendants nodes
188
- :ruby (default) All descendants are updated using the ruby algorithm.
189
- This triggers update callbacks for each descendant node
190
- :sql All descendants are updated using a single SQL statement.
191
- This strategy does not trigger update callbacks for the descendants.
192
- This strategy is available only for PostgreSql implementations
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 wildchard (i.e.: `%`)
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` statemens.
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
- collum to allow `nulls`. Change the column create accordingly: `null: true`.
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
- ## Mysql Storage options
376
+ ## MySQL Storage options
374
377
 
375
378
  ### ascii field collation
376
379
 
377
- The currently suggested way to create a postgres field is using `'C'` collation:
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
- Mysql supports per column character sets. Using a character set of `ascii` will
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 descenant queries, has one less `OR` statement in the queries, and
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 runnnig.
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 AddDepthCachToTable < ActiveRecord::Migration[6.1]
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 depth_cache: true
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 object
5
- if object.is_a?(self.ancestry_base_class)
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 depth_options, depth
14
- depth_options.inject(self.ancestry_base_class) do |scope, option|
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.new(I18n.t("ancestry.unknown_depth_option", scope_name: scope_name))
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 options = {}
32
+ def arrange(options = {})
42
33
  if (order = options.delete(:order))
43
- arrange_nodes self.ancestry_base_class.order(order).where(options)
34
+ arrange_nodes(ancestry_base_class.order(order).where(options))
44
35
  else
45
- arrange_nodes self.ancestry_base_class.where(options)
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
- index[node.parent_id][node] = children
61
- arranged[node] = children unless node_ids.include?(node.parent_id)
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
- # Arrangement to nested array for serialization
78
- # You can also supply your own serialization logic using blocks
79
- # also allows you to pass the order just as you can pass it to the arrange method
80
- def arrange_serializable options={}, nodes=nil, &block
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 = arrange unless 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, &block)
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! options = {}
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
- begin
135
- # ... check validity of ancestry column
136
- if !node.sane_ancestor_ids?
137
- raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.invalid_ancestry_column",
138
- :node_id => node.id,
139
- :ancestry_column => "#{node.read_attribute node.ancestry_column}"
140
- ))
141
- end
142
- # ... check that all ancestors exist
143
- node.ancestor_ids.each do |ancestor_id|
144
- unless exists? ancestor_id
145
- raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.reference_nonexistent_node",
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
- rescue Ancestry::AncestryIntegrityException => integrity_exception
163
- case options[:report]
164
- when :list then exceptions << integrity_exception
165
- when :echo then puts integrity_exception
166
- else raise integrity_exception
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
- self.ancestry_base_class.transaction do
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! column=:parent_id, parent_id = nil, ancestor_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 Ancestry::AncestryException.new(I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to? :depth_cache_column
227
+ raise(Ancestry::AncestryException, I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to?(:depth_cache_column)
229
228
 
230
- self.ancestry_base_class.transaction do
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 self.ancestry_base_class.default_scoped.unscope(:where)
268
+ yield ancestry_base_class.default_scoped.unscope(:where)
241
269
  end
242
270
 
243
271
  ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ancestry
2
4
  class AncestryException < RuntimeError
3
5
  end
4
6
 
5
7
  class AncestryIntegrityException < AncestryException
6
8
  end
7
- end
9
+ end