closure_tree 6.5.0 → 7.4.0

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