# frozen_string_literal: false module PGTrunk::Operations::Triggers # @abstract # @private # Base class for operations with triggers class Base < PGTrunk::Operation # All attributes that can be used by trigger-related commands attribute :columns, :pg_trunk_array_of_strings, default: [] attribute :constraint, :boolean attribute :events, :pg_trunk_array_of_symbols, default: [] attribute :for_each, :pg_trunk_symbol attribute :function, :pg_trunk_qualified_name attribute :initially, :pg_trunk_symbol attribute :replace_existing, :boolean attribute :table, :pg_trunk_qualified_name attribute :type, :pg_trunk_symbol attribute :when, :string # Generate missed name of the trigger after_initialize { self.name = generated_name if name.blank? } after_initialize { self.type ||= :after if constraint } after_initialize { self.for_each ||= :row if constraint } # Ensure correctness of present values validates :table, presence: true validates :for_each, inclusion: { in: %i[row statement] }, allow_nil: true validates :initially, inclusion: { in: %i[immediate deferred] }, allow_nil: true validates :type, inclusion: { in: %i[after before instead_of] }, allow_nil: true validates :events, inclusion: { in: %i[insert update delete truncate] }, allow_blank: true validate do next if name.blank? errors.add :name, "can't have a schema" unless name.current_schema? end validate do next unless initially && !constraint errors.add :initially, "can be used for constraints only" end validate do next unless columns.present? && type == :instead_of errors.add :columns, "can be defined for before/after update triggers only" end validate do next unless constraint && type != :after && for_each != :row errors.add :base, "Only AFTER EACH ROW triggers can be constraints" end validate do next unless self.when && type == :instead_of errors.add :when, "is not supported for INSTEAD OF triggers" end validate do next if new_name.blank? || new_name.current_schema? errors.add :base, "New name can't specify the schema" end # triggers are ordered by table and name def <=>(other) return unless other.is_a?(self.class) result = table <=> other.table result.zero? ? super : result end # Support `table` and `name` in positional arguments. # @example # add_trigger :users, :my_trigger, **opts ruby_params :table, :name ruby_snippet do |s| s.ruby_param(table.lean) if table.present? s.ruby_param(name.lean) if custom_name? s.ruby_param(to: new_name.lean) if custom_name?(new_name) s.ruby_param(if_exists: true) if if_exists s.ruby_param(:replace_existing, true) if replace_existing s.ruby_line(:function, function.lean) if function.present? s.ruby_line(:when, self.when) s.ruby_line(:constraint, true) if constraint s.ruby_line(:for_each, for_each) if for_each&.== :row s.ruby_line(:type, type) if type.present? s.ruby_line(:events, events) if events.present? s.ruby_line(:columns, columns) if columns.present? s.ruby_line(:initially, initially) s.ruby_line(:comment, comment, from: from_comment) end private # Generate the name of the trigger using the essential options # @return [PGTrunk::QualifiedName] def generated_name return @generated_name if instance_variable_defined?(:@generated_name) @generated_name = begin return if [table, function, type, events].any?(&:blank?) key_options = to_h.reject { |_, v| v.blank? }.slice( :table, :function, :for_each, :type, :events, ) identifier = "#{table.lean}_#{key_options}_tg" hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) PGTrunk::QualifiedName.wrap("tg_rails_#{hashed_identifier}") end end def custom_name?(qname = name) qname&.differs_from?(/^tg_rails_\w{10}$/) end end end