summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-07-06 15:31:54 +0200
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-07-07 15:08:15 +0200
commite036a3743069b7d398117c6742bd6c21239e879a (patch)
tree47e25ee132070d4083cc30b31f4ea9446fcbd230
parent24a4199ac5eb517e7a27e458d5d1b4f937480a31 (diff)
downloadgitlab-ce-e036a3743069b7d398117c6742bd6c21239e879a.tar.gz
Add walk_table_in_batches and refactor migration helpers
-rw-r--r--lib/gitlab/database/migration_helpers.rb75
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb53
2 files changed, 97 insertions, 31 deletions
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 0643c56db9b..29e812c3c78 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -221,17 +221,19 @@ module Gitlab
# make things _more_ complex).
#
# rubocop: disable Metrics/AbcSize
- def update_column_in_batches(table, column, value)
+ def update_column_in_batches(table, column, value, &scope)
if transaction_open?
- raise 'update_column_in_batches can not be run inside a transaction, ' \
- 'you can disable transactions by calling disable_ddl_transaction! ' \
- 'in the body of your migration class'
+ raise <<-MSG
+ update_column_in_batches helper can not be run inside a transaction.
+ You can disable transactions by calling `disable_ddl_transaction!`
+ method in the body of your migration class.
+ MSG
end
- table = Arel::Table.new(table)
+ table_arel = Arel::Table.new(table)
- count_arel = table.project(Arel.star.count.as('count'))
- count_arel = yield table, count_arel if block_given?
+ count_arel = table_arel.project(Arel.star.count.as('count'))
+ count_arel = yield table_arel, count_arel if block_given?
total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i
@@ -246,37 +248,56 @@ module Gitlab
# rows for GitLab.com.
batch_size = max_size if batch_size > max_size
+ walk_table_in_batches(table, of: batch_size, scope: scope) do
+ Arel::UpdateManager.new(ActiveRecord::Base)
+ .table(table_arel)
+ .set([[table_arel[column], value]])
+ end
+ end
+
+ def walk_table_in_batches(table, of: 1000, scope: nil)
+ if transaction_open?
+ raise <<-MSG
+ walk_table_in_batches helper can not be run inside a transaction.
+ You can disable transactions by calling `disable_ddl_transaction!`
+ method in the body of your migration class.
+ MSG
+ end
+
+ table = Arel::Table.new(table)
+
start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
- start_arel = yield table, start_arel if block_given?
- start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i
+ start_arel = scope.call(table, start_arel) if scope
+ start_id = exec_query(start_arel.to_sql).to_hash.first.to_h['id'].to_i
- loop do
+ 1.step do |batch|
stop_arel = table.project(table[:id])
.where(table[:id].gteq(start_id))
.order(table[:id].asc)
.take(1)
- .skip(batch_size)
-
- stop_arel = yield table, stop_arel if block_given?
- stop_row = exec_query(stop_arel.to_sql).to_hash.first
+ .skip(of)
- update_arel = Arel::UpdateManager.new(ActiveRecord::Base)
- .table(table)
- .set([[table[column], value]])
- .where(table[:id].gteq(start_id))
+ stop_arel = scope.call(table, stop_arel) if scope
+ stop_id = exec_query(stop_arel.to_sql)
+ .to_hash.first.to_h['id'].to_i
- if stop_row
- stop_id = stop_row['id'].to_i
- start_id = stop_id
- update_arel = update_arel.where(table[:id].lt(stop_id))
- end
+ action = yield(batch, start_id, stop_id)
- update_arel = yield table, update_arel if block_given?
+ if action.is_a?(Arel::TreeManager)
+ exec_arel = action.where(table[:id].gteq(start_id))
+ exec_arel = exec_arel.where(table[:id].lt(stop_id)) if stop_id.nonzero?
+ exec_arel = scope.call(table, exec_arel) if scope
- execute(update_arel.to_sql)
+ execute(exec_arel.to_sql)
+ end
- # There are no more rows left to update.
- break unless stop_row
+ if stop_id.zero?
+ # there are no more rows left to update
+ break
+ else
+ # next loop
+ start_id = stop_id
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4259be3f522..65af2e1cb52 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -2,9 +2,7 @@ require 'spec_helper'
describe Gitlab::Database::MigrationHelpers, lib: true do
let(:model) do
- ActiveRecord::Migration.new.extend(
- Gitlab::Database::MigrationHelpers
- )
+ ActiveRecord::Migration.new.extend(described_class)
end
before do
@@ -264,7 +262,7 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
describe '#update_column_in_batches' do
context 'when running outside of a transaction' do
before do
- expect(model).to receive(:transaction_open?).and_return(false)
+ expect(model).to receive(:transaction_open?).twice.and_return(false)
create_list(:empty_project, 5)
end
@@ -313,6 +311,53 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#walk_table_in_batches' do
+ context 'when running outside of a transaction' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
+
+ create_list(:empty_project, 6)
+ end
+
+ it 'yields for each batch' do
+ expect { |b| model.walk_table_in_batches(:projects, of: 2, &b) }
+ .to yield_control.exactly(3).times
+ end
+
+ it 'yields successive ranges' do
+ expect { |b| model.walk_table_in_batches(:projects, of: 2, &b) }
+ .to yield_successive_args([1, Integer, Integer],
+ [2, Integer, Integer],
+ [3, Integer, 0])
+ end
+
+ context 'when a scope is provided' do
+ it 'limits the scope of the statement provided inside the block' do
+ first_id = Project.first.id
+ scope = ->(table, query) { query.where(table[:id].eq(first_id)) }
+
+ model.walk_table_in_batches(:projects, scope: scope) do
+ Arel::UpdateManager.new(ActiveRecord::Base)
+ .table(Arel::Table.new(:projects))
+ .set([[Arel::Table.new(:projects)[:archived], true]])
+ end
+
+ expect(Project.where(archived: true).count).to eq(1)
+ end
+ end
+ end
+
+ context 'when running inside the transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1'))
+ end.to raise_error(RuntimeError)
+ end
+ end
+ end
+
describe '#add_column_with_default' do
context 'outside of a transaction' do
context 'when a column limit is not set' do