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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +98 -0
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/Appraisals +90 -7
- data/CHANGELOG.md +100 -42
- data/Gemfile +3 -11
- data/README.md +68 -24
- data/Rakefile +16 -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/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
|
|
@@ -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
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/#!/
|
664
|
+
Closure tree is [tested under every valid combination](http://travis-ci.org/#!/ClosureTree/closure_tree) of
|
621
665
|
|
622
|
-
* Ruby 2.
|
623
|
-
* ActiveRecord 4.2 and
|
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/
|
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/
|
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
|
-
|
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}")
|
@@ -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
|
-
|
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
|
@@ -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
|
-
|
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.
|
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
|
-
)
|
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
|
-
|
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
|