closure_tree 6.5.0 → 7.3.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 +96 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -1
  5. data/Appraisals +90 -7
  6. data/CHANGELOG.md +94 -42
  7. data/Gemfile +0 -12
  8. data/README.md +66 -23
  9. data/Rakefile +7 -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/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)
@@ -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
 
@@ -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.2, 2.3
623
- * ActiveRecord 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
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}")
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