summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/database/migration_helpers_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/database/migration_helpers_spec.rb')
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb513
1 files changed, 513 insertions, 0 deletions
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index e007044868c..dfa3ae9142e 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#remove_concurrent_index' do
+ context 'outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'using PostgreSQL' do
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ allow(model).to receive(:disable_statement_timeout)
+ end
+
+ it 'removes the index concurrently' do
+ expect(model).to receive(:remove_index).
+ with(:users, { algorithm: :concurrently, column: :foo })
+
+ model.remove_concurrent_index(:users, :foo)
+ end
+ end
+
+ context 'using MySQL' do
+ it 'removes an index' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:remove_index).
+ with(:users, { column: :foo })
+
+ model.remove_concurrent_index(:users, :foo)
+ end
+ end
+ end
+
+ context 'inside a transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.remove_concurrent_index(:users, :foo) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
describe '#add_concurrent_foreign_key' do
context 'inside a transaction' do
it 'raises an error' do
@@ -133,6 +175,50 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#true_value' do
+ context 'using PostgreSQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.true_value).to eq("'t'")
+ end
+ end
+
+ context 'using MySQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.true_value).to eq(1)
+ end
+ end
+ end
+
+ describe '#false_value' do
+ context 'using PostgreSQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.false_value).to eq("'f'")
+ end
+ end
+
+ context 'using MySQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.false_value).to eq(0)
+ end
+ end
+ end
+
describe '#update_column_in_batches' do
before do
create_list(:empty_project, 5)
@@ -252,4 +338,431 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
end
+
+ describe '#rename_column_concurrently' do
+ context 'in a transaction' do
+ it 'raises RuntimeError' do
+ allow(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.rename_column_concurrently(:users, :old, :new) }.
+ to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ let(:old_column) do
+ double(:column,
+ type: :integer,
+ limit: 8,
+ default: 0,
+ null: false,
+ precision: 5,
+ scale: 1)
+ end
+
+ let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) }
+
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:column_for).and_return(old_column)
+
+ # Since MySQL and PostgreSQL use different quoting styles we'll just
+ # stub the methods used for this to make testing easier.
+ allow(model).to receive(:quote_column_name) { |name| name.to_s }
+ allow(model).to receive(:quote_table_name) { |name| name.to_s }
+ end
+
+ context 'using MySQL' do
+ it 'renames a column concurrently' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:install_rename_triggers_for_mysql).
+ with(trigger_name, 'users', 'old', 'new')
+
+ expect(model).to receive(:add_column).
+ with(:users, :new, :integer,
+ limit: old_column.limit,
+ precision: old_column.precision,
+ scale: old_column.scale)
+
+ expect(model).to receive(:change_column_default).
+ with(:users, :new, old_column.default)
+
+ expect(model).to receive(:update_column_in_batches)
+
+ expect(model).to receive(:change_column_null).with(:users, :new, false)
+
+ expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+ expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+ model.rename_column_concurrently(:users, :old, :new)
+ end
+ end
+
+ context 'using PostgreSQL' do
+ it 'renames a column concurrently' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:install_rename_triggers_for_postgresql).
+ with(trigger_name, 'users', 'old', 'new')
+
+ expect(model).to receive(:add_column).
+ with(:users, :new, :integer,
+ limit: old_column.limit,
+ precision: old_column.precision,
+ scale: old_column.scale)
+
+ expect(model).to receive(:change_column_default).
+ with(:users, :new, old_column.default)
+
+ expect(model).to receive(:update_column_in_batches)
+
+ expect(model).to receive(:change_column_null).with(:users, :new, false)
+
+ expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+ expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+ model.rename_column_concurrently(:users, :old, :new)
+ end
+ end
+ end
+ end
+
+ describe '#cleanup_concurrent_column_rename' do
+ it 'cleans up the renaming procedure for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:remove_rename_triggers_for_postgresql).
+ with(:users, /trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, :old)
+
+ model.cleanup_concurrent_column_rename(:users, :old, :new)
+ end
+
+ it 'cleans up the renaming procedure for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:remove_rename_triggers_for_mysql).
+ with(/trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, :old)
+
+ model.cleanup_concurrent_column_rename(:users, :old, :new)
+ end
+ end
+
+ describe '#change_column_type_concurrently' do
+ it 'changes the column type' do
+ expect(model).to receive(:rename_column_concurrently).
+ with('users', 'username', 'username_for_type_change', type: :text)
+
+ model.change_column_type_concurrently('users', 'username', :text)
+ end
+ end
+
+ describe '#cleanup_concurrent_column_type_change' do
+ it 'cleans up the type changing procedure' do
+ expect(model).to receive(:cleanup_concurrent_column_rename).
+ with('users', 'username', 'username_for_type_change')
+
+ expect(model).to receive(:rename_column).
+ with('users', 'username_for_type_change', 'username')
+
+ model.cleanup_concurrent_column_type_change('users', 'username')
+ end
+ end
+
+ describe '#install_rename_triggers_for_postgresql' do
+ it 'installs the triggers for PostgreSQL' do
+ expect(model).to receive(:execute).
+ with(/CREATE OR REPLACE FUNCTION foo()/m)
+
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo/m)
+
+ model.install_rename_triggers_for_postgresql('foo', :users, :old, :new)
+ end
+ end
+
+ describe '#install_rename_triggers_for_mysql' do
+ it 'installs the triggers for MySQL' do
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo_insert.+ON users/m)
+
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo_update.+ON users/m)
+
+ model.install_rename_triggers_for_mysql('foo', :users, :old, :new)
+ end
+ end
+
+ describe '#remove_rename_triggers_for_postgresql' do
+ it 'removes the function and trigger' do
+ expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar')
+ expect(model).to receive(:execute).with('DROP FUNCTION foo()')
+
+ model.remove_rename_triggers_for_postgresql('bar', 'foo')
+ end
+ end
+
+ describe '#remove_rename_triggers_for_mysql' do
+ it 'removes the triggers' do
+ expect(model).to receive(:execute).with('DROP TRIGGER foo_insert')
+ expect(model).to receive(:execute).with('DROP TRIGGER foo_update')
+
+ model.remove_rename_triggers_for_mysql('foo')
+ end
+ end
+
+ describe '#rename_trigger_name' do
+ it 'returns a String' do
+ expect(model.rename_trigger_name(:users, :foo, :bar)).
+ to match(/trigger_.{12}/)
+ end
+ end
+
+ describe '#indexes_for' do
+ it 'returns the indexes for a column' do
+ idx1 = double(:idx, columns: %w(project_id))
+ idx2 = double(:idx, columns: %w(user_id))
+
+ allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])
+
+ expect(model.indexes_for('table', :user_id)).to eq([idx2])
+ end
+ end
+
+ describe '#foreign_keys_for' do
+ it 'returns the foreign keys for a column' do
+ fk1 = double(:fk, column: 'project_id')
+ fk2 = double(:fk, column: 'user_id')
+
+ allow(model).to receive(:foreign_keys).with('table').and_return([fk1, fk2])
+
+ expect(model.foreign_keys_for('table', :user_id)).to eq([fk2])
+ end
+ end
+
+ describe '#copy_indexes' do
+ context 'using a regular index using a single column' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [])
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using a regular index with multiple columns' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id foobar),
+ name: 'index_on_issues_project_id_foobar',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id foobar),
+ unique: false,
+ name: 'index_on_issues_gl_project_id_foobar',
+ length: [],
+ order: [])
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with a WHERE clause' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: 'foo',
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ where: 'foo')
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with a USING clause' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ where: nil,
+ using: 'foo',
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ using: 'foo')
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with custom operator classes' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: nil,
+ opclasses: { 'project_id' => 'bar' },
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ opclasses: { 'gl_project_id' => 'bar' })
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ describe 'using an index of which the name does not contain the source column' do
+ it 'raises RuntimeError' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_foobar_index',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '#copy_foreign_keys' do
+ it 'copies foreign keys from one column to another' do
+ fk = double(:fk,
+ from_table: 'issues',
+ to_table: 'projects',
+ on_delete: :cascade)
+
+ allow(model).to receive(:foreign_keys_for).with(:issues, :project_id).
+ and_return([fk])
+
+ expect(model).to receive(:add_concurrent_foreign_key).
+ with('issues', 'projects', column: :gl_project_id, on_delete: :cascade)
+
+ model.copy_foreign_keys(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ describe '#column_for' do
+ it 'returns a column object for an existing column' do
+ column = model.column_for(:users, :id)
+
+ expect(column.name).to eq('id')
+ end
+
+ it 'returns nil when a column does not exist' do
+ expect(model.column_for(:users, :kittens)).to be_nil
+ end
+ end
+
+ describe '#replace_sql' do
+ context 'using postgres' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(false)
+ end
+
+ it 'builds the sql with correct functions' do
+ expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s).
+ to include('regexp_replace')
+ end
+ end
+
+ context 'using mysql' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+ end
+
+ it 'builds the sql with the correct functions' do
+ expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s).
+ to include('locate', 'insert')
+ end
+ end
+
+ describe 'results' do
+ let!(:user) { create(:user, name: 'Kathy Alice Aliceson') }
+
+ it 'replaces the correct part of the string' do
+ model.update_column_in_batches(:users, :name, model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve'))
+ expect(user.reload.name).to eq('Kathy Eve Aliceson')
+ end
+ end
+ end
end