closure_tree 6.6.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +7 -6
  3. data/Appraisals +59 -1
  4. data/CHANGELOG.md +21 -0
  5. data/Gemfile +0 -12
  6. data/README.md +40 -3
  7. data/closure_tree.gemspec +10 -7
  8. data/lib/closure_tree/finders.rb +15 -5
  9. data/lib/closure_tree/has_closure_tree.rb +2 -0
  10. data/lib/closure_tree/has_closure_tree_root.rb +2 -6
  11. data/lib/closure_tree/hash_tree_support.rb +1 -1
  12. data/lib/closure_tree/hierarchy_maintenance.rb +3 -3
  13. data/lib/closure_tree/model.rb +31 -1
  14. data/lib/closure_tree/numeric_deterministic_ordering.rb +11 -2
  15. data/lib/closure_tree/numeric_order_support.rb +3 -0
  16. data/lib/closure_tree/support.rb +7 -3
  17. data/lib/closure_tree/support_attributes.rb +9 -0
  18. data/lib/closure_tree/support_flags.rb +1 -4
  19. data/lib/closure_tree/version.rb +1 -1
  20. data/lib/generators/closure_tree/migration_generator.rb +8 -0
  21. data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
  22. metadata +28 -76
  23. data/gemfiles/activerecord_4.2.gemfile +0 -19
  24. data/gemfiles/activerecord_5.0.gemfile +0 -19
  25. data/gemfiles/activerecord_5.1.gemfile +0 -19
  26. data/gemfiles/activerecord_edge.gemfile +0 -20
  27. data/img/example.png +0 -0
  28. data/img/preorder.png +0 -0
  29. data/spec/cache_invalidation_spec.rb +0 -39
  30. data/spec/cuisine_type_spec.rb +0 -38
  31. data/spec/db/database.yml +0 -21
  32. data/spec/db/models.rb +0 -128
  33. data/spec/db/schema.rb +0 -166
  34. data/spec/fixtures/tags.yml +0 -98
  35. data/spec/generators/migration_generator_spec.rb +0 -48
  36. data/spec/has_closure_tree_root_spec.rb +0 -154
  37. data/spec/hierarchy_maintenance_spec.rb +0 -16
  38. data/spec/label_spec.rb +0 -554
  39. data/spec/matcher_spec.rb +0 -34
  40. data/spec/metal_spec.rb +0 -55
  41. data/spec/model_spec.rb +0 -9
  42. data/spec/namespace_type_spec.rb +0 -13
  43. data/spec/parallel_spec.rb +0 -159
  44. data/spec/pool_spec.rb +0 -27
  45. data/spec/spec_helper.rb +0 -24
  46. data/spec/support/database.rb +0 -52
  47. data/spec/support/database_cleaner.rb +0 -14
  48. data/spec/support/exceed_query_limit.rb +0 -18
  49. data/spec/support/hash_monkey_patch.rb +0 -13
  50. data/spec/support/query_counter.rb +0 -18
  51. data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
  52. data/spec/support_spec.rb +0 -14
  53. data/spec/tag_examples.rb +0 -665
  54. data/spec/tag_spec.rb +0 -6
  55. data/spec/user_spec.rb +0 -174
  56. data/spec/uuid_tag_spec.rb +0 -6
@@ -1,34 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'ClosureTree::Test::Matcher' do
4
-
5
- describe 'be_a_closure_tree' do
6
- it { expect(UUIDTag).to be_a_closure_tree }
7
- it { expect(User).to be_a_closure_tree }
8
- it { expect(Label).to be_a_closure_tree.ordered }
9
- it { expect(Metal).to be_a_closure_tree.ordered(:sort_order) }
10
- it { expect(MenuItem).to be_a_closure_tree }
11
- it { expect(Contract).not_to be_a_closure_tree }
12
- end
13
-
14
- describe 'ordered' do
15
- it { expect(Label).to be_a_closure_tree.ordered }
16
- it { expect(UUIDTag).to be_a_closure_tree.ordered }
17
- it { expect(Metal).to be_a_closure_tree.ordered(:sort_order) }
18
- end
19
-
20
- describe 'advisory_lock' do
21
- it 'should use advisory lock' do
22
- expect(User).to be_a_closure_tree.with_advisory_lock
23
- expect(Label).to be_a_closure_tree.ordered.with_advisory_lock
24
- expect(Metal).to be_a_closure_tree.ordered(:sort_order).with_advisory_lock
25
- end
26
-
27
- describe MenuItem do
28
- it 'should not use advisory lock' do
29
- is_expected.to be_a_closure_tree.without_advisory_lock
30
- end
31
- end
32
- end
33
-
34
- end
@@ -1,55 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Metal do
4
- describe '#find_or_create_by_path' do
5
- def assert_correctness(grandchild)
6
- expect(grandchild).to be_a(Metal)
7
- expect(grandchild.description).to eq('slag')
8
- child = grandchild.parent
9
- expect(child).to be_a(Unobtanium)
10
- expect(child.description).to eq('frames')
11
- expect(child.value).to eq('child')
12
- parent = child.parent
13
- expect(parent).to be_a(Adamantium)
14
- expect(parent.description).to eq('claws')
15
- expect(parent.value).to eq('parent')
16
- end
17
-
18
- let(:attr_path) do
19
- [
20
- {value: 'parent', description: 'claws', metal_type: 'Adamantium'},
21
- {value: 'child', description: 'frames', metal_type: 'Unobtanium'},
22
- {value: 'grandchild', description: 'slag', metal_type: 'Metal'}
23
- ]
24
- end
25
-
26
- before do
27
- # ensure the correct root is used in find_or_create_by_path:
28
- [Metal, Adamantium, Unobtanium].each do |metal|
29
- metal.find_or_create_by_path(%w(parent child grandchild))
30
- end
31
- end if false
32
-
33
- it 'creates children from the proper root' do
34
- assert_correctness(Metal.find_or_create_by_path(attr_path))
35
- end
36
-
37
- it 'supports STI from the base class' do
38
- assert_correctness(Metal.find_or_create_by_path(attr_path))
39
- end
40
-
41
- it 'supports STI from a subclass' do
42
- parent = Adamantium.create!(value: 'parent', description: 'claws')
43
- assert_correctness(parent.find_or_create_by_path(attr_path.last(2)))
44
- end
45
-
46
- it 'maintains the current STI subclass if attributes are not specified' do
47
- leaf = Unobtanium.find_or_create_by_path(%w(a b c d))
48
- expect(leaf).to be_a(Unobtanium)
49
- expect(leaf.ancestors.map(&:value)).to eq(%w(c b a))
50
- leaf.ancestors.each do |anc|
51
- expect(anc).to be_a(Unobtanium)
52
- end
53
- end
54
- end
55
- end
@@ -1,9 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe ClosureTree::Model do
4
- describe '#_ct' do
5
- it 'should delegate to the Support instance on the class' do
6
- expect(Tag.new._ct).to eq(Tag._ct)
7
- end
8
- end
9
- end
@@ -1,13 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Namespace::Type do
4
-
5
- context "class injection" do
6
- it "should build hierarchy classname correctly" do
7
- expect(Namespace::Type.hierarchy_class.to_s).to eq("Namespace::TypeHierarchy")
8
- expect(Namespace::Type._ct.hierarchy_class_name).to eq("Namespace::TypeHierarchy")
9
- expect(Namespace::Type._ct.short_hierarchy_class_name).to eq("TypeHierarchy")
10
- end
11
- end
12
-
13
- end
@@ -1,159 +0,0 @@
1
- require 'spec_helper'
2
-
3
- # We don't need to run the expensive parallel tests for every combination of prefix/suffix.
4
- # Those affect SQL generation, not parallelism.
5
- # SQLite doesn't support concurrency reliably, either.
6
- def run_parallel_tests?
7
- !sqlite? &&
8
- ActiveRecord::Base.table_name_prefix.empty? &&
9
- ActiveRecord::Base.table_name_suffix.empty?
10
- end
11
-
12
- def max_threads
13
- 5
14
- end
15
-
16
- class WorkerBase
17
- extend Forwardable
18
- attr_reader :name
19
- def_delegators :@thread, :join, :wakeup, :status, :to_s
20
-
21
- def log(msg)
22
- puts("#{Thread.current}: #{msg}") if ENV['VERBOSE']
23
- end
24
-
25
- def initialize(target, name)
26
- @target = target
27
- @name = name
28
- @thread = Thread.new do
29
- ActiveRecord::Base.connection_pool.with_connection { before_work } if respond_to? :before_work
30
- log 'going to sleep...'
31
- sleep
32
- log 'woke up...'
33
- ActiveRecord::Base.connection_pool.with_connection { work }
34
- log 'done.'
35
- end
36
- end
37
- end
38
-
39
- class FindOrCreateWorker < WorkerBase
40
- def work
41
- path = [name, :a, :b, :c]
42
- log "making #{path}..."
43
- t = (@target || Tag).find_or_create_by_path(path)
44
- log "made #{t.id}, #{t.ancestry_path}"
45
- end
46
- end
47
-
48
- describe 'Concurrent creation' do
49
- before :each do
50
- @target = nil
51
- @iterations = 5
52
- end
53
-
54
- def log(msg)
55
- puts(msg) if ENV['VERBOSE']
56
- end
57
-
58
- def run_workers(worker_class = FindOrCreateWorker)
59
- @names = @iterations.times.map { |iter| "iteration ##{iter}" }
60
- @names.each do |name|
61
- workers = max_threads.times.map { worker_class.new(@target, name) }
62
- # Wait for all the threads to get ready:
63
- while true
64
- unready_workers = workers.select { |ea| ea.status != 'sleep' }
65
- if unready_workers.empty?
66
- break
67
- else
68
- log "Not ready to wakeup: #{unready_workers.map { |ea| [ea.to_s, ea.status] }}"
69
- sleep(0.1)
70
- end
71
- end
72
- sleep(0.25)
73
- # OK, GO!
74
- log 'Calling .wakeup on all workers...'
75
- workers.each(&:wakeup)
76
- sleep(0.25)
77
- # Then wait for them to finish:
78
- log 'Calling .join on all workers...'
79
- workers.each(&:join)
80
- end
81
- # Ensure we're still connected:
82
- ActiveRecord::Base.connection_pool.connection
83
- end
84
-
85
- it 'will not create dupes from class methods' do
86
- run_workers
87
- expect(Tag.roots.collect { |ea| ea.name }).to match_array(@names)
88
- # No dupe children:
89
- %w(a b c).each do |ea|
90
- expect(Tag.where(name: ea).size).to eq(@iterations)
91
- end
92
- end
93
-
94
- it 'will not create dupes from instance methods' do
95
- @target = Tag.create!(name: 'root')
96
- run_workers
97
- expect(@target.reload.children.collect { |ea| ea.name }).to match_array(@names)
98
- expect(Tag.where(name: @names).size).to eq(@iterations)
99
- %w(a b c).each do |ea|
100
- expect(Tag.where(name: ea).size).to eq(@iterations)
101
- end
102
- end
103
-
104
- it 'creates dupe roots without advisory locks' do
105
- # disable with_advisory_lock:
106
- allow(Tag).to receive(:with_advisory_lock) { |_lock_name, &block| block.call }
107
- run_workers
108
- # duplication from at least one iteration:
109
- expect(Tag.where(name: @names).size).to be > @iterations
110
- end
111
-
112
- class SiblingPrependerWorker < WorkerBase
113
- def before_work
114
- @target.reload
115
- @sibling = Label.new(name: SecureRandom.hex(10))
116
- end
117
-
118
- def work
119
- @target.prepend_sibling @sibling
120
- end
121
- end
122
-
123
- it 'fails to deadlock while simultaneously deleting items from the same hierarchy' do
124
- target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s })
125
- emails = target.self_and_ancestors.to_a.map(&:email).shuffle
126
- Parallel.map(emails, :in_threads => max_threads) do |email|
127
- ActiveRecord::Base.connection_pool.with_connection do
128
- User.transaction do
129
- log "Destroying #{email}..."
130
- User.where(email: email).destroy_all
131
- end
132
- end
133
- end
134
- User.connection.reconnect!
135
- expect(User.all).to be_empty
136
- end
137
-
138
- class SiblingPrependerWorker < WorkerBase
139
- def before_work
140
- @target.reload
141
- @sibling = Label.new(name: SecureRandom.hex(10))
142
- end
143
-
144
- def work
145
- @target.prepend_sibling @sibling
146
- end
147
- end
148
-
149
- it 'fails to deadlock from prepending siblings' do
150
- @target = Label.find_or_create_by_path %w(root parent)
151
- run_workers(SiblingPrependerWorker)
152
- children = Label.roots
153
- uniq_order_values = children.collect { |ea| ea.order_value }.uniq
154
- expect(children.size).to eq(uniq_order_values.size)
155
-
156
- # The only non-root node should be "root":
157
- expect(Label.all.select { |ea| ea.root? }).to eq([@target.parent])
158
- end
159
- end if run_parallel_tests?
@@ -1,27 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'Configuration' do
4
- it 'returns connection to the pool after has_closure_tree setup' do
5
- class TypeDuplicate < ActiveRecord::Base
6
- self.table_name = "namespace_type#{table_name_suffix}"
7
- has_closure_tree
8
- end
9
- expect(ActiveRecord::Base.connection_pool.active_connection?).to be_falsey # +false+ in AR 4, +nil+ in AR 5
10
- end
11
-
12
- it 'returns connection to the pool after has_closure_tree setup with order' do
13
- class MetalDuplicate < ActiveRecord::Base
14
- self.table_name = "#{table_name_prefix}metal#{table_name_suffix}"
15
- has_closure_tree order: 'sort_order', name_column: 'value'
16
- end
17
- expect(ActiveRecord::Base.connection_pool.active_connection?).to be_falsey
18
- end
19
-
20
- it 'returns connection to the pool after has_closure_tree_root setup' do
21
- class GroupDuplicate < ActiveRecord::Base
22
- self.table_name = "#{table_name_prefix}group#{table_name_suffix}"
23
- has_closure_tree_root :root_user
24
- end
25
- expect(ActiveRecord::Base.connection_pool.active_connection?).to be_falsey
26
- end
27
- end
@@ -1,24 +0,0 @@
1
- $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
2
-
3
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
4
- require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5
-
6
- require 'database_cleaner'
7
- require 'closure_tree/test/matcher'
8
- require 'tmpdir'
9
- require 'timecop'
10
- require 'forwardable'
11
- require 'parallel'
12
- begin
13
- require 'foreigner'
14
- rescue LoadError
15
- #Foreigner is not needed in ActiveRecord 4.2+
16
- end
17
-
18
- Thread.abort_on_exception = true
19
-
20
- Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
21
-
22
- RSpec.configure do |config|
23
- config.include ClosureTree::Test::Matcher
24
- end
@@ -1,52 +0,0 @@
1
- database_folder = "#{File.dirname(__FILE__)}/../db"
2
- database_adapter = ENV['DB'] ||= 'postgresql'
3
-
4
- def sqlite?
5
- ENV['DB'] == 'sqlite'
6
- end
7
-
8
- log = Logger.new('db.log')
9
- log.sev_threshold = Logger::DEBUG
10
- ActiveRecord::Base.logger = log
11
-
12
- ActiveRecord::Migration.verbose = false
13
- ActiveRecord::Base.table_name_prefix = ENV['DB_PREFIX'].to_s
14
- ActiveRecord::Base.table_name_suffix = ENV['DB_SUFFIX'].to_s
15
-
16
- def db_name
17
- @db_name ||= "closure_tree_test_#{rand(1..2**31)}"
18
- end
19
-
20
- ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read("#{database_folder}/database.yml")).result)
21
-
22
- config = ActiveRecord::Base.configurations[database_adapter]
23
-
24
- begin
25
- case database_adapter
26
- when 'sqlite'
27
- ActiveRecord::Base.establish_connection(database_adapter.to_sym)
28
- when 'mysql'
29
- ActiveRecord::Base.establish_connection(config.merge('database' => nil))
30
- ActiveRecord::Base.connection.recreate_database(config['database'], {charset: 'utf8', collation: 'utf8_unicode_ci'})
31
- when 'postgresql'
32
- ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
33
- ActiveRecord::Base.connection.recreate_database(config['database'], config.merge('encoding' => 'utf8'))
34
- end
35
- end unless ENV['NONUKES']
36
-
37
- ActiveRecord::Base.establish_connection(config)
38
- # Drop this when support for ActiveRecord 4.1 is removed
39
- Foreigner.load if defined?(Foreigner)
40
-
41
- require "#{database_folder}/schema"
42
- require "#{database_folder}/models"
43
-
44
- # See http://stackoverflow.com/a/22388177/1268016
45
- def count_queries(&block)
46
- count = 0
47
- counter_fn = ->(name, started, finished, unique_id, payload) do
48
- count += 1 unless %w[CACHE SCHEMA].include? payload[:name]
49
- end
50
- ActiveSupport::Notifications.subscribed(counter_fn, 'sql.active_record', &block)
51
- count
52
- end
@@ -1,14 +0,0 @@
1
- RSpec.configure do |config|
2
-
3
- DatabaseCleaner.strategy = :truncation
4
-
5
- config.before(:each) do
6
- ActiveRecord::Base.connection_pool.connection
7
- DatabaseCleaner.start
8
- end
9
-
10
- config.after(:each) do
11
- ActiveRecord::Base.connection_pool.connection
12
- DatabaseCleaner.clean
13
- end
14
- end
@@ -1,18 +0,0 @@
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
@@ -1,13 +0,0 @@
1
- class Hash
2
- def render_from_yield(&block)
3
- reduce({}) do |h, entry|
4
- k, v = entry
5
- h[block.call(k)] = if v.is_a?(Hash) then
6
- v.render_from_yield(&block)
7
- else
8
- block.call(v)
9
- end
10
- h
11
- end
12
- end
13
- end
@@ -1,18 +0,0 @@
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