pg_sql_triggers 1.3.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 (117) 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 +354 -0
  7. data/COVERAGE.md +39 -41
  8. data/LICENSE +0 -0
  9. data/README.md +44 -26
  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 +137 -74
  29. data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
  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 -27
  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 +27 -18
  35. data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
  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 -14
  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/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -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 +176 -152
  48. data/docs/audit-trail.md +1 -1
  49. data/docs/configuration.md +196 -3
  50. data/docs/getting-started.md +31 -16
  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 +112 -67
  56. data/docs/web-ui.md +3 -103
  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 +11 -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 +29 -0
  64. data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -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 +26 -13
  70. data/lib/pg_sql_triggers/drift/detector.rb +59 -38
  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 +68 -20
  74. data/lib/pg_sql_triggers/dsl.rb +0 -0
  75. data/lib/pg_sql_triggers/engine.rb +49 -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 +85 -82
  80. data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
  81. data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
  82. data/lib/pg_sql_triggers/migrator.rb +137 -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 +60 -21
  87. data/lib/pg_sql_triggers/registry/validator.rb +287 -6
  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 +154 -275
  91. data/lib/pg_sql_triggers/sql.rb +0 -6
  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 +21 -1
  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 +68 -22
  105. data/Goal.md +0 -742
  106. data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
  107. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
  108. data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
  109. data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
  110. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
  111. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
  112. data/lib/generators/trigger/migration_generator.rb +0 -60
  113. data/lib/pg_sql_triggers/generator/form.rb +0 -80
  114. data/lib/pg_sql_triggers/generator/service.rb +0 -339
  115. data/lib/pg_sql_triggers/generator.rb +0 -8
  116. data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
  117. data/lib/pg_sql_triggers/sql/executor.rb +0 -200
@@ -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 false
503
+ self.version = 1
504
+ self.enabled = true
621
505
  timing :before
622
- when_env :production
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(number)`
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(state)`
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,7 +607,110 @@ 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
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.
716
714
 
717
715
  ## TriggerRegistry Model
718
716
 
@@ -728,12 +726,13 @@ trigger.table_name # => "users"
728
726
  trigger.function_name # => "validate_user_email"
729
727
  trigger.events # => ["insert", "update"]
730
728
  trigger.version # => 1
731
- trigger.enabled # => false
732
- trigger.timing # => "before" or "after"
733
- trigger.environments # => ["production"]
734
- trigger.condition # => "NEW.status = 'active'" or nil
735
- trigger.created_at # => 2023-12-15 12:00:00 UTC
736
- trigger.updated_at # => 2023-12-15 12:00:00 UTC
729
+ trigger.enabled # => true
730
+ trigger.timing # => "before" or "after"
731
+ trigger.for_each # => "row" or "statement"
732
+ trigger.environments # => [] (when_env is deprecated)
733
+ trigger.condition # => "NEW.status = 'active'" or nil
734
+ trigger.created_at # => 2023-12-15 12:00:00 UTC
735
+ trigger.updated_at # => 2023-12-15 12:00:00 UTC
737
736
  ```
738
737
 
739
738
  ### Instance Methods
@@ -816,7 +815,7 @@ trigger.re_execute!(
816
815
  - `confirmation` (String, optional): Kill switch confirmation text
817
816
  - `actor` (Hash, optional): Information about who is performing the operation
818
817
 
819
- **Raises**: `ArgumentError` if reason is blank or function_body is missing, `PgSqlTriggers::KillSwitchError`, `StandardError`
818
+ **Raises**: `ArgumentError` if reason is blank, `PgSqlTriggers::KillSwitchError`, `StandardError`
820
819
 
821
820
  **Returns**: `true` on success
822
821
 
@@ -885,6 +884,34 @@ prod_triggers = PgSqlTriggers::TriggerRegistry.for_environment("production")
885
884
 
886
885
  **Returns**: Array of `TriggerRegistry` records
887
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
+
888
915
  ## Usage Examples
889
916
 
890
917
  ### Complete Workflow
@@ -904,10 +931,6 @@ triggers = PgSqlTriggers::Registry.list
904
931
  puts "Total triggers: #{triggers.count}"
905
932
 
906
933
  # 4. Check for drift
907
- drift = PgSqlTriggers::Registry.diff
908
- puts "Drifted triggers: #{drift[:drifted].count}"
909
-
910
- # Alternative: Use dedicated query methods
911
934
  drifted = PgSqlTriggers::Registry.drifted
912
935
  in_sync = PgSqlTriggers::Registry.in_sync
913
936
  unknown = PgSqlTriggers::Registry.unknown_triggers
@@ -955,16 +978,16 @@ end
955
978
  ### Inspection and Reporting
956
979
 
957
980
  ```ruby
958
- # Generate a drift report
959
- drift = PgSqlTriggers::Registry.diff
960
-
981
+ # Generate a drift report using dedicated query methods
961
982
  puts "=== Drift Report ==="
962
- puts "In Sync: #{drift[:in_sync].count}"
963
- puts "Drifted: #{drift[:drifted].count}"
983
+ puts "In Sync: #{PgSqlTriggers::Registry.in_sync.count}"
984
+ puts "Drifted: #{PgSqlTriggers::Registry.drifted.count}"
985
+ puts "Dropped: #{PgSqlTriggers::Registry.dropped.count}"
986
+ puts "Unknown: #{PgSqlTriggers::Registry.unknown_triggers.count}"
987
+
988
+ # Or get the full categorised hash
989
+ drift = PgSqlTriggers::Registry.diff
964
990
  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
991
 
969
992
  # List all triggers with details
970
993
  triggers = PgSqlTriggers::Registry.list
@@ -976,6 +999,7 @@ triggers.each do |trigger|
976
999
  puts " Function: #{trigger.function_name}"
977
1000
  puts " Events: #{trigger.events.join(', ')}"
978
1001
  puts " Timing: #{trigger.timing}"
1002
+ puts " For Each: #{trigger.for_each}"
979
1003
  puts " Version: #{trigger.version}"
980
1004
  puts " Enabled: #{trigger.enabled}"
981
1005
  puts " Drift: #{trigger.drift_status}"
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
 
@@ -50,6 +53,25 @@ config.default_environment = -> {
50
53
  config.default_environment = -> { 'production' }
51
54
  ```
52
55
 
56
+ ### `db_schema`
57
+
58
+ 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.
59
+
60
+ - **Type**: String
61
+ - **Default**: `"public"`
62
+
63
+ ```ruby
64
+ config.db_schema = "public" # default
65
+
66
+ # Use a custom schema
67
+ config.db_schema = "app"
68
+ ```
69
+
70
+ You can also set this at runtime:
71
+ ```ruby
72
+ PgSqlTriggers.db_schema = "app"
73
+ ```
74
+
53
75
  ### `mount_path`
54
76
 
55
77
  Customize where the web UI is mounted (configured in routes, not initializer).
@@ -192,6 +214,8 @@ Custom authorization logic for the web UI and API.
192
214
  - **Returns**: Boolean
193
215
  - **Default**: `->(_actor, _action, _environment) { true }`
194
216
 
217
+ > **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.
218
+
195
219
  ```ruby
196
220
  # Default: allow all (development only!)
197
221
  config.permission_checker = ->(_actor, _action, _environment) { true }
@@ -210,9 +234,9 @@ config.permission_checker = ->(actor, action, environment) {
210
234
  case action
211
235
  when :view_triggers, :view_diffs
212
236
  user.present? # Viewer level
213
- when :enable_trigger, :disable_trigger, :apply_trigger, :generate_trigger, :test_trigger, :dry_run_sql
237
+ when :enable_trigger, :disable_trigger, :apply_trigger, :test_trigger
214
238
  user.operator? || user.admin? # Operator level
215
- when :drop_trigger, :execute_sql, :override_drift
239
+ when :drop_trigger, :override_drift
216
240
  user.admin? # Admin level
217
241
  else
218
242
  false
@@ -287,7 +311,7 @@ The permission checker should handle three levels:
287
311
  #### `:admin`
288
312
  - All `:operate` permissions
289
313
  - Drop triggers
290
- - Execute SQL capsules
314
+ - Override drift
291
315
  - Modify registry directly
292
316
 
293
317
  ## Environment Detection
@@ -319,6 +343,175 @@ config.default_environment = -> {
319
343
  }
320
344
  ```
321
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
+
322
515
  ## Advanced Configuration
323
516
 
324
517
  ### Complete Example