diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/concerns/ignorable_column.rb | 28 | ||||
-rw-r--r-- | app/models/concerns/renameable_column.rb | 107 | ||||
-rw-r--r-- | app/models/user.rb | 7 |
3 files changed, 142 insertions, 0 deletions
diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb new file mode 100644 index 00000000000..eb9f3423e48 --- /dev/null +++ b/app/models/concerns/ignorable_column.rb @@ -0,0 +1,28 @@ +# Module that can be included into a model to make it easier to ignore database +# columns. +# +# Example: +# +# class User < ActiveRecord::Base +# include IgnorableColumn +# +# ignore_column :updated_at +# end +# +module IgnorableColumn + extend ActiveSupport::Concern + + module ClassMethods + def columns + super.reject { |column| ignored_columns.include?(column.name) } + end + + def ignored_columns + @ignored_columns ||= Set.new + end + + def ignore_column(name) + ignored_columns << name.to_s + end + end +end diff --git a/app/models/concerns/renameable_column.rb b/app/models/concerns/renameable_column.rb new file mode 100644 index 00000000000..3f905520fe9 --- /dev/null +++ b/app/models/concerns/renameable_column.rb @@ -0,0 +1,107 @@ +module RenameableColumn + extend ActiveSupport::Concern + + module ClassMethods + def rename_column(old_column, new_column, migrations:) + unless migration_exists?(migrations[:add_new]) + raise "Cannot find migration '#{migrations[:add_new]}'" + end + + unless migration_exists?(migrations[:migrate_data]) + raise "Cannot find migration '#{migrations[:migrate_data]}'" + end + + unless migration_exists?(migrations[:remove_old]) + raise "Cannot find migration '#{migrations[:remove_old]}'" + end + + if migration_ran?(migrations[:remove_old]) + old_column_removed(old_column, new_column) + elsif migration_ran?(migrations[:migrate_data]) + data_migrated_from_old_to_new_column(old_column, new_column) + elsif migration_ran?(migrations[:add_new]) + new_column_added(old_column, new_column) + end + end + + private + + def new_column_added(old_column, new_column) + log_column_rename_status(old_column, new_column) + Rails.logger.info "The `#{new_column}` column has been added, but the data has not yet been migrated, and the `#{old_column}` column has not yet been removed." + log_column_usage_instructions(old_column, new_column) + + before_save do + self[new_column] = self[old_column] + + true + end + + define_method "#{new_column}=" do |new_value| + raise "Use `#{self.class.name}##{old_column}=` until data is migrated from `#{old_column}` to `#{new_column}`" + end + + define_singleton_method column_name_method(new_column) do + old_column + end + end + + def data_migrated_from_old_to_new_column(old_column, new_column) + log_column_rename_status(old_column, new_column) + Rails.logger.info "The `#{new_column}` column has been added and the data has been migrated, but the `#{old_column}` column has not yet been removed." + log_column_usage_instructions(old_column, new_column) + + # We read and write to and from `new_column`, but the code still says `old_column` + include IgnorableColumn + + ignore_column old_column + + alias_attribute old_column, new_column + + define_singleton_method column_name_method(new_column) do + new_column + end + end + + def old_column_removed(old_column, new_column) + warn "WARNING: `#{self.name}` column `#{old_column}` has been renamed to `#{new_column}`." + warn "All code should be updated to use `#{new_column}` where `#{old_column}` or the value of `#{self.name}.#{column_name_method(new_column)}` is currently used, and `rename_column #{old_column.inspect}, #{new_column.inspect}` should be removed from `#{self.name}`." + + alias_attribute old_column, new_column + + define_singleton_method column_name_method(new_column) do + new_column + end + end + + def log_column_rename_status(old_column, new_column) + Rails.logger.info "`#{self.name}` column `#{old_column}` is in the process of being renamed to `#{new_column}`." + end + + def log_column_usage_instructions(old_column, new_column) + Rails.logger.info "Until the rename is complete, all code should continue to read and write from `#{old_column}`, which may be the actual attribute or an ActiveRecord alias. However, plain SQL and Arel queries should use `#{self.name}.#{column_name_method(new_column)}`, which will return `#{old_column.inspect}` or `#{new_column.inspect}` based on the current state of the database." + end + + def column_name_method(new_column) + "#{new_column}_column_name" + end + + def all_migrations + @all_migrations ||= Set.new(ActiveRecord::Migrator.migrations(['db/release_migrations']).map(&:version)) + end + + def migration_exists?(name) + version = name.match(/^([0-9]+)/)[1].to_i + all_migrations.include?(version) + end + + def ran_migrations + @ran_migrations ||= Set.new(ActiveRecord::Migrator.get_all_versions) + end + + def migration_ran?(name) + version = name.match(/^([0-9]+)/)[1].to_i + ran_migrations.include?(version) + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 39c1281179b..a3bec974f1d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,13 @@ require 'carrierwave/orm/activerecord' class User < ActiveRecord::Base + include RenameableColumn + rename_column :username, :handle, migrations: { + add_new: '20170316184328_add_handle_column_to_users', + migrate_data: '20170316190016_migrate_users_username_to_handle', + remove_old: '20170316190051_remove_username_column_from_users' + } + extend Gitlab::ConfigHelper include Gitlab::ConfigHelper |