closure_tree 6.1.0 → 6.2.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 +4 -4
- data/.gitignore +0 -0
- data/.rspec +0 -0
- data/.travis.yml +0 -0
- data/.yardopts +0 -0
- data/Appraisals +0 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +0 -0
- data/MIT-LICENSE +0 -0
- data/README.md +6 -1
- data/Rakefile +0 -0
- data/closure_tree.gemspec +0 -0
- data/gemfiles/activerecord_4.1.gemfile +0 -0
- data/gemfiles/activerecord_4.2.gemfile +0 -0
- data/gemfiles/activerecord_5.0.gemfile +0 -0
- data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -0
- data/gemfiles/activerecord_edge.gemfile +0 -0
- data/img/example.png +0 -0
- data/img/preorder.png +0 -0
- data/lib/closure_tree.rb +2 -0
- data/lib/closure_tree/active_record_support.rb +0 -0
- data/lib/closure_tree/configuration.rb +0 -0
- data/lib/closure_tree/deterministic_ordering.rb +0 -0
- data/lib/closure_tree/digraphs.rb +0 -0
- data/lib/closure_tree/finders.rb +3 -3
- data/lib/closure_tree/has_closure_tree.rb +0 -0
- data/lib/closure_tree/has_closure_tree_root.rb +88 -0
- data/lib/closure_tree/hash_tree.rb +0 -0
- data/lib/closure_tree/hash_tree_support.rb +0 -0
- data/lib/closure_tree/hierarchy_maintenance.rb +0 -0
- data/lib/closure_tree/model.rb +0 -0
- data/lib/closure_tree/numeric_deterministic_ordering.rb +1 -1
- data/lib/closure_tree/numeric_order_support.rb +0 -0
- data/lib/closure_tree/support.rb +0 -0
- data/lib/closure_tree/support_attributes.rb +1 -2
- data/lib/closure_tree/support_flags.rb +0 -0
- data/lib/closure_tree/test/matcher.rb +0 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/config_generator.rb +0 -0
- data/lib/generators/closure_tree/migration_generator.rb +0 -0
- data/lib/generators/closure_tree/templates/config.rb +0 -0
- data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +0 -0
- data/spec/cache_invalidation_spec.rb +0 -0
- data/spec/cuisine_type_spec.rb +0 -0
- data/spec/db/database.yml +0 -0
- data/spec/db/models.rb +24 -2
- data/spec/db/schema.rb +22 -0
- data/spec/fixtures/tags.yml +0 -0
- data/spec/generators/migration_generator_spec.rb +0 -0
- data/spec/has_closure_tree_root_spec.rb +132 -0
- data/spec/hierarchy_maintenance_spec.rb +0 -0
- data/spec/label_spec.rb +0 -0
- data/spec/matcher_spec.rb +0 -0
- data/spec/metal_spec.rb +0 -0
- data/spec/model_spec.rb +0 -0
- data/spec/namespace_type_spec.rb +0 -0
- data/spec/parallel_spec.rb +0 -0
- data/spec/spec_helper.rb +0 -0
- data/spec/support/database.rb +0 -0
- data/spec/support/database_cleaner.rb +0 -0
- data/spec/support/exceed_query_limit.rb +18 -0
- data/spec/support/hash_monkey_patch.rb +0 -0
- data/spec/support/query_counter.rb +18 -0
- data/spec/support/sqlite3_with_advisory_lock.rb +0 -0
- data/spec/support_spec.rb +0 -0
- data/spec/tag_examples.rb +0 -0
- data/spec/tag_spec.rb +0 -0
- data/spec/user_spec.rb +0 -0
- data/spec/uuid_tag_spec.rb +0 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3b96ba78aea59298da3138316379a458d28c713
|
|
4
|
+
data.tar.gz: f1c786fb41309ea4460a06ae296b7393ea058b63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5850fadca83ecdc88e53f4fac48a19faaafda82c7b6bd93f8051e264b632a94b56edbc3b22d9d060d658cf76dca02268fda625835c74e9e6cfb6ef75e9430d6
|
|
7
|
+
data.tar.gz: cae0ca262a5f9f76d1a01b24aac119a5307a49637a77e9287305630d2e97c2b3265a767180b5e76eedff9a4a9ada44ff07aebe648d0b109949c7ed2e012b9b93
|
data/.gitignore
CHANGED
|
File without changes
|
data/.rspec
CHANGED
|
File without changes
|
data/.travis.yml
CHANGED
|
File without changes
|
data/.yardopts
CHANGED
|
File without changes
|
data/Appraisals
CHANGED
|
File without changes
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
### 6.2.0
|
|
4
|
+
|
|
5
|
+
* Fix for [MySQL lock lengths](https://github.com/mceachen/closure_tree/issues/231).
|
|
6
|
+
Thanks to [Liam](https://github.com/hut8)!
|
|
7
|
+
* [Tom Smyth](https://github.com/hooverlunch) added [eager tree loading](https://github.com/mceachen/closure_tree/pull/232)
|
|
8
|
+
* Merged [PR 200](https://github.com/mceachen/closure_tree/pull/200) which may or may not add support to SQLServer 2008 (but this is not a supported RDBMS).
|
|
9
|
+
|
|
3
10
|
### 6.1.0
|
|
4
11
|
|
|
5
12
|
* Added official support for ActiveRecord 5.0! Thanks to [Abdelkader Boudih](https://github.com/seuros),
|
data/Gemfile
CHANGED
|
File without changes
|
data/MIT-LICENSE
CHANGED
|
File without changes
|
data/README.md
CHANGED
|
@@ -468,7 +468,12 @@ Yup! [Ilya Bodrov](https://github.com/bodrovis) wrote [Nested Comments with Rail
|
|
|
468
468
|
|
|
469
469
|
### Does this work well with ```#default_scope```?
|
|
470
470
|
|
|
471
|
-
No
|
|
471
|
+
**No.** Please see [issue 86](https://github.com/mceachen/closure_tree/issues/86) for details.
|
|
472
|
+
|
|
473
|
+
### Can I update parentage with `update_attribute`?
|
|
474
|
+
|
|
475
|
+
**No.** `update_attribute` skips the validation hook that is required for maintaining the
|
|
476
|
+
hierarchy table.
|
|
472
477
|
|
|
473
478
|
### Does this gem support multiple parents?
|
|
474
479
|
|
data/Rakefile
CHANGED
|
File without changes
|
data/closure_tree.gemspec
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/img/example.png
CHANGED
|
File without changes
|
data/img/preorder.png
CHANGED
|
File without changes
|
data/lib/closure_tree.rb
CHANGED
|
@@ -4,6 +4,7 @@ module ClosureTree
|
|
|
4
4
|
extend ActiveSupport::Autoload
|
|
5
5
|
|
|
6
6
|
autoload :HasClosureTree
|
|
7
|
+
autoload :HasClosureTreeRoot
|
|
7
8
|
autoload :Support
|
|
8
9
|
autoload :HierarchyMaintenance
|
|
9
10
|
autoload :Model
|
|
@@ -25,4 +26,5 @@ end
|
|
|
25
26
|
|
|
26
27
|
ActiveSupport.on_load :active_record do
|
|
27
28
|
ActiveRecord::Base.send :extend, ClosureTree::HasClosureTree
|
|
29
|
+
ActiveRecord::Base.send :extend, ClosureTree::HasClosureTreeRoot
|
|
28
30
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/closure_tree/finders.rb
CHANGED
|
@@ -39,7 +39,7 @@ module ClosureTree
|
|
|
39
39
|
SELECT descendant_id
|
|
40
40
|
FROM #{_ct.quoted_hierarchy_table_name}
|
|
41
41
|
WHERE ancestor_id = #{_ct.quote(self.id)}
|
|
42
|
-
GROUP BY
|
|
42
|
+
GROUP BY descendant_id
|
|
43
43
|
HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
|
|
44
44
|
) AS descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
|
|
45
45
|
SQL
|
|
@@ -74,7 +74,7 @@ module ClosureTree
|
|
|
74
74
|
INNER JOIN (
|
|
75
75
|
SELECT ancestor_id
|
|
76
76
|
FROM #{_ct.quoted_hierarchy_table_name}
|
|
77
|
-
GROUP BY
|
|
77
|
+
GROUP BY ancestor_id
|
|
78
78
|
HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
|
|
79
79
|
) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
|
|
80
80
|
SQL
|
|
@@ -100,7 +100,7 @@ module ClosureTree
|
|
|
100
100
|
INNER JOIN (
|
|
101
101
|
SELECT ancestor_id, descendant_id
|
|
102
102
|
FROM #{_ct.quoted_hierarchy_table_name}
|
|
103
|
-
GROUP BY
|
|
103
|
+
GROUP BY ancestor_id, descendant_id
|
|
104
104
|
HAVING MAX(generations) = #{generation_level.to_i}
|
|
105
105
|
) AS descendants ON (
|
|
106
106
|
#{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module ClosureTree
|
|
2
|
+
class MultipleRootError < StandardError; end
|
|
3
|
+
|
|
4
|
+
module HasClosureTreeRoot
|
|
5
|
+
|
|
6
|
+
def has_closure_tree_root(assoc_name, options = {})
|
|
7
|
+
options.assert_valid_keys(
|
|
8
|
+
:class_name,
|
|
9
|
+
:foreign_key
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
options[:class_name] ||= assoc_name.to_s.sub(/\Aroot_/, "").classify
|
|
13
|
+
options[:foreign_key] ||= self.name.underscore << "_id"
|
|
14
|
+
|
|
15
|
+
has_one assoc_name, -> { where(parent: nil) }, options
|
|
16
|
+
|
|
17
|
+
# Fetches the association, eager loading all children and given associations
|
|
18
|
+
define_method("#{assoc_name}_including_tree") do |assoc_map_or_reload = nil, assoc_map = nil|
|
|
19
|
+
reload = false
|
|
20
|
+
if assoc_map_or_reload.is_a?(::Hash)
|
|
21
|
+
assoc_map = assoc_map_or_reload
|
|
22
|
+
else
|
|
23
|
+
reload = assoc_map_or_reload
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless reload
|
|
27
|
+
# Memoize
|
|
28
|
+
@closure_tree_roots ||= {}
|
|
29
|
+
@closure_tree_roots[assoc_name] ||= {}
|
|
30
|
+
if @closure_tree_roots[assoc_name].has_key?(assoc_map)
|
|
31
|
+
return @closure_tree_roots[assoc_name][assoc_map]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
roots = options[:class_name].constantize.where(parent: nil, options[:foreign_key] => id).to_a
|
|
36
|
+
|
|
37
|
+
return nil if roots.empty?
|
|
38
|
+
|
|
39
|
+
if roots.size > 1
|
|
40
|
+
raise MultipleRootError.new("#{self.class.name}: has_closure_tree_root requires a single root")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
temp_root = roots.first
|
|
44
|
+
root = nil
|
|
45
|
+
id_hash = {}
|
|
46
|
+
parent_col_id = temp_root.class._ct.options[:parent_column_name]
|
|
47
|
+
|
|
48
|
+
# Lookup inverse belongs_to association reflection on target class.
|
|
49
|
+
inverse = temp_root.class.reflections.values.detect do |r|
|
|
50
|
+
r.macro == :belongs_to && r.klass == self.class
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Fetch all descendants in constant number of queries.
|
|
54
|
+
# This is the last query-triggering statement in the method.
|
|
55
|
+
temp_root.self_and_descendants.includes(assoc_map).each do |node|
|
|
56
|
+
id_hash[node.id] = node
|
|
57
|
+
parent_node = id_hash[node[parent_col_id]]
|
|
58
|
+
|
|
59
|
+
# Pre-assign parent association
|
|
60
|
+
parent_assoc = node.association(:parent)
|
|
61
|
+
parent_assoc.loaded!
|
|
62
|
+
parent_assoc.target = parent_node
|
|
63
|
+
|
|
64
|
+
# Pre-assign children association as empty for now,
|
|
65
|
+
# children will be added in subsequent loop iterations
|
|
66
|
+
children_assoc = node.association(:children)
|
|
67
|
+
children_assoc.loaded!
|
|
68
|
+
|
|
69
|
+
if parent_node
|
|
70
|
+
parent_node.association(:children).target << node
|
|
71
|
+
else
|
|
72
|
+
# Capture the root we're going to use
|
|
73
|
+
root = node
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Pre-assign inverse association back to this class, if it exists on target class.
|
|
77
|
+
if inverse
|
|
78
|
+
inverse_assoc = node.association(inverse.name)
|
|
79
|
+
inverse_assoc.loaded!
|
|
80
|
+
inverse_assoc.target = self
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@closure_tree_roots[assoc_name][assoc_map] = root
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/closure_tree/model.rb
CHANGED
|
File without changes
|
|
@@ -72,7 +72,7 @@ module ClosureTree
|
|
|
72
72
|
JOIN (
|
|
73
73
|
SELECT descendant_id, max(generations) AS max_depth
|
|
74
74
|
FROM #{_ct.quoted_hierarchy_table_name}
|
|
75
|
-
GROUP BY
|
|
75
|
+
GROUP BY descendant_id
|
|
76
76
|
) AS depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
|
|
77
77
|
SQL
|
|
78
78
|
joins(join_sql)
|
|
File without changes
|
data/lib/closure_tree/support.rb
CHANGED
|
File without changes
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
require 'forwardable'
|
|
2
2
|
module ClosureTree
|
|
3
3
|
module SupportAttributes
|
|
4
|
-
|
|
5
4
|
extend Forwardable
|
|
6
5
|
def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
|
|
7
6
|
|
|
8
7
|
def advisory_lock_name
|
|
9
|
-
"ClosureTree::#{base_class.name}"
|
|
8
|
+
Digest::SHA1.hexdigest("ClosureTree::#{base_class.name}")[0..32]
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def quoted_table_name
|
|
File without changes
|
|
File without changes
|
data/lib/closure_tree/version.rb
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/spec/cuisine_type_spec.rb
CHANGED
|
File without changes
|
data/spec/db/database.yml
CHANGED
|
File without changes
|
data/spec/db/models.rb
CHANGED
|
@@ -35,13 +35,30 @@ end
|
|
|
35
35
|
class DestroyedTag < ActiveRecord::Base
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
class Group < ActiveRecord::Base
|
|
39
|
+
has_closure_tree_root :root_user
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Grouping < ActiveRecord::Base
|
|
43
|
+
has_closure_tree_root :root_person, class_name: "User", foreign_key: :group_id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class UserSet < ActiveRecord::Base
|
|
47
|
+
has_closure_tree_root :root_user, class_name: "Useur"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Team < ActiveRecord::Base
|
|
51
|
+
has_closure_tree_root :root_user, class_name: "User", foreign_key: :grp_id
|
|
52
|
+
end
|
|
53
|
+
|
|
38
54
|
class User < ActiveRecord::Base
|
|
39
55
|
acts_as_tree :parent_column_name => "referrer_id",
|
|
40
56
|
:name_column => 'email',
|
|
41
57
|
:hierarchy_class_name => 'ReferralHierarchy',
|
|
42
58
|
:hierarchy_table_name => 'referral_hierarchies'
|
|
43
59
|
|
|
44
|
-
has_many :contracts
|
|
60
|
+
has_many :contracts, inverse_of: :user
|
|
61
|
+
belongs_to :group # Can't use and don't need inverse_of here when using has_closure_tree_root.
|
|
45
62
|
|
|
46
63
|
def indirect_contracts
|
|
47
64
|
Contract.where(:user_id => descendant_ids)
|
|
@@ -53,7 +70,12 @@ class User < ActiveRecord::Base
|
|
|
53
70
|
end
|
|
54
71
|
|
|
55
72
|
class Contract < ActiveRecord::Base
|
|
56
|
-
belongs_to :user
|
|
73
|
+
belongs_to :user, inverse_of: :contracts
|
|
74
|
+
belongs_to :contract_type, inverse_of: :contracts
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class ContractType < ActiveRecord::Base
|
|
78
|
+
has_many :contracts, inverse_of: :contract_type
|
|
57
79
|
end
|
|
58
80
|
|
|
59
81
|
class Label < ActiveRecord::Base
|
data/spec/db/schema.rb
CHANGED
|
@@ -43,9 +43,26 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
|
43
43
|
add_index "tag_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "tag_anc_desc_idx"
|
|
44
44
|
add_index "tag_hierarchies", [:descendant_id], :name => "tag_desc_idx"
|
|
45
45
|
|
|
46
|
+
create_table "groups" do |t|
|
|
47
|
+
t.string "name", null: false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
create_table "groupings" do |t|
|
|
51
|
+
t.string "name", null: false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
create_table "user_sets" do |t|
|
|
55
|
+
t.string "name", null: false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
create_table "teams" do |t|
|
|
59
|
+
t.string "name", null: false
|
|
60
|
+
end
|
|
61
|
+
|
|
46
62
|
create_table "users" do |t|
|
|
47
63
|
t.string "email"
|
|
48
64
|
t.integer "referrer_id"
|
|
65
|
+
t.integer "group_id"
|
|
49
66
|
t.timestamps null: false
|
|
50
67
|
end
|
|
51
68
|
|
|
@@ -53,6 +70,11 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
|
53
70
|
|
|
54
71
|
create_table "contracts" do |t|
|
|
55
72
|
t.integer "user_id", :null => false
|
|
73
|
+
t.integer "contract_type_id"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
create_table "contract_types" do |t|
|
|
77
|
+
t.string "name", :null => false
|
|
56
78
|
end
|
|
57
79
|
|
|
58
80
|
create_table "referral_hierarchies", :id => false do |t|
|
data/spec/fixtures/tags.yml
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe "has_closure_tree_root" do
|
|
4
|
+
let!(:ct1) { ContractType.create!(name: "Type1") }
|
|
5
|
+
let!(:ct2) { ContractType.create!(name: "Type2") }
|
|
6
|
+
let!(:user1) { User.create!(email: "1@example.com", group_id: group.id) }
|
|
7
|
+
let!(:user2) { User.create!(email: "2@example.com", group_id: group.id) }
|
|
8
|
+
let!(:user3) { User.create!(email: "3@example.com", group_id: group.id) }
|
|
9
|
+
let!(:user4) { User.create!(email: "4@example.com", group_id: group.id) }
|
|
10
|
+
let!(:user5) { User.create!(email: "5@example.com", group_id: group.id) }
|
|
11
|
+
let!(:user6) { User.create!(email: "6@example.com", group_id: group.id) }
|
|
12
|
+
let!(:group_reloaded) { group.class.find(group.id) } # Ensures were starting fresh.
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
# The tree (contract types in parens)
|
|
16
|
+
#
|
|
17
|
+
# U1(1)
|
|
18
|
+
# / \
|
|
19
|
+
# U2(1) U3(1&2)
|
|
20
|
+
# / / \
|
|
21
|
+
# U4(2) U5(1) U6(2)
|
|
22
|
+
|
|
23
|
+
user1.children << user2
|
|
24
|
+
user1.children << user3
|
|
25
|
+
user2.children << user4
|
|
26
|
+
user3.children << user5
|
|
27
|
+
user3.children << user6
|
|
28
|
+
|
|
29
|
+
user1.contracts.create!(contract_type: ct1)
|
|
30
|
+
user2.contracts.create!(contract_type: ct1)
|
|
31
|
+
user3.contracts.create!(contract_type: ct1)
|
|
32
|
+
user3.contracts.create!(contract_type: ct2)
|
|
33
|
+
user4.contracts.create!(contract_type: ct2)
|
|
34
|
+
user5.contracts.create!(contract_type: ct1)
|
|
35
|
+
user6.contracts.create!(contract_type: ct2)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context "with basic config" do
|
|
39
|
+
let!(:group) { Group.create!(name: "TheGroup") }
|
|
40
|
+
|
|
41
|
+
it "loads all nodes and associations in a constant number of queries" do
|
|
42
|
+
expect do
|
|
43
|
+
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
|
44
|
+
expect(root.children[0].email).to eq "2@example.com"
|
|
45
|
+
expect(root.children[0].parent.children[1].email).to eq "3@example.com"
|
|
46
|
+
expect(root.children[1].contracts.map(&:contract_type).map(&:name)).to eq %w(Type1 Type2)
|
|
47
|
+
expect(root.children[1].children[0].contracts[0].contract_type.name).to eq "Type1"
|
|
48
|
+
expect(root.children[0].children[0].contracts[0].user.
|
|
49
|
+
parent.parent.children[1].children[1].contracts[0].contract_type.name).to eq "Type2"
|
|
50
|
+
end.to_not exceed_query_limit(4) # Without this feature, this is 15, and scales with number of nodes.
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "memoizes by assoc_map" do
|
|
54
|
+
group_reloaded.root_user_including_tree.email = "x"
|
|
55
|
+
expect(group_reloaded.root_user_including_tree.email).to eq "x"
|
|
56
|
+
group_reloaded.root_user_including_tree(contracts: :contract_type).email = "y"
|
|
57
|
+
expect(group_reloaded.root_user_including_tree(contracts: :contract_type).email).to eq "y"
|
|
58
|
+
expect(group_reloaded.root_user_including_tree.email).to eq "x"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "doesn't memoize if true argument passed" do
|
|
62
|
+
group_reloaded.root_user_including_tree.email = "x"
|
|
63
|
+
expect(group_reloaded.root_user_including_tree(true).email).to eq "1@example.com"
|
|
64
|
+
group_reloaded.root_user_including_tree(contracts: :contract_type).email = "y"
|
|
65
|
+
expect(group_reloaded.root_user_including_tree(true, contracts: :contract_type).email).
|
|
66
|
+
to eq "1@example.com"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "eager loads inverse association to group" do
|
|
70
|
+
expect do
|
|
71
|
+
root = group_reloaded.root_user_including_tree
|
|
72
|
+
expect(root.group).to eq group
|
|
73
|
+
expect(root.children[0].group).to eq group
|
|
74
|
+
end.to_not exceed_query_limit(2)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "works if eager load association map is not given" do
|
|
78
|
+
expect do
|
|
79
|
+
root = group_reloaded.root_user_including_tree
|
|
80
|
+
expect(root.children[0].email).to eq "2@example.com"
|
|
81
|
+
expect(root.children[0].parent.children[1].children[0].email).to eq "5@example.com"
|
|
82
|
+
end.to_not exceed_query_limit(2)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context "with no tree root" do
|
|
86
|
+
let(:group2) { Group.create!(name: "OtherGroup") }
|
|
87
|
+
|
|
88
|
+
it "should return nil" do
|
|
89
|
+
expect(group2.root_user_including_tree(contracts: :contract_type)).to be_nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "with multiple tree roots" do
|
|
94
|
+
let!(:other_root) { User.create!(email: "10@example.com", group_id: group.id) }
|
|
95
|
+
|
|
96
|
+
it "should error" do
|
|
97
|
+
expect do
|
|
98
|
+
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
|
99
|
+
end.to raise_error(ClosureTree::MultipleRootError)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context "with explicit class_name and foreign_key" do
|
|
105
|
+
let(:group) { Grouping.create!(name: "TheGrouping") }
|
|
106
|
+
|
|
107
|
+
it "should still work" do
|
|
108
|
+
root = group_reloaded.root_person_including_tree(contracts: :contract_type)
|
|
109
|
+
expect(root.children[0].email).to eq "2@example.com"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context "with bad class_name" do
|
|
114
|
+
let(:group) { UserSet.create!(name: "TheUserSet") }
|
|
115
|
+
|
|
116
|
+
it "should error" do
|
|
117
|
+
expect do
|
|
118
|
+
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
|
119
|
+
end.to raise_error(NameError)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
context "with bad foreign_key" do
|
|
124
|
+
let(:group) { Team.create!(name: "TheTeam") }
|
|
125
|
+
|
|
126
|
+
it "should error" do
|
|
127
|
+
expect do
|
|
128
|
+
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
|
129
|
+
end.to raise_error(ActiveRecord::StatementInvalid)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
File without changes
|
data/spec/label_spec.rb
CHANGED
|
File without changes
|
data/spec/matcher_spec.rb
CHANGED
|
File without changes
|
data/spec/metal_spec.rb
CHANGED
|
File without changes
|
data/spec/model_spec.rb
CHANGED
|
File without changes
|
data/spec/namespace_type_spec.rb
CHANGED
|
File without changes
|
data/spec/parallel_spec.rb
CHANGED
|
File without changes
|
data/spec/spec_helper.rb
CHANGED
|
File without changes
|
data/spec/support/database.rb
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Derived from http://stackoverflow.com/a/13423584/153896. Updated for RSpec 3.
|
|
2
|
+
RSpec::Matchers.define :exceed_query_limit do |expected|
|
|
3
|
+
supports_block_expectations
|
|
4
|
+
|
|
5
|
+
match do |block|
|
|
6
|
+
query_count(&block) > expected
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
failure_message_when_negated do |actual|
|
|
10
|
+
"Expected to run maximum #{expected} queries, got #{@counter.query_count}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def query_count(&block)
|
|
14
|
+
@counter = ActiveRecord::QueryCounter.new
|
|
15
|
+
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
|
|
16
|
+
@counter.query_count
|
|
17
|
+
end
|
|
18
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# From http://stackoverflow.com/a/13423584/153896
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
class QueryCounter
|
|
4
|
+
attr_reader :query_count
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@query_count = 0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_proc
|
|
11
|
+
lambda(&method(:callback))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def callback(name, start, finish, message_id, values)
|
|
15
|
+
@query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name])
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
File without changes
|
data/spec/support_spec.rb
CHANGED
|
File without changes
|
data/spec/tag_examples.rb
CHANGED
|
File without changes
|
data/spec/tag_spec.rb
CHANGED
|
File without changes
|
data/spec/user_spec.rb
CHANGED
|
File without changes
|
data/spec/uuid_tag_spec.rb
CHANGED
|
File without changes
|
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.2.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: 2016-
|
|
11
|
+
date: 2016-10-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -154,6 +154,7 @@ files:
|
|
|
154
154
|
- lib/closure_tree/digraphs.rb
|
|
155
155
|
- lib/closure_tree/finders.rb
|
|
156
156
|
- lib/closure_tree/has_closure_tree.rb
|
|
157
|
+
- lib/closure_tree/has_closure_tree_root.rb
|
|
157
158
|
- lib/closure_tree/hash_tree.rb
|
|
158
159
|
- lib/closure_tree/hash_tree_support.rb
|
|
159
160
|
- lib/closure_tree/hierarchy_maintenance.rb
|
|
@@ -177,6 +178,7 @@ files:
|
|
|
177
178
|
- spec/db/schema.rb
|
|
178
179
|
- spec/fixtures/tags.yml
|
|
179
180
|
- spec/generators/migration_generator_spec.rb
|
|
181
|
+
- spec/has_closure_tree_root_spec.rb
|
|
180
182
|
- spec/hierarchy_maintenance_spec.rb
|
|
181
183
|
- spec/label_spec.rb
|
|
182
184
|
- spec/matcher_spec.rb
|
|
@@ -187,7 +189,9 @@ files:
|
|
|
187
189
|
- spec/spec_helper.rb
|
|
188
190
|
- spec/support/database.rb
|
|
189
191
|
- spec/support/database_cleaner.rb
|
|
192
|
+
- spec/support/exceed_query_limit.rb
|
|
190
193
|
- spec/support/hash_monkey_patch.rb
|
|
194
|
+
- spec/support/query_counter.rb
|
|
191
195
|
- spec/support/sqlite3_with_advisory_lock.rb
|
|
192
196
|
- spec/support_spec.rb
|
|
193
197
|
- spec/tag_examples.rb
|
|
@@ -227,6 +231,7 @@ test_files:
|
|
|
227
231
|
- spec/db/schema.rb
|
|
228
232
|
- spec/fixtures/tags.yml
|
|
229
233
|
- spec/generators/migration_generator_spec.rb
|
|
234
|
+
- spec/has_closure_tree_root_spec.rb
|
|
230
235
|
- spec/hierarchy_maintenance_spec.rb
|
|
231
236
|
- spec/label_spec.rb
|
|
232
237
|
- spec/matcher_spec.rb
|
|
@@ -237,7 +242,9 @@ test_files:
|
|
|
237
242
|
- spec/spec_helper.rb
|
|
238
243
|
- spec/support/database.rb
|
|
239
244
|
- spec/support/database_cleaner.rb
|
|
245
|
+
- spec/support/exceed_query_limit.rb
|
|
240
246
|
- spec/support/hash_monkey_patch.rb
|
|
247
|
+
- spec/support/query_counter.rb
|
|
241
248
|
- spec/support/sqlite3_with_advisory_lock.rb
|
|
242
249
|
- spec/support_spec.rb
|
|
243
250
|
- spec/tag_examples.rb
|