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
data/docs/web-ui.md ADDED
@@ -0,0 +1,353 @@
1
+ # Web UI Documentation
2
+
3
+ The PgSqlTriggers web interface provides a visual dashboard for managing triggers, migrations, and monitoring drift status.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Accessing the Web UI](#accessing-the-web-ui)
8
+ - [Dashboard Overview](#dashboard-overview)
9
+ - [Managing Triggers](#managing-triggers)
10
+ - [Migration Management](#migration-management)
11
+ - [SQL Capsules](#sql-capsules)
12
+ - [Permissions and Safety](#permissions-and-safety)
13
+
14
+ ## Accessing the Web UI
15
+
16
+ By default, the web UI is mounted at:
17
+
18
+ ```
19
+ http://localhost:3000/pg_sql_triggers
20
+ ```
21
+
22
+ You can customize the mount path in your routes:
23
+
24
+ ```ruby
25
+ # config/routes.rb
26
+ Rails.application.routes.draw do
27
+ mount PgSqlTriggers::Engine, at: "/admin/triggers" # Custom path
28
+ end
29
+ ```
30
+
31
+ ## Dashboard Overview
32
+
33
+ The dashboard provides a comprehensive view of your trigger ecosystem.
34
+
35
+ ### Main Features
36
+
37
+ 1. **Trigger List**: View all triggers with their current status
38
+ 2. **Drift Detection**: Visual indicators for drift states
39
+ 3. **Migration Status**: See pending and applied migrations
40
+ 4. **Quick Actions**: Enable/disable triggers, run migrations
41
+ 5. **Kill Switch Status**: Production environment indicator
42
+
43
+ ![Dashboard Screenshot](screenshots/dashboard.png)
44
+
45
+ ### Status Indicators
46
+
47
+ - **✓ Green**: Managed & In Sync
48
+ - **⚠ Yellow**: Drifted or Manual Override
49
+ - **✗ Red**: Dropped or Error
50
+ - **○ Gray**: Disabled
51
+ - **? Purple**: Unknown
52
+
53
+ ## Managing Triggers
54
+
55
+ ### Viewing Trigger Details
56
+
57
+ Click on any trigger to view:
58
+ - Current status and drift state
59
+ - Table and function information
60
+ - Version history
61
+ - Enabled/disabled state
62
+ - Environment configuration
63
+
64
+ ### Enabling/Disabling Triggers
65
+
66
+ #### Enable a Trigger
67
+
68
+ 1. Navigate to the trigger in the dashboard
69
+ 2. Click the "Enable" button
70
+ 3. In production environments, enter the confirmation text when prompted
71
+ 4. Confirm the action
72
+
73
+ #### Disable a Trigger
74
+
75
+ 1. Navigate to the trigger in the dashboard
76
+ 2. Click the "Disable" button
77
+ 3. In production environments, enter the confirmation text when prompted
78
+ 4. Confirm the action
79
+
80
+ ### Viewing Drift Status
81
+
82
+ The dashboard automatically shows drift status for each trigger:
83
+
84
+ - **In Sync**: Green checkmark, no action needed
85
+ - **Drifted**: Yellow warning, shows differences between DSL and database
86
+ - **Manual Override**: Yellow warning, indicates changes made outside PgSqlTriggers
87
+ - **Dropped**: Red X, trigger removed from database but still in registry
88
+
89
+ ### Trigger Actions
90
+
91
+ Available actions depend on trigger state and your permissions:
92
+
93
+ - **Enable/Disable**: Toggle trigger activation
94
+ - **Apply**: Apply generated trigger definition
95
+ - **Drop**: Remove trigger from database (Admin only)
96
+ - **View SQL**: See the trigger's SQL definition
97
+ - **View Diff**: Compare DSL vs database state
98
+
99
+ ## Migration Management
100
+
101
+ The Web UI provides full migration management capabilities.
102
+
103
+ ### Migration Status View
104
+
105
+ Navigate to the "Migrations" tab to see:
106
+
107
+ - **Pending Migrations**: Not yet applied (down state)
108
+ - **Applied Migrations**: Successfully run (up state)
109
+ - **Migration Details**: Timestamp, name, and status
110
+
111
+ ### Applying Migrations
112
+
113
+ #### Apply All Pending Migrations
114
+
115
+ 1. Click "Apply All Pending Migrations" button
116
+ 2. Review the list of migrations to be applied
117
+ 3. In production, enter confirmation text: `EXECUTE UI_MIGRATION_UP`
118
+ 4. Confirm the action
119
+ 5. Wait for completion and review results
120
+
121
+ #### Apply Individual Migration
122
+
123
+ 1. Find the migration in the status table
124
+ 2. Click the "Up" button next to the migration
125
+ 3. In production, enter confirmation text
126
+ 4. Confirm the action
127
+
128
+ ### Rolling Back Migrations
129
+
130
+ #### Rollback Last Migration
131
+
132
+ 1. Click "Rollback Last Migration" button
133
+ 2. Review which migration will be rolled back
134
+ 3. In production, enter confirmation text: `EXECUTE UI_MIGRATION_DOWN`
135
+ 4. Confirm the action
136
+
137
+ #### Rollback Individual Migration
138
+
139
+ 1. Find the migration in the status table
140
+ 2. Click the "Down" button next to the migration
141
+ 3. In production, enter confirmation text
142
+ 4. Confirm the action
143
+
144
+ ### Redo Migrations
145
+
146
+ Redo (rollback and re-apply) a migration:
147
+
148
+ 1. Click "Redo Last Migration" for the most recent migration
149
+ 2. Or click "Redo" button next to a specific migration
150
+ 3. In production, enter confirmation text: `EXECUTE UI_MIGRATION_REDO`
151
+ 4. Confirm the action
152
+
153
+ ### Migration Feedback
154
+
155
+ After each migration action:
156
+ - **Success**: Green flash message with details
157
+ - **Error**: Red flash message with error details
158
+ - **Warnings**: Yellow flash message if issues occurred
159
+
160
+ ## SQL Capsules
161
+
162
+ SQL Capsules provide emergency escape hatches for executing SQL directly.
163
+
164
+ ### When to Use SQL Capsules
165
+
166
+ Use SQL Capsules for:
167
+ - Emergency fixes in production
168
+ - Quick database queries
169
+ - Testing SQL functions
170
+ - Debugging trigger behavior
171
+
172
+ ### Executing SQL
173
+
174
+ 1. Navigate to "SQL Capsules" tab
175
+ 2. Enter your SQL query:
176
+ ```sql
177
+ SELECT * FROM pg_sql_triggers_registry;
178
+ ```
179
+ 3. Click "Execute"
180
+ 4. In production, enter confirmation text: `EXECUTE SQL`
181
+ 5. Review results in the output panel
182
+
183
+ ### Safety Features
184
+
185
+ - **Production Protection**: Requires confirmation in protected environments
186
+ - **Read-Only Mode**: Optional configuration for limiting to SELECT queries
187
+ - **Query Logging**: All SQL execution is logged
188
+ - **Permission Checks**: Requires Admin permission level
189
+
190
+ ### Example SQL Capsules
191
+
192
+ #### View All Triggers
193
+ ```sql
194
+ SELECT
195
+ trigger_name,
196
+ event_object_table,
197
+ action_timing,
198
+ event_manipulation
199
+ FROM information_schema.triggers
200
+ WHERE trigger_schema = 'public';
201
+ ```
202
+
203
+ #### Check Function Definitions
204
+ ```sql
205
+ SELECT
206
+ routine_name,
207
+ routine_type
208
+ FROM information_schema.routines
209
+ WHERE routine_schema = 'public'
210
+ AND routine_name LIKE '%trigger%';
211
+ ```
212
+
213
+ #### Verify Trigger State
214
+ ```sql
215
+ SELECT * FROM pg_sql_triggers_registry
216
+ WHERE trigger_name = 'users_email_validation';
217
+ ```
218
+
219
+ ## Permissions and Safety
220
+
221
+ ### Permission Levels
222
+
223
+ The Web UI enforces three permission levels:
224
+
225
+ #### Viewer (Read-Only)
226
+ - View triggers and their status
227
+ - View drift information
228
+ - Check migration status
229
+ - View SQL definitions
230
+
231
+ Cannot:
232
+ - Enable/disable triggers
233
+ - Run migrations
234
+ - Execute SQL
235
+ - Drop triggers
236
+
237
+ #### Operator
238
+ - All Viewer permissions
239
+ - Enable/disable triggers
240
+ - Apply generated triggers
241
+ - Run migrations (up/down/redo)
242
+
243
+ Cannot:
244
+ - Drop triggers
245
+ - Execute arbitrary SQL
246
+
247
+ #### Admin (Full Access)
248
+ - All Operator permissions
249
+ - Drop triggers
250
+ - Execute SQL via capsules
251
+ - Modify registry directly
252
+
253
+ ### Kill Switch Protection
254
+
255
+ In protected environments (production, staging), the Web UI enforces additional safety:
256
+
257
+ 1. **Status Indicator**: Kill switch badge shows protection status
258
+ 2. **Confirmation Required**: Dangerous operations require typed confirmation
259
+ 3. **Warning Banners**: Visual alerts for production environment
260
+ 4. **Audit Logging**: All protected operations are logged
261
+
262
+ ### Configuring Permissions
263
+
264
+ Set up custom permission checking in the initializer:
265
+
266
+ ```ruby
267
+ # config/initializers/pg_sql_triggers.rb
268
+ PgSqlTriggers.configure do |config|
269
+ config.permission_checker = ->(actor, action, environment) {
270
+ user = User.find(actor[:id])
271
+
272
+ case action
273
+ when :view
274
+ user.present?
275
+ when :operate
276
+ user.admin? || user.operator?
277
+ when :admin
278
+ user.admin?
279
+ else
280
+ false
281
+ end
282
+ }
283
+ end
284
+ ```
285
+
286
+ ## Screenshots
287
+
288
+ ### Main Dashboard
289
+ ![Main Dashboard](screenshots/dashboard.png)
290
+
291
+ ### Trigger Generator
292
+
293
+ The trigger generator provides a comprehensive form for creating triggers:
294
+
295
+ 1. **Basic Information**: Trigger name, table name, function name, and function body
296
+ 2. **Trigger Events**: Select timing (BEFORE/AFTER) and events (INSERT, UPDATE, DELETE, TRUNCATE)
297
+ 3. **Configuration**: Version, environments, WHEN condition, and enabled state
298
+ 4. **Preview**: Review generated DSL and migration code with timing and condition information
299
+
300
+ The preview page displays:
301
+ - Generated DSL code with timing
302
+ - Trigger configuration summary (timing, events, table, function, condition)
303
+ - PL/pgSQL function body (editable)
304
+ - SQL validation results
305
+
306
+ ![Trigger Generator](screenshots/generator.png)
307
+
308
+ ### Migration Management
309
+ ![Migration Management](screenshots/migrations.png)
310
+
311
+ ### Kill Switch Protection
312
+ ![Kill Switch](screenshots/kill-switch.png)
313
+
314
+ ### SQL Capsules
315
+ ![SQL Capsules](screenshots/sql-capsules.png)
316
+
317
+ ## Tips and Best Practices
318
+
319
+ 1. **Check Status Regularly**: Monitor drift detection to catch unexpected changes
320
+ 2. **Use Confirmations**: Don't bypass production confirmations without understanding the impact
321
+ 3. **Test in Development**: Always test UI actions in development before production
322
+ 4. **Review Logs**: Check application logs after important operations
323
+ 5. **Document Changes**: Add comments when making manual changes via SQL Capsules
324
+
325
+ ## Troubleshooting
326
+
327
+ ### UI Not Accessible
328
+
329
+ Check that:
330
+ 1. The engine is mounted in routes
331
+ 2. Your database migrations are up to date
332
+ 3. The registry table exists
333
+
334
+ ### Permission Denied
335
+
336
+ Verify:
337
+ 1. Your permission checker is configured correctly
338
+ 2. Your user has the required permission level
339
+ 3. The kill switch isn't blocking your operation
340
+
341
+ ### Migration Failures
342
+
343
+ If migrations fail:
344
+ 1. Check the error message in the flash notification
345
+ 2. Review the migration SQL in `db/triggers/`
346
+ 3. Test the SQL in a console first
347
+ 4. Check database logs for detailed error information
348
+
349
+ ## Next Steps
350
+
351
+ - [Kill Switch Documentation](kill-switch.md) - Understand production safety
352
+ - [Configuration](configuration.md) - Customize UI behavior
353
+ - [API Reference](api-reference.md) - Programmatic access
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.0]
3
+ class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.1]
4
4
  def change
5
5
  # Registry table - source of truth for all triggers
6
6
  create_table :pg_sql_triggers_registry do |t|
@@ -14,6 +14,7 @@ class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.0]
14
14
  t.text :definition # Stored DSL or SQL definition
15
15
  t.text :function_body # The actual function body
16
16
  t.text :condition # Optional WHEN clause condition
17
+ t.string :timing, default: "before", null: false # Trigger timing: before or after
17
18
  t.datetime :installed_at
18
19
  t.datetime :last_verified_at
19
20
 
@@ -25,6 +26,7 @@ class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.0]
25
26
  add_index :pg_sql_triggers_registry, :enabled
26
27
  add_index :pg_sql_triggers_registry, :source
27
28
  add_index :pg_sql_triggers_registry, :environment
29
+ add_index :pg_sql_triggers_registry, :timing
28
30
 
29
31
  # Trigger migrations table - tracks which trigger migrations have been run
30
32
  create_table :trigger_migrations do |t|
@@ -1,10 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  PgSqlTriggers.configure do |config|
4
- # Enable or disable the production kill switch
5
- # When enabled, all destructive operations in production require explicit confirmation
4
+ # ========== Kill Switch Configuration ==========
5
+ # The Kill Switch is a safety mechanism that prevents accidental destructive operations
6
+ # in protected environments (production, staging, etc.)
7
+
8
+ # Enable or disable the kill switch globally
9
+ # Default: true (recommended for safety)
6
10
  config.kill_switch_enabled = true
7
11
 
12
+ # Specify which environments should be protected by the kill switch
13
+ # Default: %i[production staging]
14
+ config.kill_switch_environments = %i[production staging]
15
+
16
+ # Require confirmation text for kill switch overrides
17
+ # When true, users must type a specific confirmation text to proceed
18
+ # Default: true (recommended for maximum safety)
19
+ config.kill_switch_confirmation_required = true
20
+
21
+ # Custom confirmation pattern generator
22
+ # Takes an operation symbol and returns the required confirmation text
23
+ # Default: "EXECUTE <OPERATION_NAME>"
24
+ config.kill_switch_confirmation_pattern = ->(operation) { "EXECUTE #{operation.to_s.upcase}" }
25
+
26
+ # Logger for kill switch events
27
+ # Default: Rails.logger
28
+ config.kill_switch_logger = Rails.logger
29
+
30
+ # Enable audit trail for kill switch events (optional enhancement)
31
+ # When enabled, all kill switch events are logged to a database table
32
+ # Default: false (can be enabled later)
33
+ # config.kill_switch_audit_trail_enabled = false
34
+
35
+ # Time-window auto-lock configuration (optional enhancement)
36
+ # Automatically enable kill switch during specific time windows
37
+ # Default: false
38
+ # config.kill_switch_auto_lock_enabled = false
39
+ # config.kill_switch_auto_lock_window = 30.minutes
40
+ # config.kill_switch_auto_lock_after = -> { Time.current.hour.between?(22, 6) } # Night hours
41
+
8
42
  # Set the default environment detection
9
43
  # By default, uses Rails.env
10
44
  config.default_environment = -> { Rails.env }
@@ -24,4 +58,12 @@ PgSqlTriggers.configure do |config|
24
58
  # Add additional tables you want to exclude:
25
59
  # config.excluded_tables = %w[audit_logs temporary_data]
26
60
  config.excluded_tables = []
61
+
62
+ # ========== Migration Safety Configuration ==========
63
+ # Prevent unsafe DROP + CREATE operations in migrations
64
+ # When false (default), migrations with DROP + CREATE patterns will be blocked
65
+ # Set to true to allow unsafe operations (not recommended)
66
+ # You can also override per-migration with ALLOW_UNSAFE_MIGRATIONS=true environment variable
67
+ # Default: false (recommended for safety)
68
+ config.allow_unsafe_migrations = false
27
69
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ module Drift
5
+ module DbQueries
6
+ class << self
7
+ # Fetch all triggers from database
8
+ def all_triggers
9
+ sql = <<~SQL.squish
10
+ SELECT
11
+ t.oid AS trigger_oid,
12
+ t.tgname AS trigger_name,
13
+ c.relname AS table_name,
14
+ n.nspname AS schema_name,
15
+ p.proname AS function_name,
16
+ pg_get_triggerdef(t.oid) AS trigger_definition,
17
+ pg_get_functiondef(p.oid) AS function_definition,
18
+ t.tgenabled AS enabled,
19
+ t.tgisinternal AS is_internal
20
+ FROM pg_trigger t
21
+ JOIN pg_class c ON t.tgrelid = c.oid
22
+ JOIN pg_namespace n ON c.relnamespace = n.oid
23
+ JOIN pg_proc p ON t.tgfoid = p.oid
24
+ WHERE NOT t.tgisinternal
25
+ AND n.nspname = 'public'
26
+ AND t.tgname NOT LIKE 'RI_%'
27
+ ORDER BY c.relname, t.tgname;
28
+ SQL
29
+
30
+ execute_query(sql)
31
+ end
32
+
33
+ # Fetch single trigger
34
+ def find_trigger(trigger_name)
35
+ sql = <<~SQL.squish
36
+ SELECT
37
+ t.oid AS trigger_oid,
38
+ t.tgname AS trigger_name,
39
+ c.relname AS table_name,
40
+ n.nspname AS schema_name,
41
+ p.proname AS function_name,
42
+ pg_get_triggerdef(t.oid) AS trigger_definition,
43
+ pg_get_functiondef(p.oid) AS function_definition,
44
+ t.tgenabled AS enabled,
45
+ t.tgisinternal AS is_internal
46
+ FROM pg_trigger t
47
+ JOIN pg_class c ON t.tgrelid = c.oid
48
+ JOIN pg_namespace n ON c.relnamespace = n.oid
49
+ JOIN pg_proc p ON t.tgfoid = p.oid
50
+ WHERE t.tgname = $1
51
+ AND NOT t.tgisinternal
52
+ AND n.nspname = 'public';
53
+ SQL
54
+
55
+ result = execute_query(sql, [trigger_name])
56
+ result.first
57
+ end
58
+
59
+ # Fetch triggers for a specific table
60
+ def find_triggers_for_table(table_name)
61
+ sql = <<~SQL.squish
62
+ SELECT
63
+ t.oid AS trigger_oid,
64
+ t.tgname AS trigger_name,
65
+ c.relname AS table_name,
66
+ n.nspname AS schema_name,
67
+ p.proname AS function_name,
68
+ pg_get_triggerdef(t.oid) AS trigger_definition,
69
+ pg_get_functiondef(p.oid) AS function_definition,
70
+ t.tgenabled AS enabled,
71
+ t.tgisinternal AS is_internal
72
+ FROM pg_trigger t
73
+ JOIN pg_class c ON t.tgrelid = c.oid
74
+ JOIN pg_namespace n ON c.relnamespace = n.oid
75
+ JOIN pg_proc p ON t.tgfoid = p.oid
76
+ WHERE c.relname = $1
77
+ AND NOT t.tgisinternal
78
+ AND n.nspname = 'public'
79
+ AND t.tgname NOT LIKE 'RI_%'
80
+ ORDER BY t.tgname;
81
+ SQL
82
+
83
+ execute_query(sql, [table_name])
84
+ end
85
+
86
+ # Fetch function body by function name
87
+ def find_function(function_name)
88
+ sql = <<~SQL.squish
89
+ SELECT
90
+ p.proname AS function_name,
91
+ pg_get_functiondef(p.oid) AS function_definition
92
+ FROM pg_proc p
93
+ JOIN pg_namespace n ON p.pronamespace = n.oid
94
+ WHERE p.proname = $1
95
+ AND n.nspname = 'public';
96
+ SQL
97
+
98
+ result = execute_query(sql, [function_name])
99
+ result.first
100
+ end
101
+
102
+ private
103
+
104
+ def execute_query(sql, params = [])
105
+ if params.any?
106
+ # Use ActiveRecord's connection to execute parameterized queries
107
+ result = ActiveRecord::Base.connection.exec_query(sql, "SQL", params)
108
+ result.to_a
109
+ else
110
+ ActiveRecord::Base.connection.execute(sql).to_a
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end