pg_sql_triggers 1.3.0 → 1.4.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/CHANGELOG.md +253 -1
- data/GEM_ANALYSIS.md +368 -0
- data/README.md +20 -23
- data/app/models/pg_sql_triggers/trigger_registry.rb +42 -6
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +1 -4
- data/app/views/pg_sql_triggers/tables/index.html.erb +1 -4
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- data/config/routes.rb +0 -14
- data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/api-reference.md +44 -153
- data/docs/configuration.md +24 -3
- data/docs/getting-started.md +17 -16
- data/docs/usage-guide.md +38 -67
- data/docs/web-ui.md +3 -103
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -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/pg_sql_triggers/drift/db_queries.rb +12 -8
- data/lib/pg_sql_triggers/drift/detector.rb +51 -38
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
- data/lib/pg_sql_triggers/engine.rb +14 -0
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
- data/lib/pg_sql_triggers/migrator.rb +53 -6
- data/lib/pg_sql_triggers/registry/manager.rb +36 -11
- data/lib/pg_sql_triggers/registry/validator.rb +62 -5
- data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +4 -1
- data/pg_sql_triggers.gemspec +53 -0
- metadata +7 -13
- 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/getting-started.md
CHANGED
|
@@ -45,9 +45,19 @@ After installation, you should have:
|
|
|
45
45
|
|
|
46
46
|
## Quick Start Example
|
|
47
47
|
|
|
48
|
-
### 1.
|
|
48
|
+
### 1. Scaffold a Trigger (DSL + Migration)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Use the CLI generator to create both files at once:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
rails generate pg_sql_triggers:trigger users_email_validation users insert update --timing before --function validate_user_email
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This creates:
|
|
57
|
+
- `app/triggers/users_email_validation.rb` — DSL stub
|
|
58
|
+
- `db/triggers/TIMESTAMP_users_email_validation.rb` — migration with function + trigger SQL
|
|
59
|
+
|
|
60
|
+
Edit the DSL stub to match your requirements:
|
|
51
61
|
|
|
52
62
|
```ruby
|
|
53
63
|
# app/triggers/users_email_validation.rb
|
|
@@ -56,21 +66,12 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
|
|
|
56
66
|
on :insert, :update
|
|
57
67
|
function :validate_user_email
|
|
58
68
|
|
|
59
|
-
version 1
|
|
60
|
-
enabled
|
|
61
|
-
|
|
62
|
-
when_env :production
|
|
69
|
+
self.version = 1
|
|
70
|
+
self.enabled = true
|
|
71
|
+
timing :before
|
|
63
72
|
end
|
|
64
73
|
```
|
|
65
74
|
|
|
66
|
-
### 2. Generate a Migration
|
|
67
|
-
|
|
68
|
-
Create a trigger migration to implement the function:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
rails generate trigger:migration add_email_validation
|
|
72
|
-
```
|
|
73
|
-
|
|
74
75
|
Edit the generated migration in `db/triggers/`:
|
|
75
76
|
|
|
76
77
|
```ruby
|
|
@@ -104,7 +105,7 @@ class AddEmailValidation < PgSqlTriggers::Migration
|
|
|
104
105
|
end
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
###
|
|
108
|
+
### 2. Run the Migration
|
|
108
109
|
|
|
109
110
|
Apply the trigger migration:
|
|
110
111
|
|
|
@@ -112,7 +113,7 @@ Apply the trigger migration:
|
|
|
112
113
|
rake trigger:migrate
|
|
113
114
|
```
|
|
114
115
|
|
|
115
|
-
###
|
|
116
|
+
### 3. Access the Web UI
|
|
116
117
|
|
|
117
118
|
Open your browser and navigate to:
|
|
118
119
|
|
data/docs/usage-guide.md
CHANGED
|
@@ -15,7 +15,7 @@ PgSqlTriggers provides a Ruby DSL for defining triggers. Trigger definitions are
|
|
|
15
15
|
|
|
16
16
|
### Basic Trigger Definition
|
|
17
17
|
|
|
18
|
-
Create trigger definition files in `app/triggers
|
|
18
|
+
Create trigger definition files in `app/triggers/` (or scaffold them with the CLI generator — see below):
|
|
19
19
|
|
|
20
20
|
```ruby
|
|
21
21
|
# app/triggers/users_email_validation.rb
|
|
@@ -24,11 +24,9 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
|
|
|
24
24
|
on :insert, :update
|
|
25
25
|
function :validate_user_email
|
|
26
26
|
|
|
27
|
-
version 1
|
|
28
|
-
enabled
|
|
27
|
+
self.version = 1
|
|
28
|
+
self.enabled = true
|
|
29
29
|
timing :before
|
|
30
|
-
|
|
31
|
-
when_env :production
|
|
32
30
|
end
|
|
33
31
|
```
|
|
34
32
|
|
|
@@ -61,25 +59,30 @@ function :validate_user_email
|
|
|
61
59
|
Version number for tracking changes:
|
|
62
60
|
|
|
63
61
|
```ruby
|
|
64
|
-
version 1 # Increment when trigger logic changes
|
|
62
|
+
self.version = 1 # Increment when trigger logic changes
|
|
65
63
|
```
|
|
66
64
|
|
|
67
65
|
#### `enabled`
|
|
68
|
-
Initial state of the trigger:
|
|
66
|
+
Initial state of the trigger (defaults to `true`):
|
|
69
67
|
|
|
70
68
|
```ruby
|
|
71
|
-
enabled true # Trigger is active
|
|
72
|
-
enabled false # Trigger is inactive
|
|
69
|
+
self.enabled = true # Trigger is active (default)
|
|
70
|
+
self.enabled = false # Trigger is inactive
|
|
73
71
|
```
|
|
74
72
|
|
|
75
|
-
#### `
|
|
76
|
-
|
|
73
|
+
#### `for_each_row` / `for_each_statement`
|
|
74
|
+
PostgreSQL execution granularity (defaults to row-level):
|
|
77
75
|
|
|
78
76
|
```ruby
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
for_each_row # FOR EACH ROW (default)
|
|
78
|
+
for_each_statement # FOR EACH STATEMENT
|
|
81
79
|
```
|
|
82
80
|
|
|
81
|
+
#### `when_env`
|
|
82
|
+
**Deprecated.** Environment-specific trigger declarations cause schema drift between environments.
|
|
83
|
+
Use application-level configuration to gate trigger behaviour by environment instead.
|
|
84
|
+
Calling `when_env` emits a deprecation warning and will be removed in a future major version.
|
|
85
|
+
|
|
83
86
|
#### `timing`
|
|
84
87
|
Specifies when the trigger fires relative to the event (BEFORE or AFTER):
|
|
85
88
|
|
|
@@ -97,76 +100,44 @@ PgSqlTriggers::DSL.pg_sql_trigger "orders_billing_trigger" do
|
|
|
97
100
|
on :insert, :update
|
|
98
101
|
function :calculate_order_total
|
|
99
102
|
|
|
100
|
-
version 2
|
|
101
|
-
enabled true
|
|
103
|
+
self.version = 2
|
|
104
|
+
self.enabled = true
|
|
102
105
|
timing :after
|
|
103
|
-
|
|
104
|
-
when_env :production, :staging
|
|
106
|
+
for_each_row
|
|
105
107
|
end
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
## Trigger Generator
|
|
109
111
|
|
|
110
|
-
PgSqlTriggers provides a
|
|
112
|
+
PgSqlTriggers provides a CLI generator that scaffolds a DSL definition and migration together from the command line.
|
|
111
113
|
|
|
112
|
-
###
|
|
114
|
+
### CLI Generator
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
116
|
+
```bash
|
|
117
|
+
rails generate pg_sql_triggers:trigger TRIGGER_NAME TABLE_NAME [EVENTS...] [--timing before|after] [--function fn_name]
|
|
118
|
+
```
|
|
137
119
|
|
|
138
|
-
|
|
120
|
+
Example:
|
|
139
121
|
|
|
140
122
|
```bash
|
|
141
|
-
|
|
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
|
|
123
|
+
rails generate pg_sql_triggers:trigger users_email_validation users insert update --timing before --function validate_user_email
|
|
146
124
|
```
|
|
147
125
|
|
|
148
|
-
This creates
|
|
126
|
+
This creates:
|
|
127
|
+
- `app/triggers/users_email_validation.rb` — DSL stub
|
|
128
|
+
- `db/triggers/TIMESTAMP_users_email_validation.rb` — migration with function + trigger SQL
|
|
149
129
|
|
|
150
|
-
|
|
130
|
+
Files are written directly into the working tree and go through version control and code review like any other source file.
|
|
151
131
|
|
|
152
|
-
|
|
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
|
|
132
|
+
### Migration-Only Generator
|
|
159
133
|
|
|
160
|
-
|
|
134
|
+
To generate a standalone trigger migration without a DSL stub:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
rails generate pg_sql_triggers:trigger_migration add_user_validation
|
|
138
|
+
```
|
|
161
139
|
|
|
162
|
-
|
|
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)
|
|
140
|
+
This creates a timestamped file in `db/triggers/` that you can edit to add your trigger logic.
|
|
170
141
|
|
|
171
142
|
## Trigger Migrations
|
|
172
143
|
|
|
@@ -177,7 +148,7 @@ Trigger migrations work similarly to Rails schema migrations but are specificall
|
|
|
177
148
|
Create a new trigger migration:
|
|
178
149
|
|
|
179
150
|
```bash
|
|
180
|
-
rails generate
|
|
151
|
+
rails generate pg_sql_triggers:trigger_migration add_validation_trigger
|
|
181
152
|
```
|
|
182
153
|
|
|
183
154
|
This creates a timestamped file in `db/triggers/`:
|
data/docs/web-ui.md
CHANGED
|
@@ -8,7 +8,6 @@ The PgSqlTriggers web interface provides a visual dashboard for managing trigger
|
|
|
8
8
|
- [Dashboard Overview](#dashboard-overview)
|
|
9
9
|
- [Managing Triggers](#managing-triggers)
|
|
10
10
|
- [Migration Management](#migration-management)
|
|
11
|
-
- [SQL Capsules](#sql-capsules)
|
|
12
11
|
- [Audit Log](#audit-log)
|
|
13
12
|
- [Permissions and Safety](#permissions-and-safety)
|
|
14
13
|
|
|
@@ -96,7 +95,7 @@ Each table row displays:
|
|
|
96
95
|
- **Status**: Summary of enabled/disabled triggers
|
|
97
96
|
- **Actions**:
|
|
98
97
|
- "View Details" - Navigate to the table detail page
|
|
99
|
-
- "Create Trigger" -
|
|
98
|
+
- "Create Trigger" - Scaffold a new trigger for this table using the CLI generator (see [Trigger Generator](usage-guide.md#trigger-generator))
|
|
100
99
|
|
|
101
100
|
### Table Detail Page
|
|
102
101
|
|
|
@@ -303,83 +302,6 @@ After each migration action:
|
|
|
303
302
|
- **Error**: Red flash message with error details
|
|
304
303
|
- **Warnings**: Yellow flash message if issues occurred
|
|
305
304
|
|
|
306
|
-
## SQL Capsules
|
|
307
|
-
|
|
308
|
-
SQL Capsules provide emergency escape hatches for executing SQL directly with comprehensive safety checks and audit logging.
|
|
309
|
-
|
|
310
|
-
### When to Use SQL Capsules
|
|
311
|
-
|
|
312
|
-
Use SQL Capsules for:
|
|
313
|
-
- Emergency fixes in production
|
|
314
|
-
- Critical data corrections
|
|
315
|
-
- Testing SQL functions
|
|
316
|
-
- Debugging trigger behavior
|
|
317
|
-
- One-off database operations
|
|
318
|
-
|
|
319
|
-
### Creating and Executing SQL Capsules
|
|
320
|
-
|
|
321
|
-
1. Navigate to "SQL Capsules" → "New SQL Capsule"
|
|
322
|
-
2. Fill in the capsule form:
|
|
323
|
-
- **Name**: Unique identifier (alphanumeric, underscores, hyphens only)
|
|
324
|
-
- **Environment**: Target environment (e.g., production, staging)
|
|
325
|
-
- **Purpose**: Detailed explanation of what the SQL does and why (required for audit trail)
|
|
326
|
-
- **SQL**: The SQL statement(s) to execute
|
|
327
|
-
3. Click "Create and Execute" or "Save for Later"
|
|
328
|
-
4. Review the capsule details on the confirmation page
|
|
329
|
-
5. In protected environments, enter confirmation text when prompted
|
|
330
|
-
6. Click "Execute" to run the SQL
|
|
331
|
-
7. Review the execution results
|
|
332
|
-
|
|
333
|
-
### Viewing Capsule History
|
|
334
|
-
|
|
335
|
-
1. Navigate to "SQL Capsules" → "History"
|
|
336
|
-
2. View list of previously executed capsules with:
|
|
337
|
-
- Name and purpose
|
|
338
|
-
- Environment and timestamp
|
|
339
|
-
- SQL checksum
|
|
340
|
-
- Execution status
|
|
341
|
-
3. Click on a capsule to view details
|
|
342
|
-
4. Re-execute historical capsules if needed
|
|
343
|
-
|
|
344
|
-
### Safety Features
|
|
345
|
-
|
|
346
|
-
- **Admin Permission Required**: Only Admin users can create and execute SQL capsules
|
|
347
|
-
- **Production Protection**: Requires typed confirmation in protected environments
|
|
348
|
-
- **Kill Switch Integration**: All executions are protected by kill switch
|
|
349
|
-
- **Comprehensive Logging**: All operations logged with actor, timestamp, and checksum
|
|
350
|
-
- **Transactional Execution**: SQL runs in a transaction and rolls back on error
|
|
351
|
-
- **Registry Storage**: All capsules are stored in the registry with checksums
|
|
352
|
-
- **Purpose Tracking**: Required purpose field ensures all executions are documented
|
|
353
|
-
|
|
354
|
-
### Example SQL Capsules
|
|
355
|
-
|
|
356
|
-
#### View All Triggers
|
|
357
|
-
```sql
|
|
358
|
-
SELECT
|
|
359
|
-
trigger_name,
|
|
360
|
-
event_object_table,
|
|
361
|
-
action_timing,
|
|
362
|
-
event_manipulation
|
|
363
|
-
FROM information_schema.triggers
|
|
364
|
-
WHERE trigger_schema = 'public';
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
#### Check Function Definitions
|
|
368
|
-
```sql
|
|
369
|
-
SELECT
|
|
370
|
-
routine_name,
|
|
371
|
-
routine_type
|
|
372
|
-
FROM information_schema.routines
|
|
373
|
-
WHERE routine_schema = 'public'
|
|
374
|
-
AND routine_name LIKE '%trigger%';
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
#### Verify Trigger State
|
|
378
|
-
```sql
|
|
379
|
-
SELECT * FROM pg_sql_triggers_registry
|
|
380
|
-
WHERE trigger_name = 'users_email_validation';
|
|
381
|
-
```
|
|
382
|
-
|
|
383
305
|
## Audit Log
|
|
384
306
|
|
|
385
307
|
The Audit Log provides a comprehensive view of all trigger operations performed through the web UI, console APIs, and CLI. This feature is essential for compliance, debugging, and tracking changes to your trigger ecosystem.
|
|
@@ -446,7 +368,6 @@ All of the following operations are logged to the audit log:
|
|
|
446
368
|
- **Disable Trigger**: Success/failure, before/after state
|
|
447
369
|
- **Drop Trigger**: Success/failure, reason, state changes
|
|
448
370
|
- **Re-execute Trigger**: Success/failure, reason, drift diff information
|
|
449
|
-
- **SQL Capsule Execution**: Success/failure, capsule details
|
|
450
371
|
- **Migration Operations**: Up, down, and redo operations (infrastructure ready)
|
|
451
372
|
|
|
452
373
|
Each log entry includes:
|
|
@@ -497,7 +418,6 @@ Cannot:
|
|
|
497
418
|
#### Admin (Full Access)
|
|
498
419
|
- All Operator permissions
|
|
499
420
|
- Drop triggers
|
|
500
|
-
- Execute SQL via capsules
|
|
501
421
|
- Modify registry directly
|
|
502
422
|
|
|
503
423
|
### Kill Switch Protection
|
|
@@ -529,9 +449,9 @@ PgSqlTriggers.configure do |config|
|
|
|
529
449
|
case action
|
|
530
450
|
when :view_triggers, :view_diffs
|
|
531
451
|
user.present? # Viewer level
|
|
532
|
-
when :enable_trigger, :disable_trigger, :apply_trigger, :
|
|
452
|
+
when :enable_trigger, :disable_trigger, :apply_trigger, :test_trigger
|
|
533
453
|
user.operator? || user.admin? # Operator level
|
|
534
|
-
when :drop_trigger, :
|
|
454
|
+
when :drop_trigger, :override_drift
|
|
535
455
|
user.admin? # Admin level
|
|
536
456
|
else
|
|
537
457
|
false
|
|
@@ -550,32 +470,12 @@ end
|
|
|
550
470
|
### Main Dashboard
|
|
551
471
|

|
|
552
472
|
|
|
553
|
-
### Trigger Generator
|
|
554
|
-
|
|
555
|
-
The trigger generator provides a comprehensive form for creating triggers:
|
|
556
|
-
|
|
557
|
-
1. **Basic Information**: Trigger name, table name, function name, and function body
|
|
558
|
-
2. **Trigger Events**: Select timing (BEFORE/AFTER) and events (INSERT, UPDATE, DELETE, TRUNCATE)
|
|
559
|
-
3. **Configuration**: Version, environments, WHEN condition, and enabled state
|
|
560
|
-
4. **Preview**: Review generated DSL and migration code with timing and condition information
|
|
561
|
-
|
|
562
|
-
The preview page displays:
|
|
563
|
-
- Generated DSL code with timing
|
|
564
|
-
- Trigger configuration summary (timing, events, table, function, condition)
|
|
565
|
-
- PL/pgSQL function body (editable)
|
|
566
|
-
- SQL validation results
|
|
567
|
-
|
|
568
|
-

|
|
569
|
-
|
|
570
473
|
### Migration Management
|
|
571
474
|

|
|
572
475
|
|
|
573
476
|
### Kill Switch Protection
|
|
574
477
|

|
|
575
478
|
|
|
576
|
-
### SQL Capsules
|
|
577
|
-

|
|
578
|
-
|
|
579
479
|
## Dashboard Enhancements (v1.3.0+)
|
|
580
480
|
|
|
581
481
|
### Last Applied Column
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= trigger_class_name %> < PgSqlTriggers::Migration
|
|
4
|
+
def up
|
|
5
|
+
execute <<-SQL
|
|
6
|
+
CREATE OR REPLACE FUNCTION <%= function_name %>()
|
|
7
|
+
RETURNS TRIGGER AS $$
|
|
8
|
+
BEGIN
|
|
9
|
+
-- TODO: implement trigger logic
|
|
10
|
+
RETURN NEW;
|
|
11
|
+
END;
|
|
12
|
+
$$ LANGUAGE plpgsql;
|
|
13
|
+
SQL
|
|
14
|
+
|
|
15
|
+
execute <<-SQL
|
|
16
|
+
CREATE TRIGGER <%= trigger_name %>
|
|
17
|
+
<%= timing.upcase %> <%= events_sql %> ON <%= table_name %>
|
|
18
|
+
FOR EACH ROW
|
|
19
|
+
EXECUTE FUNCTION <%= function_name %>();
|
|
20
|
+
SQL
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def down
|
|
24
|
+
execute <<-SQL
|
|
25
|
+
DROP TRIGGER IF EXISTS <%= trigger_name %> ON <%= table_name %>;
|
|
26
|
+
DROP FUNCTION IF EXISTS <%= function_name %>();
|
|
27
|
+
SQL
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
|
|
7
|
+
module PgSqlTriggers
|
|
8
|
+
module Generators
|
|
9
|
+
class TriggerGenerator < Rails::Generators::Base
|
|
10
|
+
include Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Generates a pg_sql_triggers DSL file and migration for a new trigger."
|
|
15
|
+
|
|
16
|
+
argument :trigger_name, type: :string,
|
|
17
|
+
desc: "Name of the trigger (e.g. notify_on_insert_users)"
|
|
18
|
+
argument :table_name, type: :string,
|
|
19
|
+
desc: "Database table the trigger attaches to (e.g. users)"
|
|
20
|
+
argument :events, type: :array, default: ["insert"], banner: "EVENT ...",
|
|
21
|
+
desc: "Trigger events: insert, update, delete (default: insert)"
|
|
22
|
+
|
|
23
|
+
class_option :timing, type: :string, default: "before",
|
|
24
|
+
desc: "Trigger timing: before or after (default: before)"
|
|
25
|
+
class_option :function, type: :string,
|
|
26
|
+
desc: "Function name (default: TRIGGER_NAME_function)"
|
|
27
|
+
|
|
28
|
+
def self.next_migration_number(_dirname)
|
|
29
|
+
existing = if Rails.root.join("db/triggers").exist?
|
|
30
|
+
Rails.root.glob("db/triggers/*.rb")
|
|
31
|
+
.map { |f| File.basename(f, ".rb").split("_").first.to_i }
|
|
32
|
+
.reject(&:zero?)
|
|
33
|
+
.max || 0
|
|
34
|
+
else
|
|
35
|
+
0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
now = Time.now.utc
|
|
39
|
+
base = now.strftime("%Y%m%d%H%M%S").to_i
|
|
40
|
+
base = existing + 1 if existing.positive? && base <= existing
|
|
41
|
+
base
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_dsl_file
|
|
45
|
+
template "trigger_dsl.rb.tt", "app/triggers/#{trigger_name}.rb"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_migration_file
|
|
49
|
+
template "trigger_migration_full.rb.tt", "db/triggers/#{migration_file_name}.rb"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def function_name
|
|
55
|
+
options[:function].presence || "#{trigger_name}_function"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def timing
|
|
59
|
+
options[:timing]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def events_list
|
|
63
|
+
events.map { |e| ":#{e}" }.join(", ")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def events_sql
|
|
67
|
+
events.map(&:upcase).join(" OR ")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def trigger_class_name
|
|
71
|
+
"Add#{trigger_name.camelize}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def migration_file_name
|
|
75
|
+
"#{migration_number}_#{trigger_name}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def migration_number
|
|
79
|
+
self.class.next_migration_number(nil)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -22,12 +22,12 @@ module PgSqlTriggers
|
|
|
22
22
|
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
23
23
|
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
24
24
|
WHERE NOT t.tgisinternal
|
|
25
|
-
AND n.nspname =
|
|
25
|
+
AND n.nspname = $1
|
|
26
26
|
AND t.tgname NOT LIKE 'RI_%'
|
|
27
27
|
ORDER BY c.relname, t.tgname;
|
|
28
28
|
SQL
|
|
29
29
|
|
|
30
|
-
execute_query(sql)
|
|
30
|
+
execute_query(sql, [schema_name])
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Fetch single trigger
|
|
@@ -49,10 +49,10 @@ module PgSqlTriggers
|
|
|
49
49
|
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
50
50
|
WHERE t.tgname = $1
|
|
51
51
|
AND NOT t.tgisinternal
|
|
52
|
-
AND n.nspname =
|
|
52
|
+
AND n.nspname = $2;
|
|
53
53
|
SQL
|
|
54
54
|
|
|
55
|
-
result = execute_query(sql, [trigger_name])
|
|
55
|
+
result = execute_query(sql, [trigger_name, schema_name])
|
|
56
56
|
result.first
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -75,12 +75,12 @@ module PgSqlTriggers
|
|
|
75
75
|
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
76
76
|
WHERE c.relname = $1
|
|
77
77
|
AND NOT t.tgisinternal
|
|
78
|
-
AND n.nspname =
|
|
78
|
+
AND n.nspname = $2
|
|
79
79
|
AND t.tgname NOT LIKE 'RI_%'
|
|
80
80
|
ORDER BY t.tgname;
|
|
81
81
|
SQL
|
|
82
82
|
|
|
83
|
-
execute_query(sql, [table_name])
|
|
83
|
+
execute_query(sql, [table_name, schema_name])
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Fetch function body by function name
|
|
@@ -92,15 +92,19 @@ module PgSqlTriggers
|
|
|
92
92
|
FROM pg_proc p
|
|
93
93
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
94
94
|
WHERE p.proname = $1
|
|
95
|
-
AND n.nspname =
|
|
95
|
+
AND n.nspname = $2;
|
|
96
96
|
SQL
|
|
97
97
|
|
|
98
|
-
result = execute_query(sql, [function_name])
|
|
98
|
+
result = execute_query(sql, [function_name, schema_name])
|
|
99
99
|
result.first
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
private
|
|
103
103
|
|
|
104
|
+
def schema_name
|
|
105
|
+
PgSqlTriggers.db_schema.to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
104
108
|
def execute_query(sql, params = [])
|
|
105
109
|
if params.any?
|
|
106
110
|
# Use ActiveRecord's connection to execute parameterized queries
|