class Alterity

Constants

Configuration
CurrentState
VERSION

Public Class Methods

after_running_migrations() click to toggle source
# File lib/alterity.rb, line 46
def after_running_migrations
  state.migrating = false
end
before_running_migrations() click to toggle source

hooks

# File lib/alterity.rb, line 39
def before_running_migrations
  require "open3"
  state.migrating = true
  set_database_config
  prepare_replicas_dsns_table
end
configure() { |config| ... } click to toggle source
# File lib/alterity/configuration.rb, line 38
def configure
  yield config
end
disable() { || ... } click to toggle source
# File lib/alterity/configuration.rb, line 42
def disable
  state.disabled = true
  yield
ensure
  state.disabled = false
end
process_sql_query(sql, &block) click to toggle source
# File lib/alterity.rb, line 10
def process_sql_query(sql, &block)
  return block.call if state.disabled

  case sql.tr("\n", " ").strip
  when /^alter\s+table\s+(?<table>.+?)\s+(?<updates>.+)/i
    table = $~[:table]
    updates = $~[:updates]
    if updates.split(",").all? { |s| s =~ /^\s*drop\s+foreign\s+key/i } ||
       updates.split(",").all? { |s| s =~ /^\s*add\s+constraint/i }
      block.call
    elsif updates =~ /drop\s+foreign\s+key/i || updates =~ /add\s+constraint/i
      # ADD CONSTRAINT / DROP FOREIGN KEY have to go to the original table,
      # other alterations need to got to the new table.
      raise "[Alterity] Can't change a FK and do something else in the same query. Split it."
    else
      execute_alter(table, updates)
    end
  when /^create\s+index\s+(?<index>.+?)\s+on\s+(?<table>.+?)\s+(?<updates>.+)/i
    execute_alter($~[:table], "ADD INDEX #{$~[:index]} #{$~[:updates]}")
  when /^create\s+unique\s+index\s+(?<index>.+?)\s+on\s+(?<table>.+?)\s+(?<updates>.+)/i
    execute_alter($~[:table], "ADD UNIQUE INDEX #{$~[:index]} #{$~[:updates]}")
  when /^drop\s+index\s+(?<index>.+?)\s+on\s+(?<table>.+)/i
    execute_alter($~[:table], "DROP INDEX #{$~[:index]}")
  else
    block.call
  end
end
reset_state_and_configuration() click to toggle source
# File lib/alterity/configuration.rb, line 17
def reset_state_and_configuration
  self.config = Configuration.new
  class << config
    def replicas(database:, table:, dsns:)
      return ArgumentError.new("database & table must be present") if database.blank? || table.blank?

      self.replicas_dsns_database = database
      self.replicas_dsns_table = table
      self.replicas_dsns = dsns.uniq.map do |dsn|
        parts = dsn.split(",")
        # automatically add default port
        parts << "P=3306" unless parts.any? { |part| part.start_with?("P=") }
        parts.join(",")
      end.compact
    end
  end

  self.state = CurrentState.new
  load "#{__dir__}/default_configuration.rb"
end

Private Class Methods

execute_alter(table, updates) click to toggle source
# File lib/alterity.rb, line 52
def execute_alter(table, updates)
  altered_table = table.delete("`")
  alter_argument = %("#{updates.gsub('"', '\\"').gsub('`', '\\\`')}")
  prepared_command = config.command.call(altered_table, alter_argument).to_s.gsub(/\n/, "\\\n")
  puts "[Alterity] Will execute: #{prepared_command}"
  config.before_command&.call(prepared_command)

  result_str = +""
  exit_status = nil
  Open3.popen2e(prepared_command) do |_stdin, stdout_and_stderr, wait_thr|
    stdout_and_stderr.each do |line|
      puts line
      result_str << line
      config.on_command_output&.call(line)
    end
    exit_status = wait_thr.value
    config.after_command&.call(wait_thr.value.to_i)
  end

  raise("[Alterity] Command failed. Full output: #{result_str}") unless exit_status.success?
end
prepare_replicas_dsns_table() click to toggle source

Optional: Automatically set up table PT-OSC will monitor for replica lag.

# File lib/alterity.rb, line 82
    def prepare_replicas_dsns_table
      return if config.replicas_dsns_table.blank?

      dsns = config.replicas_dsns.dup
      # Automatically remove master
      dsns.reject! { |dsn| dsn.split(",").include?("h=#{config.host}") }

      database = config.replicas_dsns_database
      table = "#{database}.#{config.replicas_dsns_table}"
      connection = ActiveRecord::Base.connection
      connection.execute "CREATE DATABASE IF NOT EXISTS #{database}"
      connection.execute <<~SQL
        CREATE TABLE IF NOT EXISTS #{table} (
          id INT(11) NOT NULL AUTO_INCREMENT,
          parent_id INT(11) DEFAULT NULL,
          dsn VARCHAR(255) NOT NULL,
          PRIMARY KEY (id)
        ) ENGINE=InnoDB
      SQL
      connection.execute "TRUNCATE #{table}"
      return if dsns.empty?

      connection.execute <<~SQL
        INSERT INTO #{table} (dsn) VALUES
         #{dsns.map { |dsn| "('#{dsn}')" }.join(',')}
      SQL
    end
set_database_config() click to toggle source
# File lib/alterity.rb, line 74
def set_database_config
  db_config_hash = ActiveRecord::Base.connection_db_config.configuration_hash
  %i[host port database username password].each do |key|
    config[key] = db_config_hash[key]
  end
end