pg_sql_triggers 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.erb_lint.yml +47 -0
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +112 -1
- data/COVERAGE.md +58 -0
- data/Goal.md +450 -123
- data/README.md +53 -215
- data/app/controllers/pg_sql_triggers/application_controller.rb +46 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -1
- data/app/controllers/pg_sql_triggers/generator_controller.rb +76 -8
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +18 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +93 -12
- data/app/views/layouts/pg_sql_triggers/application.html.erb +34 -1
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +70 -30
- data/app/views/pg_sql_triggers/generator/new.html.erb +22 -4
- data/app/views/pg_sql_triggers/generator/preview.html.erb +244 -16
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +221 -0
- data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +40 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +0 -2
- data/app/views/pg_sql_triggers/tables/show.html.erb +3 -4
- data/config/initializers/pg_sql_triggers.rb +69 -0
- data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +3 -1
- data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/README.md +66 -0
- data/docs/api-reference.md +681 -0
- data/docs/configuration.md +541 -0
- data/docs/getting-started.md +135 -0
- data/docs/kill-switch.md +586 -0
- data/docs/screenshots/.gitkeep +1 -0
- data/docs/screenshots/Generate Trigger.png +0 -0
- data/docs/screenshots/Triggers Page.png +0 -0
- data/docs/screenshots/kill error.png +0 -0
- data/docs/screenshots/kill modal for migration down.png +0 -0
- data/docs/usage-guide.md +493 -0
- data/docs/web-ui.md +353 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +3 -1
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +44 -2
- data/lib/pg_sql_triggers/drift/db_queries.rb +116 -0
- data/lib/pg_sql_triggers/drift/detector.rb +187 -0
- data/lib/pg_sql_triggers/drift/reporter.rb +179 -0
- data/lib/pg_sql_triggers/drift.rb +14 -11
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +15 -1
- data/lib/pg_sql_triggers/generator/form.rb +3 -1
- data/lib/pg_sql_triggers/generator/service.rb +82 -26
- data/lib/pg_sql_triggers/migration.rb +1 -1
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +344 -0
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +143 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +258 -0
- data/lib/pg_sql_triggers/migrator.rb +85 -3
- data/lib/pg_sql_triggers/registry/manager.rb +100 -13
- data/lib/pg_sql_triggers/sql/kill_switch.rb +300 -0
- data/lib/pg_sql_triggers/testing/dry_run.rb +5 -7
- data/lib/pg_sql_triggers/testing/function_tester.rb +66 -24
- data/lib/pg_sql_triggers/testing/safe_executor.rb +23 -11
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +24 -1
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +24 -0
- data/lib/tasks/trigger_migrations.rake +33 -0
- data/scripts/generate_coverage_report.rb +129 -0
- metadata +45 -5
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../drift/db_queries"
|
|
4
|
+
|
|
5
|
+
module PgSqlTriggers
|
|
6
|
+
class Migrator
|
|
7
|
+
# Pre-apply comparator that extracts expected SQL from migrations
|
|
8
|
+
# and compares it with the current database state
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class PreApplyComparator
|
|
11
|
+
class << self
|
|
12
|
+
# Compare expected state from migration with actual database state
|
|
13
|
+
# Returns a comparison result with diff information
|
|
14
|
+
def compare(migration_instance, direction: :up)
|
|
15
|
+
expected = extract_expected_state(migration_instance, direction)
|
|
16
|
+
actual = extract_actual_state(expected)
|
|
17
|
+
generate_diff(expected, actual)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Extract expected SQL and state from migration instance
|
|
23
|
+
def extract_expected_state(migration_instance, direction)
|
|
24
|
+
captured_sql = capture_sql(migration_instance, direction)
|
|
25
|
+
parse_sql_to_state(captured_sql)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Capture SQL that would be executed by the migration
|
|
29
|
+
def capture_sql(migration_instance, direction)
|
|
30
|
+
captured = []
|
|
31
|
+
|
|
32
|
+
# Override execute to capture SQL instead of executing
|
|
33
|
+
# Since we use a separate instance for comparison, we don't need to restore
|
|
34
|
+
migration_instance.define_singleton_method(:execute) do |sql|
|
|
35
|
+
captured << sql.to_s.strip
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Call the migration method (up or down) to capture SQL
|
|
39
|
+
migration_instance.public_send(direction)
|
|
40
|
+
|
|
41
|
+
captured
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parse captured SQL into structured state (triggers, functions)
|
|
45
|
+
def parse_sql_to_state(sql_array)
|
|
46
|
+
state = {
|
|
47
|
+
functions: [],
|
|
48
|
+
triggers: []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sql_array.each do |sql|
|
|
52
|
+
sql_normalized = sql.squish
|
|
53
|
+
|
|
54
|
+
# Parse CREATE FUNCTION statements
|
|
55
|
+
if sql_normalized.match?(/CREATE\s+(OR\s+REPLACE\s+)?FUNCTION/i)
|
|
56
|
+
function_info = parse_function_sql(sql)
|
|
57
|
+
state[:functions] << function_info if function_info
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Parse CREATE TRIGGER statements
|
|
61
|
+
if sql_normalized.match?(/CREATE\s+TRIGGER/i)
|
|
62
|
+
trigger_info = parse_trigger_sql(sql)
|
|
63
|
+
state[:triggers] << trigger_info if trigger_info
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Parse DROP statements (for down migrations)
|
|
67
|
+
next unless sql_normalized.match?(/DROP\s+(TRIGGER|FUNCTION)/i)
|
|
68
|
+
|
|
69
|
+
drop_info = parse_drop_sql(sql)
|
|
70
|
+
state[:drops] ||= []
|
|
71
|
+
state[:drops] << drop_info if drop_info
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
state
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Parse function SQL to extract function name and body
|
|
78
|
+
def parse_function_sql(sql)
|
|
79
|
+
# Match CREATE [OR REPLACE] FUNCTION function_name(...) ... AS $$ body $$
|
|
80
|
+
match = sql.match(/CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(\w+)\s*\([^)]*\)/i)
|
|
81
|
+
return nil unless match
|
|
82
|
+
|
|
83
|
+
function_name = match[1]
|
|
84
|
+
|
|
85
|
+
# Extract function body (between $$ markers or AS ...)
|
|
86
|
+
body_match = sql.match(/\$\$(.+?)\$\$/m) || sql.match(/AS\s+(.+)/im)
|
|
87
|
+
function_body = body_match ? body_match[1].strip : sql
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
function_name: function_name,
|
|
91
|
+
function_body: function_body,
|
|
92
|
+
full_sql: sql
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Parse trigger SQL to extract trigger details
|
|
97
|
+
def parse_trigger_sql(sql)
|
|
98
|
+
# Match CREATE TRIGGER trigger_name BEFORE/AFTER events ON table_name ...
|
|
99
|
+
match = sql.match(/CREATE\s+TRIGGER\s+(\w+)\s+(BEFORE|AFTER)\s+(.+?)\s+ON\s+(\w+)/i)
|
|
100
|
+
return nil unless match
|
|
101
|
+
|
|
102
|
+
trigger_name = match[1]
|
|
103
|
+
timing = match[2]
|
|
104
|
+
events = match[3].strip.split(/\s+OR\s+/i).map(&:strip)
|
|
105
|
+
table_name = match[4]
|
|
106
|
+
|
|
107
|
+
# Extract WHEN condition if present
|
|
108
|
+
condition_match = sql.match(/WHEN\s+\(([^)]+)\)/i)
|
|
109
|
+
condition = condition_match ? condition_match[1].strip : nil
|
|
110
|
+
|
|
111
|
+
# Extract function name
|
|
112
|
+
function_match = sql.match(/EXECUTE\s+FUNCTION\s+(\w+)\s*\(\)/i)
|
|
113
|
+
function_name = function_match ? function_match[1] : nil
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
trigger_name: trigger_name,
|
|
117
|
+
table_name: table_name,
|
|
118
|
+
timing: timing,
|
|
119
|
+
events: events,
|
|
120
|
+
condition: condition,
|
|
121
|
+
function_name: function_name,
|
|
122
|
+
full_sql: sql
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Parse DROP SQL statements
|
|
127
|
+
def parse_drop_sql(sql)
|
|
128
|
+
if sql.match?(/DROP\s+TRIGGER/i)
|
|
129
|
+
match = sql.match(/DROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?(\w+)\s+ON\s+(\w+)/i)
|
|
130
|
+
return nil unless match
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
type: :trigger,
|
|
134
|
+
name: match[1],
|
|
135
|
+
table_name: match[2]
|
|
136
|
+
}
|
|
137
|
+
elsif sql.match?(/DROP\s+FUNCTION/i)
|
|
138
|
+
match = sql.match(/DROP\s+FUNCTION\s+(?:IF\s+EXISTS\s+)?(\w+)\s*\(\)/i)
|
|
139
|
+
return nil unless match
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
type: :function,
|
|
143
|
+
name: match[1]
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Extract actual state from database
|
|
149
|
+
def extract_actual_state(expected)
|
|
150
|
+
actual = {
|
|
151
|
+
functions: {},
|
|
152
|
+
triggers: {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Get actual functions from database
|
|
156
|
+
expected[:functions].each do |expected_func|
|
|
157
|
+
db_func = Drift::DbQueries.find_function(expected_func[:function_name])
|
|
158
|
+
actual[:functions][expected_func[:function_name]] = if db_func
|
|
159
|
+
{
|
|
160
|
+
function_name: db_func["function_name"],
|
|
161
|
+
function_body: db_func["function_definition"],
|
|
162
|
+
exists: true
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
{
|
|
166
|
+
function_name: expected_func[:function_name],
|
|
167
|
+
exists: false
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get actual triggers from database
|
|
173
|
+
expected[:triggers].each do |expected_trigger|
|
|
174
|
+
db_trigger = Drift::DbQueries.find_trigger(expected_trigger[:trigger_name])
|
|
175
|
+
if db_trigger
|
|
176
|
+
actual[:triggers][expected_trigger[:trigger_name]] = {
|
|
177
|
+
trigger_name: db_trigger["trigger_name"],
|
|
178
|
+
table_name: db_trigger["table_name"],
|
|
179
|
+
function_name: db_trigger["function_name"],
|
|
180
|
+
trigger_definition: db_trigger["trigger_definition"],
|
|
181
|
+
function_definition: db_trigger["function_definition"],
|
|
182
|
+
exists: true
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
actual[:triggers][expected_trigger[:trigger_name]] = {
|
|
186
|
+
trigger_name: expected_trigger[:trigger_name],
|
|
187
|
+
table_name: expected_trigger[:table_name],
|
|
188
|
+
exists: false
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
actual
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Generate diff between expected and actual state
|
|
197
|
+
# rubocop:disable Metrics/MethodLength
|
|
198
|
+
def generate_diff(expected, actual)
|
|
199
|
+
diff = {
|
|
200
|
+
has_differences: false,
|
|
201
|
+
functions: [],
|
|
202
|
+
triggers: [],
|
|
203
|
+
drops: expected[:drops] || []
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Compare functions
|
|
207
|
+
expected[:functions].each do |expected_func|
|
|
208
|
+
func_name = expected_func[:function_name]
|
|
209
|
+
actual_func = actual[:functions][func_name]
|
|
210
|
+
|
|
211
|
+
if !actual_func || !actual_func[:exists]
|
|
212
|
+
diff[:functions] << {
|
|
213
|
+
function_name: func_name,
|
|
214
|
+
status: :new,
|
|
215
|
+
expected: expected_func[:function_body],
|
|
216
|
+
actual: nil,
|
|
217
|
+
message: "Function will be created"
|
|
218
|
+
}
|
|
219
|
+
diff[:has_differences] = true
|
|
220
|
+
elsif actual_func[:function_body] != expected_func[:function_body]
|
|
221
|
+
diff[:functions] << {
|
|
222
|
+
function_name: func_name,
|
|
223
|
+
status: :modified,
|
|
224
|
+
expected: expected_func[:function_body],
|
|
225
|
+
actual: actual_func[:function_body],
|
|
226
|
+
message: "Function body differs from expected"
|
|
227
|
+
}
|
|
228
|
+
diff[:has_differences] = true
|
|
229
|
+
else
|
|
230
|
+
diff[:functions] << {
|
|
231
|
+
function_name: func_name,
|
|
232
|
+
status: :unchanged,
|
|
233
|
+
message: "Function matches expected state"
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Compare triggers
|
|
239
|
+
expected[:triggers].each do |expected_trigger|
|
|
240
|
+
trigger_name = expected_trigger[:trigger_name]
|
|
241
|
+
actual_trigger = actual[:triggers][trigger_name]
|
|
242
|
+
|
|
243
|
+
if !actual_trigger || !actual_trigger[:exists]
|
|
244
|
+
diff[:triggers] << {
|
|
245
|
+
trigger_name: trigger_name,
|
|
246
|
+
status: :new,
|
|
247
|
+
expected: expected_trigger[:full_sql],
|
|
248
|
+
actual: nil,
|
|
249
|
+
message: "Trigger will be created"
|
|
250
|
+
}
|
|
251
|
+
diff[:has_differences] = true
|
|
252
|
+
else
|
|
253
|
+
# Compare trigger definitions
|
|
254
|
+
expected_def = normalize_trigger_definition(expected_trigger)
|
|
255
|
+
actual_def = normalize_trigger_definition_from_db(actual_trigger)
|
|
256
|
+
|
|
257
|
+
if expected_def == actual_def
|
|
258
|
+
diff[:triggers] << {
|
|
259
|
+
trigger_name: trigger_name,
|
|
260
|
+
status: :unchanged,
|
|
261
|
+
message: "Trigger matches expected state"
|
|
262
|
+
}
|
|
263
|
+
else
|
|
264
|
+
diff[:triggers] << {
|
|
265
|
+
trigger_name: trigger_name,
|
|
266
|
+
status: :modified,
|
|
267
|
+
expected: expected_trigger[:full_sql],
|
|
268
|
+
actual: actual_trigger[:trigger_definition],
|
|
269
|
+
message: "Trigger definition differs from expected",
|
|
270
|
+
differences: compare_trigger_details(expected_trigger, actual_trigger)
|
|
271
|
+
}
|
|
272
|
+
diff[:has_differences] = true
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
diff
|
|
278
|
+
end
|
|
279
|
+
# rubocop:enable Metrics/MethodLength
|
|
280
|
+
|
|
281
|
+
# Normalize trigger definition for comparison
|
|
282
|
+
def normalize_trigger_definition(trigger)
|
|
283
|
+
{
|
|
284
|
+
trigger_name: trigger[:trigger_name],
|
|
285
|
+
table_name: trigger[:table_name],
|
|
286
|
+
events: trigger[:events].sort,
|
|
287
|
+
condition: trigger[:condition],
|
|
288
|
+
function_name: trigger[:function_name]
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Normalize trigger definition from database for comparison
|
|
293
|
+
def normalize_trigger_definition_from_db(db_trigger)
|
|
294
|
+
# Parse trigger definition from pg_get_triggerdef output
|
|
295
|
+
def_str = db_trigger[:trigger_definition] || ""
|
|
296
|
+
|
|
297
|
+
# Extract events, condition, etc. from definition string
|
|
298
|
+
events_match = def_str.match(/BEFORE\s+(.+?)\s+ON/i) || def_str.match(/AFTER\s+(.+?)\s+ON/i)
|
|
299
|
+
events = events_match ? events_match[1].split(/\s+OR\s+/i).map(&:strip).sort : []
|
|
300
|
+
|
|
301
|
+
condition_match = def_str.match(/WHEN\s+\(([^)]+)\)/i)
|
|
302
|
+
condition = condition_match ? condition_match[1].strip : nil
|
|
303
|
+
|
|
304
|
+
{
|
|
305
|
+
trigger_name: db_trigger[:trigger_name],
|
|
306
|
+
table_name: db_trigger[:table_name],
|
|
307
|
+
events: events,
|
|
308
|
+
condition: condition,
|
|
309
|
+
function_name: db_trigger[:function_name]
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Compare trigger details to find specific differences
|
|
314
|
+
def compare_trigger_details(expected, actual_db_trigger)
|
|
315
|
+
differences = []
|
|
316
|
+
|
|
317
|
+
# Normalize actual trigger for comparison
|
|
318
|
+
actual = normalize_trigger_definition_from_db(actual_db_trigger)
|
|
319
|
+
|
|
320
|
+
if expected[:table_name] != actual[:table_name]
|
|
321
|
+
differences << "Table name: expected '#{expected[:table_name]}', actual '#{actual[:table_name]}'"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
expected_events = expected[:events].sort
|
|
325
|
+
actual_events = actual[:events] || []
|
|
326
|
+
if expected_events != actual_events.sort
|
|
327
|
+
differences << "Events: expected #{expected_events.inspect}, actual #{actual_events.inspect}"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
if expected[:condition] != actual[:condition]
|
|
331
|
+
differences << "Condition: expected '#{expected[:condition]}', actual '#{actual[:condition]}'"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
if expected[:function_name] != actual[:function_name]
|
|
335
|
+
differences << "Function: expected '#{expected[:function_name]}', actual '#{actual[:function_name]}'"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
differences
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
# rubocop:enable Metrics/ClassLength
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
class Migrator
|
|
5
|
+
# Formats pre-apply comparison results into human-readable diff reports
|
|
6
|
+
class PreApplyDiffReporter
|
|
7
|
+
class << self
|
|
8
|
+
# Format diff result as a string report
|
|
9
|
+
def format(diff_result, migration_name: nil)
|
|
10
|
+
return "No differences detected. Migration is safe to apply." unless diff_result[:has_differences]
|
|
11
|
+
|
|
12
|
+
output = []
|
|
13
|
+
output << ("=" * 80)
|
|
14
|
+
output << "Pre-Apply Comparison Report"
|
|
15
|
+
output << "Migration: #{migration_name}" if migration_name
|
|
16
|
+
output << ("=" * 80)
|
|
17
|
+
output << ""
|
|
18
|
+
|
|
19
|
+
# Report on functions
|
|
20
|
+
if diff_result[:functions].any?
|
|
21
|
+
output << "Functions:"
|
|
22
|
+
output << ("-" * 80)
|
|
23
|
+
diff_result[:functions].each do |func_diff|
|
|
24
|
+
output.concat(format_function_diff(func_diff))
|
|
25
|
+
output << ""
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Report on triggers
|
|
30
|
+
if diff_result[:triggers].any?
|
|
31
|
+
output << "Triggers:"
|
|
32
|
+
output << ("-" * 80)
|
|
33
|
+
diff_result[:triggers].each do |trigger_diff|
|
|
34
|
+
output.concat(format_trigger_diff(trigger_diff))
|
|
35
|
+
output << ""
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Report on drops (for down migrations)
|
|
40
|
+
if diff_result[:drops]&.any?
|
|
41
|
+
output << "Drops:"
|
|
42
|
+
output << ("-" * 80)
|
|
43
|
+
diff_result[:drops].each do |drop|
|
|
44
|
+
output << " - Will #{drop[:type] == :trigger ? 'drop trigger' : 'drop function'}: #{drop[:name]}"
|
|
45
|
+
end
|
|
46
|
+
output << ""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
output << ("=" * 80)
|
|
50
|
+
output << ""
|
|
51
|
+
output << "⚠️ WARNING: This migration will modify existing database objects."
|
|
52
|
+
output << "Review the differences above before proceeding."
|
|
53
|
+
|
|
54
|
+
output.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Format a concise summary for console output
|
|
58
|
+
def format_summary(diff_result)
|
|
59
|
+
return "✓ No differences - safe to apply" unless diff_result[:has_differences]
|
|
60
|
+
|
|
61
|
+
summary = []
|
|
62
|
+
summary << "⚠️ Differences detected:"
|
|
63
|
+
|
|
64
|
+
new_count = diff_result[:functions].count { |f| f[:status] == :new } +
|
|
65
|
+
diff_result[:triggers].count { |t| t[:status] == :new }
|
|
66
|
+
modified_count = diff_result[:functions].count { |f| f[:status] == :modified } +
|
|
67
|
+
diff_result[:triggers].count { |t| t[:status] == :modified }
|
|
68
|
+
|
|
69
|
+
summary << " - #{new_count} new object(s) will be created"
|
|
70
|
+
summary << " - #{modified_count} existing object(s) will be modified"
|
|
71
|
+
|
|
72
|
+
summary.join("\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def format_function_diff(func_diff)
|
|
78
|
+
case func_diff[:status]
|
|
79
|
+
when :new
|
|
80
|
+
[
|
|
81
|
+
" Function: #{func_diff[:function_name]}",
|
|
82
|
+
" Status: NEW (will be created)"
|
|
83
|
+
]
|
|
84
|
+
when :modified
|
|
85
|
+
[
|
|
86
|
+
" Function: #{func_diff[:function_name]}",
|
|
87
|
+
" Status: MODIFIED (will overwrite existing function)",
|
|
88
|
+
" Expected:",
|
|
89
|
+
indent_text(func_diff[:expected], 6),
|
|
90
|
+
" Current:",
|
|
91
|
+
indent_text(func_diff[:actual], 6)
|
|
92
|
+
]
|
|
93
|
+
when :unchanged
|
|
94
|
+
[
|
|
95
|
+
" Function: #{func_diff[:function_name]}",
|
|
96
|
+
" Status: UNCHANGED"
|
|
97
|
+
]
|
|
98
|
+
else
|
|
99
|
+
[
|
|
100
|
+
" Function: #{func_diff[:function_name]}",
|
|
101
|
+
" Status: #{func_diff[:status]}"
|
|
102
|
+
]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def format_trigger_diff(trigger_diff)
|
|
107
|
+
output = []
|
|
108
|
+
output << " Trigger: #{trigger_diff[:trigger_name]}"
|
|
109
|
+
case trigger_diff[:status]
|
|
110
|
+
when :new
|
|
111
|
+
output << " Status: NEW (will be created)"
|
|
112
|
+
output << " Definition:"
|
|
113
|
+
output << indent_text(trigger_diff[:expected], 6)
|
|
114
|
+
when :modified
|
|
115
|
+
output << " Status: MODIFIED (will overwrite existing trigger)"
|
|
116
|
+
|
|
117
|
+
if trigger_diff[:differences]&.any?
|
|
118
|
+
output << " Differences:"
|
|
119
|
+
trigger_diff[:differences].each do |diff|
|
|
120
|
+
output << " - #{diff}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
output << " Expected:"
|
|
125
|
+
output << indent_text(trigger_diff[:expected], 6)
|
|
126
|
+
output << " Current:"
|
|
127
|
+
output << indent_text(trigger_diff[:actual], 6)
|
|
128
|
+
when :unchanged
|
|
129
|
+
output << " Status: UNCHANGED"
|
|
130
|
+
else
|
|
131
|
+
output << " Status: #{trigger_diff[:status]}"
|
|
132
|
+
end
|
|
133
|
+
output
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def indent_text(text, spaces)
|
|
137
|
+
indent = " " * spaces
|
|
138
|
+
text.to_s.lines.map { |line| "#{indent}#{line.chomp}" }.join("\n")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|