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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/.erb_lint.yml +0 -0
  3. data/.rspec +0 -0
  4. data/.rubocop.yml +6 -16
  5. data/AGENTS.md +8 -0
  6. data/CHANGELOG.md +104 -2
  7. data/COVERAGE.md +39 -41
  8. data/LICENSE +0 -0
  9. data/README.md +24 -3
  10. data/RELEASE.md +0 -0
  11. data/Rakefile +5 -0
  12. data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
  13. data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
  14. data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
  15. data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
  16. data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
  17. data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
  18. data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
  19. data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
  20. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
  21. data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
  22. data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
  23. data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
  24. data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
  25. data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
  26. data/app/models/pg_sql_triggers/application_record.rb +0 -0
  27. data/app/models/pg_sql_triggers/audit_log.rb +29 -47
  28. data/app/models/pg_sql_triggers/trigger_registry.rb +105 -78
  29. data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -0
  30. data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
  31. data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -24
  32. data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
  33. data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
  34. data/app/views/pg_sql_triggers/tables/index.html.erb +26 -14
  35. data/app/views/pg_sql_triggers/tables/show.html.erb +0 -0
  36. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
  37. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
  38. data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
  39. data/config/initializers/pg_sql_triggers.rb +0 -0
  40. data/config/routes.rb +0 -0
  41. data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
  42. data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
  43. data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
  44. 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
  45. data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
  46. data/docs/README.md +3 -0
  47. data/docs/api-reference.md +133 -0
  48. data/docs/audit-trail.md +1 -1
  49. data/docs/configuration.md +172 -0
  50. data/docs/getting-started.md +14 -0
  51. data/docs/kill-switch.md +0 -0
  52. data/docs/permissions.md +6 -9
  53. data/docs/troubleshooting.md +0 -0
  54. data/docs/ui-guide.md +0 -0
  55. data/docs/usage-guide.md +74 -0
  56. data/docs/web-ui.md +0 -0
  57. data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
  58. data/lib/generators/pg_sql_triggers/templates/README +0 -0
  59. data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
  60. data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
  61. data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +0 -0
  62. data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
  63. data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +0 -0
  64. data/lib/generators/pg_sql_triggers/trigger_generator.rb +0 -0
  65. data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
  66. data/lib/pg_sql_triggers/alerting.rb +77 -0
  67. data/lib/pg_sql_triggers/database_introspection.rb +0 -0
  68. data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
  69. data/lib/pg_sql_triggers/drift/db_queries.rb +14 -5
  70. data/lib/pg_sql_triggers/drift/detector.rb +9 -1
  71. data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
  72. data/lib/pg_sql_triggers/drift.rb +5 -0
  73. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +56 -2
  74. data/lib/pg_sql_triggers/dsl.rb +0 -0
  75. data/lib/pg_sql_triggers/engine.rb +35 -0
  76. data/lib/pg_sql_triggers/errors.rb +0 -0
  77. data/lib/pg_sql_triggers/events_checksum.rb +114 -0
  78. data/lib/pg_sql_triggers/migration.rb +5 -6
  79. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +77 -73
  80. data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
  81. data/lib/pg_sql_triggers/migrator/safety_validator.rb +3 -1
  82. data/lib/pg_sql_triggers/migrator.rb +90 -94
  83. data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
  84. data/lib/pg_sql_triggers/permissions.rb +1 -0
  85. data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
  86. data/lib/pg_sql_triggers/registry/manager.rb +27 -13
  87. data/lib/pg_sql_triggers/registry/validator.rb +226 -2
  88. data/lib/pg_sql_triggers/registry.rb +0 -0
  89. data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
  90. data/lib/pg_sql_triggers/sql/kill_switch.rb +2 -1
  91. data/lib/pg_sql_triggers/sql.rb +0 -0
  92. data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
  93. data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
  94. data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
  95. data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
  96. data/lib/pg_sql_triggers/testing.rb +0 -0
  97. data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
  98. data/lib/pg_sql_triggers/version.rb +1 -1
  99. data/lib/pg_sql_triggers.rb +17 -0
  100. data/lib/tasks/trigger_migrations.rake +235 -152
  101. data/rakelib/pg_sql_triggers_environment.rake +9 -0
  102. data/scripts/generate_coverage_report.rb +4 -1
  103. data/sig/pg_sql_triggers.rbs +0 -0
  104. metadata +65 -13
  105. data/GEM_ANALYSIS.md +0 -368
  106. data/Goal.md +0 -742
  107. 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
@@ -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
 
@@ -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
 
@@ -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
@@ -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 SQL capsule execution
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
- - Execute SQL capsules
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 | Execute SQL capsules |
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
- # Execute SQL capsule (requires Admin)
276
- PgSqlTriggers::SQL::Executor.execute(
277
- capsule,
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
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
@@ -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