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.
- 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/spec/parallel_spec.rb
DELETED
@@ -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
|
data/spec/support/database.rb
DELETED
@@ -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,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
|
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
|