pg_sql_triggers 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.erb_lint.yml +0 -0
- data/.rspec +0 -0
- data/.rubocop.yml +6 -16
- data/AGENTS.md +8 -0
- data/CHANGELOG.md +104 -2
- data/COVERAGE.md +39 -41
- data/LICENSE +0 -0
- data/README.md +24 -3
- data/RELEASE.md +0 -0
- data/Rakefile +5 -0
- data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
- data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
- data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
- data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
- data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
- data/app/models/pg_sql_triggers/application_record.rb +0 -0
- data/app/models/pg_sql_triggers/audit_log.rb +29 -47
- data/app/models/pg_sql_triggers/trigger_registry.rb +105 -78
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -0
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -24
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +26 -14
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
- data/config/initializers/pg_sql_triggers.rb +0 -0
- data/config/routes.rb +0 -0
- data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
- data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
- data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
- data/db/migrate/{20260228000001_add_for_each_to_pg_sql_triggers_registry.rb → 20260228162233_add_for_each_to_pg_sql_triggers_registry.rb} +0 -0
- data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
- data/docs/README.md +3 -0
- data/docs/api-reference.md +133 -0
- data/docs/audit-trail.md +1 -1
- data/docs/configuration.md +172 -0
- data/docs/getting-started.md +14 -0
- data/docs/kill-switch.md +0 -0
- data/docs/permissions.md +6 -9
- data/docs/troubleshooting.md +0 -0
- data/docs/ui-guide.md +0 -0
- data/docs/usage-guide.md +74 -0
- data/docs/web-ui.md +0 -0
- data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/README +0 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +0 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +0 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +0 -0
- data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
- data/lib/pg_sql_triggers/alerting.rb +77 -0
- data/lib/pg_sql_triggers/database_introspection.rb +0 -0
- data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
- data/lib/pg_sql_triggers/drift/db_queries.rb +14 -5
- data/lib/pg_sql_triggers/drift/detector.rb +9 -1
- data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
- data/lib/pg_sql_triggers/drift.rb +5 -0
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +56 -2
- data/lib/pg_sql_triggers/dsl.rb +0 -0
- data/lib/pg_sql_triggers/engine.rb +35 -0
- data/lib/pg_sql_triggers/errors.rb +0 -0
- data/lib/pg_sql_triggers/events_checksum.rb +114 -0
- data/lib/pg_sql_triggers/migration.rb +5 -6
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +77 -73
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +3 -1
- data/lib/pg_sql_triggers/migrator.rb +90 -94
- data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
- data/lib/pg_sql_triggers/permissions.rb +1 -0
- data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
- data/lib/pg_sql_triggers/registry/manager.rb +27 -13
- data/lib/pg_sql_triggers/registry/validator.rb +226 -2
- data/lib/pg_sql_triggers/registry.rb +0 -0
- data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
- data/lib/pg_sql_triggers/sql/kill_switch.rb +2 -1
- data/lib/pg_sql_triggers/sql.rb +0 -0
- data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
- data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
- data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
- data/lib/pg_sql_triggers/testing.rb +0 -0
- data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +17 -0
- data/lib/tasks/trigger_migrations.rake +235 -152
- data/rakelib/pg_sql_triggers_environment.rake +9 -0
- data/scripts/generate_coverage_report.rb +4 -1
- data/sig/pg_sql_triggers.rbs +0 -0
- metadata +65 -13
- data/GEM_ANALYSIS.md +0 -368
- data/Goal.md +0 -742
- data/pg_sql_triggers.gemspec +0 -53
|
@@ -131,6 +131,39 @@
|
|
|
131
131
|
</div>
|
|
132
132
|
<% end %>
|
|
133
133
|
|
|
134
|
+
<% rel = @dependency_related || { prerequisites: [], dependents: [] } %>
|
|
135
|
+
<% if rel[:prerequisites].any? || rel[:dependents].any? %>
|
|
136
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
137
|
+
<h3 style="margin-top: 0;">Trigger ordering (depends_on)</h3>
|
|
138
|
+
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 1rem;">
|
|
139
|
+
PostgreSQL runs triggers of the same kind in alphabetical order by name. The DSL <code>depends_on</code> records intended order;
|
|
140
|
+
<code>rake trigger:validate_order</code> and <code>PgSqlTriggers::Registry.validate!</code> check that names and dependencies line up.
|
|
141
|
+
</p>
|
|
142
|
+
|
|
143
|
+
<% if rel[:prerequisites].any? %>
|
|
144
|
+
<div style="margin-bottom: 1rem;">
|
|
145
|
+
<strong>Runs after (depends_on):</strong>
|
|
146
|
+
<ul style="margin-top: 0.5rem;">
|
|
147
|
+
<% rel[:prerequisites].each do |dep_row| %>
|
|
148
|
+
<li><%= link_to dep_row.trigger_name, trigger_path(dep_row), style: "color: #007bff;" %></li>
|
|
149
|
+
<% end %>
|
|
150
|
+
</ul>
|
|
151
|
+
</div>
|
|
152
|
+
<% end %>
|
|
153
|
+
|
|
154
|
+
<% if rel[:dependents].any? %>
|
|
155
|
+
<div>
|
|
156
|
+
<strong>Runs before (other triggers depend on this one):</strong>
|
|
157
|
+
<ul style="margin-top: 0.5rem;">
|
|
158
|
+
<% rel[:dependents].each do |dep_row| %>
|
|
159
|
+
<li><%= link_to dep_row.trigger_name, trigger_path(dep_row), style: "color: #007bff;" %></li>
|
|
160
|
+
<% end %>
|
|
161
|
+
</ul>
|
|
162
|
+
</div>
|
|
163
|
+
<% end %>
|
|
164
|
+
</div>
|
|
165
|
+
<% end %>
|
|
166
|
+
|
|
134
167
|
<!-- SQL Diff (if drift detected) -->
|
|
135
168
|
<% if @drift_info[:has_drift] && @drift_info[:expected_sql].present? %>
|
|
136
169
|
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
File without changes
|
data/config/routes.rb
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddConstraintDeferralToPgSqlTriggersRegistry < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :pg_sql_triggers_registry, :constraint_trigger, :boolean, default: false, null: false
|
|
6
|
+
add_column :pg_sql_triggers_registry, :deferrable, :string
|
|
7
|
+
add_column :pg_sql_triggers_registry, :initially, :string
|
|
8
|
+
end
|
|
9
|
+
end
|
data/docs/README.md
CHANGED
|
@@ -27,6 +27,9 @@ Welcome to the PgSqlTriggers documentation. This directory contains comprehensiv
|
|
|
27
27
|
#### Install PgSqlTriggers
|
|
28
28
|
Start with [Getting Started](getting-started.md)
|
|
29
29
|
|
|
30
|
+
#### See bundled Rails schema migrations (`db/migrate`)
|
|
31
|
+
[Getting Started — Gem schema migrations](getting-started.md#gem-schema-migrations) lists versioned migration files and run order.
|
|
32
|
+
|
|
30
33
|
#### Learn the DSL syntax
|
|
31
34
|
See [Usage Guide - Declaring Triggers](usage-guide.md#declaring-triggers)
|
|
32
35
|
|
data/docs/api-reference.md
CHANGED
|
@@ -607,6 +607,111 @@ timing :after # Trigger fires after constraint checks
|
|
|
607
607
|
**Parameters**:
|
|
608
608
|
- `timing_value` (Symbol or String): Either `:before` or `:after`
|
|
609
609
|
|
|
610
|
+
#### `on_update_of(*columns)`
|
|
611
|
+
|
|
612
|
+
Column-level trigger. Sets the event to `update` and records the column list; the generated
|
|
613
|
+
SQL emits `UPDATE OF "col1", "col2"` so the trigger only fires when those columns change.
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
on_update_of :email, :status
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Parameters**:
|
|
620
|
+
- `columns` (Symbols or Strings): One or more simple SQL identifiers
|
|
621
|
+
|
|
622
|
+
Notes: calling `on(...)` clears the column list. The column list is part of the checksum.
|
|
623
|
+
Names must match `[a-zA-Z_][a-zA-Z0-9_]*` — invalid identifiers are rejected by the validator.
|
|
624
|
+
|
|
625
|
+
#### `constraint_trigger!` / `self.deferrable =` / `self.initially =`
|
|
626
|
+
|
|
627
|
+
Declares a `CREATE CONSTRAINT TRIGGER` (instead of a regular `CREATE TRIGGER`) and optionally
|
|
628
|
+
makes it deferrable.
|
|
629
|
+
|
|
630
|
+
```ruby
|
|
631
|
+
constraint_trigger!
|
|
632
|
+
self.deferrable = :deferrable
|
|
633
|
+
self.initially = :deferred
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**Valid values**:
|
|
637
|
+
- `deferrable`: `:deferrable`, `:not_deferrable`, or `nil`
|
|
638
|
+
- `initially`: `:deferred`, `:immediate`, or `nil` (only when `deferrable == :deferrable`)
|
|
639
|
+
|
|
640
|
+
**Rules** enforced by `Registry::Validator`:
|
|
641
|
+
- `deferrable` / `initially` require `constraint_trigger!`.
|
|
642
|
+
- Constraint triggers must use `timing :after` and cannot use `:truncate` events.
|
|
643
|
+
- `initially` requires `deferrable = :deferrable`.
|
|
644
|
+
|
|
645
|
+
Drift detection reads deferral state from `pg_trigger.tgdeferrable` and `pg_trigger.tginitdeferred`,
|
|
646
|
+
so out-of-band modifications are detected.
|
|
647
|
+
|
|
648
|
+
#### `depends_on(*names)`
|
|
649
|
+
|
|
650
|
+
Declares that this trigger must run after the named trigger(s) on the same table. PostgreSQL
|
|
651
|
+
fires same-kind triggers in **alphabetical name order**, so `depends_on` does not change runtime
|
|
652
|
+
ordering — it is a metadata hint that `Registry::Validator` uses to verify declared intent
|
|
653
|
+
matches naming.
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
depends_on "validate_user_email"
|
|
657
|
+
depends_on "a_trigger", "b_trigger"
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Validation** (`rake trigger:validate_order` or `Registry.validate!`) checks:
|
|
661
|
+
- Referenced triggers exist as DSL entries.
|
|
662
|
+
- Prerequisite is on the same table with the same timing and `FOR EACH` granularity.
|
|
663
|
+
- Events overlap with the dependent.
|
|
664
|
+
- No circular dependencies.
|
|
665
|
+
- The prerequisite's name sorts alphabetically before the dependent's.
|
|
666
|
+
|
|
667
|
+
Related DSL triggers are surfaced on the trigger detail page via
|
|
668
|
+
`Registry::Validator.related_triggers_for_show`.
|
|
669
|
+
|
|
670
|
+
## Drift Alerting API
|
|
671
|
+
|
|
672
|
+
### `PgSqlTriggers::Drift.check_and_notify`
|
|
673
|
+
|
|
674
|
+
Convenience wrapper for `PgSqlTriggers::Alerting.check_and_notify`. Runs full drift detection,
|
|
675
|
+
invokes `PgSqlTriggers.drift_notifier` when any result is in a drifted / dropped / unknown
|
|
676
|
+
state, and emits an `ActiveSupport::Notifications` event `pg_sql_triggers.drift_check`.
|
|
677
|
+
|
|
678
|
+
```ruby
|
|
679
|
+
outcome = PgSqlTriggers::Drift.check_and_notify
|
|
680
|
+
outcome[:results] # All drift detection results
|
|
681
|
+
outcome[:alertable] # Subset that triggered notification
|
|
682
|
+
outcome[:notified] # true if the drift_notifier was called successfully
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Notifier signature**:
|
|
686
|
+
```ruby
|
|
687
|
+
PgSqlTriggers.drift_notifier = lambda do |drift_results, all_results:|
|
|
688
|
+
# drift_results: Array of alertable hashes (drifted / dropped / unknown)
|
|
689
|
+
# all_results: full result set for context
|
|
690
|
+
end
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Errors raised by the notifier are caught, logged to `Rails.logger`, and surfaced via the
|
|
694
|
+
`:notifier_error` key on the `pg_sql_triggers.drift_check` notification payload.
|
|
695
|
+
|
|
696
|
+
## Registry Validator API
|
|
697
|
+
|
|
698
|
+
### `PgSqlTriggers::Registry::Validator.validate!`
|
|
699
|
+
|
|
700
|
+
Loads all DSL triggers from the registry and raises `PgSqlTriggers::ValidationError` when any
|
|
701
|
+
are misconfigured. Validates events, timing, `for_each`, column lists, deferral combinations,
|
|
702
|
+
and `depends_on` references (existence, table/timing/granularity/event overlap, cycles, and
|
|
703
|
+
alphabetical name order).
|
|
704
|
+
|
|
705
|
+
### `PgSqlTriggers::Registry::Validator.trigger_order_validation_errors`
|
|
706
|
+
|
|
707
|
+
Returns only the dependency/order error messages (without raising). Used by
|
|
708
|
+
`rake trigger:validate_order`.
|
|
709
|
+
|
|
710
|
+
### `PgSqlTriggers::Registry::Validator.related_triggers_for_show(record)`
|
|
711
|
+
|
|
712
|
+
Returns `{ prerequisites: [...], dependents: [...] }` — arrays of `TriggerRegistry` records
|
|
713
|
+
declared via `depends_on`. Used by the trigger detail view.
|
|
714
|
+
|
|
610
715
|
## TriggerRegistry Model
|
|
611
716
|
|
|
612
717
|
The `TriggerRegistry` ActiveRecord model represents a trigger in the registry.
|
|
@@ -779,6 +884,34 @@ prod_triggers = PgSqlTriggers::TriggerRegistry.for_environment("production")
|
|
|
779
884
|
|
|
780
885
|
**Returns**: Array of `TriggerRegistry` records
|
|
781
886
|
|
|
887
|
+
## Rake Tasks
|
|
888
|
+
|
|
889
|
+
| Task | Description |
|
|
890
|
+
|------|-------------|
|
|
891
|
+
| `trigger:migrate` | Apply pending trigger migrations (respects kill switch). |
|
|
892
|
+
| `trigger:rollback [STEP=n]` | Rollback trigger migrations. |
|
|
893
|
+
| `trigger:migrate:status` | Show status of all trigger migrations. |
|
|
894
|
+
| `trigger:migrate:up VERSION=…` | Run a specific migration up. |
|
|
895
|
+
| `trigger:migrate:down VERSION=…` | Run a specific migration down. |
|
|
896
|
+
| `trigger:migrate:redo [STEP=n\|VERSION=…]` | Rollback and re-apply. |
|
|
897
|
+
| `trigger:version` | Print the current trigger migration version. |
|
|
898
|
+
| `trigger:abort_if_pending_migrations` | Raise if pending trigger migrations exist. |
|
|
899
|
+
| `trigger:check_drift [FAIL_ON_DRIFT=1]` | Run drift detection; invoke `drift_notifier`; optionally exit non-zero on drift. |
|
|
900
|
+
| `trigger:validate_order` | Validate `depends_on` references, cycles, compatibility, and PostgreSQL name order. |
|
|
901
|
+
| `trigger:dump [FILE=…]` | Write managed triggers to `db/trigger_structure.sql` (or `FILE=…`). |
|
|
902
|
+
| `trigger:load [FILE=…]` | Execute SQL from the snapshot file (respects kill switch). |
|
|
903
|
+
| `db:migrate:with_triggers` | Run `db:migrate` then `trigger:migrate`. |
|
|
904
|
+
| `db:rollback:with_triggers` | Rollback the most recent schema or trigger migration. |
|
|
905
|
+
| `db:migrate:status:with_triggers` | Show both schema and trigger migration status. |
|
|
906
|
+
| `db:version:with_triggers` | Show both schema and trigger current versions. |
|
|
907
|
+
|
|
908
|
+
`ENV` flags that apply across tasks:
|
|
909
|
+
|
|
910
|
+
- `CONFIRMATION_TEXT=…` — satisfy the kill switch in protected environments.
|
|
911
|
+
- `SKIP_TRIGGER_MIGRATE_AFTER_SCHEMA_LOAD=1` — skip the `db:schema:load` → `trigger:migrate`
|
|
912
|
+
hook for a single invocation.
|
|
913
|
+
- `TRIGGER_STRUCTURE_SQL=path` — alternative to `FILE=path` for `trigger:dump` / `trigger:load`.
|
|
914
|
+
|
|
782
915
|
## Usage Examples
|
|
783
916
|
|
|
784
917
|
### Complete Workflow
|
data/docs/audit-trail.md
CHANGED
|
@@ -385,7 +385,7 @@ PgSqlTriggers::AuditLog
|
|
|
385
385
|
**Problem**: Operations are not being logged.
|
|
386
386
|
|
|
387
387
|
**Solution**:
|
|
388
|
-
- Check that migrations are run (`rails db:migrate`)
|
|
388
|
+
- Check that migrations are run (`rails db:migrate`). The audit log table is created by `20260103114508_create_pg_sql_triggers_audit_log.rb` (see [Gem schema migrations](getting-started.md#gem-schema-migrations)).
|
|
389
389
|
- Verify the audit log table exists
|
|
390
390
|
- Check Rails logs for audit logging errors
|
|
391
391
|
|
data/docs/configuration.md
CHANGED
|
@@ -10,6 +10,9 @@ Complete reference for configuring PgSqlTriggers in your Rails application.
|
|
|
10
10
|
- [Kill Switch Configuration](#kill-switch-configuration)
|
|
11
11
|
- [Permission System](#permission-system)
|
|
12
12
|
- [Environment Detection](#environment-detection)
|
|
13
|
+
- [Drift alerting](#drift-alerting)
|
|
14
|
+
- [schema.rb and structure.sql integration](#schemarb-and-structuresql-integration)
|
|
15
|
+
- [Migration and Safety Settings](#migration-and-safety-settings)
|
|
13
16
|
- [Advanced Configuration](#advanced-configuration)
|
|
14
17
|
- [Examples](#examples)
|
|
15
18
|
|
|
@@ -340,6 +343,175 @@ config.default_environment = -> {
|
|
|
340
343
|
}
|
|
341
344
|
```
|
|
342
345
|
|
|
346
|
+
## Drift alerting
|
|
347
|
+
|
|
348
|
+
Drift checks can run from the web UI, Ruby API, or Rake. To push notifications when triggers are **drifted**, **dropped**, or **unknown** (external), set a notifier and schedule `trigger:check_drift` (for example via cron or your job runner).
|
|
349
|
+
|
|
350
|
+
### `drift_notifier`
|
|
351
|
+
|
|
352
|
+
- **Type**: `Proc`, `lambda`, or any object responding to `call`
|
|
353
|
+
- **Default**: `nil` (no notifications)
|
|
354
|
+
|
|
355
|
+
The callable receives:
|
|
356
|
+
|
|
357
|
+
1. **First argument**: an Array of result hashes for problematic triggers only (same shape as `PgSqlTriggers::Drift::Detector.detect_all` elements).
|
|
358
|
+
2. **Keyword argument** `all_results:`: the full result set from drift detection.
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
PgSqlTriggers.configure do |config|
|
|
362
|
+
config.drift_notifier = lambda do |drift_results, all_results:|
|
|
363
|
+
next if drift_results.empty?
|
|
364
|
+
|
|
365
|
+
# Example: log, Slack, PagerDuty, email, etc.
|
|
366
|
+
Rails.logger.warn("[PgSqlTriggers] Drift: #{drift_results.map { |r| r[:state] }.tally.inspect}")
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Rake task
|
|
372
|
+
|
|
373
|
+
Run this from your **Rails application root** (tasks are loaded by the engine), or from the **gem repository** when developing the gem (the root `Rakefile` loads `rakelib/` so `trigger:*` tasks are available there too).
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
bundle exec rake trigger:check_drift
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- Set `FAIL_ON_DRIFT=1` to exit with status **1** when any problematic trigger exists (useful in CI or monitoring scripts that treat non-zero as failure).
|
|
380
|
+
- If problems exist and no notifier is configured, the task prints a reminder to set `drift_notifier`.
|
|
381
|
+
|
|
382
|
+
### `ActiveSupport::Notifications`
|
|
383
|
+
|
|
384
|
+
When Active Support is loaded, each run emits `pg_sql_triggers.drift_check` with payload keys including `:results`, `:alertable`, `:alertable_count`, `:total_count`, and `:notified`. Subscribe in an initializer to forward metrics to your APM or logging pipeline.
|
|
385
|
+
|
|
386
|
+
### Example: Slack incoming webhook
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
require "net/http"
|
|
390
|
+
require "json"
|
|
391
|
+
require "uri"
|
|
392
|
+
|
|
393
|
+
PgSqlTriggers.configure do |config|
|
|
394
|
+
config.drift_notifier = lambda do |drift_results, all_results:|
|
|
395
|
+
uri = URI(ENV.fetch("SLACK_DRIFT_WEBHOOK_URL"))
|
|
396
|
+
text = "PgSqlTriggers drift: #{drift_results.size} issue(s)\n" +
|
|
397
|
+
drift_results.map { |r|
|
|
398
|
+
name = r[:registry_entry]&.trigger_name || r[:db_trigger]&.dig("trigger_name")
|
|
399
|
+
"- #{name}: #{r[:state]}"
|
|
400
|
+
}.join("\n")
|
|
401
|
+
Net::HTTP.post(uri, { text: text }.to_json, "Content-Type" => "application/json")
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Example: PagerDuty Events API v2
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
require "net/http"
|
|
410
|
+
require "json"
|
|
411
|
+
require "uri"
|
|
412
|
+
|
|
413
|
+
PgSqlTriggers.configure do |config|
|
|
414
|
+
config.drift_notifier = lambda do |drift_results, all_results:|
|
|
415
|
+
uri = URI("https://events.pagerduty.com/v2/enqueue")
|
|
416
|
+
body = {
|
|
417
|
+
routing_key: ENV.fetch("PAGERDUTY_INTEGRATION_KEY"),
|
|
418
|
+
event_action: "trigger",
|
|
419
|
+
payload: {
|
|
420
|
+
summary: "PgSqlTriggers: #{drift_results.size} drift issue(s)",
|
|
421
|
+
severity: "error",
|
|
422
|
+
source: "pg_sql_triggers",
|
|
423
|
+
custom_details: { triggers: drift_results.map { |r| r.slice(:state, :details) } }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json")
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## schema.rb and structure.sql integration
|
|
432
|
+
|
|
433
|
+
`rails db:schema:dump` cannot represent PostgreSQL triggers in `schema.rb`. PgSqlTriggers ships
|
|
434
|
+
three opt-in hooks that keep triggers aligned with the Rails schema workflow.
|
|
435
|
+
|
|
436
|
+
### `append_trigger_notes_to_schema_dump`
|
|
437
|
+
|
|
438
|
+
When `true` (default), the engine prepends an extension onto `ActiveRecord::SchemaDumper` that
|
|
439
|
+
appends a short comment block to `schema.rb` listing managed triggers and pointing at the
|
|
440
|
+
relevant rake tasks. Disable it if you prefer `schema.rb` to be untouched.
|
|
441
|
+
|
|
442
|
+
- **Type**: Boolean
|
|
443
|
+
- **Default**: `true`
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
config.append_trigger_notes_to_schema_dump = false
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The annotation is suppressed automatically when `config.active_record.schema_format = :sql`.
|
|
450
|
+
|
|
451
|
+
### `trigger_structure_sql_path`
|
|
452
|
+
|
|
453
|
+
Path (or callable returning a path) for the SQL snapshot written by `rake trigger:dump` and
|
|
454
|
+
read by `rake trigger:load`. Defaults to `Rails.root.join("db/trigger_structure.sql")`.
|
|
455
|
+
|
|
456
|
+
- **Type**: `String`, `Pathname`, or callable returning one
|
|
457
|
+
- **Default**: `nil` (uses `db/trigger_structure.sql`)
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
config.trigger_structure_sql_path = Rails.root.join("db/triggers/snapshot.sql")
|
|
461
|
+
|
|
462
|
+
# Or a lambda (useful for multi-tenant setups)
|
|
463
|
+
config.trigger_structure_sql_path = -> {
|
|
464
|
+
Rails.root.join("db/triggers", "#{Apartment::Tenant.current}.sql")
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`FILE=path` or `TRIGGER_STRUCTURE_SQL=path` on the rake command line overrides this value per
|
|
469
|
+
invocation.
|
|
470
|
+
|
|
471
|
+
### `migrate_triggers_after_schema_load`
|
|
472
|
+
|
|
473
|
+
When `true` (default), `db:schema:load` is enhanced to automatically invoke `trigger:migrate`
|
|
474
|
+
after the schema has been loaded so pending trigger migrations apply to a freshly-loaded
|
|
475
|
+
database.
|
|
476
|
+
|
|
477
|
+
- **Type**: Boolean
|
|
478
|
+
- **Default**: `true`
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
config.migrate_triggers_after_schema_load = false
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Set the environment variable `SKIP_TRIGGER_MIGRATE_AFTER_SCHEMA_LOAD=1` to skip the hook for a
|
|
485
|
+
single invocation without changing configuration.
|
|
486
|
+
|
|
487
|
+
## Migration and Safety Settings
|
|
488
|
+
|
|
489
|
+
### `excluded_tables`
|
|
490
|
+
|
|
491
|
+
Tables whose triggers are ignored by drift detection and registry listings. Useful when other
|
|
492
|
+
systems (e.g. auditing extensions, logical replication) manage triggers on tables you do not
|
|
493
|
+
want pg_sql_triggers to touch.
|
|
494
|
+
|
|
495
|
+
- **Type**: Array of strings/symbols
|
|
496
|
+
- **Default**: `[]`
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
config.excluded_tables = %w[ar_internal_metadata schema_migrations]
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### `allow_unsafe_migrations`
|
|
503
|
+
|
|
504
|
+
When `false` (default), trigger migrations are vetted by `PgSqlTriggers::Migrator::SafetyValidator`
|
|
505
|
+
before they execute. Setting this to `true` bypasses safety checks entirely — intended only for
|
|
506
|
+
local development or one-off recovery operations.
|
|
507
|
+
|
|
508
|
+
- **Type**: Boolean
|
|
509
|
+
- **Default**: `false`
|
|
510
|
+
|
|
511
|
+
```ruby
|
|
512
|
+
config.allow_unsafe_migrations = Rails.env.development?
|
|
513
|
+
```
|
|
514
|
+
|
|
343
515
|
## Advanced Configuration
|
|
344
516
|
|
|
345
517
|
### Complete Example
|
data/docs/getting-started.md
CHANGED
|
@@ -34,6 +34,20 @@ This will:
|
|
|
34
34
|
2. Create migrations for the registry table
|
|
35
35
|
3. Mount the engine at `/pg_sql_triggers`
|
|
36
36
|
|
|
37
|
+
## Gem schema migrations
|
|
38
|
+
|
|
39
|
+
The gem’s Rails schema migrations live under `db/migrate/` in this repository. Versions use the standard **`YYYYMMDDHHMMSS`** prefix (14 digits: date and clock time). Rails runs them in version order.
|
|
40
|
+
|
|
41
|
+
Run order (oldest first):
|
|
42
|
+
|
|
43
|
+
1. `20251222104327_create_pg_sql_triggers_tables.rb` — creates `pg_sql_triggers_registry`
|
|
44
|
+
2. `20251229071916_add_timing_to_pg_sql_triggers_registry.rb` — adds `timing` on the registry
|
|
45
|
+
3. `20260103114508_create_pg_sql_triggers_audit_log.rb` — creates `pg_sql_triggers_audit_log`
|
|
46
|
+
4. `20260228162233_add_for_each_to_pg_sql_triggers_registry.rb` — adds `for_each` on the registry
|
|
47
|
+
5. `20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb` — adds `constraint_trigger`, `deferrable`, `initially` on the registry
|
|
48
|
+
|
|
49
|
+
If you upgrade the gem and rename migration files locally, update the corresponding rows in `schema_migrations` so the version strings match these filenames.
|
|
50
|
+
|
|
37
51
|
## Verify Installation
|
|
38
52
|
|
|
39
53
|
After installation, you should have:
|
data/docs/kill-switch.md
CHANGED
|
File without changes
|
data/docs/permissions.md
CHANGED
|
@@ -18,7 +18,7 @@ The permission system provides three levels of access:
|
|
|
18
18
|
|
|
19
19
|
- **Viewer**: Read-only access to view triggers and their status
|
|
20
20
|
- **Operator**: Can enable/disable triggers, apply migrations, generate triggers
|
|
21
|
-
- **Admin**: Full access including drop, re-execute, and
|
|
21
|
+
- **Admin**: Full access including drop, re-execute, and the reserved `execute_sql` permission action for host-defined privileged SQL
|
|
22
22
|
|
|
23
23
|
By default, all permissions are allowed (permissive mode). **You must configure permissions in production** to enforce access controls.
|
|
24
24
|
|
|
@@ -49,7 +49,7 @@ Admins have full access:
|
|
|
49
49
|
- All Operator permissions
|
|
50
50
|
- Drop triggers
|
|
51
51
|
- Re-execute triggers
|
|
52
|
-
-
|
|
52
|
+
- `execute_sql` (privileged SQL; for custom integrations — not used by built-in UI)
|
|
53
53
|
- Override drift detection
|
|
54
54
|
|
|
55
55
|
## Actions and Required Roles
|
|
@@ -67,7 +67,7 @@ The following actions are mapped to permission levels:
|
|
|
67
67
|
| `generate_trigger` | Operator | Generate triggers via UI |
|
|
68
68
|
| `test_trigger` | Operator | Test trigger functions |
|
|
69
69
|
| `drop_trigger` | Admin | Drop a trigger from database |
|
|
70
|
-
| `execute_sql` | Admin |
|
|
70
|
+
| `execute_sql` | Admin | Privileged SQL (`permission_checker` / custom tooling; not used by built-in UI) |
|
|
71
71
|
| `override_drift` | Admin | Override drift detection warnings |
|
|
72
72
|
|
|
73
73
|
## Configuration
|
|
@@ -272,12 +272,9 @@ PgSqlTriggers::Registry.drop(
|
|
|
272
272
|
confirmation: "EXECUTE TRIGGER_DROP"
|
|
273
273
|
)
|
|
274
274
|
|
|
275
|
-
#
|
|
276
|
-
PgSqlTriggers::
|
|
277
|
-
|
|
278
|
-
actor: current_user,
|
|
279
|
-
confirmation: "EXECUTE SQL"
|
|
280
|
-
)
|
|
275
|
+
# Custom tooling: gate raw SQL with the Admin-level :execute_sql action (requires Admin)
|
|
276
|
+
PgSqlTriggers::Permissions.check!(current_user, :execute_sql)
|
|
277
|
+
# ... your application's SQL execution ...
|
|
281
278
|
```
|
|
282
279
|
|
|
283
280
|
### Permission Errors in Console
|
data/docs/troubleshooting.md
CHANGED
|
File without changes
|
data/docs/ui-guide.md
CHANGED
|
File without changes
|
data/docs/usage-guide.md
CHANGED
|
@@ -91,6 +91,80 @@ timing :before # Trigger fires before constraint checks (default)
|
|
|
91
91
|
timing :after # Trigger fires after constraint checks
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
#### `on_update_of`
|
|
95
|
+
Declares a **column-level** trigger that fires only when specific columns change
|
|
96
|
+
(PostgreSQL `UPDATE OF col1, col2`). This is a common performance optimisation for audit
|
|
97
|
+
triggers: they run only when the columns you care about are modified.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
on_update_of :email, :status # Sets event to UPDATE and records columns
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Notes:
|
|
104
|
+
- `on_update_of` sets the event to `update` and stores the column list; calling `on(...)` again
|
|
105
|
+
clears the column list.
|
|
106
|
+
- Column names must be simple SQL identifiers (`[a-zA-Z_][a-zA-Z0-9_]*`). The gem quotes them
|
|
107
|
+
in generated SQL, so pass them unquoted.
|
|
108
|
+
- The column list is included in the checksum, so changing the list is detected as drift.
|
|
109
|
+
|
|
110
|
+
#### `constraint_trigger!` / `deferrable` / `initially`
|
|
111
|
+
Declares a **constraint trigger** (`CREATE CONSTRAINT TRIGGER`) with optional deferral. Constraint
|
|
112
|
+
triggers must be `AFTER` triggers, cannot use `TRUNCATE`, and are the only triggers that can be
|
|
113
|
+
deferred.
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
PgSqlTriggers::DSL.pg_sql_trigger "orders_integrity_check" do
|
|
117
|
+
table :orders
|
|
118
|
+
on :insert, :update
|
|
119
|
+
function :check_orders_referential_integrity
|
|
120
|
+
|
|
121
|
+
constraint_trigger! # Emits CREATE CONSTRAINT TRIGGER, forces timing :after
|
|
122
|
+
self.deferrable = :deferrable
|
|
123
|
+
self.initially = :deferred # Evaluate at transaction commit
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Valid combinations:
|
|
128
|
+
|
|
129
|
+
| `deferrable` | `initially` | SQL clause |
|
|
130
|
+
|---------------------|----------------------------|-------------------------------------|
|
|
131
|
+
| `:deferrable` | `:deferred` | `DEFERRABLE INITIALLY DEFERRED` |
|
|
132
|
+
| `:deferrable` | `:immediate` (or `nil`) | `DEFERRABLE INITIALLY IMMEDIATE` |
|
|
133
|
+
| `:not_deferrable` | (must be `nil`) | `NOT DEFERRABLE` |
|
|
134
|
+
| `nil` | `nil` | (no deferral clause) |
|
|
135
|
+
|
|
136
|
+
`Registry::Validator` rejects `deferrable`/`initially` unless `constraint_trigger!` is set,
|
|
137
|
+
and rejects constraint triggers that use `:before` timing or `TRUNCATE` events. Drift detection
|
|
138
|
+
reads deferral state from `pg_trigger.tgdeferrable` / `tginitdeferred` so changes made outside
|
|
139
|
+
the gem are surfaced as drift.
|
|
140
|
+
|
|
141
|
+
#### `depends_on`
|
|
142
|
+
Declares an **ordering hint** relative to another trigger on the same table. PostgreSQL fires
|
|
143
|
+
same-kind triggers in alphabetical order by name; `depends_on` captures the intended order so the
|
|
144
|
+
registry validator can verify that naming matches dependency declarations.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
PgSqlTriggers::DSL.pg_sql_trigger "log_user_change" do
|
|
148
|
+
table :users
|
|
149
|
+
on :insert, :update
|
|
150
|
+
function :log_user_change
|
|
151
|
+
timing :after
|
|
152
|
+
|
|
153
|
+
depends_on "validate_user_email" # Must exist on same table, same timing/FOR EACH, overlapping events
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Run `rake trigger:validate_order` (or `PgSqlTriggers::Registry.validate!`) to enforce:
|
|
158
|
+
|
|
159
|
+
- Referenced prerequisite triggers exist and are on the same table.
|
|
160
|
+
- Prerequisites share timing (`before`/`after`), `FOR EACH` granularity, and have at least one
|
|
161
|
+
overlapping event with the dependent.
|
|
162
|
+
- There are no circular dependencies.
|
|
163
|
+
- Names sort alphabetically in the required order (the prerequisite's name must sort before
|
|
164
|
+
the dependent's).
|
|
165
|
+
|
|
166
|
+
The trigger detail page in the web UI displays prerequisites and dependents for DSL triggers.
|
|
167
|
+
|
|
94
168
|
### Complete Example
|
|
95
169
|
|
|
96
170
|
```ruby
|
data/docs/web-ui.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -66,4 +66,18 @@ PgSqlTriggers.configure do |config|
|
|
|
66
66
|
# You can also override per-migration with ALLOW_UNSAFE_MIGRATIONS=true environment variable
|
|
67
67
|
# Default: false (recommended for safety)
|
|
68
68
|
config.allow_unsafe_migrations = false
|
|
69
|
+
|
|
70
|
+
# ========== Schema / structure.sql integration ==========
|
|
71
|
+
# Triggers are not included in db/schema.rb. After db:schema:load, the engine runs
|
|
72
|
+
# trigger:migrate by default. Set SKIP_TRIGGER_MIGRATE_AFTER_SCHEMA_LOAD=1 to skip.
|
|
73
|
+
# config.migrate_triggers_after_schema_load = true
|
|
74
|
+
#
|
|
75
|
+
# Optional: append comments to schema.rb pointing at db/trigger_structure.sql.
|
|
76
|
+
# config.append_trigger_notes_to_schema_dump = true
|
|
77
|
+
#
|
|
78
|
+
# Snapshot live trigger SQL: bin/rails trigger:dump (apply with trigger:load).
|
|
79
|
+
# config.trigger_structure_sql_path = Rails.root.join("db/trigger_structure.sql")
|
|
80
|
+
#
|
|
81
|
+
# For databases where triggers must appear in the main SQL dump, prefer:
|
|
82
|
+
# config.active_record.schema_format = :sql
|
|
69
83
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|