HornsAndHooves/pg_saurus

View on GitHub
lib/pg_saurus/connection_adapters/postgresql_adapter/trigger_methods.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
# to support db triggers.
module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::TriggerMethods

  # :nodoc
  def supports_triggers?
    true
  end

  # See lib/pg_saurus/connection_adapters/trigger_methods.rb
  def create_trigger(table_name, proc_name, event, options = {})
    proc_name = "#{proc_name}"
    proc_name = "#{proc_name}()" unless proc_name.end_with?(')')

    for_each   = options[:for_each] || 'ROW'
    constraint = options[:constraint]

    sql = "CREATE #{!!constraint ? "CONSTRAINT " : ""}TRIGGER #{trigger_name(proc_name, options)}\n  #{event}\n"

    sql << "  ON #{quote_table_or_view(table_name, options)}\n"
    if constraint
      sql << if options[:deferrable]
               "  DEFERRABLE INITIALLY #{!!options[:initially_deferred] ? 'DEFERRED' : 'IMMEDIATE'}\n"
             else
               "  NOT DEFERRABLE\n"
             end
    end
    sql << "  FOR EACH #{for_each}\n"
    if condition = options[:condition]
      sql << "  WHEN (#{condition})\n"
    end
    sql << "  EXECUTE PROCEDURE #{proc_name}"

    execute sql
  end

  # See lib/pg_saurus/connection_adapters/trigger_methods.rb
  def remove_trigger(table_name, proc_name, options = {})
    execute "DROP TRIGGER #{trigger_name(proc_name, options)} ON #{quote_table_or_view(table_name, options)}"
  end

  # Returns the listing of currently defined db triggers
  #
  # @return [Array<::PgSaurus::ConnectionAdapters::TriggerDefinition>]
  def triggers
    res = select_all <<-SQL
      SELECT n.nspname as schema,
             c.relname as table,
             t.tgname as trigger_name,
             t.tgenabled as enable_mode,
             t.tgdeferrable as is_deferrable,
             t.tginitdeferred as is_initially_deferrable,
             pg_catalog.pg_get_triggerdef(t.oid, true) as trigger_definition
      FROM pg_catalog.pg_trigger t
        INNER JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
        INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
      WHERE c.relkind IN ('r', 'v')
        AND NOT t.tgisinternal
      ORDER BY 1, 2, 3;
    SQL

    res.inject([]) do |buffer, row|
      schema                = row['schema']
      table                 = row['table']
      trigger_name          = row['trigger_name']
      is_deferrable         = row['is_deferrable']
      is_initially_deferred = row['is_initially_deferred']

      trigger_definition = row['trigger_definition']

      is_constraint = is_constraint?(trigger_definition)
      proc_name     = parse_proc_name(trigger_definition)
      event         = parse_event(trigger_definition, trigger_name)
      condition     = parse_condition(trigger_definition)

      for_every = !!(trigger_definition =~ /FOR[\s]EACH[\s]ROW/) ? :row : :statement

      if proc_name && event
        buffer << ::PgSaurus::ConnectionAdapters::TriggerDefinition.new(
          trigger_name,
          proc_name,
          is_constraint,
          event,
          for_every,
          is_deferrable,
          is_initially_deferred,
          condition,
          table,
          schema
        )
      end
      buffer
    end
  end

  # Parse the condition from the trigger definition.
  def parse_condition(trigger_definition)
    trigger_definition[/WHEN[\s](.*?)[\s]EXECUTE[\s](FUNCTION|PROCEDURE)/m, 1]
  end
  private :parse_condition

  # Parse the event from the trigger definition.
  def parse_event(trigger_definition, trigger_name)
    trigger_definition[/^CREATE[\sA-Z]+TRIGGER[\s]#{Regexp.escape(trigger_name)}[\s](.*?)[\s]ON[\s]/m, 1]
  end
  private :parse_event

  # Parse the procedure name from the trigger definition.
  def parse_proc_name(trigger_definition)
    trigger_definition[/EXECUTE[\s](FUNCTION|PROCEDURE)[\s](.*?)$/m, 2]
  end
  private :parse_proc_name

  # Whether the trigger is a constraint.
  def is_constraint?(trigger_definition)
    !!(trigger_definition =~ /^CREATE CONSTRAINT TRIGGER/)
  end
  private :is_constraint?

  # Properly quote the table name or view name.
  def quote_table_or_view(name, options)
    schema = options[:schema]
    if schema
      "\"#{schema}\".\"#{name}\""
    else
      "\"#{name}\""
    end
  end
  private :quote_table_or_view

  # The name provided in the options, or constructed from the procedure name.
  def trigger_name(proc_name, options)
    if name = options[:name]
      name
    else
      "trigger_#{proc_name.gsub('(', '').gsub(')', '')}"
    end
  end
  private :trigger_name

end