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.
- 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
|
[![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/
|
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/
|
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
|
-
![Example tree](https://raw.github.com/
|
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/
|
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
|
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
|