closure_tree 6.5.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +98 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -1
  5. data/Appraisals +90 -7
  6. data/CHANGELOG.md +100 -42
  7. data/Gemfile +3 -11
  8. data/README.md +68 -24
  9. data/Rakefile +16 -10
  10. data/_config.yml +1 -0
  11. data/bin/appraisal +29 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/closure_tree.gemspec +16 -9
  15. data/lib/closure_tree/finders.rb +32 -9
  16. data/lib/closure_tree/has_closure_tree.rb +4 -0
  17. data/lib/closure_tree/has_closure_tree_root.rb +5 -7
  18. data/lib/closure_tree/hash_tree_support.rb +4 -4
  19. data/lib/closure_tree/hierarchy_maintenance.rb +28 -8
  20. data/lib/closure_tree/model.rb +42 -16
  21. data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
  22. data/lib/closure_tree/numeric_order_support.rb +7 -3
  23. data/lib/closure_tree/support.rb +18 -12
  24. data/lib/closure_tree/support_attributes.rb +10 -1
  25. data/lib/closure_tree/support_flags.rb +1 -4
  26. data/lib/closure_tree/version.rb +1 -1
  27. data/lib/generators/closure_tree/migration_generator.rb +8 -0
  28. data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
  29. metadata +78 -79
  30. data/.travis.yml +0 -29
  31. data/gemfiles/activerecord_4.2.gemfile +0 -19
  32. data/gemfiles/activerecord_5.0.gemfile +0 -19
  33. data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
  34. data/gemfiles/activerecord_edge.gemfile +0 -20
  35. data/img/example.png +0 -0
  36. data/img/preorder.png +0 -0
  37. data/spec/cache_invalidation_spec.rb +0 -39
  38. data/spec/cuisine_type_spec.rb +0 -38
  39. data/spec/db/database.yml +0 -21
  40. data/spec/db/models.rb +0 -128
  41. data/spec/db/schema.rb +0 -166
  42. data/spec/fixtures/tags.yml +0 -98
  43. data/spec/generators/migration_generator_spec.rb +0 -48
  44. data/spec/has_closure_tree_root_spec.rb +0 -154
  45. data/spec/hierarchy_maintenance_spec.rb +0 -16
  46. data/spec/label_spec.rb +0 -554
  47. data/spec/matcher_spec.rb +0 -34
  48. data/spec/metal_spec.rb +0 -55
  49. data/spec/model_spec.rb +0 -9
  50. data/spec/namespace_type_spec.rb +0 -13
  51. data/spec/parallel_spec.rb +0 -159
  52. data/spec/spec_helper.rb +0 -24
  53. data/spec/support/database.rb +0 -52
  54. data/spec/support/database_cleaner.rb +0 -14
  55. data/spec/support/exceed_query_limit.rb +0 -18
  56. data/spec/support/hash_monkey_patch.rb +0 -13
  57. data/spec/support/query_counter.rb +0 -18
  58. data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
  59. data/spec/support_spec.rb +0 -14
  60. data/spec/tag_examples.rb +0 -665
  61. data/spec/tag_spec.rb +0 -6
  62. data/spec/user_spec.rb +0 -174
  63. data/spec/uuid_tag_spec.rb +0 -6
@@ -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?
data/spec/spec_helper.rb DELETED
@@ -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
@@ -1,10 +0,0 @@
1
- if sqlite?
2
- RSpec.configure do |config|
3
- config.before(:suite) do
4
- ENV['FLOCK_DIR'] = Dir.mktmpdir
5
- end
6
- config.after(:suite) do
7
- FileUtils.remove_entry_secure ENV['FLOCK_DIR']
8
- end
9
- end
10
- end
data/spec/support_spec.rb DELETED
@@ -1,14 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe ClosureTree::Support do
4
- let(:sut) { Tag._ct }
5
- it 'passes through table names without prefix and suffix' do
6
- expected = 'some_random_table_name'
7
- expect(sut.remove_prefix_and_suffix(expected)).to eq(expected)
8
- end
9
- it 'extracts through table name with prefix and suffix' do
10
- expected = 'some_random_table_name'
11
- tn = ActiveRecord::Base.table_name_prefix + expected + ActiveRecord::Base.table_name_suffix
12
- expect(sut.remove_prefix_and_suffix(tn)).to eq(expected)
13
- end
14
- end