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.
- 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 +354 -0
- data/COVERAGE.md +39 -41
- data/LICENSE +0 -0
- data/README.md +44 -26
- 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 +137 -74
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -27
- 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 +27 -18
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- 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 -14
- 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/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -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 +176 -152
- data/docs/audit-trail.md +1 -1
- data/docs/configuration.md +196 -3
- data/docs/getting-started.md +31 -16
- 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 +112 -67
- data/docs/web-ui.md +3 -103
- 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 +11 -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 +29 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -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 +26 -13
- data/lib/pg_sql_triggers/drift/detector.rb +59 -38
- 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 +68 -20
- data/lib/pg_sql_triggers/dsl.rb +0 -0
- data/lib/pg_sql_triggers/engine.rb +49 -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 +85 -82
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
- data/lib/pg_sql_triggers/migrator.rb +137 -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 +60 -21
- data/lib/pg_sql_triggers/registry/validator.rb +287 -6
- 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 +154 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- 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 +21 -1
- 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 +68 -22
- data/Goal.md +0 -742
- data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
- data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
- data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
- data/lib/generators/trigger/migration_generator.rb +0 -60
- data/lib/pg_sql_triggers/generator/form.rb +0 -80
- data/lib/pg_sql_triggers/generator/service.rb +0 -339
- data/lib/pg_sql_triggers/generator.rb +0 -8
- data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
- data/lib/pg_sql_triggers/sql/executor.rb +0 -200
data/docs/api-reference.md
CHANGED
|
@@ -11,6 +11,8 @@ Complete reference for using PgSqlTriggers programmatically from the Rails conso
|
|
|
11
11
|
- [TriggerRegistry Model](#triggerregistry-model)
|
|
12
12
|
- [Audit Log API](#audit-log-api)
|
|
13
13
|
|
|
14
|
+
> **Removed in 1.4.0**: `SQL::Capsule` and `SQL::Executor` have been removed. See [CHANGELOG](../CHANGELOG.md) for details.
|
|
15
|
+
|
|
14
16
|
## Registry API
|
|
15
17
|
|
|
16
18
|
The Registry API provides methods for inspecting and managing triggers.
|
|
@@ -485,124 +487,6 @@ end
|
|
|
485
487
|
|
|
486
488
|
**Returns**: Result of the block
|
|
487
489
|
|
|
488
|
-
## SQL Capsule API
|
|
489
|
-
|
|
490
|
-
The SQL Capsule API provides emergency SQL execution capabilities with safety checks.
|
|
491
|
-
|
|
492
|
-
### `PgSqlTriggers::SQL::Capsule.new(name:, environment:, purpose:, sql:, created_at: nil)`
|
|
493
|
-
|
|
494
|
-
Creates a new SQL capsule for emergency operations.
|
|
495
|
-
|
|
496
|
-
```ruby
|
|
497
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
498
|
-
name: "fix_user_permissions",
|
|
499
|
-
environment: "production",
|
|
500
|
-
purpose: "Emergency fix for user permission issue after deployment",
|
|
501
|
-
sql: "UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';"
|
|
502
|
-
)
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Parameters**:
|
|
506
|
-
- `name` (String): Unique name for the capsule (alphanumeric, underscores, hyphens only)
|
|
507
|
-
- `environment` (String): Target environment (e.g., "production", "staging")
|
|
508
|
-
- `purpose` (String): Description of what the capsule does and why (required for audit trail)
|
|
509
|
-
- `sql` (String): The SQL statement(s) to execute
|
|
510
|
-
- `created_at` (Time, optional): Creation timestamp (defaults to current time)
|
|
511
|
-
|
|
512
|
-
**Raises**: `ArgumentError` if validation fails
|
|
513
|
-
|
|
514
|
-
### `capsule.checksum`
|
|
515
|
-
|
|
516
|
-
Returns the SHA256 checksum of the SQL content.
|
|
517
|
-
|
|
518
|
-
```ruby
|
|
519
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(name: "fix", ...)
|
|
520
|
-
puts capsule.checksum
|
|
521
|
-
# => "a3f5b8c9d2e..."
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
**Returns**: String (SHA256 hash)
|
|
525
|
-
|
|
526
|
-
### `capsule.to_h`
|
|
527
|
-
|
|
528
|
-
Converts the capsule to a hash for storage or serialization.
|
|
529
|
-
|
|
530
|
-
```ruby
|
|
531
|
-
capsule_data = capsule.to_h
|
|
532
|
-
# => {
|
|
533
|
-
# name: "fix_user_permissions",
|
|
534
|
-
# environment: "production",
|
|
535
|
-
# purpose: "Emergency fix...",
|
|
536
|
-
# sql: "UPDATE users...",
|
|
537
|
-
# checksum: "a3f5b8c9d2e...",
|
|
538
|
-
# created_at: 2026-01-01 12:00:00 UTC
|
|
539
|
-
# }
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
**Returns**: Hash
|
|
543
|
-
|
|
544
|
-
## SQL Executor API
|
|
545
|
-
|
|
546
|
-
The SQL Executor API handles safe execution of SQL capsules with comprehensive logging.
|
|
547
|
-
|
|
548
|
-
### `PgSqlTriggers::SQL::Executor.execute(capsule, actor:, confirmation: nil, dry_run: false)`
|
|
549
|
-
|
|
550
|
-
Executes a SQL capsule with safety checks and logging.
|
|
551
|
-
|
|
552
|
-
```ruby
|
|
553
|
-
capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
554
|
-
name: "emergency_fix",
|
|
555
|
-
environment: "production",
|
|
556
|
-
purpose: "Fix critical data corruption",
|
|
557
|
-
sql: "UPDATE orders SET status = 'completed' WHERE id IN (123, 456);"
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
# Execute the capsule
|
|
561
|
-
result = PgSqlTriggers::SQL::Executor.execute(
|
|
562
|
-
capsule,
|
|
563
|
-
actor: { type: "user", id: "admin@example.com" },
|
|
564
|
-
confirmation: "EXECUTE SQL"
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
if result[:success]
|
|
568
|
-
puts "SQL executed successfully"
|
|
569
|
-
puts "Rows affected: #{result[:data][:rows_affected]}"
|
|
570
|
-
else
|
|
571
|
-
puts "Execution failed: #{result[:message]}"
|
|
572
|
-
end
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
**Parameters**:
|
|
576
|
-
- `capsule` (Capsule): The SQL capsule to execute
|
|
577
|
-
- `actor` (Hash): Information about who is executing (Admin permission required)
|
|
578
|
-
- `confirmation` (String, optional): Kill switch confirmation text
|
|
579
|
-
- `dry_run` (Boolean): If true, validates without executing (default: false)
|
|
580
|
-
|
|
581
|
-
**Returns**: Hash with `:success`, `:message`, and `:data` keys
|
|
582
|
-
|
|
583
|
-
**Raises**: Permission and kill switch errors are returned in the result hash
|
|
584
|
-
|
|
585
|
-
### `PgSqlTriggers::SQL::Executor.execute_capsule(capsule_name, actor:, confirmation: nil, dry_run: false)`
|
|
586
|
-
|
|
587
|
-
Executes a previously stored SQL capsule by name from the registry.
|
|
588
|
-
|
|
589
|
-
```ruby
|
|
590
|
-
# Execute a capsule stored in the registry
|
|
591
|
-
result = PgSqlTriggers::SQL::Executor.execute_capsule(
|
|
592
|
-
"emergency_fix",
|
|
593
|
-
actor: { type: "user", id: "admin@example.com" },
|
|
594
|
-
confirmation: "EXECUTE SQL"
|
|
595
|
-
)
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
**Parameters**:
|
|
599
|
-
- `capsule_name` (String): Name of the capsule in the registry
|
|
600
|
-
- `actor` (Hash): Information about who is executing
|
|
601
|
-
- `confirmation` (String, optional): Kill switch confirmation text
|
|
602
|
-
- `dry_run` (Boolean): If true, validates without executing
|
|
603
|
-
|
|
604
|
-
**Returns**: Hash with execution results
|
|
605
|
-
|
|
606
490
|
## DSL API
|
|
607
491
|
|
|
608
492
|
The DSL API is used to define triggers in your application.
|
|
@@ -616,10 +500,10 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
|
|
|
616
500
|
table :users
|
|
617
501
|
on :insert, :update
|
|
618
502
|
function :validate_user_email
|
|
619
|
-
version 1
|
|
620
|
-
enabled
|
|
503
|
+
self.version = 1
|
|
504
|
+
self.enabled = true
|
|
621
505
|
timing :before
|
|
622
|
-
|
|
506
|
+
for_each_row
|
|
623
507
|
end
|
|
624
508
|
```
|
|
625
509
|
|
|
@@ -651,7 +535,7 @@ on :insert, :update, :delete
|
|
|
651
535
|
```
|
|
652
536
|
|
|
653
537
|
**Parameters**:
|
|
654
|
-
- `events` (Symbols): One or more of `:insert`, `:update`, `:delete`
|
|
538
|
+
- `events` (Symbols): One or more of `:insert`, `:update`, `:delete`, `:truncate`
|
|
655
539
|
|
|
656
540
|
#### `function(function_name)`
|
|
657
541
|
|
|
@@ -664,37 +548,48 @@ function :validate_user_email
|
|
|
664
548
|
**Parameters**:
|
|
665
549
|
- `function_name` (Symbol): Function name
|
|
666
550
|
|
|
667
|
-
#### `version
|
|
551
|
+
#### `self.version = number`
|
|
668
552
|
|
|
669
553
|
Sets the trigger version.
|
|
670
554
|
|
|
671
555
|
```ruby
|
|
672
|
-
version 1
|
|
673
|
-
version 2
|
|
556
|
+
self.version = 1 # Increment when trigger logic changes
|
|
557
|
+
self.version = 2
|
|
674
558
|
```
|
|
675
559
|
|
|
676
560
|
**Parameters**:
|
|
677
561
|
- `number` (Integer): Version number
|
|
678
562
|
|
|
679
|
-
#### `enabled
|
|
563
|
+
#### `self.enabled = state`
|
|
680
564
|
|
|
681
|
-
Sets the initial enabled state.
|
|
565
|
+
Sets the initial enabled state. Defaults to `true`.
|
|
682
566
|
|
|
683
567
|
```ruby
|
|
684
|
-
enabled true
|
|
685
|
-
enabled false
|
|
568
|
+
self.enabled = true # Trigger is active (default)
|
|
569
|
+
self.enabled = false # Trigger is inactive
|
|
686
570
|
```
|
|
687
571
|
|
|
688
572
|
**Parameters**:
|
|
689
573
|
- `state` (Boolean): Initial state
|
|
690
574
|
|
|
575
|
+
#### `for_each_row` / `for_each_statement`
|
|
576
|
+
|
|
577
|
+
Specifies PostgreSQL execution granularity. Defaults to `for_each_row`.
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
for_each_row # FOR EACH ROW (default)
|
|
581
|
+
for_each_statement # FOR EACH STATEMENT
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Stored in the `for_each` column on the registry table and included in checksum calculation.
|
|
585
|
+
|
|
691
586
|
#### `when_env(*environments)`
|
|
692
587
|
|
|
693
|
-
Restricts trigger to specific environments.
|
|
588
|
+
**Deprecated.** Restricts trigger to specific environments. Emits a deprecation warning on every call and will be removed in a future major version. Use application-level configuration to gate trigger behaviour by environment instead.
|
|
694
589
|
|
|
695
590
|
```ruby
|
|
696
|
-
when_env :production
|
|
697
|
-
when_env :production, :staging
|
|
591
|
+
when_env :production # Deprecated — avoid in new code
|
|
592
|
+
when_env :production, :staging # Deprecated — avoid in new code
|
|
698
593
|
```
|
|
699
594
|
|
|
700
595
|
**Parameters**:
|
|
@@ -712,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
|
-
|
|
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
|
|
732
|
-
trigger.timing
|
|
733
|
-
trigger.
|
|
734
|
-
trigger.
|
|
735
|
-
trigger.
|
|
736
|
-
trigger.
|
|
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
|
|
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: #{
|
|
963
|
-
puts "Drifted: #{
|
|
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
|
|
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
|
|
|
@@ -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, :
|
|
237
|
+
when :enable_trigger, :disable_trigger, :apply_trigger, :test_trigger
|
|
214
238
|
user.operator? || user.admin? # Operator level
|
|
215
|
-
when :drop_trigger, :
|
|
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
|
-
-
|
|
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
|