Class: PgSqlTriggers::Testing::SyntaxValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/pg_sql_triggers/testing/syntax_validator.rb

Instance Method Summary collapse

Constructor Details

#initialize(trigger_registry) ⇒ SyntaxValidator

Returns a new instance of SyntaxValidator.



6
7
8
# File 'lib/pg_sql_triggers/testing/syntax_validator.rb', line 6

def initialize(trigger_registry)
  @trigger = trigger_registry
end

Instance Method Details

#validate_allObject

Run all validations



131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/pg_sql_triggers/testing/syntax_validator.rb', line 131

def validate_all
  dsl_result = validate_dsl
  function_result = validate_function_syntax
  condition_result = validate_condition

  {
    dsl: dsl_result,
    function: function_result,
    condition: condition_result,
    overall_valid: dsl_result[:valid] &&
      function_result[:valid] &&
      condition_result[:valid]
  }
end

#validate_conditionObject

Validate WHEN condition syntax



53
54
55
56
57
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
# File 'lib/pg_sql_triggers/testing/syntax_validator.rb', line 53

def validate_condition
  return { valid: true } if @trigger.condition.blank?
  return { valid: false, error: "Table name is required for condition validation" } if @trigger.table_name.blank?

  if @trigger.definition.blank?
    return { valid: false,
             error: "Function name is required for condition validation" }
  end

  definition = begin
    JSON.parse(@trigger.definition)
  rescue StandardError
    {}
  end
  function_name = definition["function_name"] || "test_validation_function"
  events = Array(definition["events"] || [])
  sanitized_table = ActiveRecord::Base.connection.quote_string(@trigger.table_name)
  sanitized_function = ActiveRecord::Base.connection.quote_string(function_name)
  sanitized_condition = @trigger.condition

  # Check if condition references OLD values
  condition_uses_old = sanitized_condition.match?(/\bOLD\./i)

  # Determine which event to use for validation
  # If condition uses OLD, we must use UPDATE or DELETE since INSERT doesn't have OLD
  # If condition doesn't use OLD, we can use INSERT
  if condition_uses_old
    # Condition references OLD, so it can't be used with INSERT
    if events.include?("insert")
      return {
        valid: false,
        error: "WHEN condition cannot reference OLD values for INSERT triggers. " \
               "Use UPDATE or DELETE events, or modify condition to only use NEW values."
      }
    end
    # Use UPDATE for validation if available (it has OLD), otherwise use DELETE
    test_event = events.include?("update") ? "UPDATE" : "DELETE"
  else
    # Condition doesn't reference OLD, so INSERT is fine
    test_event = "INSERT"
  end

  # Validate condition by creating a temporary trigger with the condition
  # This is the only way to validate WHEN conditions since they use NEW/OLD
  test_function_sql = <<~SQL.squish
    CREATE OR REPLACE FUNCTION #{sanitized_function}() RETURNS TRIGGER AS $$
    BEGIN
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
  SQL

  test_trigger_sql = <<~SQL.squish
    CREATE TRIGGER test_validation_trigger
    BEFORE #{test_event} ON #{sanitized_table}
    FOR EACH ROW
    WHEN (#{sanitized_condition})
    EXECUTE FUNCTION #{sanitized_function}();
  SQL

  ActiveRecord::Base.connection.execute("BEGIN")
  ActiveRecord::Base.connection.execute(test_function_sql)
  ActiveRecord::Base.connection.execute(test_trigger_sql)
  ActiveRecord::Base.connection.execute("DROP TRIGGER IF EXISTS test_validation_trigger ON #{sanitized_table}")
  ActiveRecord::Base.connection.execute("DROP FUNCTION IF EXISTS #{sanitized_function}()")
  ActiveRecord::Base.connection.execute("ROLLBACK")

  { valid: true, message: "Condition syntax is valid" }
rescue ActiveRecord::StatementInvalid => e
  begin
    ActiveRecord::Base.connection.execute("ROLLBACK")
  rescue StandardError
    # Ignore rollback errors
  end
  { valid: false, error: e.message }
end

#validate_dslObject

Validate DSL structure



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/pg_sql_triggers/testing/syntax_validator.rb', line 11

def validate_dsl
  return { valid: false, errors: ["Missing definition"], definition: {} } if @trigger.definition.blank?

  definition = begin
    JSON.parse(@trigger.definition)
  rescue StandardError
    {}
  end
  errors = []

  errors << "Missing trigger name" if definition["name"].blank?
  errors << "Missing table name" if definition["table_name"].blank?
  errors << "Missing function name" if definition["function_name"].blank?
  errors << "Missing events" if definition["events"].blank?
  errors << "Invalid version" unless definition["version"].to_i.positive?

  {
    valid: errors.empty?,
    errors: errors,
    definition: definition
  }
end

#validate_function_syntaxObject

Validate PL/pgSQL function syntax (uses PostgreSQL’s parser)



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/pg_sql_triggers/testing/syntax_validator.rb', line 35

def validate_function_syntax
  return { valid: false, error: "No function body defined" } if @trigger.function_body.blank?

  ActiveRecord::Base.connection.execute("BEGIN")
  ActiveRecord::Base.connection.execute(@trigger.function_body)
  ActiveRecord::Base.connection.execute("ROLLBACK")

  { valid: true, message: "Function syntax is valid" }
rescue ActiveRecord::StatementInvalid => e
  begin
    ActiveRecord::Base.connection.execute("ROLLBACK")
  rescue StandardError
    # Ignore rollback errors
  end
  { valid: false, error: e.message }
end