pg_sql_triggers 1.0.0 → 1.1.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.erb_lint.yml +47 -0
  3. data/.rubocop.yml +4 -1
  4. data/CHANGELOG.md +112 -1
  5. data/COVERAGE.md +58 -0
  6. data/Goal.md +450 -123
  7. data/README.md +53 -215
  8. data/app/controllers/pg_sql_triggers/application_controller.rb +46 -0
  9. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -1
  10. data/app/controllers/pg_sql_triggers/generator_controller.rb +76 -8
  11. data/app/controllers/pg_sql_triggers/migrations_controller.rb +18 -0
  12. data/app/models/pg_sql_triggers/trigger_registry.rb +93 -12
  13. data/app/views/layouts/pg_sql_triggers/application.html.erb +34 -1
  14. data/app/views/pg_sql_triggers/dashboard/index.html.erb +70 -30
  15. data/app/views/pg_sql_triggers/generator/new.html.erb +22 -4
  16. data/app/views/pg_sql_triggers/generator/preview.html.erb +244 -16
  17. data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +221 -0
  18. data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +40 -0
  19. data/app/views/pg_sql_triggers/tables/index.html.erb +0 -2
  20. data/app/views/pg_sql_triggers/tables/show.html.erb +3 -4
  21. data/config/initializers/pg_sql_triggers.rb +69 -0
  22. data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +3 -1
  23. data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +8 -0
  24. data/docs/README.md +66 -0
  25. data/docs/api-reference.md +681 -0
  26. data/docs/configuration.md +541 -0
  27. data/docs/getting-started.md +135 -0
  28. data/docs/kill-switch.md +586 -0
  29. data/docs/screenshots/.gitkeep +1 -0
  30. data/docs/screenshots/Generate Trigger.png +0 -0
  31. data/docs/screenshots/Triggers Page.png +0 -0
  32. data/docs/screenshots/kill error.png +0 -0
  33. data/docs/screenshots/kill modal for migration down.png +0 -0
  34. data/docs/usage-guide.md +493 -0
  35. data/docs/web-ui.md +353 -0
  36. data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +3 -1
  37. data/lib/generators/pg_sql_triggers/templates/initializer.rb +44 -2
  38. data/lib/pg_sql_triggers/drift/db_queries.rb +116 -0
  39. data/lib/pg_sql_triggers/drift/detector.rb +187 -0
  40. data/lib/pg_sql_triggers/drift/reporter.rb +179 -0
  41. data/lib/pg_sql_triggers/drift.rb +14 -11
  42. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +15 -1
  43. data/lib/pg_sql_triggers/generator/form.rb +3 -1
  44. data/lib/pg_sql_triggers/generator/service.rb +82 -26
  45. data/lib/pg_sql_triggers/migration.rb +1 -1
  46. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +344 -0
  47. data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +143 -0
  48. data/lib/pg_sql_triggers/migrator/safety_validator.rb +258 -0
  49. data/lib/pg_sql_triggers/migrator.rb +85 -3
  50. data/lib/pg_sql_triggers/registry/manager.rb +100 -13
  51. data/lib/pg_sql_triggers/sql/kill_switch.rb +300 -0
  52. data/lib/pg_sql_triggers/testing/dry_run.rb +5 -7
  53. data/lib/pg_sql_triggers/testing/function_tester.rb +66 -24
  54. data/lib/pg_sql_triggers/testing/safe_executor.rb +23 -11
  55. data/lib/pg_sql_triggers/testing/syntax_validator.rb +24 -1
  56. data/lib/pg_sql_triggers/version.rb +1 -1
  57. data/lib/pg_sql_triggers.rb +24 -0
  58. data/lib/tasks/trigger_migrations.rake +33 -0
  59. data/scripts/generate_coverage_report.rb +129 -0
  60. metadata +45 -5
@@ -0,0 +1,493 @@
1
+ # Usage Guide
2
+
3
+ This guide covers the core features of PgSqlTriggers: trigger definitions using the DSL, migration management, and drift detection.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Declaring Triggers](#declaring-triggers)
8
+ - [Trigger Migrations](#trigger-migrations)
9
+ - [Combined Schema and Trigger Migrations](#combined-schema-and-trigger-migrations)
10
+ - [Drift Detection](#drift-detection)
11
+
12
+ ## Declaring Triggers
13
+
14
+ PgSqlTriggers provides a Ruby DSL for defining triggers. Trigger definitions are declarative and separate from their implementation.
15
+
16
+ ### Basic Trigger Definition
17
+
18
+ Create trigger definition files in `app/triggers/`:
19
+
20
+ ```ruby
21
+ # app/triggers/users_email_validation.rb
22
+ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
23
+ table :users
24
+ on :insert, :update
25
+ function :validate_user_email
26
+
27
+ version 1
28
+ enabled false
29
+ timing :before
30
+
31
+ when_env :production
32
+ end
33
+ ```
34
+
35
+ ### DSL Reference
36
+
37
+ #### `table`
38
+ Specifies which table the trigger is attached to:
39
+
40
+ ```ruby
41
+ table :users
42
+ ```
43
+
44
+ #### `on`
45
+ Defines when the trigger fires (one or more events):
46
+
47
+ ```ruby
48
+ on :insert # Single event
49
+ on :insert, :update # Multiple events
50
+ on :delete # Delete operations
51
+ ```
52
+
53
+ #### `function`
54
+ The PostgreSQL function that the trigger executes:
55
+
56
+ ```ruby
57
+ function :validate_user_email
58
+ ```
59
+
60
+ #### `version`
61
+ Version number for tracking changes:
62
+
63
+ ```ruby
64
+ version 1 # Increment when trigger logic changes
65
+ ```
66
+
67
+ #### `enabled`
68
+ Initial state of the trigger:
69
+
70
+ ```ruby
71
+ enabled true # Trigger is active
72
+ enabled false # Trigger is inactive
73
+ ```
74
+
75
+ #### `when_env`
76
+ Environment-specific activation:
77
+
78
+ ```ruby
79
+ when_env :production # Only in production
80
+ when_env :staging, :production # Multiple environments
81
+ ```
82
+
83
+ #### `timing`
84
+ Specifies when the trigger fires relative to the event (BEFORE or AFTER):
85
+
86
+ ```ruby
87
+ timing :before # Trigger fires before constraint checks (default)
88
+ timing :after # Trigger fires after constraint checks
89
+ ```
90
+
91
+ ### Complete Example
92
+
93
+ ```ruby
94
+ # app/triggers/orders_billing_trigger.rb
95
+ PgSqlTriggers::DSL.pg_sql_trigger "orders_billing_trigger" do
96
+ table :orders
97
+ on :insert, :update
98
+ function :calculate_order_total
99
+
100
+ version 2
101
+ enabled true
102
+ timing :after
103
+
104
+ when_env :production, :staging
105
+ end
106
+ ```
107
+
108
+ ## Trigger Generator
109
+
110
+ PgSqlTriggers provides a web-based generator and Rails generators for creating trigger definitions and migrations quickly.
111
+
112
+ ### Web UI Generator
113
+
114
+ The web UI generator provides a user-friendly interface for creating triggers:
115
+
116
+ 1. Navigate to `/pg_sql_triggers/generator/new` in your browser
117
+ 2. Fill in the trigger details:
118
+ - **Trigger Name**: Lowercase letters, numbers, and underscores only
119
+ - **Table Name**: The PostgreSQL table to attach the trigger to
120
+ - **Function Name**: The PostgreSQL function name (must match the function body)
121
+ - **Timing**: When the trigger fires - BEFORE (before constraint checks) or AFTER (after constraint checks)
122
+ - **Events**: Select one or more events (INSERT, UPDATE, DELETE, TRUNCATE)
123
+ - **Function Body**: The complete PostgreSQL function definition
124
+ - **Version**: Starting version number (default: 1)
125
+ - **Enabled**: Whether the trigger should be enabled initially
126
+ - **Environments**: Optional environment restrictions
127
+ - **Condition**: Optional WHEN condition for the trigger
128
+ 3. Preview the generated DSL and migration code (includes timing and condition display)
129
+ 4. Create the trigger files
130
+
131
+ The generator creates:
132
+ - A DSL definition file in `app/triggers/`
133
+ - A migration file in `db/triggers/`
134
+ - A registry entry in the database
135
+
136
+ ### Rails Generators
137
+
138
+ You can also use Rails generators to create trigger migrations:
139
+
140
+ ```bash
141
+ # Generate a trigger migration
142
+ rails generate trigger:migration add_user_validation
143
+
144
+ # Or using the full namespace
145
+ rails generate pg_sql_triggers:trigger_migration add_user_validation
146
+ ```
147
+
148
+ This creates a migration file in `db/triggers/` that you can edit to add your trigger logic.
149
+
150
+ ### Generator Features
151
+
152
+ The generator handles:
153
+ - **Function Name Formatting**: Automatically quotes function names with special characters
154
+ - **Multiple Environments**: Supports multiple environment restrictions
155
+ - **Condition Escaping**: Properly escapes quotes in WHEN conditions
156
+ - **Event Combinations**: Handles single or multiple events (INSERT, UPDATE, DELETE, TRUNCATE)
157
+ - **Migration Numbering**: Automatically generates sequential migration numbers
158
+ - **Error Handling**: Graceful error handling with detailed error messages
159
+
160
+ ### Generator Edge Cases
161
+
162
+ The generator properly handles:
163
+ - Function names with special characters (quoted vs unquoted)
164
+ - Multiple environments in a single trigger
165
+ - Complex WHEN conditions with quotes
166
+ - All event type combinations
167
+ - Standalone gem usage (without Rails context)
168
+ - Migration number collisions
169
+ - Blank events and environments (filtered automatically)
170
+
171
+ ## Trigger Migrations
172
+
173
+ Trigger migrations work similarly to Rails schema migrations but are specifically for PostgreSQL triggers and functions.
174
+
175
+ ### Generating Migrations
176
+
177
+ Create a new trigger migration:
178
+
179
+ ```bash
180
+ rails generate trigger:migration add_validation_trigger
181
+ ```
182
+
183
+ This creates a timestamped file in `db/triggers/`:
184
+
185
+ ```
186
+ db/triggers/20231215120000_add_validation_trigger.rb
187
+ ```
188
+
189
+ ### Migration Structure
190
+
191
+ Migrations have `up` and `down` methods:
192
+
193
+ ```ruby
194
+ # db/triggers/20231215120000_add_validation_trigger.rb
195
+ class AddValidationTrigger < PgSqlTriggers::Migration
196
+ def up
197
+ execute <<-SQL
198
+ CREATE OR REPLACE FUNCTION validate_user_email()
199
+ RETURNS TRIGGER AS $$
200
+ BEGIN
201
+ IF NEW.email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
202
+ RAISE EXCEPTION 'Invalid email format';
203
+ END IF;
204
+ RETURN NEW;
205
+ END;
206
+ $$ LANGUAGE plpgsql;
207
+
208
+ CREATE TRIGGER user_email_validation
209
+ BEFORE INSERT OR UPDATE ON users
210
+ FOR EACH ROW
211
+ EXECUTE FUNCTION validate_user_email();
212
+ SQL
213
+ end
214
+
215
+ def down
216
+ execute <<-SQL
217
+ DROP TRIGGER IF EXISTS user_email_validation ON users;
218
+ DROP FUNCTION IF EXISTS validate_user_email();
219
+ SQL
220
+ end
221
+ end
222
+ ```
223
+
224
+ ### Running Migrations
225
+
226
+ #### Apply All Pending Migrations
227
+
228
+ ```bash
229
+ rake trigger:migrate
230
+ ```
231
+
232
+ #### Rollback Last Migration
233
+
234
+ ```bash
235
+ rake trigger:rollback
236
+ ```
237
+
238
+ #### Rollback Multiple Steps
239
+
240
+ ```bash
241
+ rake trigger:rollback STEP=3
242
+ ```
243
+
244
+ #### Check Migration Status
245
+
246
+ ```bash
247
+ rake trigger:migrate:status
248
+ ```
249
+
250
+ Output example:
251
+ ```
252
+ Status Migration ID Migration Name
253
+ --------------------------------------------------
254
+ up 20231215120000 Add validation trigger
255
+ up 20231216130000 Add billing trigger
256
+ down 20231217140000 Add audit trigger
257
+ ```
258
+
259
+ #### Run Specific Migration Up
260
+
261
+ ```bash
262
+ rake trigger:migrate:up VERSION=20231215120000
263
+ ```
264
+
265
+ #### Run Specific Migration Down
266
+
267
+ ```bash
268
+ rake trigger:migrate:down VERSION=20231215120000
269
+ ```
270
+
271
+ #### Redo Last Migration
272
+
273
+ ```bash
274
+ rake trigger:migrate:redo
275
+ ```
276
+
277
+ This rolls back and re-applies the last migration.
278
+
279
+ ### Migration Best Practices
280
+
281
+ 1. **Always Provide Down Method**: Ensure migrations are reversible
282
+ 2. **Use Idempotent SQL**: Use `CREATE OR REPLACE FUNCTION` and `DROP ... IF EXISTS`
283
+ 3. **Test in Development**: Verify migrations work before applying to production
284
+ 4. **Version Control**: Commit migration files to git
285
+ 5. **Incremental Changes**: Keep migrations small and focused
286
+
287
+ ### Complex Migration Example
288
+
289
+ ```ruby
290
+ # db/triggers/20231218150000_add_order_audit.rb
291
+ class AddOrderAudit < PgSqlTriggers::Migration
292
+ def up
293
+ execute <<-SQL
294
+ -- Create audit table
295
+ CREATE TABLE IF NOT EXISTS order_audits (
296
+ id SERIAL PRIMARY KEY,
297
+ order_id INTEGER NOT NULL,
298
+ operation VARCHAR(10) NOT NULL,
299
+ old_data JSONB,
300
+ new_data JSONB,
301
+ changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
302
+ );
303
+
304
+ -- Create audit function
305
+ CREATE OR REPLACE FUNCTION audit_order_changes()
306
+ RETURNS TRIGGER AS $$
307
+ BEGIN
308
+ IF TG_OP = 'DELETE' THEN
309
+ INSERT INTO order_audits (order_id, operation, old_data)
310
+ VALUES (OLD.id, 'DELETE', row_to_json(OLD));
311
+ RETURN OLD;
312
+ ELSIF TG_OP = 'UPDATE' THEN
313
+ INSERT INTO order_audits (order_id, operation, old_data, new_data)
314
+ VALUES (NEW.id, 'UPDATE', row_to_json(OLD), row_to_json(NEW));
315
+ RETURN NEW;
316
+ ELSIF TG_OP = 'INSERT' THEN
317
+ INSERT INTO order_audits (order_id, operation, new_data)
318
+ VALUES (NEW.id, 'INSERT', row_to_json(NEW));
319
+ RETURN NEW;
320
+ END IF;
321
+ END;
322
+ $$ LANGUAGE plpgsql;
323
+
324
+ -- Create trigger
325
+ CREATE TRIGGER order_audit_trigger
326
+ AFTER INSERT OR UPDATE OR DELETE ON orders
327
+ FOR EACH ROW
328
+ EXECUTE FUNCTION audit_order_changes();
329
+ SQL
330
+ end
331
+
332
+ def down
333
+ execute <<-SQL
334
+ DROP TRIGGER IF EXISTS order_audit_trigger ON orders;
335
+ DROP FUNCTION IF EXISTS audit_order_changes();
336
+ DROP TABLE IF EXISTS order_audits;
337
+ SQL
338
+ end
339
+ end
340
+ ```
341
+
342
+ ## Combined Schema and Trigger Migrations
343
+
344
+ For convenience, PgSqlTriggers provides rake tasks that run both schema and trigger migrations together.
345
+
346
+ ### Run Both Migrations
347
+
348
+ ```bash
349
+ rake db:migrate:with_triggers
350
+ ```
351
+
352
+ This runs:
353
+ 1. `rake db:migrate` (Rails schema migrations)
354
+ 2. `rake trigger:migrate` (Trigger migrations)
355
+
356
+ ### Rollback Both
357
+
358
+ ```bash
359
+ rake db:rollback:with_triggers
360
+ ```
361
+
362
+ This rolls back:
363
+ 1. The most recent trigger migration
364
+ 2. The most recent schema migration
365
+
366
+ ### Check Status of Both
367
+
368
+ ```bash
369
+ rake db:migrate:status:with_triggers
370
+ ```
371
+
372
+ Shows status of both schema and trigger migrations.
373
+
374
+ ### Get Versions of Both
375
+
376
+ ```bash
377
+ rake db:version:with_triggers
378
+ ```
379
+
380
+ Displays current versions of both migration types.
381
+
382
+ ## Drift Detection
383
+
384
+ PgSqlTriggers automatically detects when the actual database state differs from your DSL definitions.
385
+
386
+ ### Drift States
387
+
388
+ #### Managed & In Sync
389
+ The trigger exists in the database and matches the DSL definition exactly.
390
+
391
+ ```
392
+ Status: ✓ Managed & In Sync
393
+ ```
394
+
395
+ #### Managed & Drifted
396
+ The trigger exists but its definition doesn't match the DSL (e.g., function modified outside PgSqlTriggers).
397
+
398
+ ```
399
+ Status: ⚠ Managed & Drifted
400
+ ```
401
+
402
+ **Actions:**
403
+ - Review the differences
404
+ - Update the DSL to match the database
405
+ - Re-apply the migration to restore the DSL definition
406
+
407
+ #### Manual Override
408
+ The trigger was modified outside of PgSqlTriggers (e.g., via direct SQL).
409
+
410
+ ```
411
+ Status: ⚠ Manual Override
412
+ ```
413
+
414
+ **Actions:**
415
+ - Document the manual changes
416
+ - Update the DSL to reflect the changes
417
+ - Create a new migration if needed
418
+
419
+ #### Disabled
420
+ The trigger is disabled via PgSqlTriggers.
421
+
422
+ ```
423
+ Status: ○ Disabled
424
+ ```
425
+
426
+ **Actions:**
427
+ - Enable via console or Web UI
428
+ - Verify the trigger is needed
429
+
430
+ #### Dropped
431
+ The trigger was dropped but still exists in the registry.
432
+
433
+ ```
434
+ Status: ✗ Dropped
435
+ ```
436
+
437
+ **Actions:**
438
+ - Re-apply the migration
439
+ - Remove from registry if no longer needed
440
+
441
+ #### Unknown
442
+ The trigger exists in the database but not in the PgSqlTriggers registry.
443
+
444
+ ```
445
+ Status: ? Unknown
446
+ ```
447
+
448
+ **Actions:**
449
+ - Add a DSL definition for the trigger
450
+ - Create a migration to bring it under management
451
+ - Drop it if it's no longer needed
452
+
453
+ ### Checking for Drift
454
+
455
+ #### Via Console
456
+
457
+ ```ruby
458
+ # Get drift information for all triggers
459
+ PgSqlTriggers::Registry.diff
460
+ ```
461
+
462
+ #### Via Web UI
463
+
464
+ Navigate to the dashboard at `/pg_sql_triggers` to see drift status visually.
465
+
466
+ ### Resolving Drift
467
+
468
+ 1. **Review the Drift**: Understand what changed and why
469
+ 2. **Choose Resolution**:
470
+ - Update DSL to match database
471
+ - Re-apply migration to match DSL
472
+ - Create new migration for intentional changes
473
+ 3. **Verify**: Check that drift is resolved
474
+
475
+ Example:
476
+
477
+ ```ruby
478
+ # Check current state
479
+ drift = PgSqlTriggers::Registry.diff
480
+
481
+ # Review specific trigger
482
+ trigger = PgSqlTriggers::Registry.find_by(trigger_name: "users_email_validation")
483
+ trigger.drift_status # => "drifted"
484
+
485
+ # Re-apply migration to fix drift
486
+ rake trigger:migrate:redo
487
+ ```
488
+
489
+ ## Next Steps
490
+
491
+ - [Web UI Documentation](web-ui.md) - Manage triggers through the web interface
492
+ - [Kill Switch](kill-switch.md) - Production safety features
493
+ - [API Reference](api-reference.md) - Console commands and programmatic access