closure_tree 6.5.0 → 7.4.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 (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +98 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -1
  5. data/Appraisals +90 -7
  6. data/CHANGELOG.md +100 -42
  7. data/Gemfile +3 -11
  8. data/README.md +68 -24
  9. data/Rakefile +16 -10
  10. data/_config.yml +1 -0
  11. data/bin/appraisal +29 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/closure_tree.gemspec +16 -9
  15. data/lib/closure_tree/finders.rb +32 -9
  16. data/lib/closure_tree/has_closure_tree.rb +4 -0
  17. data/lib/closure_tree/has_closure_tree_root.rb +5 -7
  18. data/lib/closure_tree/hash_tree_support.rb +4 -4
  19. data/lib/closure_tree/hierarchy_maintenance.rb +28 -8
  20. data/lib/closure_tree/model.rb +42 -16
  21. data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
  22. data/lib/closure_tree/numeric_order_support.rb +7 -3
  23. data/lib/closure_tree/support.rb +18 -12
  24. data/lib/closure_tree/support_attributes.rb +10 -1
  25. data/lib/closure_tree/support_flags.rb +1 -4
  26. data/lib/closure_tree/version.rb +1 -1
  27. data/lib/generators/closure_tree/migration_generator.rb +8 -0
  28. data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
  29. metadata +78 -79
  30. data/.travis.yml +0 -29
  31. data/gemfiles/activerecord_4.2.gemfile +0 -19
  32. data/gemfiles/activerecord_5.0.gemfile +0 -19
  33. data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
  34. data/gemfiles/activerecord_edge.gemfile +0 -20
  35. data/img/example.png +0 -0
  36. data/img/preorder.png +0 -0
  37. data/spec/cache_invalidation_spec.rb +0 -39
  38. data/spec/cuisine_type_spec.rb +0 -38
  39. data/spec/db/database.yml +0 -21
  40. data/spec/db/models.rb +0 -128
  41. data/spec/db/schema.rb +0 -166
  42. data/spec/fixtures/tags.yml +0 -98
  43. data/spec/generators/migration_generator_spec.rb +0 -48
  44. data/spec/has_closure_tree_root_spec.rb +0 -154
  45. data/spec/hierarchy_maintenance_spec.rb +0 -16
  46. data/spec/label_spec.rb +0 -554
  47. data/spec/matcher_spec.rb +0 -34
  48. data/spec/metal_spec.rb +0 -55
  49. data/spec/model_spec.rb +0 -9
  50. data/spec/namespace_type_spec.rb +0 -13
  51. data/spec/parallel_spec.rb +0 -159
  52. data/spec/spec_helper.rb +0 -24
  53. data/spec/support/database.rb +0 -52
  54. data/spec/support/database_cleaner.rb +0 -14
  55. data/spec/support/exceed_query_limit.rb +0 -18
  56. data/spec/support/hash_monkey_patch.rb +0 -13
  57. data/spec/support/query_counter.rb +0 -18
  58. data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
  59. data/spec/support_spec.rb +0 -14
  60. data/spec/tag_examples.rb +0 -665
  61. data/spec/tag_spec.rb +0 -6
  62. data/spec/user_spec.rb +0 -174
  63. data/spec/uuid_tag_spec.rb +0 -6
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)
@@ -74,7 +71,7 @@ Note that closure_tree only supports ActiveRecord 4.2 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.2 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
 
@@ -340,21 +337,26 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
340
337
  * ```Tag.find_by_path(path, attributes)``` returns the node whose name path is ```path```. See (#find_or_create_by_path).
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
- * ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestor is in the given list.
344
-
340
+ * ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestors(s) is/are in the given list.
341
+ * ```Tag.with_descendant(ancestors)``` scopes to all ancestors whose descendant(s) is/are in the given list.
342
+ * ```Tag.lowest_common_ancestor(descendants)``` finds the lowest common ancestor of the descendants.
345
343
  ### Instance methods
346
344
 
347
345
  * ```tag.root``` returns the root for this node
348
346
  * ```tag.root?``` returns true if this is a root node
347
+ * ```tag.root_of?(node)``` returns true if current node is root of another one
349
348
  * ```tag.child?``` returns true if this is a child node. It has a parent.
350
349
  * ```tag.leaf?``` returns true if this is a leaf node. It has no children.
351
350
  * ```tag.leaves``` is scoped to all leaf nodes in self_and_descendants.
352
351
  * ```tag.depth``` returns the depth, or "generation", for this node in the tree. A root node will have a value of 0.
353
352
  * ```tag.parent``` returns the node's immediate parent. Root nodes will return nil.
353
+ * ```tag.parent_of?(node)``` returns true if current node is parent of another one
354
354
  * ```tag.children``` is a ```has_many``` of immediate children (just those nodes whose parent is the current node).
355
355
  * ```tag.child_ids``` is an array of the IDs of the children.
356
+ * ```tag.child_of?(node)``` returns true if current node is child of another one
356
357
  * ```tag.ancestors``` is a ordered scope of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal ```tag.depth```.
357
358
  * ```tag.ancestor_ids``` is an array of the IDs of the ancestors.
359
+ * ```tag.ancestor_of?(node)``` returns true if current node is ancestor of another one
358
360
  * ```tag.self_and_ancestors``` returns a scope containing self, parent, grandparent, great grandparent, etc.
359
361
  * ```tag.self_and_ancestors_ids``` returns IDs containing self, parent, grandparent, great grandparent, etc.
360
362
  * ```tag.siblings``` returns a scope containing all nodes with the same parent as ```tag```, excluding self.
@@ -362,8 +364,10 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
362
364
  * ```tag.self_and_siblings``` returns a scope containing all nodes with the same parent as ```tag```, including self.
363
365
  * ```tag.descendants``` returns a scope of all children, childrens' children, etc., excluding self ordered by depth.
364
366
  * ```tag.descendant_ids``` returns an array of the IDs of the descendants.
367
+ * ```tag.descendant_of?(node)``` returns true if current node is descendant of another one
365
368
  * ```tag.self_and_descendants``` returns a scope of self, all children, childrens' children, etc., ordered by depth.
366
369
  * ```tag.self_and_descendant_ids``` returns IDs of self, all children, childrens' children, etc., ordered by depth.
370
+ * ```tag.family_of?``` returns true if current node and another one have a same root.
367
371
  * ```tag.hash_tree``` returns an [ordered, nested hash](#nested-hashes) that can be depth-limited.
368
372
  * ```tag.find_by_path(path)``` returns the node whose name path *from ```tag```* is ```path```. See (#find_or_create_by_path).
369
373
  * ```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 +393,18 @@ class WhereTag < Tag ; end
389
393
  class WhatTag < Tag ; end
390
394
  ```
391
395
 
396
+ 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.
397
+
398
+ You can work around that by overloading the `rebuild!` class method in all your STI subclasses and call the super classes `rebuild!` method:
399
+ ```ruby
400
+ class WhatTag < Tag
401
+ def self.rebuild!
402
+ Tag.rebuild!
403
+ end
404
+ end
405
+ ```
406
+ This way, the complete hierarchy including all subclasses will be rebuilt.
407
+
392
408
  ## Deterministic ordering
393
409
 
394
410
  By default, children will be ordered by your database engine, which may not be what you want.
@@ -411,7 +427,7 @@ and in your model:
411
427
 
412
428
  ```ruby
413
429
  class OrderedTag < ActiveRecord::Base
414
- has_closure_tree order: 'sort_order'
430
+ has_closure_tree order: 'sort_order', numeric_order: true
415
431
  end
416
432
  ```
417
433
 
@@ -477,6 +493,25 @@ root.reload.children.pluck(:name)
477
493
  => ["b", "c", "a"]
478
494
  ```
479
495
 
496
+ ### Ordering Roots
497
+
498
+ With numeric ordering, root nodes are, by default, assigned order values globally across the whole database
499
+ table. So for instance if you have 5 nodes with no parent, they will be ordered 0 through 4 by default.
500
+ If your model represents many separate trees and you have a lot of records, this can cause performance
501
+ problems, and doesn't really make much sense.
502
+
503
+ You can disable this default behavior by passing `dont_order_roots: true` as an option to your delcaration:
504
+
505
+ ```
506
+ has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true
507
+ ```
508
+
509
+ In this case, calling `prepend_sibling` and `append_sibling` on a root node or calling
510
+ `roots_and_descendants_preordered` on the model will raise a `RootOrderingDisabledError`.
511
+
512
+ The `dont_order_roots` option will be ignored unless `numeric_order` is set to true.
513
+
514
+
480
515
  ## Concurrency
481
516
 
482
517
  Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, cannot run concurrently correctly.
@@ -484,7 +519,7 @@ Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, ca
484
519
 
485
520
  Database row-level locks work correctly with PostgreSQL, but MySQL's row-level locking is broken, and
486
521
  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)
522
+ for both MySQL and PostgreSQL, [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock)
488
523
  is used automatically to ensure correctness.
489
524
 
490
525
  If you are already managing concurrency elsewhere in your application, and want to disable the use
@@ -499,6 +534,15 @@ end
499
534
  Note that you *will eventually have data corruption* if you disable advisory locks, write to your
500
535
  database with multiple threads, and don't provide an alternative mutex.
501
536
 
537
+ ## I18n
538
+
539
+ You can customize error messages using [I18n](http://guides.rubyonrails.org/i18n.html):
540
+
541
+ ```yaml
542
+ en-US:
543
+ closure_tree:
544
+ loop_error: Your descendant cannot be your parent!
545
+ ```
502
546
 
503
547
  ## FAQ
504
548
 
@@ -508,7 +552,7 @@ Yup! [Ilya Bodrov](https://github.com/bodrovis) wrote [Nested Comments with Rail
508
552
 
509
553
  ### Does this work well with ```#default_scope```?
510
554
 
511
- **No.** Please see [issue 86](https://github.com/mceachen/closure_tree/issues/86) for details.
555
+ **No.** Please see [issue 86](https://github.com/ClosureTree/closure_tree/issues/86) for details.
512
556
 
513
557
  ### Can I update parentage with `update_attribute`?
514
558
 
@@ -517,7 +561,7 @@ hierarchy table.
517
561
 
518
562
  ### Can I assign a parent to multiple children with ```#update_all```?
519
563
 
520
- **No.** Please see [issue 197](https://github.com/mceachen/closure_tree/issues/197) for details.
564
+ **No.** Please see [issue 197](https://github.com/ClosureTree/closure_tree/issues/197) for details.
521
565
 
522
566
  ### Does this gem support multiple parents?
523
567
 
@@ -576,11 +620,11 @@ bundle install
576
620
 
577
621
  ### Object destroy fails with MySQL v5.7+
578
622
 
579
- A bug was introduced in MySQL's query optimizer. [See the workaround here](https://github.com/mceachen/closure_tree/issues/206).
623
+ A bug was introduced in MySQL's query optimizer. [See the workaround here](https://github.com/ClosureTree/closure_tree/issues/206).
580
624
 
581
625
  ### Hierarchy maintenance errors from MySQL v5.7.9-v5.7.10
582
626
 
583
- Upgrade to MySQL 5.7.12 or later if you see [this issue](https://github.com/mceachen/closure_tree/issues/190):
627
+ Upgrade to MySQL 5.7.12 or later if you see [this issue](https://github.com/ClosureTree/closure_tree/issues/190):
584
628
 
585
629
  Mysql2::Error: You can't specify target table '*_hierarchies' for update in FROM clause
586
630
 
@@ -617,10 +661,10 @@ end
617
661
 
618
662
  ## Testing
619
663
 
620
- Closure tree is [tested under every valid combination](http://travis-ci.org/#!/mceachen/closure_tree) of
664
+ Closure tree is [tested under every valid combination](http://travis-ci.org/#!/ClosureTree/closure_tree) of
621
665
 
622
- * Ruby 2.2, 2.3
623
- * ActiveRecord 4.2 and 5.0
666
+ * Ruby 2.5, 2.6 and 2.7
667
+ * ActiveRecord 4.2, 5.x and 6.0
624
668
  * PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
625
669
 
626
670
  Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
@@ -628,12 +672,12 @@ run the test matrix locally.
628
672
 
629
673
  ## Change log
630
674
 
631
- See the [change log](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md).
675
+ See the [change log](https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md).
632
676
 
633
677
  ## Thanks to
634
678
 
635
679
  * 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)!)
680
+ (see the [changelog](https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md)!)
637
681
  * https://github.com/collectiveidea/awesome_nested_set
638
682
  * https://github.com/patshaughnessy/class_factory
639
683
  * JetBrains, which provides an [open-source license](http://www.jetbrains.com/ruby/buy/buy.jsp#openSource) to
data/Rakefile CHANGED
@@ -1,23 +1,20 @@
1
- begin
2
- require 'bundler/setup'
3
- rescue LoadError
4
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
- end
1
+ # frozen_string_literal: true
6
2
 
7
- Bundler::GemHelper.install_tasks
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
8
5
 
9
- require "rspec/core/rake_task"
10
6
  RSpec::Core::RakeTask.new(:spec) do |task|
11
- task.pattern = 'spec/*_spec.rb'
7
+ task.pattern = 'spec/closure_tree/*_spec.rb'
12
8
  end
13
9
 
14
- task :default => :spec
10
+ task default: :spec
15
11
 
16
12
  namespace :spec do
17
13
  desc 'Run all spec variants'
18
14
  task :all do
19
- rake = 'bundle exec rake'
15
+ rake = 'bin/rake'
20
16
  fail unless system("#{rake} spec:generators")
17
+
21
18
  [['', ''], ['db_prefix_', ''], ['', '_db_suffix'], ['abc_', '_123']].each do |prefix, suffix|
22
19
  env = "DB_PREFIX=#{prefix} DB_SUFFIX=#{suffix}"
23
20
  fail unless system("#{rake} spec #{env}")
@@ -29,3 +26,12 @@ namespace :spec do
29
26
  task.pattern = 'spec/generators/*_spec.rb'
30
27
  end
31
28
  end
29
+
30
+ require 'github_changelog_generator/task'
31
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
32
+ config.user = 'ClosureTree'
33
+ config.project = 'closure_tree'
34
+ config.issues = false
35
+ config.future_release = '5.2.0'
36
+ config.since_tag = 'v7.3.0'
37
+ end
data/_config.yml ADDED
@@ -0,0 +1 @@
1
+ theme: jekyll-theme-leap-day
data/bin/appraisal ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'appraisal' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("appraisal", "appraisal")
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/closure_tree.gemspec CHANGED
@@ -1,5 +1,6 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
- require 'closure_tree/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/closure_tree/version'
3
4
 
4
5
  Gem::Specification.new do |gem|
5
6
  gem.name = 'closure_tree'
@@ -12,20 +13,26 @@ Gem::Specification.new do |gem|
12
13
  gem.description = gem.summary
13
14
  gem.license = 'MIT'
14
15
 
15
- gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
+ gem.files = `git ls-files`.split($/).reject do |f|
17
+ f.match(%r{^(spec|img|gemfiles)})
18
+ end
19
+
16
20
  gem.test_files = gem.files.grep(%r{^spec/})
17
21
  gem.required_ruby_version = '>= 2.0.0'
18
22
 
19
- gem.add_runtime_dependency 'activerecord', '>= 4.1.0'
20
- gem.add_runtime_dependency 'with_advisory_lock', '>= 3.0.0'
23
+ gem.add_runtime_dependency 'activerecord', '>= 4.2.10'
24
+ gem.add_runtime_dependency 'with_advisory_lock', '>= 4.0.0'
21
25
 
26
+ gem.add_development_dependency 'appraisal'
27
+ gem.add_development_dependency 'database_cleaner'
28
+ gem.add_development_dependency 'generator_spec'
29
+ gem.add_development_dependency 'parallel'
30
+ gem.add_development_dependency 'pg'
22
31
  gem.add_development_dependency 'rspec-instafail'
23
32
  gem.add_development_dependency 'rspec-rails'
24
- gem.add_development_dependency 'database_cleaner'
25
- gem.add_development_dependency 'appraisal'
33
+ gem.add_development_dependency 'sqlite3'
34
+ gem.add_development_dependency 'simplecov'
26
35
  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
36
  # gem.add_development_dependency 'byebug'
30
37
  # gem.add_development_dependency 'ruby-prof' # <- don't need this normally.
31
38
  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,18 +1,14 @@
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
- has_one assoc_name, -> { where(parent: nil) }, options
11
+ has_one assoc_name, -> { where(parent: nil) }, **options
16
12
 
17
13
  # Fetches the association, eager loading all children and given associations
18
14
  define_method("#{assoc_name}_including_tree") do |*args|
@@ -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