Class: DynamicMigrations::Postgres::Server::Database::Schema::Table::Trigger

Inherits:
DynamicMigrations::Postgres::Server::Database::Source show all
Defined in:
lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb

Overview

This class represents a postgres table trigger

Defined Under Namespace

Classes: ExpectedFunctionError, ExpectedNewRecordsTableError, ExpectedOldRecordsTableError, ExpectedTableError, UnexpectedActionOrderError, UnexpectedActionOrientationError, UnexpectedActionTimingError, UnexpectedEventManipulationError, UnexpectedParametersError, UnexpectedTemplateError, UnnormalizableActionConditionError

Instance Attribute Summary collapse

Attributes inherited from DynamicMigrations::Postgres::Server::Database::Source

#source

Instance Method Summary collapse

Methods inherited from DynamicMigrations::Postgres::Server::Database::Source

#assert_is_a_symbol!, #from_configuration?, #from_database?

Constructor Details

#initialize(source, table, name, action_timing:, event_manipulation:, parameters:, action_orientation:, function:, action_order: nil, action_condition: nil, action_reference_old_table: nil, action_reference_new_table: nil, description: nil, template: nil) ⇒ Trigger

initialize a new object to represent a trigger in a postgres table



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 58

def initialize source, table, name, action_timing:, event_manipulation:, parameters:, action_orientation:, function:, action_order: nil, action_condition: nil, action_reference_old_table: nil, action_reference_new_table: nil, description: nil, template: nil
  super source

  unless table.is_a? Table
    raise ExpectedTableError, table
  end
  @table = table

  unless name.is_a? Symbol
    raise ExpectedSymbolError, name
  end
  @name = name

  unless [:before, :after].include? action_timing
    raise UnexpectedActionTimingError, action_timing
  end
  @action_timing = action_timing

  unless [:insert, :delete, :update].include? event_manipulation
    raise UnexpectedEventManipulationError, event_manipulation
  end
  @event_manipulation = event_manipulation

  if from_configuration?
    unless action_order.nil?
      raise UnexpectedActionOrderError, "Unexpected `action_order` argument. Action order is calculated dynamically for configured triggers."
    end

  else
    unless action_order.is_a?(Integer) && action_order >= 1
      raise UnexpectedActionOrderError, "Missing valid `action_order` argument. Action order must be provided for triggers loaded from the database."
    end
    @action_order = action_order
  end

  unless action_condition.nil? || action_condition.is_a?(String)
    raise ExpectedStringError, action_condition
  end
  @action_condition = action_condition&.strip

  unless parameters.is_a?(Array) && parameters.all? { |p| p.is_a? String }
    raise UnexpectedParametersError, "unexpected parameters `#{parameters}`, currently only an array of strings is supported"
  end
  @parameters = parameters

  unless [:row, :statement].include? action_orientation
    raise UnexpectedActionOrientationError, action_orientation
  end
  @action_orientation = action_orientation

  unless function.is_a? Function
    raise ExpectedFunctionError, function
  end
  # this should never happen, but adding it just in case
  unless function.source == source
    raise "Internal error - function source `#{function.source}` does not match trigger source `#{source}`"
  end
  @function = function
  # associate this trigger with the function (so they are aware of each other)
  @function.add_trigger self

  unless action_reference_old_table.nil? || action_reference_old_table == :old_records
    raise ExpectedOldRecordsTableError, "expected :old_records or nil, but got #{action_reference_old_table}"
  end
  @action_reference_old_table = action_reference_old_table

  unless action_reference_new_table.nil? || action_reference_new_table == :new_records
    raise ExpectedNewRecordsTableError, "expected :new_records or nil, but got #{action_reference_new_table}"
  end
  @action_reference_new_table = action_reference_new_table

  unless description.nil?
    raise ExpectedStringError, description unless description.is_a? String
    @description = description.strip
    @description = nil if description == ""
  end

  unless template.nil?
    unless Generator::Trigger.has_template? template
      raise UnexpectedTemplateError, template
    end
    @template = template
  end
end

Instance Attribute Details

#action_conditionObject

Returns the value of attribute action_condition.



48
49
50
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 48

def action_condition
  @action_condition
end

#action_orientationObject (readonly)

Returns the value of attribute action_orientation.



50
51
52
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 50

def action_orientation
  @action_orientation
end

#action_reference_new_tableObject (readonly)

Returns the value of attribute action_reference_new_table.



53
54
55
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 53

def action_reference_new_table
  @action_reference_new_table
end

#action_reference_old_tableObject (readonly)

Returns the value of attribute action_reference_old_table.



52
53
54
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 52

def action_reference_old_table
  @action_reference_old_table
end

#action_timingObject (readonly)

Returns the value of attribute action_timing.



47
48
49
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 47

def action_timing
  @action_timing
end

#descriptionObject (readonly)

Returns the value of attribute description.



54
55
56
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 54

def description
  @description
end

#event_manipulationObject (readonly)

Returns the value of attribute event_manipulation.



46
47
48
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 46

def event_manipulation
  @event_manipulation
end

#functionObject (readonly)

Returns the value of attribute function.



51
52
53
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 51

def function
  @function
end

#nameObject (readonly)

Returns the value of attribute name.



45
46
47
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 45

def name
  @name
end

#parametersObject (readonly)

Returns the value of attribute parameters.



49
50
51
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 49

def parameters
  @parameters
end

#tableObject (readonly)

Returns the value of attribute table.



44
45
46
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 44

def table
  @table
end

#templateObject (readonly)

Returns the value of attribute template.



55
56
57
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 55

def template
  @template
end

Instance Method Details

#action_orderObject



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 143

def action_order
  # if the source is the database, then return the locally stored
  # representation of the action order
  if from_database?
    action_order = @action_order
    if action_order.nil?
      raise "Missing valid action_order. This should be impossible."
    end
    action_order

  # otherwise is is computed by finding the index of the trigger within a list of
  # triggers that are alphabetically sorted, all of which pertain to the same event
  # manipulation (such as update, insert, etc.) for this triggers table
  else
    pos = @table.triggers.select { |t| t.event_manipulation == event_manipulation }.sort_by(&:name).index(self)
    if pos.nil?
      raise "Trigger not found in table triggers list. This should be impossible."
    end
    pos + 1
  end
end

#add_parameter(new_parameter) ⇒ Object



172
173
174
175
176
177
178
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 172

def add_parameter new_parameter
  unless new_parameter.is_a? String
    raise UnexpectedParametersError, "unexpected parameter `#{new_parameter}`, can only add strings to the list of parameters"
  end

  @parameters << new_parameter
end

#differences_descriptions(other_trigger) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 185

def differences_descriptions other_trigger
  descriptions = method_differences_descriptions other_trigger, [
    :event_manipulation,
    :action_timing,
    :action_order,
    :normalized_action_condition,
    :parameters,
    :action_orientation,
    :action_reference_old_table,
    :action_reference_new_table
  ]
  # add the function differences descriptions
  function.differences_descriptions(other_trigger.function).each do |description|
    descriptions << "function_#{description}"
  end
  # return the combined differences
  descriptions
end

#has_description?Boolean

return true if this has a description, otherwise false

Returns:

  • (Boolean)


181
182
183
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 181

def has_description?
  !@description.nil?
end

#normalized_action_conditionObject

create a temporary table in postgres to represent this trigger and fetch the actual normalized action_condition directly from the database



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb', line 206

def normalized_action_condition
  if action_condition.nil?
    nil
  # no need to normalize action_conditions which originated from the database
  elsif from_database?
    action_condition
  else
    ac = table.schema.database.with_connection do |connection|
      # wrapped in a transaction just in case something here fails, because
      # we don't want the function, temporary table or trigger to be persisted
      connection.exec("BEGIN")

      # create the temp table and add the expected columns
      temp_enums = table.create_temp_table(connection, "trigger_normalized_action_condition_temp_table")

      # create a temporary function to trigger (triggers require a function)
      connection.exec(<<~SQL)
        CREATE OR REPLACE FUNCTION trigger_normalized_action_condition_temp_fn() returns trigger language plpgsql AS
        $$ BEGIN END $$;
      SQL

      temp_action_condition = action_condition
      # string replace any real enum names with their temp enum names
      temp_enums.each do |temp_enum_name, enum|
        temp_action_condition.gsub!("::#{enum.name}", "::#{temp_enum_name}")
        temp_action_condition.gsub!("::#{enum.full_name}", "::#{temp_enum_name}")
      end

      # create a temporary trigger, from which we will fetch the normalized action condition
      connection.exec(<<~SQL)
        CREATE TRIGGER trigger_normalized_action_condition_temp_trigger
        BEFORE UPDATE ON trigger_normalized_action_condition_temp_table
          FOR EACH ROW
            WHEN (#{action_condition})
            EXECUTE FUNCTION trigger_normalized_action_condition_temp_fn();
      SQL

      # get the normalzed version of the action condition
      rows = connection.exec(<<~SQL)
        SELECT (
          regexp_match(
            pg_get_triggerdef(oid),
            '.{35,} WHEN ((.+)) EXECUTE FUNCTION')
          )[1] as action_condition
        FROM pg_trigger
        WHERE tgname = 'trigger_normalized_action_condition_temp_trigger'
        ;
      SQL

      # delete the temp table and close the transaction
      connection.exec("ROLLBACK")

      # return the normalized action condition
      action_condition_result = rows.first["action_condition"]

      # string replace any enum names with their real enum names
      temp_enums.each do |temp_enum_name, enum|
        real_enum_name = (enum.schema == table.schema) ? enum.name : enum.full_name
        action_condition_result.gsub!("::#{temp_enum_name}", "::#{real_enum_name}")
      end

      action_condition_result
    end

    if ac.nil?
      raise UnnormalizableActionConditionError, "Failed to nomalize action condition `#{action_condition}`"
    end

    ac
  end
end