pg_sql_triggers 1.1.0 → 1.2.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/.rubocop.yml +15 -0
- data/CHANGELOG.md +61 -0
- data/COVERAGE.md +32 -28
- data/README.md +31 -2
- data/RELEASE.md +1 -1
- data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
- data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
- data/config/routes.rb +9 -0
- data/docs/README.md +2 -2
- data/docs/api-reference.md +252 -4
- data/docs/getting-started.md +1 -1
- data/docs/kill-switch.md +3 -3
- data/docs/web-ui.md +82 -17
- data/lib/generators/pg_sql_triggers/templates/README +1 -1
- data/lib/pg_sql_triggers/registry/manager.rb +28 -13
- data/lib/pg_sql_triggers/registry.rb +41 -0
- data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
- data/lib/pg_sql_triggers/sql/executor.rb +200 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- metadata +18 -12
data/docs/api-reference.md
CHANGED
|
@@ -114,6 +114,102 @@ end
|
|
|
114
114
|
|
|
115
115
|
**Returns**: `true` if all triggers are valid
|
|
116
116
|
|
|
117
|
+
### `PgSqlTriggers::Registry.enable(trigger_name, actor:, confirmation: nil)`
|
|
118
|
+
|
|
119
|
+
Enables a trigger with permission and kill switch checks.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# Enable trigger
|
|
123
|
+
PgSqlTriggers::Registry.enable(
|
|
124
|
+
"users_email_validation",
|
|
125
|
+
actor: { type: "user", id: "admin@example.com" },
|
|
126
|
+
confirmation: "EXECUTE TRIGGER_ENABLE"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# With current user as actor
|
|
130
|
+
actor = { type: "user", id: current_user.email }
|
|
131
|
+
PgSqlTriggers::Registry.enable("billing_trigger", actor: actor, confirmation: "EXECUTE TRIGGER_ENABLE")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Parameters**:
|
|
135
|
+
- `trigger_name` (String): The name of the trigger to enable
|
|
136
|
+
- `actor` (Hash): Information about who is performing the operation (requires `:type` and `:id` keys)
|
|
137
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
138
|
+
|
|
139
|
+
**Raises**: `PgSqlTriggers::PermissionError`, `PgSqlTriggers::KillSwitchError`, `ArgumentError`
|
|
140
|
+
|
|
141
|
+
**Returns**: `true` on success
|
|
142
|
+
|
|
143
|
+
### `PgSqlTriggers::Registry.disable(trigger_name, actor:, confirmation: nil)`
|
|
144
|
+
|
|
145
|
+
Disables a trigger with permission and kill switch checks.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Disable trigger
|
|
149
|
+
PgSqlTriggers::Registry.disable(
|
|
150
|
+
"users_email_validation",
|
|
151
|
+
actor: { type: "user", id: "admin@example.com" },
|
|
152
|
+
confirmation: "EXECUTE TRIGGER_DISABLE"
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Parameters**:
|
|
157
|
+
- `trigger_name` (String): The name of the trigger to disable
|
|
158
|
+
- `actor` (Hash): Information about who is performing the operation
|
|
159
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
160
|
+
|
|
161
|
+
**Raises**: `PgSqlTriggers::PermissionError`, `PgSqlTriggers::KillSwitchError`, `ArgumentError`
|
|
162
|
+
|
|
163
|
+
**Returns**: `true` on success
|
|
164
|
+
|
|
165
|
+
### `PgSqlTriggers::Registry.drop(trigger_name, actor:, reason:, confirmation: nil)`
|
|
166
|
+
|
|
167
|
+
Drops a trigger from the database and removes it from the registry.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Drop trigger
|
|
171
|
+
PgSqlTriggers::Registry.drop(
|
|
172
|
+
"old_trigger",
|
|
173
|
+
actor: { type: "user", id: "admin@example.com" },
|
|
174
|
+
reason: "No longer needed in production",
|
|
175
|
+
confirmation: "EXECUTE TRIGGER_DROP"
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Parameters**:
|
|
180
|
+
- `trigger_name` (String): The name of the trigger to drop
|
|
181
|
+
- `actor` (Hash): Information about who is performing the operation (Admin permission required)
|
|
182
|
+
- `reason` (String): Required explanation for why the trigger is being dropped (logged for audit)
|
|
183
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
184
|
+
|
|
185
|
+
**Raises**: `PgSqlTriggers::PermissionError`, `PgSqlTriggers::KillSwitchError`, `ArgumentError`
|
|
186
|
+
|
|
187
|
+
**Returns**: `true` on success
|
|
188
|
+
|
|
189
|
+
### `PgSqlTriggers::Registry.re_execute(trigger_name, actor:, reason:, confirmation: nil)`
|
|
190
|
+
|
|
191
|
+
Re-executes a trigger by dropping and recreating it from the registry definition. Useful for fixing drifted triggers.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
# Re-execute drifted trigger
|
|
195
|
+
PgSqlTriggers::Registry.re_execute(
|
|
196
|
+
"drifted_trigger",
|
|
197
|
+
actor: { type: "user", id: "admin@example.com" },
|
|
198
|
+
reason: "Fix drift detected in production",
|
|
199
|
+
confirmation: "EXECUTE TRIGGER_RE_EXECUTE"
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Parameters**:
|
|
204
|
+
- `trigger_name` (String): The name of the trigger to re-execute
|
|
205
|
+
- `actor` (Hash): Information about who is performing the operation (Admin permission required)
|
|
206
|
+
- `reason` (String): Required explanation for why the trigger is being re-executed (logged for audit)
|
|
207
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
208
|
+
|
|
209
|
+
**Raises**: `PgSqlTriggers::PermissionError`, `PgSqlTriggers::KillSwitchError`, `ArgumentError`
|
|
210
|
+
|
|
211
|
+
**Returns**: `true` on success
|
|
212
|
+
|
|
117
213
|
## Migrator API
|
|
118
214
|
|
|
119
215
|
The Migrator API manages trigger migrations programmatically.
|
|
@@ -318,6 +414,124 @@ end
|
|
|
318
414
|
|
|
319
415
|
**Returns**: Result of the block
|
|
320
416
|
|
|
417
|
+
## SQL Capsule API
|
|
418
|
+
|
|
419
|
+
The SQL Capsule API provides emergency SQL execution capabilities with safety checks.
|
|
420
|
+
|
|
421
|
+
### `PgSqlTriggers::SQL::Capsule.new(name:, environment:, purpose:, sql:, created_at: nil)`
|
|
422
|
+
|
|
423
|
+
Creates a new SQL capsule for emergency operations.
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
427
|
+
name: "fix_user_permissions",
|
|
428
|
+
environment: "production",
|
|
429
|
+
purpose: "Emergency fix for user permission issue after deployment",
|
|
430
|
+
sql: "UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';"
|
|
431
|
+
)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Parameters**:
|
|
435
|
+
- `name` (String): Unique name for the capsule (alphanumeric, underscores, hyphens only)
|
|
436
|
+
- `environment` (String): Target environment (e.g., "production", "staging")
|
|
437
|
+
- `purpose` (String): Description of what the capsule does and why (required for audit trail)
|
|
438
|
+
- `sql` (String): The SQL statement(s) to execute
|
|
439
|
+
- `created_at` (Time, optional): Creation timestamp (defaults to current time)
|
|
440
|
+
|
|
441
|
+
**Raises**: `ArgumentError` if validation fails
|
|
442
|
+
|
|
443
|
+
### `capsule.checksum`
|
|
444
|
+
|
|
445
|
+
Returns the SHA256 checksum of the SQL content.
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
capsule = PgSqlTriggers::SQL::Capsule.new(name: "fix", ...)
|
|
449
|
+
puts capsule.checksum
|
|
450
|
+
# => "a3f5b8c9d2e..."
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Returns**: String (SHA256 hash)
|
|
454
|
+
|
|
455
|
+
### `capsule.to_h`
|
|
456
|
+
|
|
457
|
+
Converts the capsule to a hash for storage or serialization.
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
capsule_data = capsule.to_h
|
|
461
|
+
# => {
|
|
462
|
+
# name: "fix_user_permissions",
|
|
463
|
+
# environment: "production",
|
|
464
|
+
# purpose: "Emergency fix...",
|
|
465
|
+
# sql: "UPDATE users...",
|
|
466
|
+
# checksum: "a3f5b8c9d2e...",
|
|
467
|
+
# created_at: 2026-01-01 12:00:00 UTC
|
|
468
|
+
# }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Returns**: Hash
|
|
472
|
+
|
|
473
|
+
## SQL Executor API
|
|
474
|
+
|
|
475
|
+
The SQL Executor API handles safe execution of SQL capsules with comprehensive logging.
|
|
476
|
+
|
|
477
|
+
### `PgSqlTriggers::SQL::Executor.execute(capsule, actor:, confirmation: nil, dry_run: false)`
|
|
478
|
+
|
|
479
|
+
Executes a SQL capsule with safety checks and logging.
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
483
|
+
name: "emergency_fix",
|
|
484
|
+
environment: "production",
|
|
485
|
+
purpose: "Fix critical data corruption",
|
|
486
|
+
sql: "UPDATE orders SET status = 'completed' WHERE id IN (123, 456);"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Execute the capsule
|
|
490
|
+
result = PgSqlTriggers::SQL::Executor.execute(
|
|
491
|
+
capsule,
|
|
492
|
+
actor: { type: "user", id: "admin@example.com" },
|
|
493
|
+
confirmation: "EXECUTE SQL"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if result[:success]
|
|
497
|
+
puts "SQL executed successfully"
|
|
498
|
+
puts "Rows affected: #{result[:data][:rows_affected]}"
|
|
499
|
+
else
|
|
500
|
+
puts "Execution failed: #{result[:message]}"
|
|
501
|
+
end
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Parameters**:
|
|
505
|
+
- `capsule` (Capsule): The SQL capsule to execute
|
|
506
|
+
- `actor` (Hash): Information about who is executing (Admin permission required)
|
|
507
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
508
|
+
- `dry_run` (Boolean): If true, validates without executing (default: false)
|
|
509
|
+
|
|
510
|
+
**Returns**: Hash with `:success`, `:message`, and `:data` keys
|
|
511
|
+
|
|
512
|
+
**Raises**: Permission and kill switch errors are returned in the result hash
|
|
513
|
+
|
|
514
|
+
### `PgSqlTriggers::SQL::Executor.execute_capsule(capsule_name, actor:, confirmation: nil, dry_run: false)`
|
|
515
|
+
|
|
516
|
+
Executes a previously stored SQL capsule by name from the registry.
|
|
517
|
+
|
|
518
|
+
```ruby
|
|
519
|
+
# Execute a capsule stored in the registry
|
|
520
|
+
result = PgSqlTriggers::SQL::Executor.execute_capsule(
|
|
521
|
+
"emergency_fix",
|
|
522
|
+
actor: { type: "user", id: "admin@example.com" },
|
|
523
|
+
confirmation: "EXECUTE SQL"
|
|
524
|
+
)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Parameters**:
|
|
528
|
+
- `capsule_name` (String): Name of the capsule in the registry
|
|
529
|
+
- `actor` (Hash): Information about who is executing
|
|
530
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
531
|
+
- `dry_run` (Boolean): If true, validates without executing
|
|
532
|
+
|
|
533
|
+
**Returns**: Hash with execution results
|
|
534
|
+
|
|
321
535
|
## DSL API
|
|
322
536
|
|
|
323
537
|
The DSL API is used to define triggers in your application.
|
|
@@ -487,17 +701,51 @@ trigger.disable!(confirmation: "EXECUTE TRIGGER_DISABLE")
|
|
|
487
701
|
|
|
488
702
|
**Returns**: `true` on success
|
|
489
703
|
|
|
490
|
-
#### `drop!(confirmation: nil)`
|
|
704
|
+
#### `drop!(reason:, confirmation: nil, actor: nil)`
|
|
491
705
|
|
|
492
|
-
Drops the trigger from the database.
|
|
706
|
+
Drops the trigger from the database and removes it from the registry.
|
|
493
707
|
|
|
494
708
|
```ruby
|
|
495
|
-
trigger = PgSqlTriggers::TriggerRegistry.find_by(trigger_name: "
|
|
496
|
-
|
|
709
|
+
trigger = PgSqlTriggers::TriggerRegistry.find_by(trigger_name: "old_trigger")
|
|
710
|
+
|
|
711
|
+
# Drop with reason (required)
|
|
712
|
+
trigger.drop!(
|
|
713
|
+
reason: "No longer needed in production",
|
|
714
|
+
confirmation: "EXECUTE TRIGGER_DROP",
|
|
715
|
+
actor: { type: "user", id: "admin@example.com" }
|
|
716
|
+
)
|
|
497
717
|
```
|
|
498
718
|
|
|
499
719
|
**Parameters**:
|
|
720
|
+
- `reason` (String, required): Explanation for why the trigger is being dropped (logged for audit trail)
|
|
500
721
|
- `confirmation` (String, optional): Kill switch confirmation text
|
|
722
|
+
- `actor` (Hash, optional): Information about who is performing the operation
|
|
723
|
+
|
|
724
|
+
**Raises**: `ArgumentError` if reason is blank, `PgSqlTriggers::KillSwitchError`
|
|
725
|
+
|
|
726
|
+
**Returns**: `true` on success
|
|
727
|
+
|
|
728
|
+
#### `re_execute!(reason:, confirmation: nil, actor: nil)`
|
|
729
|
+
|
|
730
|
+
Re-executes the trigger by dropping and recreating it from the registry definition. Useful for fixing drifted triggers.
|
|
731
|
+
|
|
732
|
+
```ruby
|
|
733
|
+
trigger = PgSqlTriggers::TriggerRegistry.find_by(trigger_name: "drifted_trigger")
|
|
734
|
+
|
|
735
|
+
# Re-execute to fix drift
|
|
736
|
+
trigger.re_execute!(
|
|
737
|
+
reason: "Fix drift detected after manual database changes",
|
|
738
|
+
confirmation: "EXECUTE TRIGGER_RE_EXECUTE",
|
|
739
|
+
actor: { type: "user", id: "admin@example.com" }
|
|
740
|
+
)
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**Parameters**:
|
|
744
|
+
- `reason` (String, required): Explanation for why the trigger is being re-executed (logged for audit trail)
|
|
745
|
+
- `confirmation` (String, optional): Kill switch confirmation text
|
|
746
|
+
- `actor` (Hash, optional): Information about who is performing the operation
|
|
747
|
+
|
|
748
|
+
**Raises**: `ArgumentError` if reason is blank or function_body is missing, `PgSqlTriggers::KillSwitchError`, `StandardError`
|
|
501
749
|
|
|
502
750
|
**Returns**: `true` on success
|
|
503
751
|
|
data/docs/getting-started.md
CHANGED
|
@@ -132,4 +132,4 @@ You should see your trigger listed in the dashboard.
|
|
|
132
132
|
|
|
133
133
|
## Examples Repository
|
|
134
134
|
|
|
135
|
-
For working examples and a complete demonstration of PgSqlTriggers in action, check out the [example repository](https://github.com/
|
|
135
|
+
For working examples and a complete demonstration of PgSqlTriggers in action, check out the [example repository](https://github.com/samaswin/pg_triggers_example).
|
data/docs/kill-switch.md
CHANGED
|
@@ -422,21 +422,21 @@ All kill switch events are logged with comprehensive information.
|
|
|
422
422
|
Operation was prevented:
|
|
423
423
|
|
|
424
424
|
```
|
|
425
|
-
[KILL_SWITCH] BLOCKED: operation=trigger_migrate environment=production actor=CLI:
|
|
425
|
+
[KILL_SWITCH] BLOCKED: operation=trigger_migrate environment=production actor=CLI:samaswin reason=no_override
|
|
426
426
|
```
|
|
427
427
|
|
|
428
428
|
#### OVERRIDDEN
|
|
429
429
|
Operation allowed with valid override:
|
|
430
430
|
|
|
431
431
|
```
|
|
432
|
-
[KILL_SWITCH] OVERRIDDEN: operation=trigger_migrate environment=production actor=CLI:
|
|
432
|
+
[KILL_SWITCH] OVERRIDDEN: operation=trigger_migrate environment=production actor=CLI:samaswin source=env_with_confirmation confirmation=EXECUTE TRIGGER_MIGRATE
|
|
433
433
|
```
|
|
434
434
|
|
|
435
435
|
#### ALLOWED
|
|
436
436
|
Operation allowed (not in protected environment):
|
|
437
437
|
|
|
438
438
|
```
|
|
439
|
-
[KILL_SWITCH] ALLOWED: operation=trigger_migrate environment=development actor=CLI:
|
|
439
|
+
[KILL_SWITCH] ALLOWED: operation=trigger_migrate environment=development actor=CLI:samaswin reason=not_protected_environment
|
|
440
440
|
```
|
|
441
441
|
|
|
442
442
|
### Audit Information
|
data/docs/web-ui.md
CHANGED
|
@@ -93,9 +93,56 @@ Available actions depend on trigger state and your permissions:
|
|
|
93
93
|
- **Enable/Disable**: Toggle trigger activation
|
|
94
94
|
- **Apply**: Apply generated trigger definition
|
|
95
95
|
- **Drop**: Remove trigger from database (Admin only)
|
|
96
|
+
- **Re-Execute**: Drop and recreate trigger from registry definition (Admin only)
|
|
96
97
|
- **View SQL**: See the trigger's SQL definition
|
|
97
98
|
- **View Diff**: Compare DSL vs database state
|
|
98
99
|
|
|
100
|
+
### Drop Trigger
|
|
101
|
+
|
|
102
|
+
The drop action permanently removes a trigger from the database and registry.
|
|
103
|
+
|
|
104
|
+
1. Navigate to the trigger detail page
|
|
105
|
+
2. Click the "Drop Trigger" button
|
|
106
|
+
3. A modal will appear requiring:
|
|
107
|
+
- **Reason**: Explanation for dropping the trigger (required for audit trail)
|
|
108
|
+
- **Confirmation**: In protected environments, type the exact confirmation text shown
|
|
109
|
+
4. Review the warning message
|
|
110
|
+
5. Click "Drop Trigger" to confirm
|
|
111
|
+
|
|
112
|
+
**Important Notes**:
|
|
113
|
+
- This action is **irreversible** - the trigger will be permanently removed
|
|
114
|
+
- Requires **Admin** permission level
|
|
115
|
+
- Protected by kill switch in production environments
|
|
116
|
+
- Reason is logged for compliance and audit purposes
|
|
117
|
+
- The trigger is removed from both the database and the registry
|
|
118
|
+
|
|
119
|
+
### Re-Execute Trigger
|
|
120
|
+
|
|
121
|
+
The re-execute action fixes drifted triggers by dropping and recreating them from the registry definition.
|
|
122
|
+
|
|
123
|
+
1. Navigate to the trigger detail page
|
|
124
|
+
2. If the trigger is drifted, you'll see a drift warning
|
|
125
|
+
3. Click the "Re-Execute" button
|
|
126
|
+
4. A modal will appear showing:
|
|
127
|
+
- **Drift Comparison**: Differences between database state and registry definition
|
|
128
|
+
- **Reason Field**: Explanation for re-executing (required for audit trail)
|
|
129
|
+
- **Confirmation**: In protected environments, type the exact confirmation text shown
|
|
130
|
+
5. Review the drift differences to understand what will change
|
|
131
|
+
6. Click "Re-Execute Trigger" to confirm
|
|
132
|
+
|
|
133
|
+
**What Happens**:
|
|
134
|
+
1. Current trigger is dropped from the database
|
|
135
|
+
2. New trigger is created using the registry definition (function_body, events, timing, condition)
|
|
136
|
+
3. Registry is updated with execution timestamp
|
|
137
|
+
4. Operation is logged with reason and actor information
|
|
138
|
+
|
|
139
|
+
**Important Notes**:
|
|
140
|
+
- Requires **Admin** permission level
|
|
141
|
+
- Protected by kill switch in production environments
|
|
142
|
+
- Reason is logged for compliance and audit purposes
|
|
143
|
+
- Executes in a database transaction (rolls back on error)
|
|
144
|
+
- Best used to fix triggers that have drifted from their DSL definition
|
|
145
|
+
|
|
99
146
|
## Migration Management
|
|
100
147
|
|
|
101
148
|
The Web UI provides full migration management capabilities.
|
|
@@ -159,33 +206,51 @@ After each migration action:
|
|
|
159
206
|
|
|
160
207
|
## SQL Capsules
|
|
161
208
|
|
|
162
|
-
SQL Capsules provide emergency escape hatches for executing SQL directly.
|
|
209
|
+
SQL Capsules provide emergency escape hatches for executing SQL directly with comprehensive safety checks and audit logging.
|
|
163
210
|
|
|
164
211
|
### When to Use SQL Capsules
|
|
165
212
|
|
|
166
213
|
Use SQL Capsules for:
|
|
167
214
|
- Emergency fixes in production
|
|
168
|
-
-
|
|
215
|
+
- Critical data corrections
|
|
169
216
|
- Testing SQL functions
|
|
170
217
|
- Debugging trigger behavior
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
- One-off database operations
|
|
219
|
+
|
|
220
|
+
### Creating and Executing SQL Capsules
|
|
221
|
+
|
|
222
|
+
1. Navigate to "SQL Capsules" → "New SQL Capsule"
|
|
223
|
+
2. Fill in the capsule form:
|
|
224
|
+
- **Name**: Unique identifier (alphanumeric, underscores, hyphens only)
|
|
225
|
+
- **Environment**: Target environment (e.g., production, staging)
|
|
226
|
+
- **Purpose**: Detailed explanation of what the SQL does and why (required for audit trail)
|
|
227
|
+
- **SQL**: The SQL statement(s) to execute
|
|
228
|
+
3. Click "Create and Execute" or "Save for Later"
|
|
229
|
+
4. Review the capsule details on the confirmation page
|
|
230
|
+
5. In protected environments, enter confirmation text when prompted
|
|
231
|
+
6. Click "Execute" to run the SQL
|
|
232
|
+
7. Review the execution results
|
|
233
|
+
|
|
234
|
+
### Viewing Capsule History
|
|
235
|
+
|
|
236
|
+
1. Navigate to "SQL Capsules" → "History"
|
|
237
|
+
2. View list of previously executed capsules with:
|
|
238
|
+
- Name and purpose
|
|
239
|
+
- Environment and timestamp
|
|
240
|
+
- SQL checksum
|
|
241
|
+
- Execution status
|
|
242
|
+
3. Click on a capsule to view details
|
|
243
|
+
4. Re-execute historical capsules if needed
|
|
182
244
|
|
|
183
245
|
### Safety Features
|
|
184
246
|
|
|
185
|
-
- **
|
|
186
|
-
- **
|
|
187
|
-
- **
|
|
188
|
-
- **
|
|
247
|
+
- **Admin Permission Required**: Only Admin users can create and execute SQL capsules
|
|
248
|
+
- **Production Protection**: Requires typed confirmation in protected environments
|
|
249
|
+
- **Kill Switch Integration**: All executions are protected by kill switch
|
|
250
|
+
- **Comprehensive Logging**: All operations logged with actor, timestamp, and checksum
|
|
251
|
+
- **Transactional Execution**: SQL runs in a transaction and rolls back on error
|
|
252
|
+
- **Registry Storage**: All capsules are stored in the registry with checksums
|
|
253
|
+
- **Purpose Tracking**: Required purpose field ensures all executions are documented
|
|
189
254
|
|
|
190
255
|
### Example SQL Capsules
|
|
191
256
|
|
|
@@ -31,6 +31,6 @@ Next steps:
|
|
|
31
31
|
|
|
32
32
|
5. Visit http://localhost:3000/pg_sql_triggers to access the UI
|
|
33
33
|
|
|
34
|
-
For more information, see: https://github.com/
|
|
34
|
+
For more information, see: https://github.com/samaswin/pg_sql_triggers
|
|
35
35
|
|
|
36
36
|
===============================================================================
|
|
@@ -33,8 +33,13 @@ module PgSqlTriggers
|
|
|
33
33
|
trigger_name = definition.name
|
|
34
34
|
|
|
35
35
|
# Use cached lookup if available to avoid N+1 queries during trigger file loading
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Explicitly check cache first to avoid query in some Ruby versions where ||= may evaluate RHS
|
|
37
|
+
existing = if _registry_cache.key?(trigger_name)
|
|
38
|
+
_registry_cache[trigger_name]
|
|
39
|
+
else
|
|
40
|
+
_registry_cache[trigger_name] =
|
|
41
|
+
PgSqlTriggers::TriggerRegistry.find_by(trigger_name: trigger_name)
|
|
42
|
+
end
|
|
38
43
|
|
|
39
44
|
# Calculate checksum using field-concatenation (consistent with TriggerRegistry model)
|
|
40
45
|
checksum = calculate_checksum(definition)
|
|
@@ -51,17 +56,27 @@ module PgSqlTriggers
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
if existing
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
# Check if attributes have actually changed to avoid unnecessary queries
|
|
60
|
+
attributes_changed = attributes.any? do |key, value|
|
|
61
|
+
existing.send(key) != value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if attributes_changed
|
|
65
|
+
begin
|
|
66
|
+
existing.update!(attributes)
|
|
67
|
+
# Update cache with the modified record (reload to get fresh data)
|
|
68
|
+
reloaded = existing.reload
|
|
69
|
+
_registry_cache[trigger_name] = reloaded
|
|
70
|
+
reloaded
|
|
71
|
+
rescue ActiveRecord::RecordNotFound
|
|
72
|
+
# Cached record was deleted, create a new one
|
|
73
|
+
new_record = PgSqlTriggers::TriggerRegistry.create!(attributes)
|
|
74
|
+
_registry_cache[trigger_name] = new_record
|
|
75
|
+
new_record
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
# No changes, return cached record without any queries
|
|
79
|
+
existing
|
|
65
80
|
end
|
|
66
81
|
else
|
|
67
82
|
new_record = PgSqlTriggers::TriggerRegistry.create!(attributes)
|
|
@@ -32,5 +32,46 @@ module PgSqlTriggers
|
|
|
32
32
|
def self.validate!
|
|
33
33
|
Validator.validate!
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# Console APIs for trigger operations
|
|
37
|
+
# These methods provide a convenient interface for managing triggers from the Rails console
|
|
38
|
+
|
|
39
|
+
def self.enable(trigger_name, actor:, confirmation: nil)
|
|
40
|
+
check_permission!(actor, :enable_trigger)
|
|
41
|
+
trigger = find_trigger!(trigger_name)
|
|
42
|
+
trigger.enable!(confirmation: confirmation)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.disable(trigger_name, actor:, confirmation: nil)
|
|
46
|
+
check_permission!(actor, :disable_trigger)
|
|
47
|
+
trigger = find_trigger!(trigger_name)
|
|
48
|
+
trigger.disable!(confirmation: confirmation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.drop(trigger_name, actor:, reason:, confirmation: nil)
|
|
52
|
+
check_permission!(actor, :drop_trigger)
|
|
53
|
+
trigger = find_trigger!(trigger_name)
|
|
54
|
+
trigger.drop!(reason: reason, confirmation: confirmation, actor: actor)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.re_execute(trigger_name, actor:, reason:, confirmation: nil)
|
|
58
|
+
check_permission!(actor, :drop_trigger) # Re-execute requires same permission as drop
|
|
59
|
+
trigger = find_trigger!(trigger_name)
|
|
60
|
+
trigger.re_execute!(reason: reason, confirmation: confirmation, actor: actor)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Private helper methods
|
|
64
|
+
|
|
65
|
+
def self.find_trigger!(trigger_name)
|
|
66
|
+
PgSqlTriggers::TriggerRegistry.find_by!(trigger_name: trigger_name)
|
|
67
|
+
rescue ActiveRecord::RecordNotFound
|
|
68
|
+
raise ArgumentError, "Trigger '#{trigger_name}' not found in registry"
|
|
69
|
+
end
|
|
70
|
+
private_class_method :find_trigger!
|
|
71
|
+
|
|
72
|
+
def self.check_permission!(actor, action)
|
|
73
|
+
PgSqlTriggers::Permissions.check!(actor, action)
|
|
74
|
+
end
|
|
75
|
+
private_class_method :check_permission!
|
|
35
76
|
end
|
|
36
77
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module PgSqlTriggers
|
|
6
|
+
module SQL
|
|
7
|
+
# Capsule represents a named SQL capsule with environment declaration and purpose
|
|
8
|
+
# Used for emergency operations and manual SQL execution
|
|
9
|
+
#
|
|
10
|
+
# @example Creating a SQL capsule
|
|
11
|
+
# capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
12
|
+
# name: "fix_user_permissions",
|
|
13
|
+
# environment: "production",
|
|
14
|
+
# purpose: "Emergency fix for user permission issue",
|
|
15
|
+
# sql: "UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';"
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class Capsule
|
|
19
|
+
attr_reader :name, :environment, :purpose, :sql, :created_at
|
|
20
|
+
|
|
21
|
+
# @param name [String] The name of the SQL capsule
|
|
22
|
+
# @param environment [String] The environment this capsule is intended for
|
|
23
|
+
# @param purpose [String] Description of what this capsule does and why
|
|
24
|
+
# @param sql [String] The SQL to execute
|
|
25
|
+
# @param created_at [Time, nil] The timestamp when the capsule was created (defaults to now)
|
|
26
|
+
def initialize(name:, environment:, purpose:, sql:, created_at: nil)
|
|
27
|
+
@name = name
|
|
28
|
+
@environment = environment
|
|
29
|
+
@purpose = purpose
|
|
30
|
+
@sql = sql
|
|
31
|
+
@created_at = created_at || Time.current
|
|
32
|
+
validate!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Calculates the checksum of the SQL content
|
|
36
|
+
# @return [String] The SHA256 checksum of the SQL
|
|
37
|
+
def checksum
|
|
38
|
+
@checksum ||= Digest::SHA256.hexdigest(sql.to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts the capsule to a hash suitable for storage
|
|
42
|
+
# @return [Hash] The capsule data
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
name: name,
|
|
46
|
+
environment: environment,
|
|
47
|
+
purpose: purpose,
|
|
48
|
+
sql: sql,
|
|
49
|
+
checksum: checksum,
|
|
50
|
+
created_at: created_at
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the registry trigger name for this capsule
|
|
55
|
+
# SQL capsules are stored in the registry with a special naming pattern
|
|
56
|
+
# @return [String] The trigger name for registry storage
|
|
57
|
+
def registry_trigger_name
|
|
58
|
+
"sql_capsule_#{name}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate!
|
|
64
|
+
errors = []
|
|
65
|
+
errors << "Name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
|
66
|
+
errors << "Environment cannot be blank" if environment.nil? || environment.to_s.strip.empty?
|
|
67
|
+
errors << "Purpose cannot be blank" if purpose.nil? || purpose.to_s.strip.empty?
|
|
68
|
+
errors << "SQL cannot be blank" if sql.nil? || sql.to_s.strip.empty?
|
|
69
|
+
|
|
70
|
+
# Validate name format (alphanumeric, underscores, hyphens only)
|
|
71
|
+
unless name.to_s.match?(/\A[a-z0-9_-]+\z/i)
|
|
72
|
+
errors << "Name must contain only letters, numbers, underscores, and hyphens"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise ArgumentError, "Invalid capsule: #{errors.join(', ')}" if errors.any?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|