closure_tree 6.0.0 → 6.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +16 -7
- data/Appraisals +11 -1
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE +1 -1
- data/README.md +36 -11
- data/closure_tree.gemspec +3 -3
- data/gemfiles/activerecord_4.1.gemfile +1 -1
- data/gemfiles/activerecord_5.0.gemfile +19 -0
- data/gemfiles/activerecord_5.0_foreigner.gemfile +20 -0
- data/lib/closure_tree.rb +10 -1
- data/lib/closure_tree/configuration.rb +9 -0
- data/lib/closure_tree/finders.rb +2 -2
- data/lib/closure_tree/has_closure_tree.rb +1 -2
- data/lib/closure_tree/hash_tree.rb +1 -1
- data/lib/closure_tree/hash_tree_support.rb +3 -7
- data/lib/closure_tree/hierarchy_maintenance.rb +1 -1
- data/lib/closure_tree/model.rb +7 -5
- data/lib/closure_tree/support.rb +4 -0
- data/lib/closure_tree/test/matcher.rb +2 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/config_generator.rb +12 -0
- data/lib/generators/closure_tree/templates/config.rb +5 -0
- data/spec/generators/migration_generator_spec.rb +48 -48
- data/spec/label_spec.rb +31 -4
- data/spec/parallel_spec.rb +7 -51
- data/spec/spec_helper.rb +1 -3
- data/spec/support/database.rb +1 -1
- data/tests.sh +3 -2
- metadata +13 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be5442160917053dfca50f710fa8276b2d2d937c
|
4
|
+
data.tar.gz: e6012dd4b630ace5fa36835de936bd395e41df20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43316c4fd1030f11ca90d59cf477f9a7d5300a16771152e3b81a779d5a25f665b9f9a288c42bc0c54183717d537cc95d704fc180166064079fbe6a9ee20d2bc5
|
7
|
+
data.tar.gz: 7f65c0281b3d878b2c333087f3e63a8e09c6aecbcabc98d798e2e1651853f50f7aefe50ece31b611bf12b17718fa3b023adc67cb39c747034a79aafae152dd93
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -2,12 +2,18 @@ cache: bundler
|
|
2
2
|
sudo: false
|
3
3
|
language: ruby
|
4
4
|
rvm:
|
5
|
-
- 2.
|
6
|
-
-
|
5
|
+
- 2.3.1
|
6
|
+
- 2.2.5
|
7
|
+
- 2.0
|
8
|
+
# these haven't been passing for a while:
|
9
|
+
# - jruby-head
|
10
|
+
# - rbx
|
7
11
|
|
8
12
|
gemfile:
|
9
13
|
- gemfiles/activerecord_4.1.gemfile
|
10
14
|
- gemfiles/activerecord_4.2.gemfile
|
15
|
+
- gemfiles/activerecord_5.0_foreigner.gemfile
|
16
|
+
- gemfiles/activerecord_5.0.gemfile
|
11
17
|
- gemfiles/activerecord_edge.gemfile
|
12
18
|
|
13
19
|
env:
|
@@ -15,16 +21,19 @@ env:
|
|
15
21
|
- DB=mysql
|
16
22
|
- DB=postgresql
|
17
23
|
|
18
|
-
#addons:
|
19
|
-
# postgresql: "9.3"
|
20
|
-
|
21
24
|
script: WITH_ADVISORY_LOCK_PREFIX=$TRAVIS_JOB_ID bundle exec rake --trace spec:all
|
22
25
|
|
23
26
|
matrix:
|
24
27
|
allow_failures:
|
25
28
|
- gemfile: gemfiles/activerecord_edge.gemfile
|
26
29
|
- rvm: jruby-head
|
30
|
+
- rvm: rbx
|
27
31
|
|
32
|
+
# rails 5+ requires ruby >= 2.2:
|
28
33
|
exclude:
|
29
|
-
- rvm:
|
30
|
-
gemfile: gemfiles/
|
34
|
+
- rvm: 2.0
|
35
|
+
gemfile: gemfiles/activerecord_5.0_foreigner.gemfile
|
36
|
+
- rvm: 2.0
|
37
|
+
gemfile: gemfiles/activerecord_5.0.gemfile
|
38
|
+
- rvm: 2.0
|
39
|
+
gemfile: gemfiles/activerecord_edge.gemfile
|
data/Appraisals
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
appraise 'activerecord-4.1' do
|
2
2
|
gem 'activerecord', '~> 4.1.0'
|
3
|
-
gem 'foreigner'
|
3
|
+
gem 'foreigner'
|
4
4
|
platforms :ruby, :rbx do
|
5
5
|
gem 'mysql2', '~> 0.3.20'
|
6
6
|
end
|
@@ -8,11 +8,21 @@ end
|
|
8
8
|
|
9
9
|
appraise 'activerecord-4.2' do
|
10
10
|
gem 'activerecord', '~> 4.2.0'
|
11
|
+
|
11
12
|
platforms :ruby, :rbx do
|
12
13
|
gem 'mysql2', '~> 0.3.20'
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
17
|
+
appraise 'activerecord-5.0-foreigner' do
|
18
|
+
gem 'activerecord', '~> 5.0.0'
|
19
|
+
gem 'foreigner'
|
20
|
+
end
|
21
|
+
|
22
|
+
appraise 'activerecord-5.0' do
|
23
|
+
gem 'activerecord', '~> 5.0.0'
|
24
|
+
end
|
25
|
+
|
16
26
|
appraise 'activerecord-edge' do
|
17
27
|
gem 'activerecord', github: 'rails/rails'
|
18
28
|
gem 'arel', github: 'rails/arel'
|
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,21 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
### 6.1.0
|
4
|
+
|
5
|
+
* Added official support for ActiveRecord 5.0! Thanks to [Abdelkader Boudih](https://github.com/seuros),
|
6
|
+
[Jay Fredlund](https://github.com/jayfredlund), Veselin Stoyanov, and
|
7
|
+
[Aaron Russell](https://github.com/aaronrussell) for all the PRs.
|
8
|
+
* Add `database_less` configuration to not raise an error during build step when
|
9
|
+
database is unavailable which is a common case in some PaaS like
|
10
|
+
(Heroku, Catalyze, ..., etc).
|
11
|
+
|
3
12
|
### 6.0.0
|
4
13
|
|
5
14
|
* [Andrew Kumanyaev](https://github.com/zzet) *dramatically* improved mutation performance on large trees.
|
6
15
|
Thanks for the PR!
|
7
16
|
* [Martin Schmidt](https://github.com/martin-schmidt) discovered and fixed build problems due to new versions
|
8
17
|
of mysql2 and ammeter which broke Travis builds. Thanks for the PR!
|
18
|
+
* [Fabien MICHEL](https://github.com/fabien-michel) updated the README with another example. Thanks for the PR!
|
9
19
|
|
10
20
|
### 6.0.0.alpha,beta,gamma
|
11
21
|
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
### Closure_tree lets your ActiveRecord models act as nodes in a [tree data structure](http://en.wikipedia.org/wiki/Tree_%28data_structure%29)
|
4
4
|
|
5
|
-
Common applications include modeling hierarchical data, like tags, page graphs in CMSes,
|
5
|
+
Common applications include modeling hierarchical data, like tags, threaded comments, page graphs in CMSes,
|
6
6
|
and tracking user referrals.
|
7
7
|
|
8
8
|
[![Build Status](https://secure.travis-ci.org/mceachen/closure_tree.png?branch=master)](http://travis-ci.org/mceachen/closure_tree)
|
@@ -26,8 +26,8 @@ closure_tree has some great features:
|
|
26
26
|
* 2 SQL INSERTs on node creation
|
27
27
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
28
28
|
* __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/mceachen/with_advisory_lock))
|
29
|
-
* __Support for ActiveRecord 4.1, 4.2 and 5.
|
30
|
-
* __Support for Ruby 2.2 and JRuby 9000__
|
29
|
+
* __Support for ActiveRecord 4.1, 4.2 and 5.0__
|
30
|
+
* __Support for Ruby 2.0, 2.1, 2.2, 2.3.1 and JRuby 9000__
|
31
31
|
* Support for reparenting children (and all their descendants)
|
32
32
|
* Support for [single-table inheritance (STI)](#sti) within the hierarchy
|
33
33
|
* ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
|
@@ -109,6 +109,9 @@ Note that closure_tree only supports ActiveRecord 4.1 and later, and has test co
|
|
109
109
|
|
110
110
|
If you're starting from scratch you don't need to call `rebuild!`.
|
111
111
|
|
112
|
+
NOTE: Run `rails g closure_tree:config` to create an initializer with extra
|
113
|
+
configurations. (Optional)
|
114
|
+
|
112
115
|
## Warning
|
113
116
|
|
114
117
|
As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model
|
@@ -145,11 +148,17 @@ child3 = Tag.new(name: 'Third Child')
|
|
145
148
|
parent.add_child child3
|
146
149
|
```
|
147
150
|
|
151
|
+
Or by setting the parent on the child :
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
Tag.create(name: 'Fourth Child', parent: parent)
|
155
|
+
```
|
156
|
+
|
148
157
|
Then:
|
149
158
|
|
150
159
|
```ruby
|
151
160
|
grandparent.self_and_descendants.collect(&:name)
|
152
|
-
=> ["Grandparent", "Parent", "First Child", "Second Child", "Third Child"]
|
161
|
+
=> ["Grandparent", "Parent", "First Child", "Second Child", "Third Child", "Fourth Child"]
|
153
162
|
|
154
163
|
child1.ancestry_path
|
155
164
|
=> ["Grandparent", "Parent", "First Child"]
|
@@ -275,6 +284,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
|
|
275
284
|
* ```:nullify``` will simply set the parent column to null. Each child node will be considered a "root" node. This is the default.
|
276
285
|
* ```:delete_all``` will delete all descendant nodes (which circumvents the destroy hooks)
|
277
286
|
* ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
|
287
|
+
* ```nil``` does nothing with descendant nodes
|
278
288
|
* ```:name_column``` used by #```find_or_create_by_path```, #```find_by_path```, and ```ancestry_path``` instance methods. This is primarily useful if the model only has one required field (like a "tag").
|
279
289
|
* ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
|
280
290
|
* ```:touch``` delegates to the `belongs_to` annotation for the parent, so `touch`ing cascades to all children (the performance of this for deep trees isn't currently optimal).
|
@@ -394,12 +404,12 @@ If your ```order``` column is an integer attribute, you'll also have these:
|
|
394
404
|
* ```node1.prepend_sibling(node2)``` which will
|
395
405
|
1. set ```node2``` to the same parent as ```node1```,
|
396
406
|
2. set ```node2```'s order column to 1 less than ```node1```'s value, and
|
397
|
-
3.
|
407
|
+
3. increment the order_column of all children of node1's parents whose order_column is > node2's new value by 1.
|
398
408
|
|
399
409
|
* ```node1.append_sibling(node2)``` which will
|
400
410
|
1. set ```node2``` to the same parent as ```node1```,
|
401
411
|
2. set ```node2```'s order column to 1 more than ```node1```'s value, and
|
402
|
-
3. increment the order_column of all children of node1's parents whose order_column is
|
412
|
+
3. increment the order_column of all children of node1's parents whose order_column is > node2's new value by 1.
|
403
413
|
|
404
414
|
```ruby
|
405
415
|
|
@@ -503,6 +513,22 @@ after do
|
|
503
513
|
end
|
504
514
|
```
|
505
515
|
|
516
|
+
### `bundle install` says `Gem::Ext::BuildError: ERROR: Failed to build gem native extension`
|
517
|
+
|
518
|
+
When building from source, the `mysql2`, `pg`, and `sqlite` gems need their native client libraries
|
519
|
+
installed on your system. Note that this error isn't specific to ClosureTree.
|
520
|
+
|
521
|
+
On Ubuntu/Debian systems, run:
|
522
|
+
|
523
|
+
```
|
524
|
+
sudo apt-get install libpq-dev libsqlite3-dev libmysqlclient-dev
|
525
|
+
bundle install
|
526
|
+
```
|
527
|
+
|
528
|
+
### Object destroy fails with MySQL 5.7+
|
529
|
+
|
530
|
+
A bug was introduced in MySQL's query optimizer. [See the workaround here](https://github.com/mceachen/closure_tree/issues/206).
|
531
|
+
|
506
532
|
## Testing with Closure Tree
|
507
533
|
|
508
534
|
Closure tree comes with some RSpec2/3 matchers which you may use for your tests:
|
@@ -538,10 +564,9 @@ end
|
|
538
564
|
|
539
565
|
Closure tree is [tested under every valid combination](http://travis-ci.org/#!/mceachen/closure_tree) of
|
540
566
|
|
541
|
-
* Ruby 2.2
|
542
|
-
*
|
543
|
-
*
|
544
|
-
* Concurrency tests for MySQL and PostgreSQL. SQLite is tested in a single-threaded environment.
|
567
|
+
* Ruby 2.0, 2.2, 2.3.1
|
568
|
+
* ActiveRecord 4.1, 4.2, and 5.0
|
569
|
+
* PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
|
545
570
|
|
546
571
|
Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
|
547
572
|
run the test matrix locally.
|
@@ -552,7 +577,7 @@ See the [change log](https://github.com/mceachen/closure_tree/blob/master/CHANGE
|
|
552
577
|
|
553
578
|
## Thanks to
|
554
579
|
|
555
|
-
* The
|
580
|
+
* The 45+ engineers around the world that have contributed their time and code to this gem
|
556
581
|
(see the [changelog](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md)!)
|
557
582
|
* https://github.com/collectiveidea/awesome_nested_set
|
558
583
|
* https://github.com/patshaughnessy/class_factory
|
data/closure_tree.gemspec
CHANGED
@@ -14,18 +14,18 @@ Gem::Specification.new do |gem|
|
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
16
16
|
gem.test_files = gem.files.grep(%r{^spec/})
|
17
|
-
gem.required_ruby_version = '>= 2.
|
17
|
+
gem.required_ruby_version = '>= 2.0.0'
|
18
18
|
|
19
19
|
gem.add_runtime_dependency 'activerecord', '>= 4.1.0'
|
20
20
|
gem.add_runtime_dependency 'with_advisory_lock', '>= 3.0.0'
|
21
21
|
|
22
22
|
gem.add_development_dependency 'rspec-instafail'
|
23
|
-
gem.add_development_dependency 'rspec-rails'
|
23
|
+
gem.add_development_dependency 'rspec-rails'
|
24
24
|
gem.add_development_dependency 'database_cleaner'
|
25
25
|
gem.add_development_dependency 'appraisal'
|
26
26
|
gem.add_development_dependency 'timecop'
|
27
27
|
gem.add_development_dependency 'parallel'
|
28
|
-
gem.add_development_dependency 'ammeter', '1.1.2' # See https://github.com/mceachen/closure_tree/issues/181
|
28
|
+
# gem.add_development_dependency 'ammeter', '1.1.2' # See https://github.com/mceachen/closure_tree/issues/181
|
29
29
|
# gem.add_development_dependency 'byebug'
|
30
30
|
# gem.add_development_dependency 'ruby-prof' # <- don't need this normally.
|
31
31
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 5.0.0"
|
6
|
+
|
7
|
+
platforms :ruby, :rbx do
|
8
|
+
gem "mysql2"
|
9
|
+
gem "pg"
|
10
|
+
gem "sqlite3"
|
11
|
+
end
|
12
|
+
|
13
|
+
platforms :jruby do
|
14
|
+
gem "activerecord-jdbcmysql-adapter"
|
15
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
16
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
17
|
+
end
|
18
|
+
|
19
|
+
gemspec :path => "../"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 5.0.0"
|
6
|
+
gem "foreigner"
|
7
|
+
|
8
|
+
platforms :ruby, :rbx do
|
9
|
+
gem "mysql2"
|
10
|
+
gem "pg"
|
11
|
+
gem "sqlite3"
|
12
|
+
end
|
13
|
+
|
14
|
+
platforms :jruby do
|
15
|
+
gem "activerecord-jdbcmysql-adapter"
|
16
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
17
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
18
|
+
end
|
19
|
+
|
20
|
+
gemspec :path => "../"
|
data/lib/closure_tree.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'active_record'
|
2
2
|
|
3
3
|
module ClosureTree
|
4
4
|
extend ActiveSupport::Autoload
|
@@ -12,6 +12,15 @@ module ClosureTree
|
|
12
12
|
autoload :Digraphs
|
13
13
|
autoload :DeterministicOrdering
|
14
14
|
autoload :NumericDeterministicOrdering
|
15
|
+
autoload :Configuration
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
yield configuration
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
15
24
|
end
|
16
25
|
|
17
26
|
ActiveSupport.on_load :active_record do
|
data/lib/closure_tree/finders.rb
CHANGED
@@ -47,12 +47,12 @@ module ClosureTree
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def without_self(scope)
|
50
|
-
scope.
|
50
|
+
scope.without_instance(self)
|
51
51
|
end
|
52
52
|
|
53
53
|
module ClassMethods
|
54
54
|
|
55
|
-
def
|
55
|
+
def without_instance(instance)
|
56
56
|
if instance.new_record?
|
57
57
|
all
|
58
58
|
else
|
@@ -30,8 +30,7 @@ module ClosureTree
|
|
30
30
|
include ClosureTree::DeterministicOrdering if _ct.order_option?
|
31
31
|
include ClosureTree::NumericDeterministicOrdering if _ct.order_is_numeric?
|
32
32
|
rescue StandardError => e
|
33
|
-
|
34
|
-
raise e unless ENV['DATABASE_URL'].to_s.include?('//user:pass@127.0.0.1/')
|
33
|
+
raise e unless ClosureTree.configuration.database_less
|
35
34
|
end
|
36
35
|
|
37
36
|
alias_method :acts_as_tree, :has_closure_tree
|
@@ -11,7 +11,7 @@ module ClosureTree
|
|
11
11
|
# There is no default depth limit. This might be crazy-big, depending
|
12
12
|
# on your tree shape. Hash huge trees at your own peril!
|
13
13
|
def hash_tree(options = {})
|
14
|
-
_ct.hash_tree(
|
14
|
+
_ct.hash_tree(_ct.default_tree_scope(all, options[:limit_depth]))
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module ClosureTree
|
2
2
|
module HashTreeSupport
|
3
|
-
def default_tree_scope(limit_depth = nil)
|
3
|
+
def default_tree_scope(scope, limit_depth = nil)
|
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}" : ''
|
@@ -13,15 +13,11 @@ module ClosureTree
|
|
13
13
|
) AS generation_depth
|
14
14
|
ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
|
15
15
|
SQL
|
16
|
-
scope_with_order(
|
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
|
-
limited_scope =
|
21
|
-
limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
|
22
|
-
else
|
23
|
-
default_tree_scope(limit_depth)
|
24
|
-
end
|
20
|
+
limited_scope = limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
|
25
21
|
build_hash_tree(limited_scope)
|
26
22
|
end
|
27
23
|
|
@@ -36,7 +36,7 @@ module ClosureTree
|
|
36
36
|
|
37
37
|
def _ct_after_save
|
38
38
|
if changes[_ct.parent_column_name] || @was_new_record
|
39
|
-
rebuild!
|
39
|
+
rebuild!
|
40
40
|
end
|
41
41
|
if changes[_ct.parent_column_name] && !@was_new_record
|
42
42
|
# Resetting the ancestral collections addresses
|
data/lib/closure_tree/model.rb
CHANGED
@@ -5,11 +5,13 @@ module ClosureTree
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
|
9
|
+
belongs_to :parent, nil, *_ct.belongs_to_with_optional_option(
|
10
|
+
class_name: _ct.model_class.to_s,
|
11
|
+
foreign_key: _ct.parent_column_name,
|
12
|
+
inverse_of: :children,
|
13
|
+
touch: _ct.options[:touch],
|
14
|
+
optional: true)
|
13
15
|
|
14
16
|
order_by_generations = "#{_ct.quoted_hierarchy_table_name}.generations asc"
|
15
17
|
|
data/lib/closure_tree/support.rb
CHANGED
@@ -76,6 +76,10 @@ module ClosureTree
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
+
def belongs_to_with_optional_option(opts)
|
80
|
+
[ActiveRecord::VERSION::MAJOR < 5 ? opts.except(:optional) : opts]
|
81
|
+
end
|
82
|
+
|
79
83
|
# lambda-ize the order, but don't apply the default order_option
|
80
84
|
def has_many_without_order_option(opts)
|
81
85
|
[lambda { order(opts[:order]) }, opts.except(:order)]
|
data/lib/closure_tree/version.rb
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
module ClosureTree
|
2
|
+
module Generators # :nodoc:
|
3
|
+
class ConfigGenerator < Rails::Generators::Base # :nodoc:
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
desc 'Install closure tree config.'
|
6
|
+
|
7
|
+
def config
|
8
|
+
template 'config.rb', 'config/initializers/closure_tree_config.rb'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,48 +1,48 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'ammeter/init'
|
3
|
-
|
4
|
-
# Generators are not automatically loaded by Rails
|
5
|
-
require 'generators/closure_tree/migration_generator'
|
6
|
-
|
7
|
-
RSpec.describe ClosureTree::Generators::MigrationGenerator, type: :generator do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
1
|
+
# require 'spec_helper'
|
2
|
+
# require 'ammeter/init'
|
3
|
+
#
|
4
|
+
# # Generators are not automatically loaded by Rails
|
5
|
+
# require 'generators/closure_tree/migration_generator'
|
6
|
+
#
|
7
|
+
# RSpec.describe ClosureTree::Generators::MigrationGenerator, type: :generator do
|
8
|
+
# TMPDIR = Dir.mktmpdir
|
9
|
+
# # Tell generator where to put its output
|
10
|
+
# destination TMPDIR
|
11
|
+
# before { prepare_destination }
|
12
|
+
#
|
13
|
+
# describe 'generator output' do
|
14
|
+
# before { run_generator %w(tag) }
|
15
|
+
# subject { migration_file('db/migrate/create_tag_hierarchies.rb') }
|
16
|
+
# it { is_expected.to be_a_migration }
|
17
|
+
# it { is_expected.to contain(/t.integer :ancestor_id, null: false/) }
|
18
|
+
# it { is_expected.to contain(/t.integer :descendant_id, null: false/) }
|
19
|
+
# it { is_expected.to contain(/t.integer :generations, null: false/) }
|
20
|
+
# it { is_expected.to contain(/add_index :tag_hierarchies/) }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# describe 'generator output with namespaced model' do
|
24
|
+
# before { run_generator %w(Namespace::Type) }
|
25
|
+
# subject { migration_file('db/migrate/create_namespace_type_hierarchies.rb') }
|
26
|
+
# it { is_expected.to be_a_migration }
|
27
|
+
# it { is_expected.to contain(/t.integer :ancestor_id, null: false/) }
|
28
|
+
# it { is_expected.to contain(/t.integer :descendant_id, null: false/) }
|
29
|
+
# it { is_expected.to contain(/t.integer :generations, null: false/) }
|
30
|
+
# it { is_expected.to contain(/add_index :namespace_type_hierarchies/) }
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# describe 'generator output with namespaced model with /' do
|
34
|
+
# before { run_generator %w(namespace/type) }
|
35
|
+
# subject { migration_file('db/migrate/create_namespace_type_hierarchies.rb') }
|
36
|
+
# it { is_expected.to be_a_migration }
|
37
|
+
# it { is_expected.to contain(/t.integer :ancestor_id, null: false/) }
|
38
|
+
# it { is_expected.to contain(/t.integer :descendant_id, null: false/) }
|
39
|
+
# it { is_expected.to contain(/t.integer :generations, null: false/) }
|
40
|
+
# it { is_expected.to contain(/add_index :namespace_type_hierarchies/) }
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# it 'should run all tasks in generator without errors' do
|
44
|
+
# gen = generator %w(tag)
|
45
|
+
# expect(gen).to receive :create_migration_file
|
46
|
+
# capture(:stdout) { gen.invoke_all }
|
47
|
+
# end
|
48
|
+
# end
|
data/spec/label_spec.rb
CHANGED
@@ -252,7 +252,7 @@ describe Label do
|
|
252
252
|
end
|
253
253
|
|
254
254
|
def children_name_and_order
|
255
|
-
name_and_order(@parent.children
|
255
|
+
name_and_order(@parent.children.reload)
|
256
256
|
end
|
257
257
|
|
258
258
|
def roots_name_and_order
|
@@ -454,7 +454,7 @@ describe Label do
|
|
454
454
|
it 'should retain sort orders of descendants when moving to a new parent' do
|
455
455
|
expected_order = ('a'..'z').to_a.shuffle
|
456
456
|
expected_order.map { |ea| first_root.add_child(Label.new(name: ea)) }
|
457
|
-
actual_order = first_root.children
|
457
|
+
actual_order = first_root.children.reload.pluck(:name)
|
458
458
|
expect(actual_order).to eq(expected_order)
|
459
459
|
last_root.append_child(first_root)
|
460
460
|
expect(last_root.self_and_descendants.pluck(:name)).to eq(%w(10 0) + expected_order)
|
@@ -465,13 +465,13 @@ describe Label do
|
|
465
465
|
z = first_root.find_or_create_by_path(path)
|
466
466
|
z_children_names = (100..150).to_a.shuffle.map { |ea| ea.to_s }
|
467
467
|
z_children_names.reverse.each { |ea| z.prepend_child(Label.new(name: ea)) }
|
468
|
-
expect(z.children
|
468
|
+
expect(z.children.reload.pluck(:name)).to eq(z_children_names)
|
469
469
|
a = first_root.find_by_path(['a'])
|
470
470
|
# move b up to a's level:
|
471
471
|
b = a.children.first
|
472
472
|
a.add_sibling(b)
|
473
473
|
expect(b.parent).to eq(first_root)
|
474
|
-
expect(z.children
|
474
|
+
expect(z.children.reload.pluck(:name)).to eq(z_children_names)
|
475
475
|
end
|
476
476
|
end
|
477
477
|
|
@@ -524,4 +524,31 @@ describe Label do
|
|
524
524
|
expect(Label.roots_and_descendants_preordered.collect { |ea| ea.name }).to eq(expected)
|
525
525
|
end
|
526
526
|
end unless sqlite? # sqlite doesn't have a power function.
|
527
|
+
|
528
|
+
context 'hash_tree' do
|
529
|
+
before do
|
530
|
+
@a = EventLabel.create(name: 'a')
|
531
|
+
@b = DateLabel.create(name: 'b')
|
532
|
+
@c = DirectoryLabel.create(name: 'c')
|
533
|
+
(1..3).each { |i| DirectoryLabel.create!(name: "c#{ i }", mother_id: @c.id) }
|
534
|
+
end
|
535
|
+
it 'should return tree with correct scope when called on class' do
|
536
|
+
tree = DirectoryLabel.hash_tree
|
537
|
+
expect(tree.keys.size).to eq(1)
|
538
|
+
expect(tree.keys.first).to eq(@c)
|
539
|
+
expect(tree[@c].keys.size).to eq(3)
|
540
|
+
end
|
541
|
+
it 'should return tree with correct scope when called on all' do
|
542
|
+
tree = DirectoryLabel.all.hash_tree
|
543
|
+
expect(tree.keys.size).to eq(1)
|
544
|
+
expect(tree.keys.first).to eq(@c)
|
545
|
+
expect(tree[@c].keys.size).to eq(3)
|
546
|
+
end
|
547
|
+
it 'should return tree with correct scope when called on scope chain' do
|
548
|
+
tree = Label.where(name: 'b').hash_tree
|
549
|
+
expect(tree.keys.size).to eq(1)
|
550
|
+
expect(tree.keys.first).to eq(@b)
|
551
|
+
expect(tree[@b]).to eq({})
|
552
|
+
end
|
553
|
+
end
|
527
554
|
end
|
data/spec/parallel_spec.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
# We don't need to run the expensive parallel tests for every combination of prefix/suffix.
|
4
|
-
# Those affect SQL generation, not parallelism
|
4
|
+
# Those affect SQL generation, not parallelism.
|
5
|
+
# SQLite doesn't support concurrency reliably, either.
|
5
6
|
def run_parallel_tests?
|
6
|
-
|
7
|
+
!sqlite? &&
|
8
|
+
ActiveRecord::Base.table_name_prefix.empty? &&
|
7
9
|
ActiveRecord::Base.table_name_suffix.empty?
|
8
10
|
end
|
9
11
|
|
@@ -11,7 +13,6 @@ def max_threads
|
|
11
13
|
5
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
16
|
class WorkerBase
|
16
17
|
extend Forwardable
|
17
18
|
attr_reader :name
|
@@ -106,7 +107,7 @@ describe 'Concurrent creation' do
|
|
106
107
|
run_workers
|
107
108
|
# duplication from at least one iteration:
|
108
109
|
expect(Tag.where(name: @names).size).to be > @iterations
|
109
|
-
end
|
110
|
+
end
|
110
111
|
|
111
112
|
class SiblingPrependerWorker < WorkerBase
|
112
113
|
def before_work
|
@@ -119,50 +120,6 @@ describe 'Concurrent creation' do
|
|
119
120
|
end
|
120
121
|
end
|
121
122
|
|
122
|
-
# TODO: this test should be rewritten to be proper producer-consumer code
|
123
|
-
xit 'fails to deadlock from parallel sibling churn' do
|
124
|
-
# target should be non-trivially long to maximize time spent in hierarchy maintenance
|
125
|
-
target = Tag.find_or_create_by_path(('a'..'z').to_a + ('A'..'Z').to_a)
|
126
|
-
expected_children = (1..100).to_a.map { |ea| "root ##{ea}" }
|
127
|
-
children_to_add = expected_children.dup
|
128
|
-
added_children = []
|
129
|
-
children_to_delete = []
|
130
|
-
deleted_children = []
|
131
|
-
creator_threads = @workers.times.map do
|
132
|
-
Thread.new do
|
133
|
-
while children_to_add.present?
|
134
|
-
name = children_to_add.shift
|
135
|
-
unless name.nil?
|
136
|
-
Tag.transaction { target.find_or_create_by_path(name) }
|
137
|
-
children_to_delete << name
|
138
|
-
added_children << name
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
run_destruction = true
|
144
|
-
destroyer_threads = @workers.times.map do
|
145
|
-
Thread.new do
|
146
|
-
begin
|
147
|
-
victim_name = children_to_delete.shift
|
148
|
-
if victim_name
|
149
|
-
Tag.transaction do
|
150
|
-
victim = target.children.where(name: victim_name).first
|
151
|
-
victim.destroy
|
152
|
-
deleted_children << victim_name
|
153
|
-
end
|
154
|
-
else
|
155
|
-
sleep rand # wait for more victims
|
156
|
-
end
|
157
|
-
end while run_destruction || !children_to_delete.empty?
|
158
|
-
end
|
159
|
-
end
|
160
|
-
creator_threads.each(&:join)
|
161
|
-
destroyer_threads.each(&:join)
|
162
|
-
expect(added_children).to match(expected_children)
|
163
|
-
expect(deleted_children).to match(expected_children)
|
164
|
-
end
|
165
|
-
|
166
123
|
it 'fails to deadlock while simultaneously deleting items from the same hierarchy' do
|
167
124
|
target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s })
|
168
125
|
emails = target.self_and_ancestors.to_a.map(&:email).shuffle
|
@@ -176,7 +133,7 @@ describe 'Concurrent creation' do
|
|
176
133
|
end
|
177
134
|
User.connection.reconnect!
|
178
135
|
expect(User.all).to be_empty
|
179
|
-
end
|
136
|
+
end
|
180
137
|
|
181
138
|
class SiblingPrependerWorker < WorkerBase
|
182
139
|
def before_work
|
@@ -198,6 +155,5 @@ describe 'Concurrent creation' do
|
|
198
155
|
|
199
156
|
# The only non-root node should be "root":
|
200
157
|
expect(Label.all.select { |ea| ea.root? }).to eq([@target.parent])
|
201
|
-
end
|
202
|
-
|
158
|
+
end
|
203
159
|
end if run_parallel_tests?
|
data/spec/spec_helper.rb
CHANGED
@@ -2,10 +2,8 @@ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
|
2
2
|
|
3
3
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
4
4
|
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
5
|
-
|
6
|
-
require 'active_record'
|
5
|
+
|
7
6
|
require 'database_cleaner'
|
8
|
-
require 'closure_tree'
|
9
7
|
require 'closure_tree/test/matcher'
|
10
8
|
require 'tmpdir'
|
11
9
|
require 'timecop'
|
data/spec/support/database.rb
CHANGED
data/tests.sh
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
#!/bin/sh -ex
|
2
2
|
|
3
|
-
for RMI in 2.
|
3
|
+
for RMI in 2.3.1 #jruby-1.6.13 :P
|
4
4
|
do
|
5
5
|
rbenv local $RMI
|
6
|
-
|
6
|
+
appraisal bundle install
|
7
|
+
for DB in mysql sqlite postgresql
|
7
8
|
do
|
8
9
|
appraisal rake spec:all WITH_ADVISORY_LOCK_PREFIX=$(date +%s) DB=$DB
|
9
10
|
done
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew McEachen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-07-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -56,16 +56,16 @@ dependencies:
|
|
56
56
|
name: rspec-rails
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: database_cleaner
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,20 +122,6 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: ammeter
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - '='
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: 1.1.2
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - '='
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: 1.1.2
|
139
125
|
description: Easily and efficiently make your ActiveRecord model support hierarchies
|
140
126
|
email:
|
141
127
|
- matthew-github@mceachen.org
|
@@ -156,11 +142,14 @@ files:
|
|
156
142
|
- closure_tree.gemspec
|
157
143
|
- gemfiles/activerecord_4.1.gemfile
|
158
144
|
- gemfiles/activerecord_4.2.gemfile
|
145
|
+
- gemfiles/activerecord_5.0.gemfile
|
146
|
+
- gemfiles/activerecord_5.0_foreigner.gemfile
|
159
147
|
- gemfiles/activerecord_edge.gemfile
|
160
148
|
- img/example.png
|
161
149
|
- img/preorder.png
|
162
150
|
- lib/closure_tree.rb
|
163
151
|
- lib/closure_tree/active_record_support.rb
|
152
|
+
- lib/closure_tree/configuration.rb
|
164
153
|
- lib/closure_tree/deterministic_ordering.rb
|
165
154
|
- lib/closure_tree/digraphs.rb
|
166
155
|
- lib/closure_tree/finders.rb
|
@@ -176,7 +165,9 @@ files:
|
|
176
165
|
- lib/closure_tree/support_flags.rb
|
177
166
|
- lib/closure_tree/test/matcher.rb
|
178
167
|
- lib/closure_tree/version.rb
|
168
|
+
- lib/generators/closure_tree/config_generator.rb
|
179
169
|
- lib/generators/closure_tree/migration_generator.rb
|
170
|
+
- lib/generators/closure_tree/templates/config.rb
|
180
171
|
- lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb
|
181
172
|
- mktree.rb
|
182
173
|
- spec/cache_invalidation_spec.rb
|
@@ -216,7 +207,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
216
207
|
requirements:
|
217
208
|
- - ">="
|
218
209
|
- !ruby/object:Gem::Version
|
219
|
-
version: 2.
|
210
|
+
version: 2.0.0
|
220
211
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
221
212
|
requirements:
|
222
213
|
- - ">="
|
@@ -224,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
224
215
|
version: '0'
|
225
216
|
requirements: []
|
226
217
|
rubyforge_project:
|
227
|
-
rubygems_version: 2.
|
218
|
+
rubygems_version: 2.5.1
|
228
219
|
signing_key:
|
229
220
|
specification_version: 4
|
230
221
|
summary: Easily and efficiently make your ActiveRecord model support hierarchies
|