closure_tree 6.6.0 → 7.0.0

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