Commit 723dc6ea authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch...

Merge branch '38358-update-migration-helpers-to-use-check-constraints-instead-of-change-column-null' into 'master'

Update migration helpers to use check constraints instead of change_column_null

See merge request gitlab-org/gitlab!30943
parents 3fe4a80b 44d881b9
...@@ -171,8 +171,39 @@ Adding or removing a NOT NULL clause (or another constraint) can typically be ...@@ -171,8 +171,39 @@ Adding or removing a NOT NULL clause (or another constraint) can typically be
done without requiring downtime. However, this does require that any application done without requiring downtime. However, this does require that any application
changes are deployed _first_. Thus, changing the constraints of a column should changes are deployed _first_. Thus, changing the constraints of a column should
happen in a post-deployment migration. happen in a post-deployment migration.
NOTE: Avoid using `change_column` as it produces inefficient query because it re-defines
the whole column type. For example, to add a NOT NULL constraint, prefer `change_column_null` NOTE: Avoid using `change_column` as it produces an inefficient query because it re-defines
the whole column type.
To add a NOT NULL constraint, use the `add_not_null_constraint` migration helper:
```ruby
# A post-deployment migration in db/post_migrate
class AddNotNull < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_not_null_constraint :users, :username
end
def down
remove_not_null_constraint :users, :username
end
end
```
If the column to be updated requires cleaning first (e.g. there are `NULL` values), you should:
1. Add the `NOT NULL` constraint with `validate: false`
`add_not_null_constraint :users, :username, validate: false`
1. Clean up the data with a data migration
1. Validate the `NOT NULL` constraint with a followup migration
`validate_not_null_constraint :users, :username`
## Changing Column Types ## Changing Column Types
......
...@@ -265,12 +265,19 @@ module Gitlab ...@@ -265,12 +265,19 @@ module Gitlab
# or `RESET ALL` is executed # or `RESET ALL` is executed
def disable_statement_timeout def disable_statement_timeout
if block_given? if block_given?
begin if statement_timeout_disabled?
execute('SET statement_timeout TO 0') # Don't do anything if the statement_timeout is already disabled
# Allows for nested calls of disable_statement_timeout without
# resetting the timeout too early (before the outer call ends)
yield yield
ensure else
execute('RESET ALL') begin
execute('SET statement_timeout TO 0')
yield
ensure
execute('RESET ALL')
end
end end
else else
unless transaction_open? unless transaction_open?
...@@ -495,7 +502,7 @@ module Gitlab ...@@ -495,7 +502,7 @@ module Gitlab
update_column_in_batches(table, column, default_after_type_cast, &block) update_column_in_batches(table, column, default_after_type_cast, &block)
end end
change_column_null(table, column, false) unless allow_null add_not_null_constraint(table, column) unless allow_null
# We want to rescue _all_ exceptions here, even those that don't inherit # We want to rescue _all_ exceptions here, even those that don't inherit
# from StandardError. # from StandardError.
rescue Exception => error # rubocop: disable all rescue Exception => error # rubocop: disable all
...@@ -1334,12 +1341,73 @@ into similar problems in the future (e.g. when new tables are created). ...@@ -1334,12 +1341,73 @@ into similar problems in the future (e.g. when new tables are created).
check_constraint_exists?(table, text_limit_name(table, column, name: constraint_name)) check_constraint_exists?(table, text_limit_name(table, column, name: constraint_name))
end end
# Migration Helpers for managing not null constraints
def add_not_null_constraint(table, column, constraint_name: nil, validate: true)
if column_is_nullable?(table, column)
add_check_constraint(
table,
"#{column} IS NOT NULL",
not_null_constraint_name(table, column, name: constraint_name),
validate: validate
)
else
warning_message = <<~MESSAGE
NOT NULL check constraint was not created:
column #{table}.#{column} is already defined as `NOT NULL`
MESSAGE
Rails.logger.warn warning_message
end
end
def validate_not_null_constraint(table, column, constraint_name: nil)
validate_check_constraint(
table,
not_null_constraint_name(table, column, name: constraint_name)
)
end
def remove_not_null_constraint(table, column, constraint_name: nil)
remove_check_constraint(
table,
not_null_constraint_name(table, column, name: constraint_name)
)
end
def check_not_null_constraint_exists?(table, column, constraint_name: nil)
check_constraint_exists?(
table,
not_null_constraint_name(table, column, name: constraint_name)
)
end
private private
def statement_timeout_disabled?
# This is a string of the form "100ms" or "0" when disabled
connection.select_value('SHOW statement_timeout') == "0"
end
def column_is_nullable?(table, column)
# Check if table.column has not been defined with NOT NULL
check_sql = <<~SQL
SELECT c.is_nullable
FROM information_schema.columns c
WHERE c.table_name = '#{table}'
AND c.column_name = '#{column}'
SQL
connection.select_value(check_sql) == 'YES'
end
def text_limit_name(table, column, name: nil) def text_limit_name(table, column, name: nil)
name.presence || check_constraint_name(table, column, 'max_length') name.presence || check_constraint_name(table, column, 'max_length')
end end
def not_null_constraint_name(table, column, name: nil)
name.presence || check_constraint_name(table, column, 'not_null')
end
def missing_schema_object_message(table, type, name) def missing_schema_object_message(table, type, name)
<<~MESSAGE <<~MESSAGE
Could not find #{type} "#{name}" on table "#{table}" which was referenced during the migration. Could not find #{type} "#{name}" on table "#{table}" which was referenced during the migration.
...@@ -1383,7 +1451,7 @@ into similar problems in the future (e.g. when new tables are created). ...@@ -1383,7 +1451,7 @@ into similar problems in the future (e.g. when new tables are created).
update_column_in_batches(table, new, Arel::Table.new(table)[old], batch_column_name: batch_column_name) update_column_in_batches(table, new, Arel::Table.new(table)[old], batch_column_name: batch_column_name)
change_column_null(table, new, false) unless old_col.null add_not_null_constraint(table, new) unless old_col.null
copy_indexes(table, old, new) copy_indexes(table, old, new)
copy_foreign_keys(table, old, new) copy_foreign_keys(table, old, new)
......
...@@ -13,10 +13,11 @@ end ...@@ -13,10 +13,11 @@ end
RSpec.shared_examples 'performs validation' do |validation_option| RSpec.shared_examples 'performs validation' do |validation_option|
it 'performs validation' do it 'performs validation' do
expect(model).to receive(:disable_statement_timeout).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).with(/RESET ALL/) expect(model).to receive(:execute).ordered.with(/RESET ALL/)
model.add_concurrent_foreign_key(*args, **options.merge(validation_option)) model.add_concurrent_foreign_key(*args, **options.merge(validation_option))
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment