pg_sql_triggers 1.3.0 → 1.5.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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.erb_lint.yml +0 -0
  3. data/.rspec +0 -0
  4. data/.rubocop.yml +6 -16
  5. data/AGENTS.md +8 -0
  6. data/CHANGELOG.md +354 -0
  7. data/COVERAGE.md +39 -41
  8. data/LICENSE +0 -0
  9. data/README.md +44 -26
  10. data/RELEASE.md +0 -0
  11. data/Rakefile +5 -0
  12. data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
  13. data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
  14. data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
  15. data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
  16. data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
  17. data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
  18. data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
  19. data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
  20. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
  21. data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
  22. data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
  23. data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
  24. data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
  25. data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
  26. data/app/models/pg_sql_triggers/application_record.rb +0 -0
  27. data/app/models/pg_sql_triggers/audit_log.rb +29 -47
  28. data/app/models/pg_sql_triggers/trigger_registry.rb +137 -74
  29. data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
  30. data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
  31. data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -27
  32. data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
  33. data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
  34. data/app/views/pg_sql_triggers/tables/index.html.erb +27 -18
  35. data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
  36. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
  37. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
  38. data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
  39. data/config/initializers/pg_sql_triggers.rb +0 -0
  40. data/config/routes.rb +0 -14
  41. data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
  42. data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
  43. data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
  44. data/db/migrate/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
  45. data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
  46. data/docs/README.md +3 -0
  47. data/docs/api-reference.md +176 -152
  48. data/docs/audit-trail.md +1 -1
  49. data/docs/configuration.md +196 -3
  50. data/docs/getting-started.md +31 -16
  51. data/docs/kill-switch.md +0 -0
  52. data/docs/permissions.md +6 -9
  53. data/docs/troubleshooting.md +0 -0
  54. data/docs/ui-guide.md +0 -0
  55. data/docs/usage-guide.md +112 -67
  56. data/docs/web-ui.md +3 -103
  57. data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
  58. data/lib/generators/pg_sql_triggers/templates/README +0 -0
  59. data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
  60. data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
  61. data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
  62. data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
  63. data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
  64. data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
  65. data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
  66. data/lib/pg_sql_triggers/alerting.rb +77 -0
  67. data/lib/pg_sql_triggers/database_introspection.rb +0 -0
  68. data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
  69. data/lib/pg_sql_triggers/drift/db_queries.rb +26 -13
  70. data/lib/pg_sql_triggers/drift/detector.rb +59 -38
  71. data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
  72. data/lib/pg_sql_triggers/drift.rb +5 -0
  73. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +68 -20
  74. data/lib/pg_sql_triggers/dsl.rb +0 -0
  75. data/lib/pg_sql_triggers/engine.rb +49 -0
  76. data/lib/pg_sql_triggers/errors.rb +0 -0
  77. data/lib/pg_sql_triggers/events_checksum.rb +114 -0
  78. data/lib/pg_sql_triggers/migration.rb +5 -6
  79. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +85 -82
  80. data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
  81. data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
  82. data/lib/pg_sql_triggers/migrator.rb +137 -94
  83. data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
  84. data/lib/pg_sql_triggers/permissions.rb +1 -0
  85. data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
  86. data/lib/pg_sql_triggers/registry/manager.rb +60 -21
  87. data/lib/pg_sql_triggers/registry/validator.rb +287 -6
  88. data/lib/pg_sql_triggers/registry.rb +0 -0
  89. data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
  90. data/lib/pg_sql_triggers/sql/kill_switch.rb +154 -275
  91. data/lib/pg_sql_triggers/sql.rb +0 -6
  92. data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
  93. data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
  94. data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
  95. data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
  96. data/lib/pg_sql_triggers/testing.rb +0 -0
  97. data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
  98. data/lib/pg_sql_triggers/version.rb +1 -1
  99. data/lib/pg_sql_triggers.rb +21 -1
  100. data/lib/tasks/trigger_migrations.rake +235 -152
  101. data/rakelib/pg_sql_triggers_environment.rake +9 -0
  102. data/scripts/generate_coverage_report.rb +4 -1
  103. data/sig/pg_sql_triggers.rbs +0 -0
  104. metadata +68 -22
  105. data/Goal.md +0 -742
  106. data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
  107. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
  108. data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
  109. data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
  110. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
  111. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
  112. data/lib/generators/trigger/migration_generator.rb +0 -60
  113. data/lib/pg_sql_triggers/generator/form.rb +0 -80
  114. data/lib/pg_sql_triggers/generator/service.rb +0 -339
  115. data/lib/pg_sql_triggers/generator.rb +0 -8
  116. data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
  117. data/lib/pg_sql_triggers/sql/executor.rb +0 -200
@@ -17,70 +17,52 @@ module PgSqlTriggers
17
17
  validates :operation, presence: true
18
18
  validates :status, presence: true, inclusion: { in: %w[success failure] }
19
19
 
20
+ # Known keyword options accepted by log_success / log_failure, in addition to
21
+ # +operation+ (required for both) and +error_message+ (required for log_failure).
22
+ SUCCESS_ATTRS = %i[trigger_name actor environment reason confirmation_text
23
+ before_state after_state diff].freeze
24
+ FAILURE_ATTRS = %i[trigger_name actor environment reason confirmation_text before_state].freeze
25
+
20
26
  # Class methods for logging operations
21
27
  class << self
22
- # Log a successful operation
28
+ # Log a successful operation.
23
29
  #
24
- # @param operation [Symbol, String] The operation being performed
25
- # @param trigger_name [String, nil] The trigger name (if applicable)
26
- # @param actor [Hash] Information about who performed the action
27
- # @param environment [String, nil] The environment
28
- # @param reason [String, nil] Reason for the operation
29
- # @param confirmation_text [String, nil] Confirmation text used
30
- # @param before_state [Hash, nil] State before operation
31
- # @param after_state [Hash, nil] State after operation
32
- # @param diff [String, nil] Diff information
33
- # rubocop:disable Metrics/ParameterLists
34
- def log_success(operation:, trigger_name: nil, actor: nil, environment: nil,
35
- reason: nil, confirmation_text: nil, before_state: nil,
36
- after_state: nil, diff: nil)
30
+ # Required: +operation:+ (Symbol/String).
31
+ # Optional (all via keyword args): trigger_name, actor, environment, reason,
32
+ # confirmation_text, before_state, after_state, diff.
33
+ def log_success(operation:, **options)
34
+ attrs = options.slice(*SUCCESS_ATTRS)
37
35
  create!(
38
- trigger_name: trigger_name,
39
- operation: operation.to_s,
40
- actor: serialize_actor(actor),
41
- environment: environment,
42
- status: "success",
43
- reason: reason,
44
- confirmation_text: confirmation_text,
45
- before_state: before_state,
46
- after_state: after_state,
47
- diff: diff
36
+ attrs.merge(
37
+ operation: operation.to_s,
38
+ actor: serialize_actor(attrs[:actor]),
39
+ status: "success"
40
+ )
48
41
  )
49
42
  rescue StandardError => e
50
43
  Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
51
44
  nil
52
45
  end
53
- # rubocop:enable Metrics/ParameterLists
54
46
 
55
- # Log a failed operation
47
+ # Log a failed operation.
56
48
  #
57
- # @param operation [Symbol, String] The operation being performed
58
- # @param trigger_name [String, nil] The trigger name (if applicable)
59
- # @param actor [Hash] Information about who performed the action
60
- # @param environment [String, nil] The environment
61
- # @param error_message [String] Error message
62
- # @param reason [String, nil] Reason for the operation (if provided before failure)
63
- # @param confirmation_text [String, nil] Confirmation text used
64
- # @param before_state [Hash, nil] State before operation
65
- # rubocop:disable Metrics/ParameterLists
66
- def log_failure(operation:, error_message:, trigger_name: nil, actor: nil, environment: nil, reason: nil,
67
- confirmation_text: nil, before_state: nil)
49
+ # Required: +operation:+ (Symbol/String) and +error_message:+ (String).
50
+ # Optional (all via keyword args): trigger_name, actor, environment, reason,
51
+ # confirmation_text, before_state.
52
+ def log_failure(operation:, error_message:, **options)
53
+ attrs = options.slice(*FAILURE_ATTRS)
68
54
  create!(
69
- trigger_name: trigger_name,
70
- operation: operation.to_s,
71
- actor: serialize_actor(actor),
72
- environment: environment,
73
- status: "failure",
74
- error_message: error_message,
75
- reason: reason,
76
- confirmation_text: confirmation_text,
77
- before_state: before_state
55
+ attrs.merge(
56
+ operation: operation.to_s,
57
+ actor: serialize_actor(attrs[:actor]),
58
+ status: "failure",
59
+ error_message: error_message
60
+ )
78
61
  )
79
62
  rescue StandardError => e
80
63
  Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
81
64
  nil
82
65
  end
83
- # rubocop:enable Metrics/ParameterLists
84
66
 
85
67
  # Get audit log entries for a specific trigger
86
68
  #
@@ -21,7 +21,9 @@ module PgSqlTriggers
21
21
  # @example Drop and re-execute triggers
22
22
  # trigger.drop!(reason: "No longer needed", actor: current_user, confirmation: "EXECUTE TRIGGER_DROP")
23
23
  # trigger.re_execute!(reason: "Fix drift", actor: current_user, confirmation: "EXECUTE TRIGGER_RE_EXECUTE")
24
- # rubocop:disable Metrics/ClassLength
24
+ # rubocop:disable Metrics/ClassLength -- core AR model: groups lifecycle operations
25
+ # (enable!/disable!/drop!/re_execute!), drift helpers, audit hooks, and SQL builders.
26
+ # Splitting further would fragment tightly-coupled state and audit concerns.
25
27
  class TriggerRegistry < PgSqlTriggers::ApplicationRecord
26
28
  self.table_name = "pg_sql_triggers_registry"
27
29
 
@@ -39,6 +41,16 @@ module PgSqlTriggers
39
41
  scope :for_environment, ->(env) { where(environment: [env, nil]) }
40
42
  scope :by_source, ->(source) { where(source: source) }
41
43
 
44
+ # Case-insensitive search on trigger name and table name (used by web dashboard).
45
+ scope :matching_search, lambda { |raw|
46
+ query = raw.to_s.strip
47
+ next all if query.blank?
48
+
49
+ sanitized = ActiveRecord::Base.sanitize_sql_like(query)
50
+ term = "%#{sanitized}%"
51
+ where("trigger_name ILIKE :term OR table_name ILIKE :term", term: term)
52
+ }
53
+
42
54
  # Returns the current drift state of this trigger.
43
55
  #
44
56
  # @return [String] One of: "in_sync", "drifted", "manual_override", "disabled", "dropped", "unknown"
@@ -119,32 +131,12 @@ module PgSqlTriggers
119
131
  end
120
132
  end
121
133
 
122
- # Update the registry record (always update, even if trigger doesn't exist)
123
- begin
124
- update!(enabled: true)
125
- after_state = capture_state
126
- log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
127
- rescue ActiveRecord::StatementInvalid, StandardError => e
128
- # If update! fails, try update_column which bypasses validations and callbacks
129
- # and might not use execute in the same way
130
- Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
131
- begin
132
- # rubocop:disable Rails/SkipsModelValidations
133
- update_column(:enabled, true)
134
- # rubocop:enable Rails/SkipsModelValidations
135
- after_state = capture_state
136
- log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
137
- rescue StandardError => update_error
138
- # If update_column also fails, just set the in-memory attribute
139
- # The test might reload, but we've done our best
140
- # rubocop:disable Layout/LineLength
141
- Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
142
- # rubocop:enable Layout/LineLength
143
- self.enabled = true
144
- after_state = capture_state
145
- log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
146
- end
147
- end
134
+ # Update the registry record (always update, even if trigger doesn't exist).
135
+ # If persistence fails for any reason, fall back to the in-memory attribute so
136
+ # callers/observers still see a consistent state for this request.
137
+ persist_enabled_state(true)
138
+ after_state = capture_state
139
+ log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
148
140
  end
149
141
 
150
142
  # Disables this trigger in the database and updates the registry.
@@ -191,32 +183,12 @@ module PgSqlTriggers
191
183
  end
192
184
  end
193
185
 
194
- # Update the registry record (always update, even if trigger doesn't exist)
195
- begin
196
- update!(enabled: false)
197
- after_state = capture_state
198
- log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
199
- rescue ActiveRecord::StatementInvalid, StandardError => e
200
- # If update! fails, try update_column which bypasses validations and callbacks
201
- # and might not use execute in the same way
202
- Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
203
- begin
204
- # rubocop:disable Rails/SkipsModelValidations
205
- update_column(:enabled, false)
206
- # rubocop:enable Rails/SkipsModelValidations
207
- after_state = capture_state
208
- log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
209
- rescue StandardError => update_error
210
- # If update_column also fails, just set the in-memory attribute
211
- # The test might reload, but we've done our best
212
- # rubocop:disable Layout/LineLength
213
- Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
214
- # rubocop:enable Layout/LineLength
215
- self.enabled = false
216
- after_state = capture_state
217
- log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
218
- end
219
- end
186
+ # Update the registry record (always update, even if trigger doesn't exist).
187
+ # If persistence fails for any reason, fall back to the in-memory attribute so
188
+ # callers/observers still see a consistent state for this request.
189
+ persist_enabled_state(false)
190
+ after_state = capture_state
191
+ log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
220
192
  end
221
193
 
222
194
  # Drops this trigger from the database and removes it from the registry.
@@ -247,7 +219,6 @@ module PgSqlTriggers
247
219
  # Execute DROP TRIGGER in transaction
248
220
  ActiveRecord::Base.transaction do
249
221
  drop_trigger_from_database
250
- trigger_name
251
222
  destroy!
252
223
  log_drop_success
253
224
  log_audit_success(:trigger_drop, actor, reason: reason, confirmation_text: confirmation,
@@ -264,8 +235,9 @@ module PgSqlTriggers
264
235
  # @param reason [String] Required reason for re-executing the trigger
265
236
  # @param confirmation [String, nil] Optional confirmation text for kill switch protection
266
237
  # @param actor [Hash, nil] Information about who is performing the action (must have :type and :id keys)
267
- # @raise [ArgumentError] If reason is missing or empty, or if function_body is blank
238
+ # @raise [ArgumentError] If reason is missing or empty
268
239
  # @raise [PgSqlTriggers::KillSwitchError] If kill switch blocks the operation
240
+ # @raise [StandardError] If SQL cannot be generated to recreate the trigger
269
241
  # @return [PgSqlTriggers::TriggerRegistry] self
270
242
  def re_execute!(reason:, confirmation: nil, actor: nil)
271
243
  actor ||= { type: "Console", id: "TriggerRegistry#re_execute!" }
@@ -286,7 +258,6 @@ module PgSqlTriggers
286
258
 
287
259
  # Validate reason is provided
288
260
  raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
289
- raise StandardError, "Cannot re-execute: missing function_body" if function_body.blank?
290
261
 
291
262
  log_re_execute_attempt(reason)
292
263
 
@@ -316,13 +287,22 @@ module PgSqlTriggers
316
287
  end
317
288
 
318
289
  def calculate_checksum
290
+ deferral = PgSqlTriggers::DeferralChecksum.parts(
291
+ constraint_trigger: constraint_trigger,
292
+ deferrable: deferrable,
293
+ initially: initially
294
+ )
295
+ events_segment = PgSqlTriggers::EventsChecksum.segment_from_definition_json(definition)
319
296
  Digest::SHA256.hexdigest([
320
297
  trigger_name,
321
298
  table_name,
322
299
  version,
323
300
  function_body || "",
324
301
  condition || "",
325
- timing || "before"
302
+ timing || "before",
303
+ for_each || "row",
304
+ events_segment,
305
+ *deferral
326
306
  ].join)
327
307
  end
328
308
 
@@ -394,7 +374,17 @@ module PgSqlTriggers
394
374
  end
395
375
 
396
376
  def recreate_trigger
397
- ActiveRecord::Base.connection.execute(function_body)
377
+ # DSL triggers are recreated from the stored JSON definition (+build_trigger_sql_from_definition+).
378
+ # Other sources may persist a full trigger SQL payload in +function_body+.
379
+ sql = if source == "dsl"
380
+ build_trigger_sql_from_definition
381
+ else
382
+ function_body.presence || build_trigger_sql_from_definition
383
+ end
384
+
385
+ raise StandardError, "Cannot re-execute: no SQL could be generated" if sql.blank?
386
+
387
+ ActiveRecord::Base.connection.execute(sql)
398
388
  if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
399
389
  Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-created trigger"
400
390
  end
@@ -405,8 +395,71 @@ module PgSqlTriggers
405
395
  raise
406
396
  end
407
397
 
398
+ # Build a CREATE TRIGGER SQL statement from the stored DSL definition JSON.
399
+ # Used by re_execute! when function_body is absent (the normal case for DSL triggers).
400
+ def build_trigger_sql_from_definition
401
+ return nil if definition.blank?
402
+
403
+ defn = JSON.parse(definition)
404
+ fn_name = defn["function_name"]
405
+ return nil if fn_name.blank?
406
+
407
+ t_name = table_name
408
+ constraint = ActiveModel::Type::Boolean.new.cast(defn["constraint_trigger"])
409
+ timing_kw = if constraint
410
+ "AFTER"
411
+ else
412
+ (defn["timing"] || timing || "before").to_s.upcase
413
+ end
414
+ events_sql = PgSqlTriggers::EventsChecksum.events_sql_fragment(
415
+ defn,
416
+ quote_column: ->(col) { quote_identifier(col) }
417
+ )
418
+ cond = defn["condition"] || condition
419
+ for_each_kw = (defn["for_each"] || for_each || "row").upcase
420
+
421
+ create_kw = constraint ? "CREATE CONSTRAINT TRIGGER" : "CREATE TRIGGER"
422
+ sql = "#{create_kw} #{quote_identifier(trigger_name)} "
423
+ sql += "#{timing_kw} #{events_sql} ON #{quote_identifier(t_name)} "
424
+ deferral = deferral_sql_fragment(defn)
425
+ sql += "#{deferral} " if deferral.present?
426
+ sql += "FOR EACH #{for_each_kw} "
427
+ sql += "WHEN (#{cond}) " if cond.present?
428
+ sql += "EXECUTE FUNCTION #{fn_name}();"
429
+ sql
430
+ rescue JSON::ParserError
431
+ nil
432
+ end
433
+
434
+ def deferral_sql_fragment(defn)
435
+ return "" unless ActiveModel::Type::Boolean.new.cast(defn["constraint_trigger"])
436
+
437
+ case defn["deferrable"].to_s
438
+ when "not_deferrable"
439
+ "NOT DEFERRABLE"
440
+ when "deferrable"
441
+ case defn["initially"].to_s
442
+ when "deferred"
443
+ "DEFERRABLE INITIALLY DEFERRED"
444
+ when "immediate"
445
+ "DEFERRABLE INITIALLY IMMEDIATE"
446
+ else
447
+ "DEFERRABLE"
448
+ end
449
+ else
450
+ ""
451
+ end
452
+ end
453
+
408
454
  def update_registry_after_re_execute
409
- update!(enabled: true, last_executed_at: Time.current)
455
+ update!(last_executed_at: Time.current)
456
+ if !enabled && ActiveRecord::Base.connection.table_exists?(table_name)
457
+ quoted_table = quote_identifier(table_name)
458
+ quoted_trigger = quote_identifier(trigger_name)
459
+ ActiveRecord::Base.connection.execute(
460
+ "ALTER TABLE #{quoted_table} DISABLE TRIGGER #{quoted_trigger};"
461
+ )
462
+ end
410
463
  Rails.logger.info "[TRIGGER_RE_EXECUTE] Updated registry" if defined?(Rails.logger)
411
464
  end
412
465
 
@@ -419,13 +472,12 @@ module PgSqlTriggers
419
472
  table_name: table_name,
420
473
  source: source,
421
474
  environment: environment,
422
- installed_at: installed_at&.iso8601
475
+ installed_at: installed_at&.iso8601,
476
+ function_body: function_body
423
477
  }
424
478
  end
425
479
 
426
- # rubocop:disable Metrics/ParameterLists
427
- def log_audit_success(operation, actor, reason: nil, confirmation_text: nil,
428
- before_state: nil, after_state: nil, diff: nil)
480
+ def log_audit_success(operation, actor, **options)
429
481
  return unless defined?(PgSqlTriggers::AuditLog)
430
482
 
431
483
  PgSqlTriggers::AuditLog.log_success(
@@ -433,18 +485,13 @@ module PgSqlTriggers
433
485
  trigger_name: trigger_name,
434
486
  actor: actor,
435
487
  environment: Rails.env,
436
- reason: reason,
437
- confirmation_text: confirmation_text,
438
- before_state: before_state,
439
- after_state: after_state,
440
- diff: diff
488
+ **options
441
489
  )
442
490
  rescue StandardError => e
443
491
  Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
444
492
  end
445
493
 
446
- def log_audit_failure(operation, actor, error_message, reason: nil,
447
- confirmation_text: nil, before_state: nil)
494
+ def log_audit_failure(operation, actor, error_message, **options)
448
495
  return unless defined?(PgSqlTriggers::AuditLog)
449
496
 
450
497
  PgSqlTriggers::AuditLog.log_failure(
@@ -453,14 +500,30 @@ module PgSqlTriggers
453
500
  actor: actor,
454
501
  environment: Rails.env,
455
502
  error_message: error_message,
456
- reason: reason,
457
- confirmation_text: confirmation_text,
458
- before_state: before_state
503
+ **options
459
504
  )
460
505
  rescue StandardError => e
461
506
  Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
462
507
  end
463
- # rubocop:enable Metrics/ParameterLists
508
+
509
+ # Persist the +enabled+ flag: +update!+, then +update_column+ if needed, then in-memory.
510
+ # Returns true when a DB write succeeded, false when only the in-memory value changed.
511
+ def persist_enabled_state(value)
512
+ update!(enabled: value)
513
+ true
514
+ rescue ActiveRecord::StatementInvalid, StandardError => e
515
+ Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
516
+ begin
517
+ # rubocop:disable Rails/SkipsModelValidations -- fallback when update! fails (callbacks/validations/locks)
518
+ update_column(:enabled, value)
519
+ # rubocop:enable Rails/SkipsModelValidations
520
+ true
521
+ rescue ActiveRecord::StatementInvalid, StandardError => e2
522
+ Rails.logger.warn("Could not update registry via update_column: #{e2.message}") if defined?(Rails.logger)
523
+ self.enabled = value
524
+ false
525
+ end
526
+ end
464
527
  end
465
528
  # rubocop:enable Metrics/ClassLength
466
529
  end
@@ -18,7 +18,6 @@
18
18
  <div>
19
19
  <%= link_to "Dashboard", root_path, style: "color: white; margin-right: 1rem;" %>
20
20
  <%= link_to "Tables", tables_path, style: "color: white; margin-right: 1rem;" %>
21
- <%= link_to "Generator", new_generator_path, style: "color: white; margin-right: 1rem;" %>
22
21
  <%= link_to "Audit Log", audit_logs_path, style: "color: white;" %>
23
22
  </div>
24
23
  </div>
@@ -4,7 +4,7 @@
4
4
  <h1 style="margin: 0;">Audit Log</h1>
5
5
  <div style="display: flex; gap: 1rem;">
6
6
  <%= link_to "Dashboard", dashboard_path, style: "padding: 0.5rem 1rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
7
- <% csv_params = params.permit(:trigger_name, :operation, :status, :environment, :actor_id).to_h %>
7
+ <% csv_params = params.permit(:trigger_name, :operation, :status, :environment, :actor_id, :q).to_h %>
8
8
  <%= link_to "Export CSV", audit_logs_path(csv_params.merge(format: :csv)), style: "padding: 0.5rem 1rem; background: #28a745; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
9
9
  </div>
10
10
  </div>
@@ -13,6 +13,10 @@
13
13
  <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
14
14
  <h3 style="margin-top: 0; margin-bottom: 1rem;">Filters</h3>
15
15
  <%= form_with url: audit_logs_path, method: :get, local: true, style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" do |f| %>
16
+ <div style="grid-column: 1 / -1;">
17
+ <%= label_tag :q, "Search", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
18
+ <%= text_field_tag :q, params[:q], placeholder: "Trigger, operation, reason, error…", style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" %>
19
+ </div>
16
20
  <div>
17
21
  <%= f.label :trigger_name, "Trigger Name", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
18
22
  <%= f.select :trigger_name, options_for_select([["All", ""]] + @available_trigger_names.map { |n| [n, n] }, params[:trigger_name]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
@@ -48,7 +52,7 @@
48
52
  <!-- Results Summary -->
49
53
  <div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
50
54
  <strong>Total Results:</strong> <%= @total_count %> entries
51
- <% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
55
+ <% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? || params[:q].present? %>
52
56
  <span style="color: #6c757d;">(filtered)</span>
53
57
  <% end %>
54
58
  </div>
@@ -144,7 +148,7 @@
144
148
  <% if @total_pages > 1 %>
145
149
  <div style="display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 2rem;">
146
150
  <% if @page > 1 %>
147
- <% prev_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
151
+ <% prev_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page, :q).to_h %>
148
152
  <%= link_to "« Previous", audit_logs_path(prev_params.merge(page: @page - 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
149
153
  <% else %>
150
154
  <span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">« Previous</span>
@@ -155,7 +159,7 @@
155
159
  </span>
156
160
 
157
161
  <% if @page < @total_pages %>
158
- <% next_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
162
+ <% next_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page, :q).to_h %>
159
163
  <%= link_to "Next »", audit_logs_path(next_params.merge(page: @page + 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
160
164
  <% else %>
161
165
  <span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">Next »</span>
@@ -166,7 +170,7 @@
166
170
  <div style="background: white; padding: 3rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center;">
167
171
  <h3 style="margin-top: 0; color: #6c757d;">No audit log entries found</h3>
168
172
  <p style="color: #6c757d;">
169
- <% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
173
+ <% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? || params[:q].present? %>
170
174
  Try adjusting your filters or <%= link_to "clear filters", audit_logs_path, style: "color: #007bff;" %>.
171
175
  <% else %>
172
176
  Audit log entries will appear here as operations are performed.