closure_tree 6.4.0 → 7.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +19 -12
  4. data/Appraisals +75 -7
  5. data/CHANGELOG.md +92 -39
  6. data/Gemfile +0 -12
  7. data/README.md +67 -24
  8. data/_config.yml +1 -0
  9. data/closure_tree.gemspec +10 -7
  10. data/lib/closure_tree/finders.rb +32 -9
  11. data/lib/closure_tree/has_closure_tree.rb +4 -0
  12. data/lib/closure_tree/has_closure_tree_root.rb +4 -6
  13. data/lib/closure_tree/hash_tree_support.rb +4 -4
  14. data/lib/closure_tree/hierarchy_maintenance.rb +31 -11
  15. data/lib/closure_tree/model.rb +42 -16
  16. data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
  17. data/lib/closure_tree/numeric_order_support.rb +7 -3
  18. data/lib/closure_tree/support.rb +18 -12
  19. data/lib/closure_tree/support_attributes.rb +10 -1
  20. data/lib/closure_tree/support_flags.rb +1 -4
  21. data/lib/closure_tree/version.rb +1 -1
  22. data/lib/generators/closure_tree/migration_generator.rb +8 -0
  23. data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
  24. metadata +29 -75
  25. data/gemfiles/activerecord_4.2.gemfile +0 -19
  26. data/gemfiles/activerecord_5.0.gemfile +0 -19
  27. data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
  28. data/gemfiles/activerecord_edge.gemfile +0 -20
  29. data/img/example.png +0 -0
  30. data/img/preorder.png +0 -0
  31. data/spec/cache_invalidation_spec.rb +0 -39
  32. data/spec/cuisine_type_spec.rb +0 -38
  33. data/spec/db/database.yml +0 -21
  34. data/spec/db/models.rb +0 -128
  35. data/spec/db/schema.rb +0 -166
  36. data/spec/fixtures/tags.yml +0 -98
  37. data/spec/generators/migration_generator_spec.rb +0 -48
  38. data/spec/has_closure_tree_root_spec.rb +0 -154
  39. data/spec/hierarchy_maintenance_spec.rb +0 -16
  40. data/spec/label_spec.rb +0 -554
  41. data/spec/matcher_spec.rb +0 -34
  42. data/spec/metal_spec.rb +0 -55
  43. data/spec/model_spec.rb +0 -9
  44. data/spec/namespace_type_spec.rb +0 -13
  45. data/spec/parallel_spec.rb +0 -159
  46. data/spec/spec_helper.rb +0 -24
  47. data/spec/support/database.rb +0 -52
  48. data/spec/support/database_cleaner.rb +0 -14
  49. data/spec/support/exceed_query_limit.rb +0 -18
  50. data/spec/support/hash_monkey_patch.rb +0 -13
  51. data/spec/support/query_counter.rb +0 -18
  52. data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
  53. data/spec/support_spec.rb +0 -14
  54. data/spec/tag_examples.rb +0 -665
  55. data/spec/tag_spec.rb +0 -6
  56. data/spec/user_spec.rb +0 -174
  57. data/spec/uuid_tag_spec.rb +0 -6
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
@@ -6,10 +6,8 @@ Common applications include modeling hierarchical data, like tags, threaded comm
6
6
  and tracking user referrals.
7
7
 
8
8
  [![Join the chat at https://gitter.im/closure_tree/Lobby](https://badges.gitter.im/closure_tree/Lobby.svg)](https://gitter.im/closure_tree/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9
- [![Build Status](https://api.travis-ci.org/mceachen/closure_tree.svg?branch=master)](http://travis-ci.org/mceachen/closure_tree)
9
+ [![Build Status](https://api.travis-ci.org/ClosureTree/closure_tree.svg?branch=master)](http://travis-ci.org/ClosureTree/closure_tree)
10
10
  [![Gem Version](https://badge.fury.io/rb/closure_tree.svg)](https://badge.fury.io/rb/closure_tree)
11
- [![Codacy Badge](https://api.codacy.com/project/badge/Grade/fa5a8ae2193d42adb30b4256732a757d)](https://www.codacy.com/app/matthew-github/closure_tree)
12
- [![Dependency Status](https://gemnasium.com/badges/github.com/mceachen/closure_tree.svg)](https://gemnasium.com/github.com/mceachen/closure_tree)
13
11
 
14
12
  Dramatically more performant than
15
13
  [ancestry](https://github.com/stefankroes/ancestry) and
@@ -26,9 +24,8 @@ closure_tree has some great features:
26
24
  * __Best-in-class mutation performance__:
27
25
  * 2 SQL INSERTs on node creation
28
26
  * 3 SQL INSERT/UPDATEs on node reparenting
29
- * __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/mceachen/with_advisory_lock))
30
- * __Support for ActiveRecord 4.2 and 5.0__
31
- * __Support for Ruby 2.2 and 2.3__
27
+ * __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock))
28
+ * __Tested against ActiveRecord 4.2, 5.0, 5.1, 5.2 and 6.0 with Ruby 2.5 and 2.6__
32
29
  * Support for reparenting children (and all their descendants)
33
30
  * Support for [single-table inheritance (STI)](#sti) within the hierarchy
34
31
  * ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
@@ -56,7 +53,7 @@ for a description of different tree storage algorithms.
56
53
 
57
54
  ## Installation
58
55
 
59
- Note that closure_tree only supports ActiveRecord 4.1 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
56
+ Note that closure_tree only supports ActiveRecord 4.2 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
60
57
 
61
58
  1. Add `gem 'closure_tree'` to your Gemfile
62
59
 
@@ -74,7 +71,7 @@ Note that closure_tree only supports ActiveRecord 4.1 and later, and has test co
74
71
  end
75
72
  ```
76
73
 
77
- Make sure you check out the [large number options](#available-options) that `has_closure_tree` accepts.
74
+ Make sure you check out the [large number of options](#available-options) that `has_closure_tree` accepts.
78
75
 
79
76
  **IMPORTANT: Make sure you add `has_closure_tree` _after_ `attr_accessible` and
80
77
  `self.table_name =` lines in your model.**
@@ -88,7 +85,7 @@ Note that closure_tree only supports ActiveRecord 4.1 and later, and has test co
88
85
  ```ruby
89
86
  class AddParentIdToTag < ActiveRecord::Migration
90
87
  def change
91
- add_column :tag, :parent_id, :integer
88
+ add_column :tags, :parent_id, :integer
92
89
  end
93
90
  end
94
91
  ```
@@ -254,7 +251,7 @@ b.hash_tree(:limit_depth => 2)
254
251
  Without this option, ```hash_tree``` will load the entire contents of that table into RAM. Your
255
252
  server may not be happy trying to do this.
256
253
 
257
- HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/mceachen/closure_tree/issues/11)
254
+ HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/ClosureTree/closure_tree/issues/11)
258
255
 
259
256
  ### Eager loading
260
257
 
@@ -305,13 +302,13 @@ File.open("example.dot", "w") { |f| f.write(Tag.root.to_dot_digraph) }
305
302
  ```
306
303
  Then, in a shell, ```dot -Tpng example.dot > example.png```, which produces:
307
304
 
308
- ![Example tree](https://raw.github.com/mceachen/closure_tree/master/img/example.png)
305
+ ![Example tree](https://raw.github.com/ClosureTree/closure_tree/master/img/example.png)
309
306
 
310
307
  If you want to customize the label value, override the ```#to_digraph_label``` instance method in your model.
311
308
 
312
309
  Just for kicks, this is the test tree I used for proving that preordered tree traversal was correct:
313
310
 
314
- ![Preordered test tree](https://raw.github.com/mceachen/closure_tree/master/img/preorder.png)
311
+ ![Preordered test tree](https://raw.github.com/ClosureTree/closure_tree/master/img/preorder.png)
315
312
 
316
313
  ### Available options
317
314
 
@@ -341,20 +338,24 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
341
338
  * ```Tag.find_or_create_by_path(path, attributes)``` returns the node whose name path is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
342
339
  * ```Tag.find_all_by_generation(generation_level)``` returns the descendant nodes who are ```generation_level``` away from a root. ```Tag.find_all_by_generation(0)``` is equivalent to ```Tag.roots```.
343
340
  * ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestor is in the given list.
344
-
341
+ * ```Tag.lowest_common_ancestor(descendants)``` finds the lowest common ancestor of the descendants.
345
342
  ### Instance methods
346
343
 
347
344
  * ```tag.root``` returns the root for this node
348
345
  * ```tag.root?``` returns true if this is a root node
346
+ * ```tag.root_of?(node)``` returns true if current node is root of another one
349
347
  * ```tag.child?``` returns true if this is a child node. It has a parent.
350
348
  * ```tag.leaf?``` returns true if this is a leaf node. It has no children.
351
349
  * ```tag.leaves``` is scoped to all leaf nodes in self_and_descendants.
352
350
  * ```tag.depth``` returns the depth, or "generation", for this node in the tree. A root node will have a value of 0.
353
351
  * ```tag.parent``` returns the node's immediate parent. Root nodes will return nil.
352
+ * ```tag.parent_of?(node)``` returns true if current node is parent of another one
354
353
  * ```tag.children``` is a ```has_many``` of immediate children (just those nodes whose parent is the current node).
355
354
  * ```tag.child_ids``` is an array of the IDs of the children.
355
+ * ```tag.child_of?(node)``` returns true if current node is child of another one
356
356
  * ```tag.ancestors``` is a ordered scope of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal ```tag.depth```.
357
357
  * ```tag.ancestor_ids``` is an array of the IDs of the ancestors.
358
+ * ```tag.ancestor_of?(node)``` returns true if current node is ancestor of another one
358
359
  * ```tag.self_and_ancestors``` returns a scope containing self, parent, grandparent, great grandparent, etc.
359
360
  * ```tag.self_and_ancestors_ids``` returns IDs containing self, parent, grandparent, great grandparent, etc.
360
361
  * ```tag.siblings``` returns a scope containing all nodes with the same parent as ```tag```, excluding self.
@@ -362,8 +363,10 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
362
363
  * ```tag.self_and_siblings``` returns a scope containing all nodes with the same parent as ```tag```, including self.
363
364
  * ```tag.descendants``` returns a scope of all children, childrens' children, etc., excluding self ordered by depth.
364
365
  * ```tag.descendant_ids``` returns an array of the IDs of the descendants.
366
+ * ```tag.descendant_of?(node)``` returns true if current node is descendant of another one
365
367
  * ```tag.self_and_descendants``` returns a scope of self, all children, childrens' children, etc., ordered by depth.
366
368
  * ```tag.self_and_descendant_ids``` returns IDs of self, all children, childrens' children, etc., ordered by depth.
369
+ * ```tag.family_of?``` returns true if current node and another one have a same root.
367
370
  * ```tag.hash_tree``` returns an [ordered, nested hash](#nested-hashes) that can be depth-limited.
368
371
  * ```tag.find_by_path(path)``` returns the node whose name path *from ```tag```* is ```path```. See (#find_or_create_by_path).
369
372
  * ```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 +392,18 @@ class WhereTag < Tag ; end
389
392
  class WhatTag < Tag ; end
390
393
  ```
391
394
 
395
+ 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.
396
+
397
+ You can work around that by overloading the `rebuild!` class method in all your STI subclasses and call the super classes `rebuild!` method:
398
+ ```ruby
399
+ class WhatTag < Tag
400
+ def self.rebuild!
401
+ Tag.rebuild!
402
+ end
403
+ end
404
+ ```
405
+ This way, the complete hierarchy including all subclasses will be rebuilt.
406
+
392
407
  ## Deterministic ordering
393
408
 
394
409
  By default, children will be ordered by your database engine, which may not be what you want.
@@ -411,7 +426,7 @@ and in your model:
411
426
 
412
427
  ```ruby
413
428
  class OrderedTag < ActiveRecord::Base
414
- has_closure_tree order: 'sort_order'
429
+ has_closure_tree order: 'sort_order', numeric_order: true
415
430
  end
416
431
  ```
417
432
 
@@ -477,6 +492,25 @@ root.reload.children.pluck(:name)
477
492
  => ["b", "c", "a"]
478
493
  ```
479
494
 
495
+ ### Ordering Roots
496
+
497
+ With numeric ordering, root nodes are, by default, assigned order values globally across the whole database
498
+ table. So for instance if you have 5 nodes with no parent, they will be ordered 0 through 4 by default.
499
+ If your model represents many separate trees and you have a lot of records, this can cause performance
500
+ problems, and doesn't really make much sense.
501
+
502
+ You can disable this default behavior by passing `dont_order_roots: true` as an option to your delcaration:
503
+
504
+ ```
505
+ has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true
506
+ ```
507
+
508
+ In this case, calling `prepend_sibling` and `append_sibling` on a root node or calling
509
+ `roots_and_descendants_preordered` on the model will raise a `RootOrderingDisabledError`.
510
+
511
+ The `dont_order_roots` option will be ignored unless `numeric_order` is set to true.
512
+
513
+
480
514
  ## Concurrency
481
515
 
482
516
  Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, cannot run concurrently correctly.
@@ -484,7 +518,7 @@ Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, ca
484
518
 
485
519
  Database row-level locks work correctly with PostgreSQL, but MySQL's row-level locking is broken, and
486
520
  erroneously reports deadlocks where there are none. To work around this, and have a consistent implementation
487
- for both MySQL and PostgreSQL, [with_advisory_lock](https://github.com/mceachen/with_advisory_lock)
521
+ for both MySQL and PostgreSQL, [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock)
488
522
  is used automatically to ensure correctness.
489
523
 
490
524
  If you are already managing concurrency elsewhere in your application, and want to disable the use
@@ -499,6 +533,15 @@ end
499
533
  Note that you *will eventually have data corruption* if you disable advisory locks, write to your
500
534
  database with multiple threads, and don't provide an alternative mutex.
501
535
 
536
+ ## I18n
537
+
538
+ You can customize error messages using [I18n](http://guides.rubyonrails.org/i18n.html):
539
+
540
+ ```yaml
541
+ en-US:
542
+ closure_tree:
543
+ loop_error: Your descendant cannot be your parent!
544
+ ```
502
545
 
503
546
  ## FAQ
504
547
 
@@ -508,7 +551,7 @@ Yup! [Ilya Bodrov](https://github.com/bodrovis) wrote [Nested Comments with Rail
508
551
 
509
552
  ### Does this work well with ```#default_scope```?
510
553
 
511
- **No.** Please see [issue 86](https://github.com/mceachen/closure_tree/issues/86) for details.
554
+ **No.** Please see [issue 86](https://github.com/ClosureTree/closure_tree/issues/86) for details.
512
555
 
513
556
  ### Can I update parentage with `update_attribute`?
514
557
 
@@ -517,7 +560,7 @@ hierarchy table.
517
560
 
518
561
  ### Can I assign a parent to multiple children with ```#update_all```?
519
562
 
520
- **No.** Please see [issue 197](https://github.com/mceachen/closure_tree/issues/197) for details.
563
+ **No.** Please see [issue 197](https://github.com/ClosureTree/closure_tree/issues/197) for details.
521
564
 
522
565
  ### Does this gem support multiple parents?
523
566
 
@@ -576,11 +619,11 @@ bundle install
576
619
 
577
620
  ### Object destroy fails with MySQL v5.7+
578
621
 
579
- A bug was introduced in MySQL's query optimizer. [See the workaround here](https://github.com/mceachen/closure_tree/issues/206).
622
+ A bug was introduced in MySQL's query optimizer. [See the workaround here](https://github.com/ClosureTree/closure_tree/issues/206).
580
623
 
581
624
  ### Hierarchy maintenance errors from MySQL v5.7.9-v5.7.10
582
625
 
583
- Upgrade to MySQL 5.7.12 or later if you see [this issue](https://github.com/mceachen/closure_tree/issues/190):
626
+ Upgrade to MySQL 5.7.12 or later if you see [this issue](https://github.com/ClosureTree/closure_tree/issues/190):
584
627
 
585
628
  Mysql2::Error: You can't specify target table '*_hierarchies' for update in FROM clause
586
629
 
@@ -617,10 +660,10 @@ end
617
660
 
618
661
  ## Testing
619
662
 
620
- Closure tree is [tested under every valid combination](http://travis-ci.org/#!/mceachen/closure_tree) of
663
+ Closure tree is [tested under every valid combination](http://travis-ci.org/#!/ClosureTree/closure_tree) of
621
664
 
622
- * Ruby 2.0, 2.2, 2.3.1
623
- * ActiveRecord 4.1, 4.2, and 5.0
665
+ * Ruby 2.5, 2.6 and 2.7
666
+ * ActiveRecord 4.2, 5.x and 6.0
624
667
  * PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
625
668
 
626
669
  Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
@@ -628,12 +671,12 @@ run the test matrix locally.
628
671
 
629
672
  ## Change log
630
673
 
631
- See the [change log](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md).
674
+ See the [change log](https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md).
632
675
 
633
676
  ## Thanks to
634
677
 
635
678
  * The 45+ engineers around the world that have contributed their time and code to this gem
636
- (see the [changelog](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md)!)
679
+ (see the [changelog](https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md)!)
637
680
  * https://github.com/collectiveidea/awesome_nested_set
638
681
  * https://github.com/patshaughnessy/class_factory
639
682
  * JetBrains, which provides an [open-source license](http://www.jetbrains.com/ruby/buy/buy.jsp#openSource) to
@@ -0,0 +1 @@
1
+ theme: jekyll-theme-leap-day
@@ -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
@@ -34,14 +34,14 @@ module ClosureTree
34
34
  end
35
35
 
36
36
  def find_all_by_generation(generation_level)
37
- s = _ct.base_class.joins(<<-SQL.strip_heredoc)
37
+ s = _ct.base_class.joins(<<-SQL.squish)
38
38
  INNER JOIN (
39
39
  SELECT descendant_id
40
40
  FROM #{_ct.quoted_hierarchy_table_name}
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
@@ -70,13 +70,13 @@ module ClosureTree
70
70
  end
71
71
 
72
72
  def leaves
73
- s = joins(<<-SQL.strip_heredoc)
73
+ s = joins(<<-SQL.squish)
74
74
  INNER JOIN (
75
75
  SELECT ancestor_id
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,41 @@ 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
+
102
+ def lowest_common_ancestor(*descendants)
103
+ descendants = descendants.first if descendants.length == 1 && descendants.first.respond_to?(:each)
104
+ ancestor_id = hierarchy_class
105
+ .where(descendant_id: descendants)
106
+ .group(:ancestor_id)
107
+ .having("COUNT(ancestor_id) = #{descendants.count}")
108
+ .order(Arel.sql('MIN(generations) ASC'))
109
+ .limit(1)
110
+ .pluck(:ancestor_id).first
111
+
112
+ find_by(primary_key => ancestor_id) if ancestor_id
113
+ end
114
+
93
115
  def find_all_by_generation(generation_level)
94
- s = joins(<<-SQL.strip_heredoc)
116
+ s = joins(<<-SQL.squish)
95
117
  INNER JOIN (
96
118
  SELECT #{primary_key} as root_id
97
119
  FROM #{_ct.quoted_table_name}
98
120
  WHERE #{_ct.quoted_parent_column_name} IS NULL
99
- ) AS roots ON (1 = 1)
121
+ ) #{ _ct.t_alias_keyword } roots ON (1 = 1)
100
122
  INNER JOIN (
101
123
  SELECT ancestor_id, descendant_id
102
124
  FROM #{_ct.quoted_hierarchy_table_name}
103
125
  GROUP BY ancestor_id, descendant_id
104
126
  HAVING MAX(generations) = #{generation_level.to_i}
105
- ) AS descendants ON (
127
+ ) #{ _ct.t_alias_keyword } descendants ON (
106
128
  #{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
107
129
  AND roots.root_id = descendants.ancestor_id
108
130
  )
@@ -112,6 +134,7 @@ module ClosureTree
112
134
 
113
135
  # Find the node whose +ancestry_path+ is +path+
114
136
  def find_by_path(path, attributes = {}, parent_id = nil)
137
+ return nil if path.blank?
115
138
  path = _ct.build_ancestry_attr_path(path, attributes)
116
139
  if path.size > _ct.max_join_tables
117
140
  return _ct.find_by_large_path(path, attributes, parent_id)
@@ -120,8 +143,8 @@ module ClosureTree
120
143
  last_joined_table = _ct.table_name
121
144
  path.reverse.each_with_index do |ea, idx|
122
145
  next_joined_table = "p#{idx}"
123
- scope = scope.joins(<<-SQL.strip_heredoc)
124
- INNER JOIN #{_ct.quoted_table_name} AS #{next_joined_table}
146
+ scope = scope.joins(<<-SQL.squish)
147
+ INNER JOIN #{_ct.quoted_table_name} #{ _ct.t_alias_keyword } #{next_joined_table}
125
148
  ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
126
149
  #{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
127
150
  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
  )
@@ -29,6 +31,8 @@ module ClosureTree
29
31
 
30
32
  include ClosureTree::DeterministicOrdering if _ct.order_option?
31
33
  include ClosureTree::NumericDeterministicOrdering if _ct.order_is_numeric?
34
+
35
+ connection_pool.release_connection
32
36
  rescue StandardError => e
33
37
  raise e unless ClosureTree.configuration.database_less
34
38
  end
@@ -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
@@ -81,6 +77,8 @@ module ClosureTree
81
77
 
82
78
  @closure_tree_roots[assoc_name][assoc_map] = root
83
79
  end
80
+
81
+ connection_pool.release_connection
84
82
  end
85
83
  end
86
84
  end
@@ -4,17 +4,17 @@ module ClosureTree
4
4
  # Deepest generation, within limit, for each descendant
5
5
  # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
6
6
  having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
7
- generation_depth = <<-SQL.strip_heredoc
7
+ generation_depth = <<-SQL.squish
8
8
  INNER JOIN (
9
9
  SELECT descendant_id, MAX(generations) as depth
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')
17
- end
17
+ end
18
18
 
19
19
  def hash_tree(tree_scope, limit_depth = nil)
20
20
  limited_scope = limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
@@ -33,4 +33,4 @@ module ClosureTree
33
33
  tree
34
34
  end
35
35
  end
36
- end
36
+ end