module HairTrigger::SchemaDumper

Public Class Methods

included(base) click to toggle source
# File lib/hair_trigger/schema_dumper.rb, line 132
def self.included(base)
  base.class_eval do
    prepend TrailerWithTriggersSupport

    class_attribute :previous_schema
  end
end

Public Instance Methods

normalize_trigger(name, definition, type) click to toggle source
# File lib/hair_trigger/schema_dumper.rb, line 74
def normalize_trigger(name, definition, type)
  @adapter_name = @connection.adapter_name.downcase.to_sym

  return definition unless HairTrigger::POSTGRESQL_ADAPTERS.include?(@adapter_name)
  # because postgres does not preserve the original CREATE TRIGGER/
  # FUNCTION statements, its decompiled reconstruction will not match
  # ours. we work around it by creating our generated trigger/function,
  # asking postgres for its definition, and then rolling back.
  @connection.transaction(requires_new: true) do
    chars = ('a'..'z').to_a + ('0'..'9').to_a + ['_']
    test_name = '_hair_trigger_test_' + (0..43).map{ chars[(rand * chars.size).to_i] }.join
    # take of the parens for gsubbing, since this version might be quoted
    name = name[0..-3] if type == :function
    @connection.execute(definition.sub(name, test_name))
    # now add them back
    if type == :function
      test_name << '()'
      name << '()'
    end
    definition = @connection.triggers(:only => [test_name], :simple_check => true).values.first
    definition.sub!(test_name, name)
    raise ActiveRecord::Rollback
  end
  definition
end
triggers(stream) click to toggle source
# File lib/hair_trigger/schema_dumper.rb, line 21
def triggers(stream)
  @adapter_name = @connection.adapter_name.downcase.to_sym

  all_triggers = @connection.triggers
  db_trigger_warnings = {}
  migration_trigger_builders = []

  db_triggers = whitelist_triggers(all_triggers)

  migration_triggers = HairTrigger.current_migrations(:in_rake_task => true, :previous_schema => self.class.previous_schema).map do |(_, builder)|
    definitions = []
    builder.generate.each do |statement|
      if statement =~ /\ACREATE(.*TRIGGER| FUNCTION) ([^ \n]+)/
        # poor man's unquote
        type = ($1 == ' FUNCTION' ? :function : :trigger)
        name = $2.gsub('"', '')

        definitions << [name, statement, type]
      end
    end
    {:builder => builder, :definitions => definitions}
  end

  migration_triggers.each do |migration|
    next unless migration[:definitions].all? do |(name, definition, type)|
      db_triggers[name] && (db_trigger_warnings[name] = true) && db_triggers[name] == normalize_trigger(name, definition, type)
    end

    migration[:definitions].each do |(name, _, _)|
      db_triggers.delete(name)
      db_trigger_warnings.delete(name)
    end

    migration_trigger_builders << migration[:builder]
  end

  db_triggers.to_a.sort_by{ |t| (t.first + 'a').sub(/\(/, '_') }.each do |(name, definition)|
    if db_trigger_warnings[name]
      stream.puts "  # WARNING: generating adapter-specific definition for #{name} due to a mismatch."
      stream.puts "  # either there's a bug in hairtrigger or you've messed up your migrations and/or db :-/"
    else
      stream.puts "  # no candidate create_trigger statement could be found, creating an adapter-specific one"
    end
    if definition =~ /\n/
      stream.print "  execute(<<-SQL)\n#{definition.rstrip}\n  SQL\n\n"
    else
      stream.print "  execute(#{definition.inspect})\n\n"
    end
  end

  migration_trigger_builders.each { |builder| stream.print builder.to_ruby('  ', false) + "\n\n" }
end
whitelist_triggers(triggers) click to toggle source
# File lib/hair_trigger/schema_dumper.rb, line 100
def whitelist_triggers(triggers)
  triggers = triggers.reject do |name, source|
    ActiveRecord::SchemaDumper.ignore_tables.any? { |ignored_table_name| source =~ /ON\s+#{@connection.quote_table_name(ignored_table_name)}\s/ }
  end

  if Configuration.allow_tables.present?
    triggers = triggers.select do |name, source|
      Array(Configuration.allow_tables).any? { |allowed_table_name| source =~ /ON\s+#{@connection.quote_table_name(allowed_table_name)}\s/ }
    end
  end

  if Configuration.allow_triggers.present?
    triggers = triggers.select do |name, source|
      Array(Configuration.allow_triggers).any? { |allowed_trigger_name| allowed_trigger_name === name } # Triple equals to allow regexps or strings as allowed_trigger_name
    end
  end

  if Configuration.ignore_tables.present?
    triggers = triggers.reject do |name, source|
      Array(Configuration.ignore_tables).any? { |allowed_table_name| source =~ /ON\s+#{@connection.quote_table_name(allowed_table_name)}\s/ }
    end
  end

  if Configuration.ignore_triggers.present?
    triggers = triggers.reject do |name, source|
      Array(Configuration.ignore_triggers).any? { |allowed_trigger_name| allowed_trigger_name === name } # Triple equals to allow regexps or strings as allowed_trigger_name
    end
  end

  triggers
end