closure_tree 6.6.0 → 7.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.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +7 -6
  3. data/Appraisals +59 -1
  4. data/CHANGELOG.md +21 -0
  5. data/Gemfile +0 -12
  6. data/README.md +40 -3
  7. data/closure_tree.gemspec +10 -7
  8. data/lib/closure_tree/finders.rb +15 -5
  9. data/lib/closure_tree/has_closure_tree.rb +2 -0
  10. data/lib/closure_tree/has_closure_tree_root.rb +2 -6
  11. data/lib/closure_tree/hash_tree_support.rb +1 -1
  12. data/lib/closure_tree/hierarchy_maintenance.rb +3 -3
  13. data/lib/closure_tree/model.rb +31 -1
  14. data/lib/closure_tree/numeric_deterministic_ordering.rb +11 -2
  15. data/lib/closure_tree/numeric_order_support.rb +3 -0
  16. data/lib/closure_tree/support.rb +7 -3
  17. data/lib/closure_tree/support_attributes.rb +9 -0
  18. data/lib/closure_tree/support_flags.rb +1 -4
  19. data/lib/closure_tree/version.rb +1 -1
  20. data/lib/generators/closure_tree/migration_generator.rb +8 -0
  21. data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
  22. metadata +28 -76
  23. data/gemfiles/activerecord_4.2.gemfile +0 -19
  24. data/gemfiles/activerecord_5.0.gemfile +0 -19
  25. data/gemfiles/activerecord_5.1.gemfile +0 -19
  26. data/gemfiles/activerecord_edge.gemfile +0 -20
  27. data/img/example.png +0 -0
  28. data/img/preorder.png +0 -0
  29. data/spec/cache_invalidation_spec.rb +0 -39
  30. data/spec/cuisine_type_spec.rb +0 -38
  31. data/spec/db/database.yml +0 -21
  32. data/spec/db/models.rb +0 -128
  33. data/spec/db/schema.rb +0 -166
  34. data/spec/fixtures/tags.yml +0 -98
  35. data/spec/generators/migration_generator_spec.rb +0 -48
  36. data/spec/has_closure_tree_root_spec.rb +0 -154
  37. data/spec/hierarchy_maintenance_spec.rb +0 -16
  38. data/spec/label_spec.rb +0 -554
  39. data/spec/matcher_spec.rb +0 -34
  40. data/spec/metal_spec.rb +0 -55
  41. data/spec/model_spec.rb +0 -9
  42. data/spec/namespace_type_spec.rb +0 -13
  43. data/spec/parallel_spec.rb +0 -159
  44. data/spec/pool_spec.rb +0 -27
  45. data/spec/spec_helper.rb +0 -24
  46. data/spec/support/database.rb +0 -52
  47. data/spec/support/database_cleaner.rb +0 -14
  48. data/spec/support/exceed_query_limit.rb +0 -18
  49. data/spec/support/hash_monkey_patch.rb +0 -13
  50. data/spec/support/query_counter.rb +0 -18
  51. data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
  52. data/spec/support_spec.rb +0 -14
  53. data/spec/tag_examples.rb +0 -665
  54. data/spec/tag_spec.rb +0 -6
  55. data/spec/user_spec.rb +0 -174
  56. data/spec/uuid_tag_spec.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c2a4b1786131c2b97479335f02da9e7c07a07f68
4
- data.tar.gz: a2a1803ef7cb252a5f6f3996b3196606abf4c396
2
+ SHA256:
3
+ metadata.gz: aad24f6acfed9da72a06c877919ef7a349680abb951a185307590b42484582e0
4
+ data.tar.gz: 543f9482cd9497d99174358b6d342caa6c860608acc51f5989e5d780c1747f58
5
5
  SHA512:
6
- metadata.gz: e088751351930a9ff34d6fbcd0468e6949bff95d4ffccdaaab36cd240163029fccc752234c3ba04ceabb819a8be9a1af39b4e11beecb38003d36da7ed17dd6de
7
- data.tar.gz: fc9d0ce048a35d852767bbed49b3b3dae1f6bd2efe92c31b37517205a5999fb69b8ae5d7c38984537121ea00702df667a4ed4e35e170c44d307eb9ddfa528136
6
+ metadata.gz: 69c034fe0ee0278b143758381412ca43663da441bd35a81b67d516d58defc5f19b79d83a28a9fea18984cb105b757b7028c9898cab34eaae98fccb56e7b0d71e
7
+ data.tar.gz: 53346203044b6eb82b784bc715105b6a2a5029d8ab8c48665d1bb02bf667bff643ba0768c6dfa6ec476b03b7f5567e28846c8b6b2c7b17344f2d17227ca1e24c
@@ -2,14 +2,14 @@ cache: bundler
2
2
  sudo: false
3
3
  language: ruby
4
4
  rvm:
5
- - 2.4.0
6
- - 2.3.3
7
- - 2.2.7
8
- # these haven't been passing for a while:
9
- # - jruby-head
10
- # - rbx
5
+ - 2.6.0
6
+ - 2.5.1
7
+ - 2.4.4
8
+ - 2.3.6
9
+ - 2.2.10
11
10
 
12
11
  gemfile:
12
+ - gemfiles/activerecord_5.2.gemfile
13
13
  - gemfiles/activerecord_5.1.gemfile
14
14
  - gemfiles/activerecord_5.0.gemfile
15
15
  - gemfiles/activerecord_4.2.gemfile
@@ -27,3 +27,4 @@ matrix:
27
27
  - gemfile: gemfiles/activerecord_edge.gemfile
28
28
  - rvm: jruby-head
29
29
  - rvm: rbx
30
+ - rvm: 2.6.0
data/Appraisals CHANGED
@@ -1,17 +1,75 @@
1
1
 
2
2
  appraise 'activerecord-4.2' do
3
3
  gem 'activerecord', '~> 4.2.0'
4
+ platforms :ruby do
5
+ gem 'mysql2', "< 0.5"
6
+ gem 'pg', "~> 0.21"
7
+ gem 'sqlite3'
8
+ end
9
+
10
+ platforms :jruby do
11
+ gem 'activerecord-jdbcmysql-adapter'
12
+ gem 'activerecord-jdbcpostgresql-adapter'
13
+ gem 'activerecord-jdbcsqlite3-adapter'
14
+ end
4
15
  end
5
16
 
6
17
  appraise 'activerecord-5.0' do
7
18
  gem 'activerecord', '~> 5.0.0'
19
+ platforms :ruby do
20
+ gem 'mysql2'
21
+ gem 'pg'
22
+ gem 'sqlite3'
23
+ end
24
+
25
+ platforms :jruby do
26
+ gem 'activerecord-jdbcmysql-adapter'
27
+ gem 'activerecord-jdbcpostgresql-adapter'
28
+ gem 'activerecord-jdbcsqlite3-adapter'
29
+ end
8
30
  end
9
31
 
10
32
  appraise 'activerecord-5.1' do
11
33
  gem 'activerecord', '~> 5.1.0'
34
+ platforms :ruby do
35
+ gem 'mysql2'
36
+ gem 'pg'
37
+ gem 'sqlite3'
38
+ end
39
+
40
+ platforms :jruby do
41
+ gem 'activerecord-jdbcmysql-adapter'
42
+ gem 'activerecord-jdbcpostgresql-adapter'
43
+ gem 'activerecord-jdbcsqlite3-adapter'
44
+ end
45
+ end
46
+
47
+ appraise 'activerecord-5.2' do
48
+ gem 'activerecord', '~> 5.2.0'
49
+ platforms :ruby do
50
+ gem 'mysql2'
51
+ gem 'pg'
52
+ gem 'sqlite3'
53
+ end
54
+
55
+ platforms :jruby do
56
+ gem 'activerecord-jdbcmysql-adapter'
57
+ gem 'activerecord-jdbcpostgresql-adapter'
58
+ gem 'activerecord-jdbcsqlite3-adapter'
59
+ end
12
60
  end
13
61
 
14
62
  appraise 'activerecord-edge' do
15
63
  gem 'activerecord', github: 'rails/rails'
16
- gem 'arel', github: 'rails/arel'
64
+ platforms :ruby do
65
+ gem 'mysql2'
66
+ gem 'pg'
67
+ gem 'sqlite3'
68
+ end
69
+
70
+ platforms :jruby do
71
+ gem 'activerecord-jdbcmysql-adapter'
72
+ gem 'activerecord-jdbcpostgresql-adapter'
73
+ gem 'activerecord-jdbcsqlite3-adapter'
74
+ end
17
75
  end
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ### 7.0.0
4
+ Closure Tree is now tested against Rails 5.2
5
+
6
+ - Postpone configuration (database introspection)[PR 264](https://github.com/ClosureTree/closure_tree/pull/264)
7
+ - Fix "tree.find_by_path([])" [PR 288](https://github.com/ClosureTree/closure_tree/pull/288)
8
+ - Fixed generator specs and added migration version [PR 292](https://github.com/ClosureTree/closure_tree/pull/292)
9
+ - Eliminate deprecation warnings in ActiveRecord 5.2 [PR 296](https://github.com/ClosureTree/closure_tree/pull/296)
10
+ - When using 'oracle_enhanced', remove 'AS' on the table_name alias. [PR 298](https://github.com/ClosureTree/closure_tree/pull/298)
11
+ - README update [PR 301](https://github.com/ClosureTree/closure_tree/pull/301)
12
+ - Add `with_descendant` finder [PR 302](https://github.com/ClosureTree/closure_tree/pull/302)
13
+ - Fix pg version for rails prior 5.1 [PR 303](https://github.com/ClosureTree/closure_tree/pull/303)
14
+ - Test on Rails 5.2 & fix mysql for older Rails [PR 304](https://github.com/ClosureTree/closure_tree/pull/304)
15
+ - Test with ActiveRecord 5.2.0 [PR 307](https://github.com/ClosureTree/closure_tree/pull/307)
16
+ - README update [PR 310](https://github.com/ClosureTree/closure_tree/pull/310)
17
+ - FactoryBot linter failing for a model that uses closure_tree [PR 311](https://github.com/ClosureTree/closure_tree/pull/311)
18
+ - Added dont_order_roots option [PR 312](https://github.com/ClosureTree/closure_tree/pull/312)
19
+ - Added instance methods to determine the relationship between 2 nodes [PR 314](https://github.com/ClosureTree/closure_tree/pull/314)
20
+ - Add an instance method to check the relationship between 2 nodes: #family_of? [PR 319](https://github.com/ClosureTree/closure_tree/pull/319)
21
+ - Remove options restrictions on has_closure_tree_root [PR 321](https://github.com/ClosureTree/closure_tree/pull/321)
22
+ - Fix uninitialized variable warnings [PR 323](https://github.com/ClosureTree/closure_tree/pull/323)
23
+
3
24
  ### 6.6.0
4
25
 
5
26
  Closure Tree is now tested against Rails 5.1, and just passed 50 contributors and
data/Gemfile CHANGED
@@ -1,15 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- platforms :ruby, :rbx do
4
- gem 'mysql2'
5
- gem 'pg'
6
- gem 'sqlite3'
7
- end
8
-
9
- platforms :jruby do
10
- gem 'activerecord-jdbcmysql-adapter'
11
- gem 'activerecord-jdbcpostgresql-adapter'
12
- gem 'activerecord-jdbcsqlite3-adapter'
13
- end
14
-
15
3
  gemspec
data/README.md CHANGED
@@ -74,7 +74,7 @@ Note that closure_tree only supports ActiveRecord 4.2 and later, and has test co
74
74
  end
75
75
  ```
76
76
 
77
- Make sure you check out the [large number options](#available-options) that `has_closure_tree` accepts.
77
+ Make sure you check out the [large number of options](#available-options) that `has_closure_tree` accepts.
78
78
 
79
79
  **IMPORTANT: Make sure you add `has_closure_tree` _after_ `attr_accessible` and
80
80
  `self.table_name =` lines in your model.**
@@ -88,7 +88,7 @@ Note that closure_tree only supports ActiveRecord 4.2 and later, and has test co
88
88
  ```ruby
89
89
  class AddParentIdToTag < ActiveRecord::Migration
90
90
  def change
91
- add_column :tag, :parent_id, :integer
91
+ add_column :tags, :parent_id, :integer
92
92
  end
93
93
  end
94
94
  ```
@@ -346,15 +346,19 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
346
346
 
347
347
  * ```tag.root``` returns the root for this node
348
348
  * ```tag.root?``` returns true if this is a root node
349
+ * ```tag.root_of?(node)``` returns true if current node is root of another one
349
350
  * ```tag.child?``` returns true if this is a child node. It has a parent.
350
351
  * ```tag.leaf?``` returns true if this is a leaf node. It has no children.
351
352
  * ```tag.leaves``` is scoped to all leaf nodes in self_and_descendants.
352
353
  * ```tag.depth``` returns the depth, or "generation", for this node in the tree. A root node will have a value of 0.
353
354
  * ```tag.parent``` returns the node's immediate parent. Root nodes will return nil.
355
+ * ```tag.parent_of?(node)``` returns true if current node is parent of another one
354
356
  * ```tag.children``` is a ```has_many``` of immediate children (just those nodes whose parent is the current node).
355
357
  * ```tag.child_ids``` is an array of the IDs of the children.
358
+ * ```tag.child_of?(node)``` returns true if current node is child of another one
356
359
  * ```tag.ancestors``` is a ordered scope of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal ```tag.depth```.
357
360
  * ```tag.ancestor_ids``` is an array of the IDs of the ancestors.
361
+ * ```tag.ancestor_of?(node)``` returns true if current node is ancestor of another one
358
362
  * ```tag.self_and_ancestors``` returns a scope containing self, parent, grandparent, great grandparent, etc.
359
363
  * ```tag.self_and_ancestors_ids``` returns IDs containing self, parent, grandparent, great grandparent, etc.
360
364
  * ```tag.siblings``` returns a scope containing all nodes with the same parent as ```tag```, excluding self.
@@ -362,8 +366,10 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
362
366
  * ```tag.self_and_siblings``` returns a scope containing all nodes with the same parent as ```tag```, including self.
363
367
  * ```tag.descendants``` returns a scope of all children, childrens' children, etc., excluding self ordered by depth.
364
368
  * ```tag.descendant_ids``` returns an array of the IDs of the descendants.
369
+ * ```tag.descendant_of?(node)``` returns true if current node is descendant of another one
365
370
  * ```tag.self_and_descendants``` returns a scope of self, all children, childrens' children, etc., ordered by depth.
366
371
  * ```tag.self_and_descendant_ids``` returns IDs of self, all children, childrens' children, etc., ordered by depth.
372
+ * ```tag.family_of?``` returns true if current node and another one have a same root.
367
373
  * ```tag.hash_tree``` returns an [ordered, nested hash](#nested-hashes) that can be depth-limited.
368
374
  * ```tag.find_by_path(path)``` returns the node whose name path *from ```tag```* is ```path```. See (#find_or_create_by_path).
369
375
  * ```tag.find_or_create_by_path(path)``` returns the node whose name path *from ```tag```* is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
@@ -389,6 +395,18 @@ class WhereTag < Tag ; end
389
395
  class WhatTag < Tag ; end
390
396
  ```
391
397
 
398
+ Note that if you call `rebuild!` on any of the subclasses, the complete Tag hierarchy will be emptied, thus taking the hiearchies of all other subclasses with it (issue #275). However, only the hierarchies for the class `rebuild!` was called on will be rebuilt, leaving the other subclasses without hierarchy entries.
399
+
400
+ You can work around that by overloading the `rebuild!` class method in all your STI subclasses and call the super classes `rebuild!` method:
401
+ ```ruby
402
+ class WhatTag < Tag
403
+ def self.rebuild!
404
+ Tag.rebuild!
405
+ end
406
+ end
407
+ ```
408
+ This way, the complete hierarchy including all subclasses will be rebuilt.
409
+
392
410
  ## Deterministic ordering
393
411
 
394
412
  By default, children will be ordered by your database engine, which may not be what you want.
@@ -411,7 +429,7 @@ and in your model:
411
429
 
412
430
  ```ruby
413
431
  class OrderedTag < ActiveRecord::Base
414
- has_closure_tree order: 'sort_order'
432
+ has_closure_tree order: 'sort_order', numeric_order: true
415
433
  end
416
434
  ```
417
435
 
@@ -477,6 +495,25 @@ root.reload.children.pluck(:name)
477
495
  => ["b", "c", "a"]
478
496
  ```
479
497
 
498
+ ### Ordering Roots
499
+
500
+ With numeric ordering, root nodes are, by default, assigned order values globally across the whole database
501
+ table. So for instance if you have 5 nodes with no parent, they will be ordered 0 through 4 by default.
502
+ If your model represents many separate trees and you have a lot of records, this can cause performance
503
+ problems, and doesn't really make much sense.
504
+
505
+ You can disable this default behavior by passing `dont_order_roots: true` as an option to your delcaration:
506
+
507
+ ```
508
+ has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true
509
+ ```
510
+
511
+ In this case, calling `prepend_sibling` and `append_sibling` on a root node or calling
512
+ `roots_and_descendants_preordered` on the model will raise a `RootOrderingDisabledError`.
513
+
514
+ The `dont_order_roots` option will be ignored unless `numeric_order` is set to true.
515
+
516
+
480
517
  ## Concurrency
481
518
 
482
519
  Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, cannot run concurrently correctly.
@@ -12,20 +12,23 @@ Gem::Specification.new do |gem|
12
12
  gem.description = gem.summary
13
13
  gem.license = 'MIT'
14
14
 
15
- gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
+ gem.files = `git ls-files`.split($/).reject do |f|
16
+ f.match(%r{^(spec|img|gemfiles)})
17
+ end
18
+
16
19
  gem.test_files = gem.files.grep(%r{^spec/})
17
20
  gem.required_ruby_version = '>= 2.0.0'
18
21
 
19
- gem.add_runtime_dependency 'activerecord', '>= 4.1.0'
20
- gem.add_runtime_dependency 'with_advisory_lock', '>= 3.0.0'
22
+ gem.add_runtime_dependency 'activerecord', '>= 4.2.10'
23
+ gem.add_runtime_dependency 'with_advisory_lock', '>= 4.0.0'
21
24
 
25
+ gem.add_development_dependency 'appraisal'
26
+ gem.add_development_dependency 'database_cleaner'
27
+ gem.add_development_dependency 'generator_spec'
28
+ gem.add_development_dependency 'parallel'
22
29
  gem.add_development_dependency 'rspec-instafail'
23
30
  gem.add_development_dependency 'rspec-rails'
24
- gem.add_development_dependency 'database_cleaner'
25
- gem.add_development_dependency 'appraisal'
26
31
  gem.add_development_dependency 'timecop'
27
- gem.add_development_dependency 'parallel'
28
- # gem.add_development_dependency 'ammeter', '1.1.2' # See https://github.com/mceachen/closure_tree/issues/181
29
32
  # gem.add_development_dependency 'byebug'
30
33
  # gem.add_development_dependency 'ruby-prof' # <- don't need this normally.
31
34
  end
@@ -41,7 +41,7 @@ module ClosureTree
41
41
  WHERE ancestor_id = #{_ct.quote(self.id)}
42
42
  GROUP BY descendant_id
43
43
  HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
44
- ) AS descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
44
+ ) #{ _ct.t_alias_keyword } descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
45
45
  SQL
46
46
  _ct.scope_with_order(s)
47
47
  end
@@ -76,7 +76,7 @@ module ClosureTree
76
76
  FROM #{_ct.quoted_hierarchy_table_name}
77
77
  GROUP BY ancestor_id
78
78
  HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
79
- ) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
79
+ ) #{ _ct.t_alias_keyword } leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
80
80
  SQL
81
81
  _ct.scope_with_order(s.readonly(false))
82
82
  end
@@ -90,19 +90,28 @@ module ClosureTree
90
90
  _ct.scope_with_order(scope)
91
91
  end
92
92
 
93
+ def with_descendant(*descendants)
94
+ descendant_ids = descendants.map { |ea| ea.is_a?(ActiveRecord::Base) ? ea._ct_id : ea }
95
+ scope = descendant_ids.blank? ? all : joins(:descendant_hierarchies).
96
+ where("#{_ct.hierarchy_table_name}.descendant_id" => descendant_ids).
97
+ where("#{_ct.hierarchy_table_name}.generations > 0").
98
+ readonly(false)
99
+ _ct.scope_with_order(scope)
100
+ end
101
+
93
102
  def find_all_by_generation(generation_level)
94
103
  s = joins(<<-SQL.strip_heredoc)
95
104
  INNER JOIN (
96
105
  SELECT #{primary_key} as root_id
97
106
  FROM #{_ct.quoted_table_name}
98
107
  WHERE #{_ct.quoted_parent_column_name} IS NULL
99
- ) AS roots ON (1 = 1)
108
+ ) #{ _ct.t_alias_keyword } roots ON (1 = 1)
100
109
  INNER JOIN (
101
110
  SELECT ancestor_id, descendant_id
102
111
  FROM #{_ct.quoted_hierarchy_table_name}
103
112
  GROUP BY ancestor_id, descendant_id
104
113
  HAVING MAX(generations) = #{generation_level.to_i}
105
- ) AS descendants ON (
114
+ ) #{ _ct.t_alias_keyword } descendants ON (
106
115
  #{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
107
116
  AND roots.root_id = descendants.ancestor_id
108
117
  )
@@ -112,6 +121,7 @@ module ClosureTree
112
121
 
113
122
  # Find the node whose +ancestry_path+ is +path+
114
123
  def find_by_path(path, attributes = {}, parent_id = nil)
124
+ return nil if path.blank?
115
125
  path = _ct.build_ancestry_attr_path(path, attributes)
116
126
  if path.size > _ct.max_join_tables
117
127
  return _ct.find_by_large_path(path, attributes, parent_id)
@@ -121,7 +131,7 @@ module ClosureTree
121
131
  path.reverse.each_with_index do |ea, idx|
122
132
  next_joined_table = "p#{idx}"
123
133
  scope = scope.joins(<<-SQL.strip_heredoc)
124
- INNER JOIN #{_ct.quoted_table_name} AS #{next_joined_table}
134
+ INNER JOIN #{_ct.quoted_table_name} #{ _ct.t_alias_keyword } #{next_joined_table}
125
135
  ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
126
136
  #{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
127
137
  SQL
@@ -8,6 +8,8 @@ module ClosureTree
8
8
  :hierarchy_table_name,
9
9
  :name_column,
10
10
  :order,
11
+ :dont_order_roots,
12
+ :numeric_order,
11
13
  :touch,
12
14
  :with_advisory_lock
13
15
  )
@@ -1,15 +1,11 @@
1
1
  module ClosureTree
2
2
  class MultipleRootError < StandardError; end
3
+ class RootOrderingDisabledError < StandardError; end
3
4
 
4
5
  module HasClosureTreeRoot
5
6
 
6
7
  def has_closure_tree_root(assoc_name, options = {})
7
- options.assert_valid_keys(
8
- :class_name,
9
- :foreign_key
10
- )
11
-
12
- options[:class_name] ||= assoc_name.to_s.sub(/\Aroot_/, "").classify
8
+ options[:class_name] ||= assoc_name.to_s.sub(/\Aroot_/, "").classify
13
9
  options[:foreign_key] ||= self.name.underscore << "_id"
14
10
 
15
11
  has_one assoc_name, -> { where(parent: nil) }, options
@@ -10,7 +10,7 @@ module ClosureTree
10
10
  FROM #{quoted_hierarchy_table_name}
11
11
  GROUP BY descendant_id
12
12
  #{having_clause}
13
- ) AS generation_depth
13
+ ) #{ t_alias_keyword } generation_depth
14
14
  ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
15
15
  SQL
16
16
  scope_with_order(scope.joins(generation_depth), 'generation_depth.depth')
@@ -20,7 +20,7 @@ module ClosureTree
20
20
  end
21
21
 
22
22
  def _ct_validate
23
- if !@_ct_skip_cycle_detection &&
23
+ if !(defined? @_ct_skip_cycle_detection) &&
24
24
  !new_record? && # don't validate for cycles if we're a new record
25
25
  changes[_ct.parent_column_name] && # don't validate for cycles if we didn't change our parent
26
26
  parent.present? && # don't validate if we're root
@@ -64,7 +64,7 @@ module ClosureTree
64
64
 
65
65
  def rebuild!(called_by_rebuild = false)
66
66
  _ct.with_advisory_lock do
67
- delete_hierarchy_references unless @was_new_record
67
+ delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
68
68
  hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
69
69
  unless root?
70
70
  _ct.connection.execute <<-SQL.strip_heredoc
@@ -102,7 +102,7 @@ module ClosureTree
102
102
  FROM #{_ct.quoted_hierarchy_table_name}
103
103
  WHERE ancestor_id = #{_ct.quote(id)}
104
104
  OR descendant_id = #{_ct.quote(id)}
105
- ) AS x )
105
+ ) #{ _ct.t_alias_keyword } x )
106
106
  SQL
107
107
  end
108
108
  end
@@ -13,7 +13,7 @@ module ClosureTree
13
13
  touch: _ct.options[:touch],
14
14
  optional: true)
15
15
 
16
- order_by_generations = "#{_ct.quoted_hierarchy_table_name}.generations asc"
16
+ order_by_generations = -> { Arel.sql("#{_ct.quoted_hierarchy_table_name}.generations ASC") }
17
17
 
18
18
  has_many :children, *_ct.has_many_with_order_option(
19
19
  class_name: _ct.model_class.to_s,
@@ -134,6 +134,36 @@ module ClosureTree
134
134
  _ct.ids_from(siblings)
135
135
  end
136
136
 
137
+ # node's parent is this record
138
+ def parent_of?(node)
139
+ self == node.parent
140
+ end
141
+
142
+ # node's root is this record
143
+ def root_of?(node)
144
+ self == node.root
145
+ end
146
+
147
+ # node's ancestors include this record
148
+ def ancestor_of?(node)
149
+ node.ancestors.include? self
150
+ end
151
+
152
+ # node is record's ancestor
153
+ def descendant_of?(node)
154
+ self.ancestors.include? node
155
+ end
156
+
157
+ # node is record's parent
158
+ def child_of?(node)
159
+ self.parent == node
160
+ end
161
+
162
+ # node and record have a same root
163
+ def family_of?(node)
164
+ self.root == node.root
165
+ end
166
+
137
167
  # Alias for appending to the children collection.
138
168
  # You can also add directly to the children collection, if you'd prefer.
139
169
  def add_child(child_node)