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
done without requiring downtime. However, this does require that any application
changes are deployed _first_. Thus, changing the constraints of a column should
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
......
......@@ -265,6 +265,12 @@ module Gitlab
# or `RESET ALL` is executed
def disable_statement_timeout
if block_given?
if statement_timeout_disabled?
# 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
else
begin
execute('SET statement_timeout TO 0')
......@@ -272,6 +278,7 @@ module Gitlab
ensure
execute('RESET ALL')
end
end
else
unless transaction_open?
raise <<~ERROR
......@@ -495,7 +502,7 @@ module Gitlab
update_column_in_batches(table, column, default_after_type_cast, &block)
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
# from StandardError.
rescue Exception => error # rubocop: disable all
......@@ -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))
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
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)
name.presence || check_constraint_name(table, column, 'max_length')
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)
<<~MESSAGE
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).
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_foreign_keys(table, old, new)
......
......@@ -13,10 +13,11 @@ end
RSpec.shared_examples 'performs validation' do |validation_option|
it 'performs validation' do
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).ordered.with(/NOT VALID/)
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))
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