pg_sql_triggers 1.2.0 → 1.4.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +397 -1
  3. data/COVERAGE.md +26 -19
  4. data/GEM_ANALYSIS.md +368 -0
  5. data/Goal.md +276 -155
  6. data/README.md +45 -22
  7. data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
  8. data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
  9. data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
  10. data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
  11. data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
  12. data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
  13. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
  14. data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
  15. data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
  16. data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
  17. data/app/models/pg_sql_triggers/audit_log.rb +106 -0
  18. data/app/models/pg_sql_triggers/trigger_registry.rb +218 -13
  19. data/app/views/layouts/pg_sql_triggers/application.html.erb +25 -6
  20. data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
  21. data/app/views/pg_sql_triggers/dashboard/index.html.erb +34 -12
  22. data/app/views/pg_sql_triggers/tables/index.html.erb +75 -5
  23. data/app/views/pg_sql_triggers/tables/show.html.erb +17 -6
  24. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
  25. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
  26. data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
  27. data/config/routes.rb +2 -14
  28. data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
  29. data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
  30. data/docs/README.md +15 -5
  31. data/docs/api-reference.md +233 -151
  32. data/docs/audit-trail.md +413 -0
  33. data/docs/configuration.md +28 -7
  34. data/docs/getting-started.md +17 -16
  35. data/docs/permissions.md +369 -0
  36. data/docs/troubleshooting.md +486 -0
  37. data/docs/ui-guide.md +211 -0
  38. data/docs/usage-guide.md +38 -67
  39. data/docs/web-ui.md +251 -128
  40. data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
  41. data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
  42. data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
  43. data/lib/pg_sql_triggers/drift/db_queries.rb +12 -8
  44. data/lib/pg_sql_triggers/drift/detector.rb +51 -38
  45. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
  46. data/lib/pg_sql_triggers/engine.rb +14 -0
  47. data/lib/pg_sql_triggers/errors.rb +245 -0
  48. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
  49. data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
  50. data/lib/pg_sql_triggers/migrator.rb +53 -6
  51. data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
  52. data/lib/pg_sql_triggers/registry/manager.rb +36 -11
  53. data/lib/pg_sql_triggers/registry/validator.rb +62 -5
  54. data/lib/pg_sql_triggers/registry.rb +141 -8
  55. data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -247
  56. data/lib/pg_sql_triggers/sql.rb +0 -6
  57. data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
  58. data/lib/pg_sql_triggers/version.rb +1 -1
  59. data/lib/pg_sql_triggers.rb +7 -7
  60. data/pg_sql_triggers.gemspec +53 -0
  61. metadata +35 -18
  62. data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
  63. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
  64. data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
  65. data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
  66. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
  67. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
  68. data/docs/screenshots/.gitkeep +0 -1
  69. data/docs/screenshots/Generate Trigger.png +0 -0
  70. data/docs/screenshots/Triggers Page.png +0 -0
  71. data/docs/screenshots/kill error.png +0 -0
  72. data/docs/screenshots/kill modal for migration down.png +0 -0
  73. data/lib/generators/trigger/migration_generator.rb +0 -60
  74. data/lib/pg_sql_triggers/generator/form.rb +0 -80
  75. data/lib/pg_sql_triggers/generator/service.rb +0 -307
  76. data/lib/pg_sql_triggers/generator.rb +0 -8
  77. data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
  78. data/lib/pg_sql_triggers/sql/executor.rb +0 -200
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54f76c538693c37b1572cb914462269eb8c3600473c1a7c26efda0a7e02eefe5
4
- data.tar.gz: 8574ff3bb8f835a11f8cec850c5adbff7f17be65a1461b8f8e87242ab3e50b62
3
+ metadata.gz: 3009d08e9604d3cad9af352b595fe76fb2688c7f1bf80cfbb92bbb9bbadeb996
4
+ data.tar.gz: 0155742bcf1ad97690f42c8193da6ff408f09dab26f8684c84a6954c8438f28f
5
5
  SHA512:
6
- metadata.gz: 5e7ccb4983c43cad0aee550a741e341a3feda8a2fd5541be1786a92ffc26894a80eb8081e57493e6eb05697a362cafaa3fee325b288a716f4f1e8c3de989966e
7
- data.tar.gz: f24548a2b4fa7f0bb274d11d2085b87e716c44e08923782eee93ffb0d86500892ffca3b6d7c69ba53e1f886743646ac0c8695097589ecb7db4280152b4c1dccf
6
+ metadata.gz: f76fc2c48a00922e7265acce7d8c87e0477686540b4749aef94bbd1d4b914f7cdf76a0c28cc3f3dd816489c24606ee6b2fe424d309dbc03e70cf160fba6daad8
7
+ data.tar.gz: 4988c497e39a54d517e1f96a4328609de0bb0451658d228b46a4bd2d448267b7866ede607eb702430510a88b16fb328d801fc81a00e0d93c231ad7d94caff216
data/CHANGELOG.md CHANGED
@@ -5,7 +5,403 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [1.4.0] - 2026-03-01
9
+
10
+ ### Added
11
+
12
+ - **[Feature 4.1] `FOR EACH ROW` / `FOR EACH STATEMENT` DSL support** — Every PostgreSQL trigger
13
+ requires a row-level or statement-level execution granularity, but the gem previously hard-coded
14
+ `FOR EACH ROW` with no way for callers to change it. Two new DSL methods, `for_each_row` and
15
+ `for_each_statement`, let trigger definitions declare the desired granularity explicitly. The
16
+ value defaults to `"row"` so all existing definitions continue to produce `FOR EACH ROW` triggers
17
+ without modification. The field is stored in a new `for_each` column on the registry table
18
+ (migration `20260228000001_add_for_each_to_pg_sql_triggers_registry.rb`), included in all three
19
+ checksum computations (`TriggerRegistry#calculate_checksum`, `Registry::Manager#calculate_checksum`,
20
+ and `Drift::Detector#calculate_db_checksum`), extracted from live trigger definitions via a new
21
+ `extract_trigger_for_each` helper, and validated by `Registry::Validator` (only `"row"` and
22
+ `"statement"` are accepted). The SQL reconstructed by `TriggerRegistry#build_trigger_sql_from_definition`
23
+ during `re_execute!` now honours the stored `for_each` value.
24
+ ([lib/pg_sql_triggers/dsl/trigger_definition.rb](lib/pg_sql_triggers/dsl/trigger_definition.rb),
25
+ [lib/pg_sql_triggers/registry/manager.rb](lib/pg_sql_triggers/registry/manager.rb),
26
+ [lib/pg_sql_triggers/registry/validator.rb](lib/pg_sql_triggers/registry/validator.rb),
27
+ [lib/pg_sql_triggers/drift/detector.rb](lib/pg_sql_triggers/drift/detector.rb),
28
+ [app/models/pg_sql_triggers/trigger_registry.rb](app/models/pg_sql_triggers/trigger_registry.rb),
29
+ [db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb](db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb))
30
+
31
+ ### Changed
32
+
33
+ - **[Refactor 5.1] `SQL::KillSwitch` reduced from 329 to 166 lines** — The module was inflated by
34
+ four separate single-purpose log helpers (`log_allowed`, `log_override`, `log_blocked`,
35
+ `format_actor`), a `raise_blocked_error` method whose two heredocs (`message` and `recovery`)
36
+ duplicated each other across 30 lines, verbose per-method comments, and a
37
+ `rubocop:disable Metrics/ModuleLength` suppressor. Collapsed the four log helpers into one
38
+ private `log(level, status, op, env, actor, extra = nil)` method; inlined `raise_blocked_error`
39
+ as a concise 5-line `raise` call; removed all comment padding; renamed `detect_environment` →
40
+ `resolve_environment` for clarity. All three-layer semantics (config → ENV override → explicit
41
+ confirmation), public API (`active?`, `check!`, `override`, `validate_confirmation!`), log
42
+ message formats, and error message content are unchanged — all existing specs continue to pass.
43
+ ([lib/pg_sql_triggers/sql/kill_switch.rb](lib/pg_sql_triggers/sql/kill_switch.rb))
44
+
45
+ - **[Design] Eliminated N+1 queries in drift detection** — `Drift::Detector.detect_all` and
46
+ `detect_for_table` previously called `detect(trigger_name)` per registry entry, which issued a
47
+ `TriggerRegistry.find_by` and a `DbQueries.find_trigger` DB query for every row (N+1 pattern).
48
+ These methods now build an `index_by` hash from the bulk-fetched result of `DbQueries.all_triggers`
49
+ and map over pre-loaded registry entries, eliminating all per-entry lookups. A new private method
50
+ `detect_with_preloaded(registry_entry, db_trigger)` performs state computation with zero
51
+ additional queries.
52
+ ([lib/pg_sql_triggers/drift/detector.rb](lib/pg_sql_triggers/drift/detector.rb))
53
+
54
+ - **[Design] Thread-safe registry cache** — `Registry::Manager._registry_cache` was stored in a
55
+ class-level instance variable mutated without synchronisation, creating a race condition on
56
+ multi-threaded Puma servers. A `REGISTRY_CACHE_MUTEX = Mutex.new` constant now guards all reads
57
+ and writes to `@_registry_cache` via `REGISTRY_CACHE_MUTEX.synchronize`.
58
+ ([lib/pg_sql_triggers/registry/manager.rb](lib/pg_sql_triggers/registry/manager.rb))
59
+
60
+ - **[Design] Configurable PostgreSQL schema** — Every SQL query in `Drift::DbQueries` hard-coded
61
+ `n.nspname = 'public'`, making the gem unusable in applications that manage triggers in
62
+ non-public schemas. A new configuration attribute `PgSqlTriggers.db_schema` (default: `"public"`)
63
+ replaces all hard-coded schema literals; the value is passed as a bind parameter via a private
64
+ `schema_name` helper. Override in an initialiser:
65
+ `PgSqlTriggers.db_schema = "app"`.
66
+ ([lib/pg_sql_triggers.rb](lib/pg_sql_triggers.rb),
67
+ [lib/pg_sql_triggers/drift/db_queries.rb](lib/pg_sql_triggers/drift/db_queries.rb))
68
+
69
+ - **[Design] Idiomatic DSL accessor methods** — `TriggerDefinition#version`, `#enabled`, and
70
+ `#timing` used a dual-purpose getter/setter pattern (`def version(v = nil)`) that silently
71
+ returned the current value when called with no argument, making typos invisible. Replaced with
72
+ standard `attr_accessor :version, :enabled` and a custom `timing=(val)` writer that converts to
73
+ string, matching the Ruby/Rails `attr_accessor` convention. DSL block syntax changes from
74
+ `version 1` to `self.version = 1` and `enabled true` to `self.enabled = true`.
75
+ ([lib/pg_sql_triggers/dsl/trigger_definition.rb](lib/pg_sql_triggers/dsl/trigger_definition.rb))
76
+
77
+ - **[Design] `capture_state` now includes `function_body`** — The audit snapshot captured by
78
+ `TriggerRegistry#capture_state` (used in before/after diffs for all audit log entries) omitted
79
+ `function_body`, making it impossible to see the actual function content in audit trail diffs.
80
+ The field is now included in all captured state hashes.
81
+ ([app/models/pg_sql_triggers/trigger_registry.rb](app/models/pg_sql_triggers/trigger_registry.rb))
82
+
83
+ - **[Design 5.4] `Migrator#run_migration` reduced from three migration instances to two** —
84
+ `run_migration` previously instantiated the migration class three times: once for
85
+ `SafetyValidator`, once for `PreApplyComparator`, and once for actual execution. This meant
86
+ the migration code ran twice before execution, making the behaviour unpredictable if the
87
+ migration had side effects that escaped the `execute` override. A new private
88
+ `capture_migration_sql(instance, direction)` helper (wrapped in a rolled-back transaction)
89
+ captures SQL once from a single inspection instance. `SafetyValidator.validate_sql!` and
90
+ `PreApplyComparator.compare_sql` are new entry points that accept pre-captured SQL directly,
91
+ avoiding a second run of the migration code. Execution still uses a fresh instance. The
92
+ existing `validate!` and `compare` instance-based APIs are retained for backward compatibility
93
+ with specs.
94
+ ([lib/pg_sql_triggers/migrator.rb](lib/pg_sql_triggers/migrator.rb),
95
+ [lib/pg_sql_triggers/migrator/safety_validator.rb](lib/pg_sql_triggers/migrator/safety_validator.rb),
96
+ [lib/pg_sql_triggers/migrator/pre_apply_comparator.rb](lib/pg_sql_triggers/migrator/pre_apply_comparator.rb))
97
+
98
+ ### Deprecated
99
+
100
+ - **`DSL::TriggerDefinition#when_env`** — Environment-specific trigger declarations cause schema
101
+ drift between environments and make triggers impossible to test fully outside production.
102
+ `when_env` now emits a `warn`-level deprecation message on every call and will be removed in a
103
+ future major version. Use application-level configuration to gate trigger behaviour by
104
+ environment instead.
105
+ ([lib/pg_sql_triggers/dsl/trigger_definition.rb](lib/pg_sql_triggers/dsl/trigger_definition.rb))
106
+
107
+ ### Removed
108
+
109
+ - **[Refactor 5.3] Web UI trigger generator removed; replaced with Rails CLI generator** —
110
+ `GeneratorController` provided a browser form for generating trigger DSL files and migrations
111
+ at runtime, writing files directly to the server's filesystem. This is a security and
112
+ auditability concern in production (server-side file writes, no code review gate). The
113
+ controller, its two views (`new`, `preview`), the `/generator` routes, `Generator::Service`,
114
+ `Generator::Form`, and all related specs have been removed. Code generation is now a local,
115
+ CLI-driven action via a new `pg_sql_triggers:trigger` Rails generator:
116
+ ```
117
+ rails generate pg_sql_triggers:trigger TRIGGER_NAME TABLE_NAME [EVENTS...] [--timing before|after] [--function fn_name]
118
+ ```
119
+ The generator produces `app/triggers/TRIGGER_NAME.rb` (DSL stub) and
120
+ `db/triggers/TIMESTAMP_TRIGGER_NAME.rb` (migration with function + trigger SQL) directly into
121
+ the working tree, where they go through version control and code review like any other source
122
+ file. The `autoload :Generator` entry is also removed from `PgSqlTriggers`.
123
+ ([lib/generators/pg_sql_triggers/trigger_generator.rb](lib/generators/pg_sql_triggers/trigger_generator.rb),
124
+ [config/routes.rb](config/routes.rb))
125
+
126
+ - **[Refactor 5.2] `SQL::Capsule` and `SQL::Executor` removed** — These classes implemented a
127
+ named-SQL-snippet execution system ("SQL capsules") for emergency operations. The feature is a
128
+ general-purpose dangerous-SQL runner that has nothing specifically to do with trigger management;
129
+ bundling it in this gem conflated concerns and enlarged the web UI's attack surface. Removed:
130
+ `lib/pg_sql_triggers/sql/capsule.rb`, `lib/pg_sql_triggers/sql/executor.rb`,
131
+ `app/controllers/pg_sql_triggers/sql_capsules_controller.rb`, the two associated views, the
132
+ `/sql_capsules` routes, and all related specs. `SQL::KillSwitch` is retained — it continues to
133
+ gate trigger re-execution and migration operations. The `PgSqlTriggers::SQL.execute_capsule`
134
+ convenience method is also removed.
135
+ ([lib/pg_sql_triggers/sql.rb](lib/pg_sql_triggers/sql.rb),
136
+ [config/routes.rb](config/routes.rb))
137
+
138
+ - **Duplicate `trigger:migration` generator namespace** — Two generator namespaces existed for the
139
+ same task: `rails g pg_sql_triggers:trigger_migration` and `rails g trigger:migration`. The
140
+ `Trigger::Generators::MigrationGenerator` (`lib/generators/trigger/`) has been removed; use
141
+ `rails g pg_sql_triggers:trigger_migration` exclusively.
142
+ ([lib/generators/trigger/migration_generator.rb](lib/generators/trigger/migration_generator.rb))
143
+
144
+ ### Security
145
+
146
+ - **[High] Production warning when no `permission_checker` is configured** — The gem's default
147
+ behaviour is to allow all actions (including admin-level operations such as `drop_trigger`,
148
+ `execute_sql`, and `override_drift`) when no `permission_checker` is set. A newly deployed
149
+ application with no extra configuration silently granted every actor full admin access.
150
+ The engine now emits a `Rails.logger.warn` at startup when the app boots in production and
151
+ `PgSqlTriggers.permission_checker` is still `nil`, making the misconfiguration visible in
152
+ production logs immediately on deploy.
153
+ ([lib/pg_sql_triggers/engine.rb](lib/pg_sql_triggers/engine.rb))
154
+
155
+ - **[High] `Registry::Validator` was a no-op stub** — `Validator.validate!` returned `true`
156
+ unconditionally, meaning malformed DSL definitions (missing `table_name`, empty or invalid
157
+ `events`, missing `function_name`, unrecognised `timing` values) silently passed validation
158
+ and were written to the registry. Replaced the stub with real validation: every `source: "dsl"`
159
+ entry in the registry has its stored `definition` JSON parsed and checked against required
160
+ fields and allowed values (`insert / update / delete / truncate` for events;
161
+ `before / after / instead_of` for timing). All errors are collected across all triggers and
162
+ surfaced in a single `ValidationError` listing every violation. Non-DSL entries are not
163
+ validated. Unparseable JSON is treated as an empty definition, which itself fails validation.
164
+ ([lib/pg_sql_triggers/registry/validator.rb](lib/pg_sql_triggers/registry/validator.rb))
165
+
166
+ ### Fixed
167
+ - **[High] `enabled: false` DSL option was cosmetic — trigger still fired in PostgreSQL** —
168
+ The `enabled` field was stored in the registry and surfaced in drift reports, but no
169
+ `ALTER TABLE … DISABLE TRIGGER` was ever issued across three code paths, so the trigger
170
+ continued to fire regardless of the DSL flag.
171
+
172
+ *Gap 1 — `Registry::Manager#register`*: A new private `sync_postgresql_enabled_state` helper
173
+ is called after every create or update that affects the `enabled` field. On **create**, the
174
+ helper fires only when `definition.enabled` is falsy (a newly created PostgreSQL trigger is
175
+ always enabled by default). On **update**, `enabled_changed` is captured before the `update!`
176
+ call so the comparison is always against the old value; the sync fires only when `enabled`
177
+ actually flipped. The helper checks `DatabaseIntrospection#trigger_exists?` before issuing
178
+ any SQL, making it safe to call at app boot before migrations have run, and rescues any error
179
+ with a `Rails.logger.warn` so a transient DB issue cannot crash the registration path.
180
+
181
+ *Gap 2 — `Migrator#run_migration` (`:up`)*: `CREATE TRIGGER` always leaves the trigger
182
+ enabled in PostgreSQL. A new private `enforce_disabled_triggers` method is called after each
183
+ `:up` migration transaction commits; it iterates over all `TriggerRegistry.disabled` entries
184
+ and issues `ALTER TABLE … DISABLE TRIGGER` for any that exist in the database. A per-iteration
185
+ `rescue` ensures one failure does not block the rest.
186
+
187
+ *Gap 3 — `TriggerRegistry#update_registry_after_re_execute`*: `re_execute!` drops and
188
+ recreates the trigger, leaving it always enabled in PostgreSQL. The method previously also
189
+ forced `enabled: true` into the registry `update!` call, overwriting any previously stored
190
+ `false` value. The `enabled: true` is removed from the `update!` so the stored state is
191
+ preserved; if `enabled` is `false`, an `ALTER TABLE … DISABLE TRIGGER` is issued immediately
192
+ after the registry update, within the same transaction.
193
+
194
+ Spec coverage added for all three gaps:
195
+ - `registry_spec.rb` — four cases in a new `"with PostgreSQL enabled state sync"` context:
196
+ create with `enabled: false` when trigger exists in DB, create when trigger not yet in DB
197
+ (no SQL, no error), update `true → false`, and update `false → true`.
198
+ - `trigger_registry_spec.rb` — new `"when registry entry has enabled: false"` context inside
199
+ `#re_execute!`: verifies `enabled` is not flipped to `true` and that `DISABLE TRIGGER` SQL
200
+ is issued after recreation.
201
+ - `migrator_spec.rb` — new `".run_migration with enforce_disabled_triggers"` describe block:
202
+ runs a real migration that creates a trigger, confirms `tgenabled = 'D'` in `pg_trigger`
203
+ afterward.
204
+
205
+ ([lib/pg_sql_triggers/registry/manager.rb](lib/pg_sql_triggers/registry/manager.rb),
206
+ [lib/pg_sql_triggers/migrator.rb](lib/pg_sql_triggers/migrator.rb),
207
+ [app/models/pg_sql_triggers/trigger_registry.rb](app/models/pg_sql_triggers/trigger_registry.rb),
208
+ [spec/pg_sql_triggers/registry_spec.rb](spec/pg_sql_triggers/registry_spec.rb),
209
+ [spec/pg_sql_triggers/trigger_registry_spec.rb](spec/pg_sql_triggers/trigger_registry_spec.rb),
210
+ [spec/pg_sql_triggers/migrator_spec.rb](spec/pg_sql_triggers/migrator_spec.rb))
211
+
212
+ - **[Medium] `enabled` defaulted to `false` in the DSL** — Every newly declared trigger had
213
+ `@enabled = false`, meaning triggers were silently disabled unless the author explicitly added
214
+ `self.enabled = true`. Deployments that omitted the flag would register a disabled trigger that
215
+ appears in the registry but never fires, with no warning. Changed the default to `true` so
216
+ triggers are active unless explicitly disabled.
217
+ ([lib/pg_sql_triggers/dsl/trigger_definition.rb](lib/pg_sql_triggers/dsl/trigger_definition.rb))
218
+
219
+ - **[Critical] Drift detector checksum excluded `timing` field** — `Drift::Detector#calculate_db_checksum`
220
+ was hashing without the `timing` attribute, while `TriggerRegistry#calculate_checksum` and
221
+ `Registry::Manager#calculate_checksum` both include it. Any trigger with a non-default timing
222
+ value (`"after"`) permanently showed as `DRIFTED` even when fully in sync.
223
+ ([lib/pg_sql_triggers/drift/detector.rb](lib/pg_sql_triggers/drift/detector.rb))
224
+
225
+ - **[Critical] `extract_function_body` returned the full `CREATE FUNCTION` statement** —
226
+ `pg_get_functiondef()` returns the complete DDL including the `CREATE OR REPLACE FUNCTION`
227
+ header and language clause. The detector was hashing that entire string while the registry
228
+ stores only the PL/pgSQL body, so checksums never matched and every trigger appeared `DRIFTED`.
229
+ Fixed by extracting only the content between dollar-quote delimiters (`$$` / `$function$`).
230
+ ([lib/pg_sql_triggers/drift/detector.rb](lib/pg_sql_triggers/drift/detector.rb))
231
+
232
+ - **[Critical] DSL triggers stored `"placeholder"` as checksum causing permanent false drift** —
233
+ `Registry::Manager#calculate_checksum` returned the literal string `"placeholder"` whenever
234
+ `function_body` was blank (the normal case for DSL-defined triggers). The drift detector then
235
+ computed a real SHA256 hash and compared it to `"placeholder"`, so all DSL triggers always
236
+ appeared `DRIFTED`. Fixed by computing a real checksum using `""` for `function_body`, and
237
+ teaching the detector to also use `""` for `function_body` when the registry source is `"dsl"`.
238
+ ([lib/pg_sql_triggers/registry/manager.rb](lib/pg_sql_triggers/registry/manager.rb),
239
+ [lib/pg_sql_triggers/drift/detector.rb](lib/pg_sql_triggers/drift/detector.rb))
240
+
241
+ - **[Critical] `re_execute!` always raised for DSL-defined triggers** —
242
+ `TriggerRegistry#re_execute!` raised `StandardError` immediately when `function_body` was
243
+ blank, which is always the case for DSL triggers (the primary use path). Added a
244
+ `build_trigger_sql_from_definition` private helper that reconstructs a valid `CREATE TRIGGER`
245
+ SQL statement from the stored DSL definition JSON (`function_name`, `timing`, `events`,
246
+ `condition`). `re_execute!` now falls back to this reconstructed SQL when `function_body` is
247
+ absent, making the method functional for DSL triggers.
248
+ ([app/models/pg_sql_triggers/trigger_registry.rb](app/models/pg_sql_triggers/trigger_registry.rb))
249
+
250
+ - **[High] `SafetyValidator#capture_sql` executed migration side effects during capture** —
251
+ `capture_sql` monkey-patched only the `execute` method, so any ActiveRecord migration helpers
252
+ (`add_column`, `create_table`, etc.) called by the migration ran for real as a side effect of
253
+ the safety check. Wrapped the migration invocation in
254
+ `ActiveRecord::Base.transaction { …; raise ActiveRecord::Rollback }` so all schema changes
255
+ are rolled back after SQL capture.
256
+ ([lib/pg_sql_triggers/migrator/safety_validator.rb](lib/pg_sql_triggers/migrator/safety_validator.rb))
257
+
258
+ - **[Low] Dead `trigger_name` expression in `drop!`** — A bare `trigger_name` expression inside
259
+ the `drop!` transaction block was a no-op whose return value was silently discarded. Removed.
260
+ ([app/models/pg_sql_triggers/trigger_registry.rb](app/models/pg_sql_triggers/trigger_registry.rb))
261
+
262
+ ## [1.3.0] - 2026-01-05
263
+
264
+ ### Added
265
+ - **Enhanced Console API**: Added missing drift query methods to Registry API for consistency
266
+ - `PgSqlTriggers::Registry.drifted` - Returns all drifted triggers
267
+ - `PgSqlTriggers::Registry.in_sync` - Returns all in-sync triggers
268
+ - `PgSqlTriggers::Registry.unknown_triggers` - Returns all unknown (external) triggers
269
+ - `PgSqlTriggers::Registry.dropped` - Returns all dropped triggers
270
+ - All console APIs now follow consistent naming conventions (query methods vs action methods)
271
+
272
+ - **Controller Concerns**: Extracted common controller functionality into reusable concerns
273
+ - `KillSwitchProtection` concern - Handles kill switch checking and confirmation helpers
274
+ - `PermissionChecking` concern - Handles permission checks and actor management
275
+ - `ErrorHandling` concern - Handles error formatting and flash message helpers
276
+ - All controllers now inherit from `ApplicationController` which includes these concerns
277
+ - Improved code organization and maintainability
278
+
279
+ - **YARD Documentation**: Comprehensive YARD documentation added to all public APIs
280
+ - `PgSqlTriggers::Registry` module - All public methods fully documented
281
+ - `PgSqlTriggers::TriggerRegistry` model - All public methods fully documented
282
+ - `PgSqlTriggers::Generator::Service` - All public class methods fully documented
283
+ - `PgSqlTriggers::SQL::Executor` - Already had documentation (verified)
284
+ - All documentation includes parameter types, return values, and examples
285
+
286
+ ### Added
287
+ - **Complete UI Action Buttons**: All trigger operations now accessible via web UI
288
+ - Enable/Disable buttons in dashboard and table detail views
289
+ - Drop trigger button with confirmation modal (Admin permission required)
290
+ - Re-execute trigger button with drift diff display (Admin permission required)
291
+ - All buttons respect permission checks and show/hide based on user role
292
+ - Kill switch integration with confirmation modals for all actions
293
+ - Buttons styled with environment-aware colors (warning colors for production)
294
+
295
+ - **Enhanced Dashboard**:
296
+ - "Last Applied" column showing `installed_at` timestamps in human-readable format
297
+ - Tooltips with exact timestamps on hover
298
+ - Default sorting by `installed_at` (most recent first)
299
+ - Drop and Re-execute buttons in dashboard table (Admin only)
300
+ - Permission-aware button visibility throughout
301
+
302
+ - **Trigger Detail Page Enhancements**:
303
+ - Breadcrumb navigation (Dashboard → Tables → Table → Trigger)
304
+ - Enhanced `installed_at` display with relative time formatting
305
+ - `last_verified_at` timestamp display
306
+ - All action buttons (enable/disable/drop/re-execute) accessible from detail page
307
+
308
+ - **Comprehensive Audit Logging System**:
309
+ - New `pg_sql_triggers_audit_log` table for tracking all operations
310
+ - `AuditLog` model with logging methods (`log_success`, `log_failure`)
311
+ - Audit logging integrated into all trigger operations:
312
+ - `enable!` - logs success/failure with before/after state
313
+ - `disable!` - logs success/failure with before/after state
314
+ - `drop!` - logs success/failure with reason and state changes
315
+ - `re_execute!` - logs success/failure with drift diff information
316
+ - All operations track actor (who performed the action)
317
+ - Complete state capture (before/after) for all operations
318
+ - Error messages logged for failed operations
319
+ - Environment and confirmation text tracking
320
+
321
+ - **Enhanced Actor Tracking**:
322
+ - All trigger operations now accept `actor` parameter
323
+ - Console APIs updated to pass actor information
324
+ - UI controllers pass `current_actor` to all operations
325
+ - Actor information stored in audit logs for complete audit trail
326
+
327
+ - **Permissions Enforcement System**:
328
+ - Permission checks enforced across all controllers (Viewer, Operator, Admin)
329
+ - `PermissionsHelper` module for view-level permission checks
330
+ - Permission helper methods in `ApplicationController` for consistent authorization
331
+ - All UI buttons and actions respect permission levels
332
+ - Console APIs (`Registry.enable/disable/drop/re_execute`, `SQL::Executor.execute`) check permissions
333
+ - Permission errors raise `PermissionError` with clear messages
334
+ - Configurable permission checker via `permission_checker` configuration option
335
+
336
+ - **Enhanced Error Handling System**:
337
+ - Comprehensive error hierarchy with base `Error` class and specialized error types
338
+ - Error classes: `PermissionError`, `KillSwitchError`, `DriftError`, `ValidationError`, `ExecutionError`, `UnsafeMigrationError`, `NotFoundError`
339
+ - Error codes for programmatic handling (e.g., `PERMISSION_DENIED`, `KILL_SWITCH_ACTIVE`, `DRIFT_DETECTED`)
340
+ - Standardized error messages with recovery suggestions
341
+ - Enhanced error display in UI with user-friendly formatting
342
+ - Context information included in all errors for better debugging
343
+ - Error handling helpers in `ApplicationController` for consistent error formatting
344
+
345
+ - **Comprehensive Documentation**:
346
+ - New `ui-guide.md` - Quick start guide for web interface
347
+ - New `permissions.md` - Complete guide to configuring and using permissions
348
+ - New `audit-trail.md` - Guide to viewing and exporting audit logs
349
+ - New `troubleshooting.md` - Common issues and solutions with error code reference
350
+ - Updated documentation index with links to all new guides
351
+
352
+ - **Audit Log UI**:
353
+ - Web interface for viewing audit log entries (`/audit_logs`)
354
+ - Filterable by trigger name, operation, status, and environment
355
+ - Sortable by date (ascending/descending)
356
+ - Pagination support (default 50 entries per page, max 200)
357
+ - CSV export functionality with applied filters
358
+ - Comprehensive view showing operation details, actor information, status, and error messages
359
+ - Links to trigger detail pages from audit log entries
360
+ - Navigation menu integration
361
+
362
+ - **Enhanced Database Tables & Triggers Page**:
363
+ - Pagination support for tables list (default 20 per page, configurable up to 100)
364
+ - Filter functionality to view:
365
+ - All tables
366
+ - Tables with triggers only
367
+ - Tables without triggers only
368
+ - Enhanced statistics dashboard showing:
369
+ - Count of tables with triggers
370
+ - Count of tables without triggers
371
+ - Total tables count
372
+ - Filter controls with visual indicators for active filter
373
+ - Pagination controls preserve filter selection when navigating pages
374
+ - Context-aware empty state messages based on selected filter
375
+
376
+ ### Changed
377
+ - **Code Organization**: Refactored `ApplicationController` to use concerns instead of inline methods
378
+ - Reduced code duplication across controllers
379
+ - Improved separation of concerns
380
+ - Better testability and maintainability
381
+
382
+ - **Service Object Patterns**: Standardized service object patterns across all service classes
383
+ - All service objects follow consistent class method patterns
384
+ - Consistent stateless service object conventions
385
+
386
+ - **Goal.md**: Updated to reflect actual implementation status
387
+ - Added technical notes documenting improvements
388
+ - Updated console API section with all implemented methods
389
+ - Documented code organization improvements
390
+
391
+ - Dashboard default sorting changed to `installed_at` (most recent first) instead of `created_at`
392
+ - Trigger detail page breadcrumbs improved navigation flow
393
+ - All trigger action buttons use consistent styling and permission checks
394
+
395
+ ### Fixed
396
+ - Actor tracking now properly passed through all operation methods
397
+ - Improved error handling with audit log integration
398
+
399
+ ### Security
400
+ - All operations now tracked in audit log for compliance and debugging
401
+ - Actor information captured for all operations (UI, Console, CLI)
402
+ - Complete state change tracking for audit trail
403
+ - Permission enforcement ensures only authorized users can perform operations
404
+ - Permission checks enforced at controller, API, and view levels
9
405
 
10
406
  ## [1.2.0] - 2026-01-02
11
407
 
data/COVERAGE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Code Coverage Report
2
2
 
3
- **Total Coverage: 95.33%**
3
+ **Total Coverage: 95.99%**
4
4
 
5
- Covered: 2040 / 2140 lines
5
+ Covered: 2319 / 2416 lines
6
6
 
7
7
  ---
8
8
 
@@ -10,6 +10,9 @@ Covered: 2040 / 2140 lines
10
10
 
11
11
  | File | Coverage | Covered Lines | Missed Lines | Total Lines |
12
12
  |------|----------|---------------|--------------|-------------|
13
+ | `lib/pg_sql_triggers/drift.rb` | 100.0% ✅ | 13 | 0 | 13 |
14
+ | `lib/pg_sql_triggers/drift/db_queries.rb` | 100.0% ✅ | 24 | 0 | 24 |
15
+ | `lib/pg_sql_triggers/dsl.rb` | 100.0% ✅ | 9 | 0 | 9 |
13
16
  | `lib/pg_sql_triggers/dsl/trigger_definition.rb` | 100.0% ✅ | 37 | 0 | 37 |
14
17
  | `lib/pg_sql_triggers/generator.rb` | 100.0% ✅ | 4 | 0 | 4 |
15
18
  | `lib/pg_sql_triggers/generator/form.rb` | 100.0% ✅ | 36 | 0 | 36 |
@@ -20,41 +23,45 @@ Covered: 2040 / 2140 lines
20
23
  | `lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb` | 100.0% ✅ | 75 | 0 | 75 |
21
24
  | `lib/pg_sql_triggers/migrator/safety_validator.rb` | 100.0% ✅ | 110 | 0 | 110 |
22
25
  | `lib/pg_sql_triggers/permissions.rb` | 100.0% ✅ | 11 | 0 | 11 |
23
- | `lib/pg_sql_triggers/permissions/checker.rb` | 100.0% ✅ | 15 | 0 | 15 |
24
- | `lib/pg_sql_triggers/registry.rb` | 100.0% ✅ | 41 | 0 | 41 |
26
+ | `lib/pg_sql_triggers/permissions/checker.rb` | 100.0% ✅ | 17 | 0 | 17 |
25
27
  | `lib/pg_sql_triggers/registry/validator.rb` | 100.0% ✅ | 5 | 0 | 5 |
26
28
  | `lib/pg_sql_triggers/sql/capsule.rb` | 100.0% ✅ | 28 | 0 | 28 |
27
29
  | `lib/pg_sql_triggers/sql/executor.rb` | 100.0% ✅ | 63 | 0 | 63 |
28
30
  | `lib/pg_sql_triggers/testing.rb` | 100.0% ✅ | 6 | 0 | 6 |
29
31
  | `lib/pg_sql_triggers/testing/syntax_validator.rb` | 100.0% ✅ | 58 | 0 | 58 |
30
32
  | `lib/pg_sql_triggers/testing/dry_run.rb` | 100.0% ✅ | 24 | 0 | 24 |
31
- | `config/initializers/pg_sql_triggers.rb` | 100.0% ✅ | 10 | 0 | 10 |
33
+ | `app/models/pg_sql_triggers/audit_log.rb` | 100.0% ✅ | 28 | 0 | 28 |
34
+ | `app/models/pg_sql_triggers/trigger_registry.rb` | 100.0% ✅ | 176 | 0 | 176 |
35
+ | `app/controllers/concerns/pg_sql_triggers/error_handling.rb` | 100.0% ✅ | 19 | 0 | 19 |
36
+ | `app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb` | 100.0% ✅ | 17 | 0 | 17 |
32
37
  | `app/models/pg_sql_triggers/application_record.rb` | 100.0% ✅ | 3 | 0 | 3 |
33
- | `app/controllers/pg_sql_triggers/application_controller.rb` | 100.0% ✅ | 28 | 0 | 28 |
34
- | `lib/pg_sql_triggers.rb` | 100.0% ✅ | 45 | 0 | 45 |
35
- | `lib/pg_sql_triggers/dsl.rb` | 100.0% ✅ | 9 | 0 | 9 |
36
- | `lib/pg_sql_triggers/drift/db_queries.rb` | 100.0% ✅ | 24 | 0 | 24 |
37
- | `lib/pg_sql_triggers/drift.rb` | 100.0% ✅ | 13 | 0 | 13 |
38
+ | `app/controllers/pg_sql_triggers/application_controller.rb` | 100.0% ✅ | 13 | 0 | 13 |
39
+ | `app/helpers/pg_sql_triggers/permissions_helper.rb` | 100.0% ✅ | 16 | 0 | 16 |
40
+ | `app/controllers/pg_sql_triggers/dashboard_controller.rb` | 100.0% ✅ | 26 | 0 | 26 |
41
+ | `config/initializers/pg_sql_triggers.rb` | 100.0% ✅ | 10 | 0 | 10 |
42
+ | `lib/pg_sql_triggers/errors.rb` | 100.0% ✅ | 83 | 0 | 83 |
43
+ | `app/controllers/pg_sql_triggers/triggers_controller.rb` | 100.0% ✅ | 75 | 0 | 75 |
44
+ | `lib/pg_sql_triggers.rb` | 100.0% ✅ | 40 | 0 | 40 |
38
45
  | `lib/pg_sql_triggers/migrator/pre_apply_comparator.rb` | 99.19% ✅ | 122 | 1 | 123 |
39
46
  | `lib/pg_sql_triggers/drift/detector.rb` | 98.48% ✅ | 65 | 1 | 66 |
40
- | `app/controllers/pg_sql_triggers/sql_capsules_controller.rb` | 97.18% ✅ | 69 | 2 | 71 |
41
- | `app/controllers/pg_sql_triggers/dashboard_controller.rb` | 96.67% ✅ | 29 | 1 | 30 |
42
- | `app/controllers/pg_sql_triggers/triggers_controller.rb` | 96.43% ✅ | 81 | 3 | 84 |
47
+ | `app/controllers/pg_sql_triggers/audit_logs_controller.rb` | 97.73% ✅ | 43 | 1 | 44 |
48
+ | `app/controllers/pg_sql_triggers/sql_capsules_controller.rb` | 97.14% ✅ | 68 | 2 | 70 |
43
49
  | `lib/generators/pg_sql_triggers/trigger_migration_generator.rb` | 96.3% ✅ | 26 | 1 | 27 |
44
- | `lib/pg_sql_triggers/sql/kill_switch.rb` | 96.0% ✅ | 96 | 4 | 100 |
50
+ | `lib/pg_sql_triggers/sql/kill_switch.rb` | 96.04% ✅ | 97 | 4 | 101 |
45
51
  | `lib/pg_sql_triggers/migrator.rb` | 95.42% ✅ | 125 | 6 | 131 |
46
52
  | `lib/pg_sql_triggers/registry/manager.rb` | 95.08% ✅ | 58 | 3 | 61 |
47
- | `app/controllers/pg_sql_triggers/tables_controller.rb` | 94.44% ✅ | 17 | 1 | 18 |
53
+ | `app/controllers/pg_sql_triggers/tables_controller.rb` | 94.74% ✅ | 18 | 1 | 19 |
48
54
  | `lib/pg_sql_triggers/database_introspection.rb` | 94.29% ✅ | 66 | 4 | 70 |
49
55
  | `lib/pg_sql_triggers/drift/reporter.rb` | 94.12% ✅ | 96 | 6 | 102 |
50
56
  | `lib/pg_sql_triggers/engine.rb` | 92.86% ✅ | 13 | 1 | 14 |
51
- | `app/models/pg_sql_triggers/trigger_registry.rb` | 92.25% ✅ | 119 | 10 | 129 |
52
57
  | `lib/pg_sql_triggers/testing/safe_executor.rb` | 91.89% ✅ | 34 | 3 | 37 |
58
+ | `lib/pg_sql_triggers/registry.rb` | 91.84% ✅ | 45 | 4 | 49 |
53
59
  | `app/controllers/pg_sql_triggers/generator_controller.rb` | 91.49% ✅ | 86 | 8 | 94 |
54
60
  | `lib/pg_sql_triggers/sql.rb` | 90.91% ✅ | 10 | 1 | 11 |
55
- | `lib/pg_sql_triggers/testing/function_tester.rb` | 89.55% ⚠️ | 60 | 7 | 67 |
56
- | `app/controllers/pg_sql_triggers/migrations_controller.rb` | 81.4% ⚠️ | 70 | 16 | 86 |
57
- | `config/routes.rb` | 12.5% | 3 | 21 | 24 |
61
+ | `lib/pg_sql_triggers/testing/function_tester.rb` | 89.71% ⚠️ | 61 | 7 | 68 |
62
+ | `app/controllers/concerns/pg_sql_triggers/permission_checking.rb` | 85.37% ⚠️ | 35 | 6 | 41 |
63
+ | `app/controllers/pg_sql_triggers/migrations_controller.rb` | 82.76% ⚠️ | 72 | 15 | 87 |
64
+ | `config/routes.rb` | 12.0% ❌ | 3 | 22 | 25 |
58
65
 
59
66
  ---
60
67