class HairTrigger::Builder

Attributes

base_compatibility[W]
show_warnings[W]
tab_spacing[W]
options[RW]
prepared_actions[R]
prepared_where[R]
triggers[R]

Public Class Methods

base_compatibility() click to toggle source
# File lib/hair_trigger/builder.rb, line 568
def base_compatibility
  @base_compatibility ||= 0
end
chainable_methods(*methods) click to toggle source
# File lib/hair_trigger/builder.rb, line 148
    def self.chainable_methods(*methods)
      methods.each do |method|
        class_eval <<-METHOD, __FILE__, __LINE__ + 1
          alias #{method}_orig #{method}
          def #{method}(*args, &block)
            @chained_calls << :#{method}
            if @triggers || @trigger_group
              @errors << ["mysql doesn't support #{method} within a trigger group", *HairTrigger::MYSQL_ADAPTERS] unless [:name, :where, :all, :of].include?(:#{method})
            end
            set_#{method}(*args, &(block_given? ? block : nil))
          end
          def set_#{method}(*args, &block)
            if @triggers # i.e. each time we say t.something within a trigger group block
              @chained_calls.pop # the subtrigger will get this, we don't need it
              @chained_calls = @chained_calls.uniq
              @triggers << trigger = clone
              trigger.#{method}(*args, &(block_given? ? block : nil))
            else
              #{method}_orig(*args, &block)
              maybe_execute(&block) if block_given?
              self
            end
          end
        METHOD
      end
    end
compatibility() click to toggle source
# File lib/hair_trigger/builder.rb, line 572
def compatibility
  @compatibility ||= begin
    if HairTrigger::VERSION <= "0.1.3"
      0 # initial releases
    else
      1 # postgres RETURN bugfix
    # TODO: add more as we implement things that change the generated
    # triggers (e.g. chained call merging)
    end
  end
end
new(name = nil, options = {}) click to toggle source
# File lib/hair_trigger/builder.rb, line 12
def initialize(name = nil, options = {})
  @adapter = options[:adapter]
  @compatibility = options.delete(:compatibility) || self.class.compatibility
  @options = {}
  @chained_calls = []
  @errors = []
  @warnings = []
  set_name(name) if name
  {:timing => :after, :for_each => :row}.update(options).each do |key, value|
    if respond_to?("set_#{key}")
      send("set_#{key}", *Array[value])
    else
      @options[key] = value
    end
  end
end
show_warnings() click to toggle source
# File lib/hair_trigger/builder.rb, line 563
def show_warnings
  @show_warnings = true if @show_warnings.nil?
  @show_warnings
end
tab_spacing() click to toggle source
# File lib/hair_trigger/builder.rb, line 559
def tab_spacing
  @tab_spacing ||= 4
end

Public Instance Methods

<=>(other) click to toggle source
# File lib/hair_trigger/builder.rb, line 277
def <=>(other)
  ret = prepared_name <=> other.prepared_name
  return ret unless ret == 0
  hash <=> other.hash
end
==(other) click to toggle source
# File lib/hair_trigger/builder.rb, line 283
def ==(other)
  components == other.components
end
after(*events) click to toggle source
# File lib/hair_trigger/builder.rb, line 67
def after(*events)
  set_timing(:after)
  set_events(*events)
end
all() click to toggle source

noop, just a way you can pass a block within a trigger group

# File lib/hair_trigger/builder.rb, line 102
def all
end
all_names() click to toggle source
# File lib/hair_trigger/builder.rb, line 138
def all_names
  [prepared_name] + (@triggers ? @triggers.map(&:prepared_name) : [])
end
all_triggers(include_self = true) click to toggle source
# File lib/hair_trigger/builder.rb, line 142
def all_triggers(include_self = true)
  triggers = []
  triggers << self if include_self
  (@triggers || []).map(&:all_triggers).inject(triggers, &:concat)
end
before(*events) click to toggle source
# File lib/hair_trigger/builder.rb, line 62
def before(*events)
  set_timing(:before)
  set_events(*events)
end
change_clause(column) click to toggle source
# File lib/hair_trigger/builder.rb, line 201
def change_clause(column)
  "NEW.#{column} <> OLD.#{column} OR (NEW.#{column} IS NULL) <> (OLD.#{column} IS NULL)"
end
components() click to toggle source
# File lib/hair_trigger/builder.rb, line 296
def components
  [@options, @prepared_actions, @explicit_where, @triggers, @compatibility]
end
create_grouped_trigger?() click to toggle source
# File lib/hair_trigger/builder.rb, line 176
def create_grouped_trigger?
  HairTrigger::MYSQL_ADAPTERS.include?(adapter_name)
end
declare(declarations) click to toggle source
# File lib/hair_trigger/builder.rb, line 97
def declare(declarations)
  options[:declarations] = declarations
end
drop_triggers() click to toggle source
# File lib/hair_trigger/builder.rb, line 42
def drop_triggers
  all_names.map{ |name| self.class.new(name, {:table => options[:table], :drop => true}) }
end
eql?(other) click to toggle source
# File lib/hair_trigger/builder.rb, line 287
def eql?(other)
  other.is_a?(HairTrigger::Builder) && self == other
end
errors() click to toggle source
# File lib/hair_trigger/builder.rb, line 300
def errors
  (@triggers || []).map(&:errors).inject(@errors, &:+)
end
events(*events) click to toggle source
# File lib/hair_trigger/builder.rb, line 121
def events(*events)
  events << :insert if events.delete(:create)
  events << :delete if events.delete(:destroy)
  raise DeclarationError, "invalid events" unless events & [:insert, :update, :delete, :truncate] == events
  @errors << ["sqlite and mysql triggers may not be shared by multiple actions", *HairTrigger::MYSQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS] if events.size > 1
  @errors << ["sqlite and mysql do not support truncate triggers", *HairTrigger::MYSQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS] if events.include?(:truncate)
  options[:events] = events.map{ |e| e.to_s.upcase }
end
for_each(for_each) click to toggle source
# File lib/hair_trigger/builder.rb, line 56
def for_each(for_each)
  @errors << ["sqlite and mysql don't support FOR EACH STATEMENT triggers", *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS] if for_each == :statement
  raise DeclarationError, "invalid for_each" unless [:row, :statement].include?(for_each)
  options[:for_each] = for_each.to_s.upcase
end
generate(validate = true) click to toggle source
# File lib/hair_trigger/builder.rb, line 222
def generate(validate = true)
  validate!(@trigger_group ? :both : :down) if validate

  return @triggers.map{ |t| t.generate(false) }.flatten if @triggers && !create_grouped_trigger?
  prepare!
  raise GenerationError, "need to specify the table" unless options[:table]
  if options[:drop]
    generate_drop_trigger
  else
    raise GenerationError, "no actions specified" if @triggers && create_grouped_trigger? ? @triggers.any?{ |t| t.raw_actions.nil? } : raw_actions.nil?
    raise GenerationError, "need to specify the event(s) (:insert, :update, :delete)" if !options[:events] || options[:events].empty?
    raise GenerationError, "need to specify the timing (:before/:after)" unless options[:timing]

    [generate_drop_trigger] +
    [case adapter_name
      when *HairTrigger::SQLITE_ADAPTERS
        generate_trigger_sqlite
      when *HairTrigger::MYSQL_ADAPTERS
        generate_trigger_mysql
      when *HairTrigger::POSTGRESQL_ADAPTERS
        generate_trigger_postgresql
      else
        raise GenerationError, "don't know how to build #{adapter_name} triggers yet"
    end].flatten
  end
end
hash() click to toggle source
# File lib/hair_trigger/builder.rb, line 291
def hash
  prepare!
  components.hash
end
initialize_copy(other) click to toggle source
# File lib/hair_trigger/builder.rb, line 29
def initialize_copy(other)
  @trigger_group = other
  @triggers = nil
  @chained_calls = []
  @errors = []
  @warnings = []
  @options = @options.dup
  @options.delete(:name) # this will be inferred (or set further down the line)
  @options.each do |key, value|
    @options[key] = value.dup rescue value
  end
end
name(name) click to toggle source
# File lib/hair_trigger/builder.rb, line 46
def name(name)
  @errors << ["trigger name cannot exceed 63 for postgres", *HairTrigger::POSTGRESQL_ADAPTERS] if name.to_s.size > 63
  options[:name] = name.to_s
end
new_as(table) click to toggle source
# File lib/hair_trigger/builder.rb, line 91
def new_as(table)
  raise DeclarationError, "`new_as' requested, but no table_name specified" unless table.present?
  options[:referencing] ||= {}
  options[:referencing][:new] = table
end
nowrap(flag = true) click to toggle source
# File lib/hair_trigger/builder.rb, line 76
def nowrap(flag = true)
  options[:nowrap] = flag
end
of(*columns) click to toggle source
# File lib/hair_trigger/builder.rb, line 80
def of(*columns)
  raise DeclarationError, "`of' requested, but no columns specified" unless columns.present?
  options[:of] = columns
end
old_as(table) click to toggle source
# File lib/hair_trigger/builder.rb, line 85
def old_as(table)
  raise DeclarationError, "`old_as' requested, but no table_name specified" unless table.present?
  options[:referencing] ||= {}
  options[:referencing][:old] = table
end
on(table) click to toggle source
# File lib/hair_trigger/builder.rb, line 51
def on(table)
  raise DeclarationError, "table has already been specified" if options[:table]
  options[:table] = table.to_s
end
prepare!() click to toggle source
# File lib/hair_trigger/builder.rb, line 180
def prepare!
  @triggers.each(&:prepare!) if @triggers
  prepare_where!
  if @actions
    @prepared_actions = @actions.is_a?(Hash) ?
      @actions.inject({}){ |hash, (key, value)| hash[key] = interpolate(value).rstrip; hash } :
      interpolate(@actions).rstrip
  end
  all_names # ensure (component) trigger names are all cached
end
prepare_where!() click to toggle source
# File lib/hair_trigger/builder.rb, line 191
def prepare_where!
  parts = []
  parts << @explicit_where = options[:where] = interpolate(options[:where]) if options[:where]
  parts << options[:of].map{ |col| change_clause(col) }.join(" OR ") if options[:of] && !supports_of?
  if parts.present?
    parts.map!{ |part| "(" + part + ")" } if parts.size > 1
    @prepared_where = parts.join(" AND ")
  end
end
prepared_name() click to toggle source
# File lib/hair_trigger/builder.rb, line 134
def prepared_name
  @prepared_name ||= options[:name] ||= infer_name
end
raw_actions() click to toggle source
# File lib/hair_trigger/builder.rb, line 130
def raw_actions
  @raw_actions ||= prepared_actions.is_a?(Hash) ? prepared_actions[adapter_name] || prepared_actions[:default] : prepared_actions
end
security(user) click to toggle source
# File lib/hair_trigger/builder.rb, line 105
def security(user)
  unless [:invoker, :definer].include?(user) || user.to_s =~ /\A'[^']+'@'[^']+'\z/ || user.to_s.downcase =~ /\Acurrent_user(\(\))?\z/
    raise DeclarationError, "trigger security should be :invoker, :definer, CURRENT_USER, or a valid user (e.g. 'user'@'host')"
  end
  # sqlite default is n/a, mysql default is :definer, postgres default is :invoker
  @errors << ["sqlite doesn't support trigger security", *HairTrigger::SQLITE_ADAPTERS]
  @errors << ["postgresql doesn't support arbitrary users for trigger security", *HairTrigger::POSTGRESQL_ADAPTERS] unless [:definer, :invoker].include?(user)
  @errors << ["mysql doesn't support invoker trigger security", *HairTrigger::MYSQL_ADAPTERS] if user == :invoker
  options[:security] = user
end
timing(timing) click to toggle source
# File lib/hair_trigger/builder.rb, line 116
def timing(timing)
  raise DeclarationError, "invalid timing" unless [:before, :after].include?(timing)
  options[:timing] = timing.to_s.upcase
end
to_ruby(indent = '', always_generated = true) click to toggle source
# File lib/hair_trigger/builder.rb, line 249
def to_ruby(indent = '', always_generated = true)
  prepare!
  if options[:drop]
    str = "#{indent}drop_trigger(#{prepared_name.inspect}, #{options[:table].inspect}"
    str << ", :generated => true" if always_generated || options[:generated]
    str << ")"
  else
    if @trigger_group
      str = "t." + chained_calls_to_ruby + " do\n"
      str << actions_to_ruby("#{indent}  ") + "\n"
      str << "#{indent}end"
    else
      str = "#{indent}create_trigger(#{prepared_name.inspect}"
      str << ", :generated => true" if always_generated || options[:generated]
      str << ", :compatibility => #{@compatibility}"
      str << ").\n#{indent}    " + chained_calls_to_ruby(".\n#{indent}    ")
      if @triggers
        str << " do |t|\n"
        str << "#{indent}  " + @triggers.map{ |t| t.to_ruby("#{indent}  ") }.join("\n\n#{indent}  ") + "\n"
      else
        str << " do\n"
        str << actions_to_ruby("#{indent}  ") + "\n"
      end
      str << "#{indent}end"
    end
  end
end
validate!(direction = :down) click to toggle source
# File lib/hair_trigger/builder.rb, line 205
def validate!(direction = :down)
  @errors.each do |(error, *adapters)|
    raise GenerationError, error if adapters.include?(adapter_name)
    $stderr.puts "WARNING: " + error if self.class.show_warnings
  end
  @warnings.each do |(error, *adapters)|
    $stderr.puts "WARNING: " + error if adapters.include?(adapter_name) && self.class.show_warnings
  end

  if direction != :up
    @triggers.each{ |t| t.validate!(:down) } if @triggers
  end
  if direction != :down
    @trigger_group.validate!(:up) if @trigger_group
  end
end
warnings() click to toggle source
# File lib/hair_trigger/builder.rb, line 304
def warnings
  (@triggers || []).map(&:warnings).inject(@warnings, &:+)
end
where(where) click to toggle source
# File lib/hair_trigger/builder.rb, line 72
def where(where)
  options[:where] = where
end

Private Instance Methods

actions_to_ruby(indent = '') click to toggle source
# File lib/hair_trigger/builder.rb, line 337
def actions_to_ruby(indent = '')
  if prepared_actions.is_a?(String) && prepared_actions =~ /\n/
    "#{indent}<<-SQL_ACTIONS\n#{prepared_actions}\n#{indent}SQL_ACTIONS"
  else
    indent + prepared_actions.inspect
  end
end
adapter() click to toggle source
# File lib/hair_trigger/builder.rb, line 388
def adapter
  @adapter ||= ActiveRecord::Base.connection
end
adapter_name() click to toggle source
# File lib/hair_trigger/builder.rb, line 384
def adapter_name
  @adapter_name ||= HairTrigger.adapter_name_for(adapter)
end
chained_calls_to_ruby(join_str = '.') click to toggle source
# File lib/hair_trigger/builder.rb, line 310
def chained_calls_to_ruby(join_str = '.')
  @chained_calls.map { |c|
    case c
      when :before, :after, :events
        "#{c}(#{options[:events].map{|c|c.downcase.to_sym.inspect}.join(', ')})"
      when :on
        "on(#{options[:table].inspect})"
      when :where
        "where(#{prepared_where.inspect})"
      when :of
        "of(#{options[:of].inspect[1..-2]})"
      when :old_as
        "old_as(#{options[:referencing][:old].inspect})"
      when :new_as
        "new_as(#{options[:referencing][:new].inspect})"
      when :for_each
        "for_each(#{options[:for_each].downcase.to_sym.inspect})"
      when :declare
        "declare(#{options[:declarations].inspect})"
      when :all
        'all'
      else
        "#{c}(#{options[c].inspect})"
    end
  }.join(join_str)
end
db_version() click to toggle source
# File lib/hair_trigger/builder.rb, line 533
def db_version
  @db_version ||= case adapter_name
    when *HairTrigger::POSTGRESQL_ADAPTERS
      adapter.send(:postgresql_version)
  end
end
declarations() click to toggle source
# File lib/hair_trigger/builder.rb, line 407
def declarations
  return unless declarations = options[:declarations]
  declarations = declarations.strip.split(/;/).map(&:strip).join(";\n")
  "\nDECLARE\n" + normalize(declarations.sub(/;?\n?\z/, ';'), 1).rstrip
end
ensure_semicolon(action) click to toggle source
# File lib/hair_trigger/builder.rb, line 370
def ensure_semicolon(action)
  action && action !~ /;\s*\z/ ? action.sub(/(\s*)\z/, ';\1') : action
end
generate_drop_trigger() click to toggle source
# File lib/hair_trigger/builder.rb, line 441
def generate_drop_trigger
  case adapter_name
    when *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS
      "DROP TRIGGER IF EXISTS #{prepared_name};\n"
    when *HairTrigger::POSTGRESQL_ADAPTERS
      "DROP TRIGGER IF EXISTS #{prepared_name} ON #{adapter.quote_table_name(options[:table])};\nDROP FUNCTION IF EXISTS #{adapter.quote_table_name(prepared_name)}();\n"
    else
      raise GenerationError, "don't know how to drop #{adapter_name} triggers yet"
  end
end
generate_trigger_mysql() click to toggle source
# File lib/hair_trigger/builder.rb, line 514
    def generate_trigger_mysql
      security = options[:security] if options[:security] && options[:security] != :definer
      sql = <<-SQL
CREATE #{security ? "DEFINER = #{security} " : ""}TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].first} ON `#{options[:table]}`
FOR EACH #{options[:for_each]}
BEGIN
      SQL
      (@triggers ? @triggers : [self]).each do |trigger|
        if trigger.prepared_where
          sql << normalize("IF #{trigger.prepared_where} THEN", 1)
          sql << normalize(trigger.raw_actions, 2)
          sql << normalize("END IF;", 1)
        else
          sql << normalize(trigger.raw_actions, 1)
        end
      end
      sql << "END\n";
    end
generate_trigger_postgresql() click to toggle source
# File lib/hair_trigger/builder.rb, line 462
    def generate_trigger_postgresql
      raise GenerationError, "truncate triggers are only supported on postgres 8.4 and greater" if db_version < 80400 && options[:events].include?('TRUNCATE')
      raise GenerationError, "FOR EACH ROW triggers may not be triggered by truncate events" if options[:for_each] == 'ROW' && options[:events].include?('TRUNCATE')
      raise GenerationError, "declare cannot be used in conjunction with nowrap" if options[:nowrap] && options[:declare]
      raise GenerationError, "security cannot be used in conjunction with nowrap" if options[:nowrap] && options[:security]
      raise GenerationError, "where can only be used in conjunction with nowrap on postgres 9.0 and greater" if options[:nowrap] && prepared_where && db_version < 90000
      raise GenerationError, "of can only be used in conjunction with nowrap on postgres 9.1 and greater" if options[:nowrap] && options[:of] && db_version < 90100
      raise GenerationError, "referencing can only be used on postgres 10.0 and greater" if options[:referencing] && db_version < 100000

      sql = ''

      if options[:nowrap]
        trigger_action = raw_actions
      else
        security = options[:security] if options[:security] && options[:security] != :invoker
        sql << <<-SQL
CREATE FUNCTION #{adapter.quote_table_name(prepared_name)}()
RETURNS TRIGGER AS $$#{declarations}
BEGIN
        SQL
        if prepared_where && db_version < 90000
          sql << normalize("IF #{prepared_where} THEN", 1)
          sql << normalize(raw_actions, 2)
          sql << normalize("END IF;", 1)
        else
          sql << normalize(raw_actions, 1)
        end
        # if no return is specified at the end, be sure we set a sane one
        unless raw_actions =~ /return [^;]+;\s*\z/i
          if options[:timing] == "AFTER" || options[:for_each] == 'STATEMENT'
            sql << normalize("RETURN NULL;", 1)
          elsif options[:events].include?('DELETE')
            sql << normalize("RETURN OLD;", 1)
          else
            sql << normalize("RETURN NEW;", 1)
          end
        end
        sql << <<-SQL
END;
$$ LANGUAGE plpgsql#{security ? " SECURITY #{security.to_s.upcase}" : ""};
        SQL

        trigger_action = "#{adapter.quote_table_name(prepared_name)}()"
      end

      [sql, <<-SQL]
CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} #{of_clause}ON #{adapter.quote_table_name(options[:table])}
#{referencing_clause}
FOR EACH #{options[:for_each]}#{prepared_where && db_version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{trigger_action};
      SQL
    end
generate_trigger_sqlite() click to toggle source
# File lib/hair_trigger/builder.rb, line 452
    def generate_trigger_sqlite
      <<-SQL
CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].first} #{of_clause}ON "#{options[:table]}"
FOR EACH #{options[:for_each]}#{prepared_where ? " WHEN " + prepared_where : ''}
BEGIN
#{normalize(raw_actions, 1).rstrip}
END;
      SQL
    end
infer_name() click to toggle source
# File lib/hair_trigger/builder.rb, line 392
def infer_name
  [options[:table],
   options[:timing],
   options[:events],
   of_clause(false),
   options[:for_each],
   @explicit_where ? 'when_' + @explicit_where : nil
  ].flatten.compact.
  join("_").downcase.gsub(/[^a-z0-9_]/, '_').gsub(/_+/, '_')[0, 60] + "_tr"
end
interpolate(str) click to toggle source
# File lib/hair_trigger/builder.rb, line 540
def interpolate(str)
  eval("%@#{str.gsub('@', '\@')}@")
end
maybe_execute(&block) click to toggle source
# File lib/hair_trigger/builder.rb, line 345
def maybe_execute(&block)
  raise DeclarationError, "of may only be specified on update triggers" if options[:of] && options[:events] != ["UPDATE"]
  if block.arity > 0 # we're creating a trigger group, so set up some stuff and pass the buck
    @errors << ["trigger group must specify timing and event(s) for mysql", *HairTrigger::MYSQL_ADAPTERS] unless options[:timing] && options[:events]
    @errors << ["nested trigger groups are not supported for mysql", *HairTrigger::MYSQL_ADAPTERS] if @trigger_group
    @triggers = []
    block.call(self)
    raise DeclarationError, "trigger group did not define any triggers" if @triggers.empty?
  else
    @actions =
      case (actions = block.call)
      when Hash then actions.map { |key, action| [key, ensure_semicolon(action)] }.to_h
      else ensure_semicolon(actions)
      end
  end
  # only the top-most block actually executes
  if !@trigger_group
    validate_names!
    if options[:execute]
      Array(generate).each{ |action| adapter.execute(action)}
    end
  end
  self
end
normalize(text, level = 0) click to toggle source
# File lib/hair_trigger/builder.rb, line 544
def normalize(text, level = 0)
  indent = level * self.class.tab_spacing
  text.gsub!(/\t/, ' ' * self.class.tab_spacing)
  existing = text.split(/\n/).map{ |line| line.sub(/[^ ].*/, '').size }.min
  if existing > indent
    text.gsub!(/^ {#{existing - indent}}/, '')
  elsif indent > existing
    text.gsub!(/^/, ' ' * (indent - existing))
  end
  text.rstrip + "\n"
end
of_clause(check_support = true) click to toggle source
# File lib/hair_trigger/builder.rb, line 403
def of_clause(check_support = true)
  "OF " + options[:of].join(", ") + " " if options[:of] && (!check_support || supports_of?)
end
referencing_clause(check_support = true) click to toggle source
# File lib/hair_trigger/builder.rb, line 424
def referencing_clause(check_support = true)
  if options[:referencing] && (!check_support || supports_referencing?)
    "REFERENCING " + options[:referencing].map{ |k, v| "#{k.to_s.upcase} TABLE AS #{v}" }.join(" ")
  end
end
supports_of?() click to toggle source
# File lib/hair_trigger/builder.rb, line 413
def supports_of?
  case adapter_name
  when *HairTrigger::SQLITE_ADAPTERS
    true
  when *HairTrigger::POSTGRESQL_ADAPTERS
    db_version >= 90000
  else
    false
  end
end
supports_referencing?() click to toggle source
# File lib/hair_trigger/builder.rb, line 430
def supports_referencing?
  case adapter_name
  when *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS
    false
  when *HairTrigger::POSTGRESQL_ADAPTERS
    db_version >= 100000
  else
    false
  end
end
validate_names!() click to toggle source
# File lib/hair_trigger/builder.rb, line 374
def validate_names!
  subtriggers = all_triggers(false)
  named_subtriggers = subtriggers.select{ |t| t.options[:name] }
  if named_subtriggers.present? && !options[:name]
    @warnings << ["nested triggers have explicit names, but trigger group does not. trigger name will be inferred", *HairTrigger::MYSQL_ADAPTERS]
  elsif subtriggers.present? && !named_subtriggers.present? && options[:name]
    @warnings << ["trigger group has an explicit name, but nested triggers do not. trigger names will be inferred", *HairTrigger::POSTGRESQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS]
  end
end