class ActiveRecord::PtOscMigration

Constants

PERCONA_FLAGS

@TODO whitelist all valid pt-osc flags

Public Class Methods

percona_flags() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 49
def self.percona_flags
  PERCONA_FLAGS
end

Protected Class Methods

tool_version() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 187
def self.tool_version
  @_tool_version ||= Gem::Version.new(get_tool_version.sub('pt-online-schema-change', '').strip)
end

Private Class Methods

get_tool_version() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 246
def self.get_tool_version
  `pt-online-schema-change --version`
end
sanitize_command(command) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 250
def self.sanitize_command(command)
  command_parts = command.shellsplit
  password_index = command_parts.find_index('--password')
  command_parts[password_index + 1] = '_hidden_' unless password_index.nil? || command_parts.length == password_index + 1
  command_parts.shelljoin
end

Public Instance Methods

migrate(direction) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 53
def migrate(direction)
  return unless respond_to?(direction)

  run_mode = percona_config[:run_mode] || 'print'
  raise ArgumentError.new('Invalid run_mode specified in database config') unless run_mode.in? %w(print execute)

  case direction
    when :up   then announce 'migrating'
    when :down then announce 'reverting'
  end

  time   = nil
  ActiveRecord::Base.connection_pool.with_connection do |conn|
    @connection = conn
    if respond_to?(:change)
      if direction == :down
        recorder = CommandRecorder.new(@connection)
        suppress_messages do
          @connection = recorder
          change
        end
        @connection = conn
        time = Benchmark.measure {
          self.revert {
            recorder.inverse.each do |cmd, args|
              send(cmd, *args)
            end
          }
        }
      else
        time = Benchmark.measure { change }
      end
    else
      time = Benchmark.measure { send(direction) }
    end

    case run_mode
      when 'execute' then time += Benchmark.measure { execute_pt_osc }
      when 'print' then print_pt_osc
    end

    @connection = nil
  end

  case direction
    when :up   then announce 'migrated (%.4fs)' % time.real; write
    when :down then announce 'reverted (%.4fs)' % time.real; write
  end
end

Protected Instance Methods

database_config() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 191
def database_config
  @db_config ||= raw_database_config.with_indifferent_access
end
execute_pt_osc() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 109
def execute_pt_osc
  return unless @connection.is_a? ActiveRecord::ConnectionAdapters::MysqlPtOscAdapter
  return if @connection.get_commanded_tables.empty?

  database_name = database_config[:database]
  announce 'running pt-online-schema-change'

  @connection.get_commanded_tables.each { |table| migrate_table(database_name, table) }
  @connection.clear_commands
end
execute_sql_for_table(execute_sql, database_name, table_name, dry_run = true) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 128
def execute_sql_for_table(execute_sql, database_name, table_name, dry_run = true)
  command = percona_command(execute_sql, database_name, table_name, execute: !dry_run)
  logger.info "Command is #{self.class.sanitize_command(command)}"

  success = Kernel.system command

  if success
    logger.info "Successfully #{dry_run ? 'dry ran' : 'executed'} on #{database_name}|#{table_name}: #{execute_sql}"
  else
    failure_message = "Unable to #{dry_run ? 'dry run' : 'execute'} query on #{database_name}|#{table_name}: #{execute_sql}"
    logger.error failure_message
    raise RuntimeError.new(failure_message)
  end
end
logfile() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 199
def logfile
  File.open(make_path_absolute(percona_config[:log] || 'log/pt_osc.log'), 'a')
end
logger() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 203
def logger
  return @logger if @logger
  @logger = Logger.new(logfile)
  @logger.formatter = Logger::Formatter.new # Don't let ActiveSupport override with SimpleFormatter
  @logger.progname = 'pt-osc'
  @logger
end
migrate_table(database_name, table_name) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 120
def migrate_table(database_name, table_name)
  execute_sql = @connection.get_commands_string(table_name)

  logger.info "Running on #{database_name}|#{table_name}: #{execute_sql}"

  [true, false].each { |dry_run| execute_sql_for_table(execute_sql, database_name, table_name, dry_run) }
end
percona_command(execute_sql, database_name, table_name, options = {}) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 162
def percona_command(execute_sql, database_name, table_name, options = {})
  command = ['pt-online-schema-change', '--alter', execute_sql || '', "D=#{database_name},t=#{table_name}"]

  # Whitelist
  options = HashWithIndifferentAccess.new(options)
  options = options.slice(*self.class.percona_flags.keys)

  # Merge config
  config = percona_config
  if config
    config.slice(*self.class.percona_flags.keys).each do |key, value|
      options[key] ||= value
    end
  end

  # Set defaults
  self.class.percona_flags.each do |flag, flag_config|
    options[flag] = flag_config[:default] if flag_config.key?(:default) && !options.key?(flag)
  end

  command_parts = command + [run_mode_flag(options)] + command_flags(options)

  command_parts.shelljoin
end
percona_config() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 195
def percona_config
  database_config[:percona] || {}
end
print_pt_osc() click to toggle source

Private Instance Methods

command_flags(options) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 216
def command_flags(options)
  options.flat_map do |key, value|
    next if key == 'execute'
    flag_options = self.class.percona_flags[key]

    # Satisfy version requirements
    if flag_options.try(:key?, :version)
      next unless Gem::Requirement.new(flag_options[:version]).satisfied_by? self.class.tool_version
    end

    # Mutate the value if needed
    if flag_options.try(:key?, :mutator)
      value = send(flag_options[:mutator], value, { all_options: options, flag_name: key }.merge(flag_options[:arguments] || {}))
      next if value.nil? # Allow a mutator to determine the flag shouldn't be used
    end

    # Handle boolean flags
    if flag_options.try(:[], :boolean)
      key = "no-#{key}" unless value
      value = nil
    end

    ["--#{key}", value]
  end.compact
end
execute(sql) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 279
def execute(sql)
  raise ActiveRecord::UnsupportedMigrationError.new("Raw `execute` isn't supported by the pt-osc gem.")
end
execute_only(flag, options = {}) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 264
def execute_only(flag, options = {})
  options[:all_options][:execute] ? flag : self.class.percona_flags[options[:flag_name]][:default]
end
get_from_config(flag, options = {}) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 268
def get_from_config(flag, options = {})
  case flag
  when nil
    database_config[options[:key_name] || options[:flag_name]]
  when false
    nil
  else
    flag
  end
end
make_path_absolute(path, _ = {}) click to toggle source

Flag mutators

# File lib/active_record/pt_osc_migration.rb, line 258
def make_path_absolute(path, _ = {})
  return path if path[0] == '/'
  # If path is not already absolute, treat it as relative to the app root
  File.expand_path(path, Dir.getwd)
end
raw_database_config() click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 212
def raw_database_config
  connection.pool.spec.config || ActiveRecord::Base.connection_config
end
run_mode_flag(options) click to toggle source
# File lib/active_record/pt_osc_migration.rb, line 242
def run_mode_flag(options)
  options[:execute] ? '--execute' : '--dry-run'
end