pg_sql_triggers 1.3.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 +253 -1
- data/GEM_ANALYSIS.md +368 -0
- data/README.md +20 -23
- data/app/models/pg_sql_triggers/trigger_registry.rb +42 -6
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +1 -4
- data/app/views/pg_sql_triggers/tables/index.html.erb +1 -4
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- data/config/routes.rb +0 -14
- data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/api-reference.md +44 -153
- data/docs/configuration.md +24 -3
- data/docs/getting-started.md +17 -16
- data/docs/usage-guide.md +38 -67
- data/docs/web-ui.md +3 -103
- 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/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/registry/manager.rb +36 -11
- data/lib/pg_sql_triggers/registry/validator.rb +62 -5
- data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +4 -1
- data/pg_sql_triggers.gemspec +53 -0
- metadata +7 -13
- 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/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 -339
- 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/README.md
CHANGED
|
@@ -50,16 +50,18 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
|
|
|
50
50
|
table :users
|
|
51
51
|
on :insert, :update
|
|
52
52
|
function :validate_user_email
|
|
53
|
-
version 1
|
|
54
|
-
enabled
|
|
55
|
-
|
|
53
|
+
self.version = 1
|
|
54
|
+
self.enabled = true
|
|
55
|
+
timing :before
|
|
56
56
|
end
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
###
|
|
59
|
+
### Generate and Run Migration
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
|
|
62
|
+
# Generate a DSL stub + migration in one command
|
|
63
|
+
rails generate pg_sql_triggers:trigger users_email_validation users insert update --timing before --function validate_user_email
|
|
64
|
+
|
|
63
65
|
rake trigger:migrate
|
|
64
66
|
```
|
|
65
67
|
|
|
@@ -83,19 +85,26 @@ Comprehensive documentation is available in the [docs](docs/) directory:
|
|
|
83
85
|
## Key Features
|
|
84
86
|
|
|
85
87
|
### Trigger DSL
|
|
86
|
-
Define triggers using a Rails-native Ruby DSL with versioning and
|
|
88
|
+
Define triggers using a Rails-native Ruby DSL with versioning, row/statement-level granularity, and timing control.
|
|
89
|
+
|
|
90
|
+
### CLI Generator
|
|
91
|
+
Scaffold a DSL stub and migration in one command:
|
|
92
|
+
```bash
|
|
93
|
+
rails generate pg_sql_triggers:trigger TRIGGER_NAME TABLE_NAME [EVENTS...] [--timing before|after] [--function fn_name]
|
|
94
|
+
```
|
|
95
|
+
Files land in `app/triggers/` and `db/triggers/` for code review like any other source change.
|
|
87
96
|
|
|
88
97
|
### Migration System
|
|
89
98
|
Manage trigger functions and definitions with a migration system similar to Rails schema migrations.
|
|
90
99
|
|
|
91
100
|
### Drift Detection
|
|
92
|
-
Automatically detect when database triggers drift from your DSL definitions.
|
|
101
|
+
Automatically detect when database triggers drift from your DSL definitions. N+1-free bulk detection across all triggers.
|
|
93
102
|
|
|
94
103
|
### Production Kill Switch
|
|
95
104
|
Multi-layered safety mechanism preventing accidental destructive operations in production environments.
|
|
96
105
|
|
|
97
106
|
### Web Dashboard
|
|
98
|
-
Visual interface for managing triggers
|
|
107
|
+
Visual interface for managing triggers and running migrations. Includes:
|
|
99
108
|
- **Quick Actions**: Enable/disable, drop, and re-execute triggers from dashboard
|
|
100
109
|
- **Last Applied Tracking**: See when triggers were last applied with human-readable timestamps
|
|
101
110
|
- **Breadcrumb Navigation**: Easy navigation between dashboard, tables, and triggers
|
|
@@ -104,18 +113,15 @@ Visual interface for managing triggers, running migrations, and executing SQL ca
|
|
|
104
113
|
### Audit Logging
|
|
105
114
|
Comprehensive audit trail for all trigger operations:
|
|
106
115
|
- Track who performed each operation (actor tracking)
|
|
107
|
-
- Before and after state capture
|
|
116
|
+
- Before and after state capture (including function body)
|
|
108
117
|
- Success/failure logging with error messages
|
|
109
118
|
- Reason tracking for drop and re-execute operations
|
|
110
119
|
|
|
111
|
-
### SQL Capsules
|
|
112
|
-
Emergency SQL execution feature for critical operations with Admin permission requirements, kill switch protection, and comprehensive logging.
|
|
113
|
-
|
|
114
120
|
### Drop & Re-Execute Flow
|
|
115
121
|
Operational controls for trigger lifecycle management with drop and re-execute capabilities, drift comparison, and required reason logging.
|
|
116
122
|
|
|
117
123
|
### Permissions
|
|
118
|
-
Three-tier permission system (Viewer, Operator, Admin) with customizable authorization.
|
|
124
|
+
Three-tier permission system (Viewer, Operator, Admin) with customizable authorization. A startup warning is emitted in production when no `permission_checker` is configured.
|
|
119
125
|
|
|
120
126
|
## Console API
|
|
121
127
|
|
|
@@ -142,15 +148,6 @@ PgSqlTriggers::Registry.disable("users_email_validation", actor: current_user, c
|
|
|
142
148
|
# Drop and re-execute triggers
|
|
143
149
|
PgSqlTriggers::Registry.drop("old_trigger", actor: current_user, reason: "No longer needed", confirmation: "EXECUTE TRIGGER_DROP")
|
|
144
150
|
PgSqlTriggers::Registry.re_execute("drifted_trigger", actor: current_user, reason: "Fix drift", confirmation: "EXECUTE TRIGGER_RE_EXECUTE")
|
|
145
|
-
|
|
146
|
-
# Execute SQL capsules
|
|
147
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
148
|
-
name: "emergency_fix",
|
|
149
|
-
environment: "production",
|
|
150
|
-
purpose: "Fix critical data issue",
|
|
151
|
-
sql: "UPDATE users SET status = 'active' WHERE id = 123"
|
|
152
|
-
)
|
|
153
|
-
PgSqlTriggers::SQL::Executor.execute(capsule, actor: current_user, confirmation: "EXECUTE SQL")
|
|
154
151
|
```
|
|
155
152
|
|
|
156
153
|
See the [API Reference](docs/api-reference.md) for complete documentation of all console APIs.
|
|
@@ -164,7 +161,7 @@ For working examples and complete demonstrations, check out the [example reposit
|
|
|
164
161
|
- **Rails-native**: Works seamlessly with Rails conventions
|
|
165
162
|
- **Explicit over magic**: No automatic execution
|
|
166
163
|
- **Safe by default**: Requires explicit confirmation for destructive actions
|
|
167
|
-
- **
|
|
164
|
+
- **Code review first**: Generator produces files into working tree; no server-side file writes
|
|
168
165
|
|
|
169
166
|
## Development
|
|
170
167
|
|
|
@@ -247,7 +247,6 @@ module PgSqlTriggers
|
|
|
247
247
|
# Execute DROP TRIGGER in transaction
|
|
248
248
|
ActiveRecord::Base.transaction do
|
|
249
249
|
drop_trigger_from_database
|
|
250
|
-
trigger_name
|
|
251
250
|
destroy!
|
|
252
251
|
log_drop_success
|
|
253
252
|
log_audit_success(:trigger_drop, actor, reason: reason, confirmation_text: confirmation,
|
|
@@ -286,7 +285,6 @@ module PgSqlTriggers
|
|
|
286
285
|
|
|
287
286
|
# Validate reason is provided
|
|
288
287
|
raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
|
|
289
|
-
raise StandardError, "Cannot re-execute: missing function_body" if function_body.blank?
|
|
290
288
|
|
|
291
289
|
log_re_execute_attempt(reason)
|
|
292
290
|
|
|
@@ -322,7 +320,8 @@ module PgSqlTriggers
|
|
|
322
320
|
version,
|
|
323
321
|
function_body || "",
|
|
324
322
|
condition || "",
|
|
325
|
-
timing || "before"
|
|
323
|
+
timing || "before",
|
|
324
|
+
for_each || "row"
|
|
326
325
|
].join)
|
|
327
326
|
end
|
|
328
327
|
|
|
@@ -394,7 +393,10 @@ module PgSqlTriggers
|
|
|
394
393
|
end
|
|
395
394
|
|
|
396
395
|
def recreate_trigger
|
|
397
|
-
|
|
396
|
+
sql = function_body.presence || build_trigger_sql_from_definition
|
|
397
|
+
raise StandardError, "Cannot re-execute: missing function_body" if sql.blank?
|
|
398
|
+
|
|
399
|
+
ActiveRecord::Base.connection.execute(sql)
|
|
398
400
|
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
399
401
|
Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-created trigger"
|
|
400
402
|
end
|
|
@@ -405,8 +407,41 @@ module PgSqlTriggers
|
|
|
405
407
|
raise
|
|
406
408
|
end
|
|
407
409
|
|
|
410
|
+
# Build a CREATE TRIGGER SQL statement from the stored DSL definition JSON.
|
|
411
|
+
# Used by re_execute! when function_body is absent (the normal case for DSL triggers).
|
|
412
|
+
def build_trigger_sql_from_definition
|
|
413
|
+
return nil if definition.blank?
|
|
414
|
+
|
|
415
|
+
defn = JSON.parse(definition)
|
|
416
|
+
fn_name = defn["function_name"]
|
|
417
|
+
return nil if fn_name.blank?
|
|
418
|
+
|
|
419
|
+
t_name = table_name
|
|
420
|
+
timing_kw = (defn["timing"] || timing || "BEFORE").upcase
|
|
421
|
+
events = Array(defn["events"]).map { |e| e.to_s.upcase }.join(" OR ")
|
|
422
|
+
events = "INSERT" if events.blank?
|
|
423
|
+
cond = defn["condition"] || condition
|
|
424
|
+
for_each_kw = (defn["for_each"] || for_each || "row").upcase
|
|
425
|
+
|
|
426
|
+
sql = "CREATE TRIGGER #{quote_identifier(trigger_name)} "
|
|
427
|
+
sql += "#{timing_kw} #{events} ON #{quote_identifier(t_name)} "
|
|
428
|
+
sql += "FOR EACH #{for_each_kw} "
|
|
429
|
+
sql += "WHEN (#{cond}) " if cond.present?
|
|
430
|
+
sql += "EXECUTE FUNCTION #{fn_name}();"
|
|
431
|
+
sql
|
|
432
|
+
rescue JSON::ParserError
|
|
433
|
+
nil
|
|
434
|
+
end
|
|
435
|
+
|
|
408
436
|
def update_registry_after_re_execute
|
|
409
|
-
update!(
|
|
437
|
+
update!(last_executed_at: Time.current)
|
|
438
|
+
if !enabled && ActiveRecord::Base.connection.table_exists?(table_name)
|
|
439
|
+
quoted_table = quote_identifier(table_name)
|
|
440
|
+
quoted_trigger = quote_identifier(trigger_name)
|
|
441
|
+
ActiveRecord::Base.connection.execute(
|
|
442
|
+
"ALTER TABLE #{quoted_table} DISABLE TRIGGER #{quoted_trigger};"
|
|
443
|
+
)
|
|
444
|
+
end
|
|
410
445
|
Rails.logger.info "[TRIGGER_RE_EXECUTE] Updated registry" if defined?(Rails.logger)
|
|
411
446
|
end
|
|
412
447
|
|
|
@@ -419,7 +454,8 @@ module PgSqlTriggers
|
|
|
419
454
|
table_name: table_name,
|
|
420
455
|
source: source,
|
|
421
456
|
environment: environment,
|
|
422
|
-
installed_at: installed_at&.iso8601
|
|
457
|
+
installed_at: installed_at&.iso8601,
|
|
458
|
+
function_body: function_body
|
|
423
459
|
}
|
|
424
460
|
end
|
|
425
461
|
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
<div>
|
|
19
19
|
<%= link_to "Dashboard", root_path, style: "color: white; margin-right: 1rem;" %>
|
|
20
20
|
<%= link_to "Tables", tables_path, style: "color: white; margin-right: 1rem;" %>
|
|
21
|
-
<%= link_to "Generator", new_generator_path, style: "color: white; margin-right: 1rem;" %>
|
|
22
21
|
<%= link_to "Audit Log", audit_logs_path, style: "color: white;" %>
|
|
23
22
|
</div>
|
|
24
23
|
</div>
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
<h2 style="margin: 0;">Trigger Dashboard</h2>
|
|
3
3
|
<div style="display: flex; gap: 1rem;">
|
|
4
4
|
<%= link_to "View Tables", tables_path, class: "btn btn-primary", style: "font-size: 1rem; padding: 0.75rem 1.5rem; text-decoration: none;" %>
|
|
5
|
-
<%= link_to "Generate New Trigger", new_generator_path,
|
|
6
|
-
class: "btn btn-success",
|
|
7
|
-
style: "font-size: 1rem; padding: 0.75rem 1.5rem; text-decoration: none;" %>
|
|
8
5
|
</div>
|
|
9
6
|
</div>
|
|
10
7
|
|
|
@@ -129,7 +126,7 @@
|
|
|
129
126
|
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1.5rem; border-radius: 4px;">
|
|
130
127
|
<h3 style="margin-top: 0;">No triggers yet</h3>
|
|
131
128
|
<p style="margin-bottom: 1rem;">Get started by generating your first trigger using the form-based wizard.</p>
|
|
132
|
-
<%= link_to "
|
|
129
|
+
<%= link_to "View Tables", tables_path, class: "btn btn-primary" %>
|
|
133
130
|
</div>
|
|
134
131
|
<% end %>
|
|
135
132
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
2
|
<h2 style="margin: 0;">Database Tables & Triggers</h2>
|
|
3
|
-
<%= link_to "Generate New Trigger", new_generator_path, class: "btn btn-success" %>
|
|
4
3
|
</div>
|
|
5
4
|
|
|
6
5
|
<!-- Statistics -->
|
|
@@ -113,7 +112,6 @@
|
|
|
113
112
|
</td>
|
|
114
113
|
<td>
|
|
115
114
|
<%= link_to "View Details", table_path(table[:table_name]), class: "btn btn-primary", style: "padding: 0.25rem 0.5rem; font-size: 0.875rem;" %>
|
|
116
|
-
<%= link_to "Create Trigger", new_generator_path(pg_sql_triggers_generator_form: { table_name: table[:table_name] }), class: "btn btn-success", style: "padding: 0.25rem 0.5rem; font-size: 0.875rem; margin-top: 0.25rem; display: block;" %>
|
|
117
115
|
</td>
|
|
118
116
|
</tr>
|
|
119
117
|
<% end %>
|
|
@@ -170,7 +168,6 @@
|
|
|
170
168
|
<% end %>
|
|
171
169
|
</p>
|
|
172
170
|
<% if @filter == 'with_triggers' || @filter == 'all' %>
|
|
173
|
-
|
|
174
|
-
<% end %>
|
|
171
|
+
<% end %>
|
|
175
172
|
</div>
|
|
176
173
|
<% end %>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<div style="margin-bottom: 2rem;">
|
|
2
2
|
<h2>Table: <%= @table_info[:table_name] %></h2>
|
|
3
3
|
<%= link_to "← Back to Tables", tables_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; margin-right: 1rem;" %>
|
|
4
|
-
<%= link_to "Create Trigger for this Table", new_generator_path(pg_sql_triggers_generator_form: { table_name: @table_info[:table_name] }), class: "btn btn-success" %>
|
|
5
4
|
</div>
|
|
6
5
|
|
|
7
6
|
<!-- Table Information -->
|
|
@@ -138,7 +137,6 @@
|
|
|
138
137
|
<% else %>
|
|
139
138
|
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1rem; border-radius: 4px;">
|
|
140
139
|
<p style="margin: 0;">No registered triggers for this table.</p>
|
|
141
|
-
<%= link_to "Create a trigger", new_generator_path(pg_sql_triggers_generator_form: { table_name: @table_info[:table_name] }), class: "btn btn-primary", style: "margin-top: 0.5rem;" %>
|
|
142
140
|
</div>
|
|
143
141
|
<% end %>
|
|
144
142
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -7,20 +7,6 @@ begin
|
|
|
7
7
|
|
|
8
8
|
resources :tables, only: %i[index show]
|
|
9
9
|
|
|
10
|
-
resources :generator, only: %i[new create] do
|
|
11
|
-
collection do
|
|
12
|
-
post :preview
|
|
13
|
-
post :validate_table
|
|
14
|
-
get :tables
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
resources :sql_capsules, only: %i[new create show] do
|
|
19
|
-
member do
|
|
20
|
-
post :execute
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
10
|
resources :migrations, only: [] do
|
|
25
11
|
collection do
|
|
26
12
|
post :up
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddForEachToPgSqlTriggersRegistry < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :pg_sql_triggers_registry, :for_each, :string, default: "row", null: false
|
|
6
|
+
add_index :pg_sql_triggers_registry, :for_each
|
|
7
|
+
end
|
|
8
|
+
end
|
data/docs/api-reference.md
CHANGED
|
@@ -11,6 +11,8 @@ Complete reference for using PgSqlTriggers programmatically from the Rails conso
|
|
|
11
11
|
- [TriggerRegistry Model](#triggerregistry-model)
|
|
12
12
|
- [Audit Log API](#audit-log-api)
|
|
13
13
|
|
|
14
|
+
> **Removed in 1.4.0**: `SQL::Capsule` and `SQL::Executor` have been removed. See [CHANGELOG](../CHANGELOG.md) for details.
|
|
15
|
+
|
|
14
16
|
## Registry API
|
|
15
17
|
|
|
16
18
|
The Registry API provides methods for inspecting and managing triggers.
|
|
@@ -485,124 +487,6 @@ end
|
|
|
485
487
|
|
|
486
488
|
**Returns**: Result of the block
|
|
487
489
|
|
|
488
|
-
## SQL Capsule API
|
|
489
|
-
|
|
490
|
-
The SQL Capsule API provides emergency SQL execution capabilities with safety checks.
|
|
491
|
-
|
|
492
|
-
### `PgSqlTriggers::SQL::Capsule.new(name:, environment:, purpose:, sql:, created_at: nil)`
|
|
493
|
-
|
|
494
|
-
Creates a new SQL capsule for emergency operations.
|
|
495
|
-
|
|
496
|
-
```ruby
|
|
497
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
498
|
-
name: "fix_user_permissions",
|
|
499
|
-
environment: "production",
|
|
500
|
-
purpose: "Emergency fix for user permission issue after deployment",
|
|
501
|
-
sql: "UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';"
|
|
502
|
-
)
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Parameters**:
|
|
506
|
-
- `name` (String): Unique name for the capsule (alphanumeric, underscores, hyphens only)
|
|
507
|
-
- `environment` (String): Target environment (e.g., "production", "staging")
|
|
508
|
-
- `purpose` (String): Description of what the capsule does and why (required for audit trail)
|
|
509
|
-
- `sql` (String): The SQL statement(s) to execute
|
|
510
|
-
- `created_at` (Time, optional): Creation timestamp (defaults to current time)
|
|
511
|
-
|
|
512
|
-
**Raises**: `ArgumentError` if validation fails
|
|
513
|
-
|
|
514
|
-
### `capsule.checksum`
|
|
515
|
-
|
|
516
|
-
Returns the SHA256 checksum of the SQL content.
|
|
517
|
-
|
|
518
|
-
```ruby
|
|
519
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(name: "fix", ...)
|
|
520
|
-
puts capsule.checksum
|
|
521
|
-
# => "a3f5b8c9d2e..."
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
**Returns**: String (SHA256 hash)
|
|
525
|
-
|
|
526
|
-
### `capsule.to_h`
|
|
527
|
-
|
|
528
|
-
Converts the capsule to a hash for storage or serialization.
|
|
529
|
-
|
|
530
|
-
```ruby
|
|
531
|
-
capsule_data = capsule.to_h
|
|
532
|
-
# => {
|
|
533
|
-
# name: "fix_user_permissions",
|
|
534
|
-
# environment: "production",
|
|
535
|
-
# purpose: "Emergency fix...",
|
|
536
|
-
# sql: "UPDATE users...",
|
|
537
|
-
# checksum: "a3f5b8c9d2e...",
|
|
538
|
-
# created_at: 2026-01-01 12:00:00 UTC
|
|
539
|
-
# }
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
**Returns**: Hash
|
|
543
|
-
|
|
544
|
-
## SQL Executor API
|
|
545
|
-
|
|
546
|
-
The SQL Executor API handles safe execution of SQL capsules with comprehensive logging.
|
|
547
|
-
|
|
548
|
-
### `PgSqlTriggers::SQL::Executor.execute(capsule, actor:, confirmation: nil, dry_run: false)`
|
|
549
|
-
|
|
550
|
-
Executes a SQL capsule with safety checks and logging.
|
|
551
|
-
|
|
552
|
-
```ruby
|
|
553
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
554
|
-
name: "emergency_fix",
|
|
555
|
-
environment: "production",
|
|
556
|
-
purpose: "Fix critical data corruption",
|
|
557
|
-
sql: "UPDATE orders SET status = 'completed' WHERE id IN (123, 456);"
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
# Execute the capsule
|
|
561
|
-
result = PgSqlTriggers::SQL::Executor.execute(
|
|
562
|
-
capsule,
|
|
563
|
-
actor: { type: "user", id: "admin@example.com" },
|
|
564
|
-
confirmation: "EXECUTE SQL"
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
if result[:success]
|
|
568
|
-
puts "SQL executed successfully"
|
|
569
|
-
puts "Rows affected: #{result[:data][:rows_affected]}"
|
|
570
|
-
else
|
|
571
|
-
puts "Execution failed: #{result[:message]}"
|
|
572
|
-
end
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
**Parameters**:
|
|
576
|
-
- `capsule` (Capsule): The SQL capsule to execute
|
|
577
|
-
- `actor` (Hash): Information about who is executing (Admin permission required)
|
|
578
|
-
- `confirmation` (String, optional): Kill switch confirmation text
|
|
579
|
-
- `dry_run` (Boolean): If true, validates without executing (default: false)
|
|
580
|
-
|
|
581
|
-
**Returns**: Hash with `:success`, `:message`, and `:data` keys
|
|
582
|
-
|
|
583
|
-
**Raises**: Permission and kill switch errors are returned in the result hash
|
|
584
|
-
|
|
585
|
-
### `PgSqlTriggers::SQL::Executor.execute_capsule(capsule_name, actor:, confirmation: nil, dry_run: false)`
|
|
586
|
-
|
|
587
|
-
Executes a previously stored SQL capsule by name from the registry.
|
|
588
|
-
|
|
589
|
-
```ruby
|
|
590
|
-
# Execute a capsule stored in the registry
|
|
591
|
-
result = PgSqlTriggers::SQL::Executor.execute_capsule(
|
|
592
|
-
"emergency_fix",
|
|
593
|
-
actor: { type: "user", id: "admin@example.com" },
|
|
594
|
-
confirmation: "EXECUTE SQL"
|
|
595
|
-
)
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
**Parameters**:
|
|
599
|
-
- `capsule_name` (String): Name of the capsule in the registry
|
|
600
|
-
- `actor` (Hash): Information about who is executing
|
|
601
|
-
- `confirmation` (String, optional): Kill switch confirmation text
|
|
602
|
-
- `dry_run` (Boolean): If true, validates without executing
|
|
603
|
-
|
|
604
|
-
**Returns**: Hash with execution results
|
|
605
|
-
|
|
606
490
|
## DSL API
|
|
607
491
|
|
|
608
492
|
The DSL API is used to define triggers in your application.
|
|
@@ -616,10 +500,10 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
|
|
|
616
500
|
table :users
|
|
617
501
|
on :insert, :update
|
|
618
502
|
function :validate_user_email
|
|
619
|
-
version 1
|
|
620
|
-
enabled
|
|
503
|
+
self.version = 1
|
|
504
|
+
self.enabled = true
|
|
621
505
|
timing :before
|
|
622
|
-
|
|
506
|
+
for_each_row
|
|
623
507
|
end
|
|
624
508
|
```
|
|
625
509
|
|
|
@@ -651,7 +535,7 @@ on :insert, :update, :delete
|
|
|
651
535
|
```
|
|
652
536
|
|
|
653
537
|
**Parameters**:
|
|
654
|
-
- `events` (Symbols): One or more of `:insert`, `:update`, `:delete`
|
|
538
|
+
- `events` (Symbols): One or more of `:insert`, `:update`, `:delete`, `:truncate`
|
|
655
539
|
|
|
656
540
|
#### `function(function_name)`
|
|
657
541
|
|
|
@@ -664,37 +548,48 @@ function :validate_user_email
|
|
|
664
548
|
**Parameters**:
|
|
665
549
|
- `function_name` (Symbol): Function name
|
|
666
550
|
|
|
667
|
-
#### `version
|
|
551
|
+
#### `self.version = number`
|
|
668
552
|
|
|
669
553
|
Sets the trigger version.
|
|
670
554
|
|
|
671
555
|
```ruby
|
|
672
|
-
version 1
|
|
673
|
-
version 2
|
|
556
|
+
self.version = 1 # Increment when trigger logic changes
|
|
557
|
+
self.version = 2
|
|
674
558
|
```
|
|
675
559
|
|
|
676
560
|
**Parameters**:
|
|
677
561
|
- `number` (Integer): Version number
|
|
678
562
|
|
|
679
|
-
#### `enabled
|
|
563
|
+
#### `self.enabled = state`
|
|
680
564
|
|
|
681
|
-
Sets the initial enabled state.
|
|
565
|
+
Sets the initial enabled state. Defaults to `true`.
|
|
682
566
|
|
|
683
567
|
```ruby
|
|
684
|
-
enabled true
|
|
685
|
-
enabled false
|
|
568
|
+
self.enabled = true # Trigger is active (default)
|
|
569
|
+
self.enabled = false # Trigger is inactive
|
|
686
570
|
```
|
|
687
571
|
|
|
688
572
|
**Parameters**:
|
|
689
573
|
- `state` (Boolean): Initial state
|
|
690
574
|
|
|
575
|
+
#### `for_each_row` / `for_each_statement`
|
|
576
|
+
|
|
577
|
+
Specifies PostgreSQL execution granularity. Defaults to `for_each_row`.
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
for_each_row # FOR EACH ROW (default)
|
|
581
|
+
for_each_statement # FOR EACH STATEMENT
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Stored in the `for_each` column on the registry table and included in checksum calculation.
|
|
585
|
+
|
|
691
586
|
#### `when_env(*environments)`
|
|
692
587
|
|
|
693
|
-
Restricts trigger to specific environments.
|
|
588
|
+
**Deprecated.** Restricts trigger to specific environments. Emits a deprecation warning on every call and will be removed in a future major version. Use application-level configuration to gate trigger behaviour by environment instead.
|
|
694
589
|
|
|
695
590
|
```ruby
|
|
696
|
-
when_env :production
|
|
697
|
-
when_env :production, :staging
|
|
591
|
+
when_env :production # Deprecated — avoid in new code
|
|
592
|
+
when_env :production, :staging # Deprecated — avoid in new code
|
|
698
593
|
```
|
|
699
594
|
|
|
700
595
|
**Parameters**:
|
|
@@ -712,8 +607,6 @@ timing :after # Trigger fires after constraint checks
|
|
|
712
607
|
**Parameters**:
|
|
713
608
|
- `timing_value` (Symbol or String): Either `:before` or `:after`
|
|
714
609
|
|
|
715
|
-
**Returns**: Current timing value if called without argument
|
|
716
|
-
|
|
717
610
|
## TriggerRegistry Model
|
|
718
611
|
|
|
719
612
|
The `TriggerRegistry` ActiveRecord model represents a trigger in the registry.
|
|
@@ -728,12 +621,13 @@ trigger.table_name # => "users"
|
|
|
728
621
|
trigger.function_name # => "validate_user_email"
|
|
729
622
|
trigger.events # => ["insert", "update"]
|
|
730
623
|
trigger.version # => 1
|
|
731
|
-
trigger.enabled
|
|
732
|
-
trigger.timing
|
|
733
|
-
trigger.
|
|
734
|
-
trigger.
|
|
735
|
-
trigger.
|
|
736
|
-
trigger.
|
|
624
|
+
trigger.enabled # => true
|
|
625
|
+
trigger.timing # => "before" or "after"
|
|
626
|
+
trigger.for_each # => "row" or "statement"
|
|
627
|
+
trigger.environments # => [] (when_env is deprecated)
|
|
628
|
+
trigger.condition # => "NEW.status = 'active'" or nil
|
|
629
|
+
trigger.created_at # => 2023-12-15 12:00:00 UTC
|
|
630
|
+
trigger.updated_at # => 2023-12-15 12:00:00 UTC
|
|
737
631
|
```
|
|
738
632
|
|
|
739
633
|
### Instance Methods
|
|
@@ -816,7 +710,7 @@ trigger.re_execute!(
|
|
|
816
710
|
- `confirmation` (String, optional): Kill switch confirmation text
|
|
817
711
|
- `actor` (Hash, optional): Information about who is performing the operation
|
|
818
712
|
|
|
819
|
-
**Raises**: `ArgumentError` if reason is blank
|
|
713
|
+
**Raises**: `ArgumentError` if reason is blank, `PgSqlTriggers::KillSwitchError`, `StandardError`
|
|
820
714
|
|
|
821
715
|
**Returns**: `true` on success
|
|
822
716
|
|
|
@@ -904,10 +798,6 @@ triggers = PgSqlTriggers::Registry.list
|
|
|
904
798
|
puts "Total triggers: #{triggers.count}"
|
|
905
799
|
|
|
906
800
|
# 4. Check for drift
|
|
907
|
-
drift = PgSqlTriggers::Registry.diff
|
|
908
|
-
puts "Drifted triggers: #{drift[:drifted].count}"
|
|
909
|
-
|
|
910
|
-
# Alternative: Use dedicated query methods
|
|
911
801
|
drifted = PgSqlTriggers::Registry.drifted
|
|
912
802
|
in_sync = PgSqlTriggers::Registry.in_sync
|
|
913
803
|
unknown = PgSqlTriggers::Registry.unknown_triggers
|
|
@@ -955,16 +845,16 @@ end
|
|
|
955
845
|
### Inspection and Reporting
|
|
956
846
|
|
|
957
847
|
```ruby
|
|
958
|
-
# Generate a drift report
|
|
959
|
-
drift = PgSqlTriggers::Registry.diff
|
|
960
|
-
|
|
848
|
+
# Generate a drift report using dedicated query methods
|
|
961
849
|
puts "=== Drift Report ==="
|
|
962
|
-
puts "In Sync: #{
|
|
963
|
-
puts "Drifted: #{
|
|
850
|
+
puts "In Sync: #{PgSqlTriggers::Registry.in_sync.count}"
|
|
851
|
+
puts "Drifted: #{PgSqlTriggers::Registry.drifted.count}"
|
|
852
|
+
puts "Dropped: #{PgSqlTriggers::Registry.dropped.count}"
|
|
853
|
+
puts "Unknown: #{PgSqlTriggers::Registry.unknown_triggers.count}"
|
|
854
|
+
|
|
855
|
+
# Or get the full categorised hash
|
|
856
|
+
drift = PgSqlTriggers::Registry.diff
|
|
964
857
|
puts "Manual Override: #{drift[:manual_override].count}"
|
|
965
|
-
puts "Disabled: #{drift[:disabled].count}"
|
|
966
|
-
puts "Dropped: #{drift[:dropped].count}"
|
|
967
|
-
puts "Unknown: #{drift[:unknown].count}"
|
|
968
858
|
|
|
969
859
|
# List all triggers with details
|
|
970
860
|
triggers = PgSqlTriggers::Registry.list
|
|
@@ -976,6 +866,7 @@ triggers.each do |trigger|
|
|
|
976
866
|
puts " Function: #{trigger.function_name}"
|
|
977
867
|
puts " Events: #{trigger.events.join(', ')}"
|
|
978
868
|
puts " Timing: #{trigger.timing}"
|
|
869
|
+
puts " For Each: #{trigger.for_each}"
|
|
979
870
|
puts " Version: #{trigger.version}"
|
|
980
871
|
puts " Enabled: #{trigger.enabled}"
|
|
981
872
|
puts " Drift: #{trigger.drift_status}"
|
data/docs/configuration.md
CHANGED
|
@@ -50,6 +50,25 @@ config.default_environment = -> {
|
|
|
50
50
|
config.default_environment = -> { 'production' }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### `db_schema`
|
|
54
|
+
|
|
55
|
+
The PostgreSQL schema in which triggers are managed. All system catalog queries use this value. Override when your triggers live in a non-`public` schema.
|
|
56
|
+
|
|
57
|
+
- **Type**: String
|
|
58
|
+
- **Default**: `"public"`
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.db_schema = "public" # default
|
|
62
|
+
|
|
63
|
+
# Use a custom schema
|
|
64
|
+
config.db_schema = "app"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also set this at runtime:
|
|
68
|
+
```ruby
|
|
69
|
+
PgSqlTriggers.db_schema = "app"
|
|
70
|
+
```
|
|
71
|
+
|
|
53
72
|
### `mount_path`
|
|
54
73
|
|
|
55
74
|
Customize where the web UI is mounted (configured in routes, not initializer).
|
|
@@ -192,6 +211,8 @@ Custom authorization logic for the web UI and API.
|
|
|
192
211
|
- **Returns**: Boolean
|
|
193
212
|
- **Default**: `->(_actor, _action, _environment) { true }`
|
|
194
213
|
|
|
214
|
+
> **Important**: If `permission_checker` is `nil` (not configured) and the app boots in **production**, the engine emits a `Rails.logger.warn` at startup. Configure a real checker before deploying to production.
|
|
215
|
+
|
|
195
216
|
```ruby
|
|
196
217
|
# Default: allow all (development only!)
|
|
197
218
|
config.permission_checker = ->(_actor, _action, _environment) { true }
|
|
@@ -210,9 +231,9 @@ config.permission_checker = ->(actor, action, environment) {
|
|
|
210
231
|
case action
|
|
211
232
|
when :view_triggers, :view_diffs
|
|
212
233
|
user.present? # Viewer level
|
|
213
|
-
when :enable_trigger, :disable_trigger, :apply_trigger, :
|
|
234
|
+
when :enable_trigger, :disable_trigger, :apply_trigger, :test_trigger
|
|
214
235
|
user.operator? || user.admin? # Operator level
|
|
215
|
-
when :drop_trigger, :
|
|
236
|
+
when :drop_trigger, :override_drift
|
|
216
237
|
user.admin? # Admin level
|
|
217
238
|
else
|
|
218
239
|
false
|
|
@@ -287,7 +308,7 @@ The permission checker should handle three levels:
|
|
|
287
308
|
#### `:admin`
|
|
288
309
|
- All `:operate` permissions
|
|
289
310
|
- Drop triggers
|
|
290
|
-
-
|
|
311
|
+
- Override drift
|
|
291
312
|
- Modify registry directly
|
|
292
313
|
|
|
293
314
|
## Environment Detection
|