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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +96 -0
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/Appraisals +90 -7
- data/CHANGELOG.md +94 -42
- data/Gemfile +0 -12
- data/README.md +66 -23
- data/Rakefile +7 -10
- data/_config.yml +1 -0
- data/bin/appraisal +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/closure_tree.gemspec +16 -9
- data/lib/closure_tree/finders.rb +32 -9
- data/lib/closure_tree/has_closure_tree.rb +4 -0
- data/lib/closure_tree/has_closure_tree_root.rb +5 -7
- data/lib/closure_tree/hash_tree_support.rb +4 -4
- data/lib/closure_tree/hierarchy_maintenance.rb +28 -8
- data/lib/closure_tree/model.rb +42 -16
- data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
- data/lib/closure_tree/numeric_order_support.rb +7 -3
- data/lib/closure_tree/support.rb +18 -12
- data/lib/closure_tree/support_attributes.rb +10 -1
- data/lib/closure_tree/support_flags.rb +1 -4
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/migration_generator.rb +8 -0
- data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
- metadata +78 -79
- data/.travis.yml +0 -29
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
- data/gemfiles/activerecord_edge.gemfile +0 -20
- data/img/example.png +0 -0
- data/img/preorder.png +0 -0
- data/spec/cache_invalidation_spec.rb +0 -39
- data/spec/cuisine_type_spec.rb +0 -38
- data/spec/db/database.yml +0 -21
- data/spec/db/models.rb +0 -128
- data/spec/db/schema.rb +0 -166
- data/spec/fixtures/tags.yml +0 -98
- data/spec/generators/migration_generator_spec.rb +0 -48
- data/spec/has_closure_tree_root_spec.rb +0 -154
- data/spec/hierarchy_maintenance_spec.rb +0 -16
- data/spec/label_spec.rb +0 -554
- data/spec/matcher_spec.rb +0 -34
- data/spec/metal_spec.rb +0 -55
- data/spec/model_spec.rb +0 -9
- data/spec/namespace_type_spec.rb +0 -13
- data/spec/parallel_spec.rb +0 -159
- data/spec/spec_helper.rb +0 -24
- data/spec/support/database.rb +0 -52
- data/spec/support/database_cleaner.rb +0 -14
- data/spec/support/exceed_query_limit.rb +0 -18
- data/spec/support/hash_monkey_patch.rb +0 -13
- data/spec/support/query_counter.rb +0 -18
- data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
- data/spec/support_spec.rb +0 -14
- data/spec/tag_examples.rb +0 -665
- data/spec/tag_spec.rb +0 -6
- data/spec/user_spec.rb +0 -174
- 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
|
[](https://gitter.im/closure_tree/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
9
|
-
[](http://travis-ci.org/ClosureTree/closure_tree)
|
10
10
|
[](https://badge.fury.io/rb/closure_tree)
|
11
|
-
[](https://www.codacy.com/app/matthew-github/closure_tree)
|
12
|
-
[](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/
|
30
|
-
*
|
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 :
|
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/
|
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
|
-

|
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
|
-

|
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/
|
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/
|
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/
|
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/
|
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/
|
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/#!/
|
663
|
+
Closure tree is [tested under every valid combination](http://travis-ci.org/#!/ClosureTree/closure_tree) of
|
621
664
|
|
622
|
-
* Ruby 2.
|
623
|
-
* ActiveRecord 4.2 and
|
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/
|
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/
|
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
|
-
|
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
|
-
|
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 :
|
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 = '
|
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
|
-
|
2
|
-
|
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
|
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.
|
20
|
-
gem.add_runtime_dependency 'with_advisory_lock', '>=
|
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 '
|
25
|
-
gem.add_development_dependency '
|
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
|
data/lib/closure_tree/finders.rb
CHANGED
@@ -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.
|
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
|
-
)
|
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.
|
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
|
-
)
|
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.
|
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
|
-
)
|
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
|
-
)
|
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.
|
124
|
-
INNER JOIN #{_ct.quoted_table_name}
|
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
|