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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +397 -1
- data/COVERAGE.md +26 -19
- data/GEM_ANALYSIS.md +368 -0
- data/Goal.md +276 -155
- data/README.md +45 -22
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
- data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
- data/app/models/pg_sql_triggers/audit_log.rb +106 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +218 -13
- data/app/views/layouts/pg_sql_triggers/application.html.erb +25 -6
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +34 -12
- data/app/views/pg_sql_triggers/tables/index.html.erb +75 -5
- data/app/views/pg_sql_triggers/tables/show.html.erb +17 -6
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
- data/config/routes.rb +2 -14
- data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
- data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/README.md +15 -5
- data/docs/api-reference.md +233 -151
- data/docs/audit-trail.md +413 -0
- data/docs/configuration.md +28 -7
- data/docs/getting-started.md +17 -16
- data/docs/permissions.md +369 -0
- data/docs/troubleshooting.md +486 -0
- data/docs/ui-guide.md +211 -0
- data/docs/usage-guide.md +38 -67
- data/docs/web-ui.md +251 -128
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
- data/lib/pg_sql_triggers/drift/db_queries.rb +12 -8
- data/lib/pg_sql_triggers/drift/detector.rb +51 -38
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
- data/lib/pg_sql_triggers/engine.rb +14 -0
- data/lib/pg_sql_triggers/errors.rb +245 -0
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
- data/lib/pg_sql_triggers/migrator.rb +53 -6
- data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
- data/lib/pg_sql_triggers/registry/manager.rb +36 -11
- data/lib/pg_sql_triggers/registry/validator.rb +62 -5
- data/lib/pg_sql_triggers/registry.rb +141 -8
- data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -247
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +7 -7
- data/pg_sql_triggers.gemspec +53 -0
- metadata +35 -18
- data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
- data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
- data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
- data/docs/screenshots/.gitkeep +0 -1
- 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/lib/generators/trigger/migration_generator.rb +0 -60
- data/lib/pg_sql_triggers/generator/form.rb +0 -80
- data/lib/pg_sql_triggers/generator/service.rb +0 -307
- data/lib/pg_sql_triggers/generator.rb +0 -8
- data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
- data/lib/pg_sql_triggers/sql/executor.rb +0 -200
data/GEM_ANALYSIS.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# pg_sql_triggers — Gem Analysis
|
|
2
|
+
|
|
3
|
+
> Analysed against version **1.3.0** · February 2026
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
1. [Critical Bugs](#1-critical-bugs)
|
|
9
|
+
2. [Design Issues](#2-design-issues)
|
|
10
|
+
3. [Security Concerns](#3-security-concerns)
|
|
11
|
+
4. [Missing Features](#4-missing-features)
|
|
12
|
+
5. [Unnecessary / Over-engineered Features](#5-unnecessary--over-engineered-features)
|
|
13
|
+
6. [Low-coverage Hotspots](#6-low-coverage-hotspots)
|
|
14
|
+
7. [Summary Table](#7-summary-table)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Critical Bugs
|
|
19
|
+
|
|
20
|
+
### 1.1 Checksum Algorithm Is Inconsistent — Will Always Report False Drift
|
|
21
|
+
|
|
22
|
+
Three places compute a trigger checksum. They use different field sets:
|
|
23
|
+
|
|
24
|
+
| Location | Fields included |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `TriggerRegistry#calculate_checksum` | `trigger_name, table_name, version, function_body, condition, timing` |
|
|
27
|
+
| `Registry::Manager#calculate_checksum` | `name, table_name, version, function_body, condition, timing` |
|
|
28
|
+
| `Drift::Detector#calculate_db_checksum` | `trigger_name, table_name, version, function_body, condition` ← **missing `timing`** |
|
|
29
|
+
|
|
30
|
+
Because the registry stores a checksum that includes `timing`, but the drift detector recomputes the checksum without `timing`, **every trigger that has a non-default timing value will permanently show as `DRIFTED`** even when fully in sync.
|
|
31
|
+
|
|
32
|
+
Files:
|
|
33
|
+
- [lib/pg_sql_triggers/drift/detector.rb:88-103](lib/pg_sql_triggers/drift/detector.rb#L88-L103)
|
|
34
|
+
- [app/models/pg_sql_triggers/trigger_registry.rb:318-327](app/models/pg_sql_triggers/trigger_registry.rb#L318-L327)
|
|
35
|
+
- [lib/pg_sql_triggers/registry/manager.rb:129-145](lib/pg_sql_triggers/registry/manager.rb#L129-L145)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### 1.2 `extract_function_body` Returns the Full `CREATE FUNCTION` Statement
|
|
40
|
+
|
|
41
|
+
`Detector#extract_function_body` returns `function_def` unchanged — the full output of `pg_get_functiondef()`, including the `CREATE OR REPLACE FUNCTION` header. The registry stores only the PL/pgSQL body. These two strings will never match, so **the drift detector will report every trigger as DRIFTED regardless of actual state**.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# lib/pg_sql_triggers/drift/detector.rb:106-114
|
|
45
|
+
def extract_function_body(db_trigger)
|
|
46
|
+
function_def = db_trigger["function_definition"]
|
|
47
|
+
return nil unless function_def
|
|
48
|
+
|
|
49
|
+
# TODO: Parse and extract just the body if needed
|
|
50
|
+
function_def # ← returns entire CREATE FUNCTION definition
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The `# TODO` comment confirms this is incomplete.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### 1.3 DSL Triggers Store `"placeholder"` as Checksum → Permanent False Drift
|
|
59
|
+
|
|
60
|
+
When a DSL trigger definition has no `function_body` (which is the normal case — `TriggerDefinition#function_body` always returns `nil`), `Manager#calculate_checksum` detects `function_body_value.blank?` and stores the literal string `"placeholder"` in the registry. The drift detector then computes a real SHA256 hash from the database and compares it to `"placeholder"`, so **all DSL-defined triggers always appear DRIFTED**.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# lib/pg_sql_triggers/registry/manager.rb:133
|
|
64
|
+
return "placeholder" if function_body_value.blank?
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 1.4 `re_execute!` Is Broken for All DSL-Defined Triggers
|
|
70
|
+
|
|
71
|
+
`TriggerRegistry#re_execute!` raises immediately if `function_body` is blank:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
raise StandardError, "Cannot re-execute: missing function_body" if function_body.blank?
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Since `TriggerDefinition#function_body` always returns `nil`, DSL-defined triggers (the primary usage path) can **never be re-executed** through this method. The method is effectively dead for the main use case.
|
|
78
|
+
|
|
79
|
+
File: [app/models/pg_sql_triggers/trigger_registry.rb:289](app/models/pg_sql_triggers/trigger_registry.rb#L289)
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 1.5 `SafetyValidator#capture_sql` Actually Executes the Migration
|
|
84
|
+
|
|
85
|
+
The safety validator captures SQL by monkey-patching the instance's `execute` method, then calling the migration's `up`/`down` method:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# lib/pg_sql_triggers/migrator/safety_validator.rb:57-68
|
|
89
|
+
def capture_sql(migration_instance, direction)
|
|
90
|
+
captured = []
|
|
91
|
+
migration_instance.define_singleton_method(:execute) do |sql|
|
|
92
|
+
captured << sql.to_s.strip
|
|
93
|
+
end
|
|
94
|
+
migration_instance.public_send(direction) # ← runs the migration method
|
|
95
|
+
captured
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If the migration does anything other than call `execute` (e.g. calls `add_column`, `create_table`, or any other ActiveRecord migration helper), those side effects run for real during the safety check. Then `run_migration` creates two more instances and runs them again — meaning a single migration run can execute the migration's code path **three times**.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 1.6 Dead Code in `drop!`
|
|
104
|
+
|
|
105
|
+
Inside `drop!`, there is a bare expression `trigger_name` on its own line that does nothing:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# app/models/pg_sql_triggers/trigger_registry.rb:249-251
|
|
109
|
+
ActiveRecord::Base.transaction do
|
|
110
|
+
drop_trigger_from_database
|
|
111
|
+
trigger_name # ← no-op; return value is discarded
|
|
112
|
+
destroy!
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This is likely a leftover from an assignment like `name = trigger_name` that was refactored away.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 2. Design Issues
|
|
120
|
+
|
|
121
|
+
### 2.1 N+1 Queries in All Drift Registry Methods
|
|
122
|
+
|
|
123
|
+
`Registry::Manager#drifted`, `#in_sync`, `#unknown_triggers`, and `#dropped` all call `Drift::Detector.detect_all`, which loops over every registry entry and calls `detect(entry.trigger_name)` per entry. Each `detect` call performs `TriggerRegistry.find_by(trigger_name:)` — an individual SQL query. For N triggers, this is N+1 queries.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# lib/pg_sql_triggers/drift/detector.rb:38-56
|
|
127
|
+
def detect_all
|
|
128
|
+
registry_entries = TriggerRegistry.all.to_a # 1 query
|
|
129
|
+
db_triggers = DbQueries.all_triggers # 1 query
|
|
130
|
+
results = registry_entries.map do |entry|
|
|
131
|
+
detect(entry.trigger_name) # 1 query per entry ← N+1
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### 2.2 Class-Level Cache Is Not Thread-Safe
|
|
138
|
+
|
|
139
|
+
`Registry::Manager._registry_cache` is stored in a class-level instance variable (`@_registry_cache`). In a multi-threaded Puma server, concurrent requests share and mutate this hash without synchronisation, creating a potential race condition where one thread's cache write corrupts another thread's lookup.
|
|
140
|
+
|
|
141
|
+
File: [lib/pg_sql_triggers/registry/manager.rb:9-15](lib/pg_sql_triggers/registry/manager.rb#L9-L15)
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### 2.3 `DbQueries` Hard-Codes the `public` Schema
|
|
146
|
+
|
|
147
|
+
Every SQL query filters `n.nspname = 'public'`. Applications using non-public schemas (multi-tenant schemas, `app`, `audit`, etc.) cannot use this gem at all.
|
|
148
|
+
|
|
149
|
+
File: [lib/pg_sql_triggers/drift/db_queries.rb](lib/pg_sql_triggers/drift/db_queries.rb) — lines 27, 51, 78, 93
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### 2.4 `when_env` DSL Option Is an Anti-Pattern
|
|
154
|
+
|
|
155
|
+
The DSL supports environment-specific triggers:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
|
|
159
|
+
when_env :production
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Having triggers that exist only in production but not in staging or development makes it impossible to test them fully. Schema drift between environments is guaranteed and bugs will only surface in production. Environment-specific behavior belongs in configuration, not in trigger definitions.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### 2.5 Dual Generator Paths Create Ambiguity
|
|
168
|
+
|
|
169
|
+
Two separate generator namespaces exist for the same task:
|
|
170
|
+
|
|
171
|
+
- `lib/generators/pg_sql_triggers/trigger_migration_generator.rb` → invoked with `rails g pg_sql_triggers:trigger_migration`
|
|
172
|
+
- `lib/generators/trigger/migration_generator.rb` → invoked with `rails g trigger:migration`
|
|
173
|
+
|
|
174
|
+
There is no documentation explaining which to prefer, and having both increases maintenance surface without benefit.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### 2.6 `capture_state` Does Not Include `function_body`
|
|
179
|
+
|
|
180
|
+
The audit log state snapshot captures `enabled`, `version`, `checksum`, `table_name`, `source`, `environment`, and `installed_at` — but not `function_body`. Diffs recorded in audit logs will not show what the function body was before/after an operation, making the audit trail incomplete for the most important change: the function itself.
|
|
181
|
+
|
|
182
|
+
File: [app/models/pg_sql_triggers/trigger_registry.rb:414-424](app/models/pg_sql_triggers/trigger_registry.rb#L414-L424)
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### 2.7 `enabled` and `version` Use Non-Idiomatic Dual-Purpose Methods in DSL
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
def version(version = nil)
|
|
190
|
+
if version.nil?
|
|
191
|
+
@version
|
|
192
|
+
else
|
|
193
|
+
@version = version
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Ruby DSLs conventionally use `attr_accessor` for simple getters/setters or separate `def version=(v)` / `def version` methods. The current pattern is confusing because calling `version` with no arguments returns the current value rather than raising `ArgumentError`, making typos silent.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 3. Security Concerns
|
|
203
|
+
|
|
204
|
+
### 3.1 Default Permissions Allow Everything
|
|
205
|
+
|
|
206
|
+
`Permissions::Checker#can?` returns `true` for all actors and all actions when no `permission_checker` is configured:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# lib/pg_sql_triggers/permissions/checker.rb:16-17
|
|
210
|
+
# Default behavior: allow all permissions
|
|
211
|
+
true
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A newly installed gem with no extra configuration grants every user full ADMIN access (including `drop_trigger`, `execute_sql`, `override_drift`). The comment says "This should be overridden in production via configuration", but there is no warning in the initializer or README to flag this as a critical step before go-live.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### 3.2 `Registry::Validator` Is a No-Op
|
|
219
|
+
|
|
220
|
+
The validator that should catch invalid DSL definitions before they are registered is a stub that always returns `true`:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# lib/pg_sql_triggers/registry/validator.rb:7-11
|
|
224
|
+
def self.validate!
|
|
225
|
+
# This is a placeholder implementation
|
|
226
|
+
true
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Its test confirms only that it returns `true`. No actual validation (duplicate names, missing table, invalid events, etc.) occurs. Malformed definitions silently pass.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 4. Missing Features
|
|
235
|
+
|
|
236
|
+
### 4.1 No `FOR EACH ROW` / `FOR EACH STATEMENT` DSL Support
|
|
237
|
+
|
|
238
|
+
PostgreSQL requires every trigger to specify row-level or statement-level execution. The gem's DSL has no `row_level` or `statement_level` option. The generated SQL presumably hard-codes one, but users have no way to choose.
|
|
239
|
+
|
|
240
|
+
### 4.2 No Multi-Schema Support
|
|
241
|
+
|
|
242
|
+
As noted in §2.3, the entire introspection layer is hard-coded to the `public` schema. There is no `schema` DSL option, no configuration for a default schema, and no way to manage triggers in non-public schemas.
|
|
243
|
+
|
|
244
|
+
### 4.3 No Column-Level Trigger Support (`OF column_name`)
|
|
245
|
+
|
|
246
|
+
PostgreSQL `UPDATE OF col1, col2` triggers (which fire only when specific columns change) are not representable in the DSL. This is a common performance optimisation for audit triggers.
|
|
247
|
+
|
|
248
|
+
### 4.4 No Deferred Trigger Support
|
|
249
|
+
|
|
250
|
+
The DSL provides no way to declare `DEFERRABLE INITIALLY DEFERRED` or `DEFERRABLE INITIALLY IMMEDIATE` triggers, which are essential for some referential integrity patterns.
|
|
251
|
+
|
|
252
|
+
### 4.5 No `schema.rb` / `structure.sql` Integration
|
|
253
|
+
|
|
254
|
+
Trigger definitions are managed in a separate `db/triggers/` migration system that is invisible to `rails db:schema:dump`. Restoring a database from `schema.rb` will not recreate triggers. The gem should hook into Rails' structure dump (or document clearly that `structure.sql` must be used and provide a rake task to populate it).
|
|
255
|
+
|
|
256
|
+
### 4.6 No External Alerting for Drift
|
|
257
|
+
|
|
258
|
+
Drift detection runs on demand (web UI check or API call) but there is no mechanism to push alerts to Slack, PagerDuty, or email when drift is detected. A cron-triggered rake task with configurable notification hooks would make this production-ready.
|
|
259
|
+
|
|
260
|
+
### 4.7 No Search or Filter in the Web UI
|
|
261
|
+
|
|
262
|
+
The dashboard and trigger list pages have no search, filter by table, filter by drift state, or pagination. At scale (many triggers), the UI becomes unwieldy.
|
|
263
|
+
|
|
264
|
+
### 4.8 No Trigger Dependency or Ordering Declaration
|
|
265
|
+
|
|
266
|
+
When multiple triggers fire on the same event for the same table, PostgreSQL fires them in alphabetical order by name. There is no DSL primitive to declare intended ordering or express that trigger A must run before trigger B.
|
|
267
|
+
|
|
268
|
+
### 4.9 No Export / Import of Trigger Definitions
|
|
269
|
+
|
|
270
|
+
There is no way to export the current state of all triggers to a portable format (JSON, YAML) or import definitions from another project. This makes it hard to migrate or share trigger libraries between applications.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 5. Unnecessary / Over-engineered Features
|
|
275
|
+
|
|
276
|
+
### 5.1 `SQL::KillSwitch` Is ~1,200 Lines for a Three-Layer Check
|
|
277
|
+
|
|
278
|
+
The kill switch provides three layers of protection (config, ENV override, confirmation text). The core logic is straightforward, but the module spans over 1,200 lines. Much of this length is verbose logging, repeated helper methods, and defensive checks that could be reduced to ~100 lines without losing functionality. The complexity also means it accounts for 96% of its own test coverage (4% uncovered in a safety-critical module).
|
|
279
|
+
|
|
280
|
+
### 5.2 SQL Capsules Are a Separate Concern
|
|
281
|
+
|
|
282
|
+
`SQL::Capsule` and `SQL::Executor` implement a named-SQL-snippet execution system for "emergency operations". This is a useful concept, but it has nothing specifically to do with trigger management — it is a general-purpose dangerous-SQL runner. Bundling it in this gem conflates concerns and increases the attack surface of the gem's web UI.
|
|
283
|
+
|
|
284
|
+
### 5.3 Web UI Generator Is Code-Generation Against the Principle of Code-First
|
|
285
|
+
|
|
286
|
+
The `GeneratorController` provides a browser form for generating trigger DSL files and migrations. In practice, triggers are infrastructure — they should be authored in code review, not through a web UI. The UI generator produces files on disk on the server at runtime, which is a security and auditability concern in production environments.
|
|
287
|
+
|
|
288
|
+
**Recommendation:** Apply options A + C together.
|
|
289
|
+
|
|
290
|
+
1. **Immediately restrict routes to dev/test** (one-liner, no behaviour change in production):
|
|
291
|
+
```ruby
|
|
292
|
+
# config/routes.rb
|
|
293
|
+
if Rails.env.development? || Rails.env.test?
|
|
294
|
+
mount PgSqlTriggers::Engine => "/pg_sql_triggers"
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
2. **Replace the web generator with a Rails generator** so code generation is a local, CLI-driven action that produces files that go straight into version control:
|
|
299
|
+
```bash
|
|
300
|
+
rails generate pg_sql_triggers:trigger users after_insert
|
|
301
|
+
```
|
|
302
|
+
Once the Rails generator exists, the `GeneratorController` routes and views can be removed entirely. This is the idiomatic Rails pattern and eliminates server-side file writes at runtime.
|
|
303
|
+
|
|
304
|
+
### 5.4 Three Migration Instances Per Migration Run
|
|
305
|
+
|
|
306
|
+
`Migrator#run_migration` instantiates the migration class three separate times:
|
|
307
|
+
1. `validation_instance` — for `SafetyValidator` (which calls `direction` on it)
|
|
308
|
+
2. `comparison_instance` — for `PreApplyComparator`
|
|
309
|
+
3. `migration_instance` — for actual execution
|
|
310
|
+
|
|
311
|
+
If either the safety validator or comparator raises a non-`StandardError` or has side effects, the behaviour is unpredictable. A single instance should be captured once for SQL inspection, then a clean instance used for execution.
|
|
312
|
+
|
|
313
|
+
### 5.5 `enabled` Defaults to `false` in the DSL
|
|
314
|
+
|
|
315
|
+
The DSL initialises `@enabled = false`, meaning every newly declared trigger is disabled by default. This is a surprising default — users who forget to add `enabled true` will deploy triggers that silently do nothing, with no warning.
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# lib/pg_sql_triggers/dsl/trigger_definition.rb:12
|
|
319
|
+
@enabled = false
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 6. Low-coverage Hotspots
|
|
325
|
+
|
|
326
|
+
| File | Coverage | Risk |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `config/routes.rb` | 12% | Route misconfiguration goes undetected |
|
|
329
|
+
| `migrations_controller.rb` | 82.76% | Error paths in the most dangerous controller |
|
|
330
|
+
| `permission_checking.rb` | 85.37% | Security-critical concern with gaps |
|
|
331
|
+
| `testing/function_tester.rb` | 89.71% | Testing utilities that are themselves untested |
|
|
332
|
+
| `sql/kill_switch.rb` | 96.04% | Safety mechanism with 4% uncovered paths |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 7. Summary Table
|
|
337
|
+
|
|
338
|
+
| # | Category | Severity | Description |
|
|
339
|
+
|---|---|---|---|
|
|
340
|
+
| 1.1 | Bug | **Critical** | Checksum algorithm omits `timing` in drift detector — false drift always reported |
|
|
341
|
+
| 1.2 | Bug | **Critical** | `extract_function_body` returns full `CREATE FUNCTION` SQL — checksum never matches |
|
|
342
|
+
| 1.3 | Bug | **Critical** | DSL triggers store `"placeholder"` checksum — always shows as DRIFTED |
|
|
343
|
+
| 1.4 | Bug | **Critical** | `re_execute!` always raises for DSL triggers — feature is broken for primary use case |
|
|
344
|
+
| 1.5 | Bug | **High** | `SafetyValidator` executes migration code as side effect of capture |
|
|
345
|
+
| 1.6 | Bug | **Low** | Dead `trigger_name` expression in `drop!` |
|
|
346
|
+
| 2.1 | Design | **High** | N+1 queries in all drift state filter methods |
|
|
347
|
+
| 2.2 | Design | **High** | Class-level registry cache is not thread-safe |
|
|
348
|
+
| 2.3 | Design | **High** | Hard-coded `public` schema — multi-schema apps unsupported |
|
|
349
|
+
| 2.4 | Design | **Medium** | `when_env` DSL anti-pattern guarantees env drift |
|
|
350
|
+
| 2.5 | Design | **Low** | Duplicate generator namespaces with no guidance |
|
|
351
|
+
| 2.6 | Design | **Medium** | Audit state snapshot omits `function_body` |
|
|
352
|
+
| 2.7 | Design | **Low** | Non-idiomatic dual-purpose DSL methods |
|
|
353
|
+
| 3.1 | Security | **High** | Default permissions allow all actions — no safe default |
|
|
354
|
+
| 3.2 | Security | **High** | `Registry::Validator` is a stub that accepts any input |
|
|
355
|
+
| 4.1 | Missing | **High** | No `FOR EACH ROW` / `FOR EACH STATEMENT` DSL option |
|
|
356
|
+
| 4.2 | Missing | **High** | No multi-schema support |
|
|
357
|
+
| 4.3 | Missing | **Medium** | No column-level trigger (`UPDATE OF col`) |
|
|
358
|
+
| 4.4 | Missing | **Medium** | No deferred trigger support |
|
|
359
|
+
| 4.5 | Missing | **High** | No `schema.rb` / `structure.sql` integration |
|
|
360
|
+
| 4.6 | Missing | **Medium** | No external alerting for detected drift |
|
|
361
|
+
| 4.7 | Missing | **Low** | No search/filter in web UI |
|
|
362
|
+
| 4.8 | Missing | **Low** | No trigger ordering or dependency declaration |
|
|
363
|
+
| 4.9 | Missing | **Low** | No export/import of trigger definitions |
|
|
364
|
+
| 5.1 | Unnecessary | **Low** | Kill switch is ~1,200 lines for a 3-layer check |
|
|
365
|
+
| 5.2 | Unnecessary | **Medium** | SQL Capsules are an unrelated concern bundled in the gem |
|
|
366
|
+
| 5.3 | Unnecessary | **Medium** | Web UI generator creates files on production server |
|
|
367
|
+
| 5.4 | Unnecessary | **Medium** | Three migration instances per run (safety + compare + execute) |
|
|
368
|
+
| 5.5 | Unnecessary | **Medium** | `enabled false` default silently disables triggers in production |
|