pg_sql_triggers 1.1.0 → 1.2.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/CHANGELOG.md +61 -0
  4. data/COVERAGE.md +32 -28
  5. data/README.md +31 -2
  6. data/RELEASE.md +1 -1
  7. data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
  8. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
  9. data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
  10. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
  11. data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
  12. data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
  13. data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
  14. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
  15. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
  16. data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
  17. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
  18. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
  19. data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
  20. data/config/routes.rb +9 -0
  21. data/docs/README.md +2 -2
  22. data/docs/api-reference.md +252 -4
  23. data/docs/getting-started.md +1 -1
  24. data/docs/kill-switch.md +3 -3
  25. data/docs/web-ui.md +82 -17
  26. data/lib/generators/pg_sql_triggers/templates/README +1 -1
  27. data/lib/pg_sql_triggers/registry/manager.rb +28 -13
  28. data/lib/pg_sql_triggers/registry.rb +41 -0
  29. data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
  30. data/lib/pg_sql_triggers/sql/executor.rb +200 -0
  31. data/lib/pg_sql_triggers/version.rb +1 -1
  32. metadata +18 -12
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ module SQL
5
+ # Executor handles the execution of SQL capsules with safety checks and logging
6
+ #
7
+ # @example Execute a SQL capsule
8
+ # capsule = PgSqlTriggers::SQL::Capsule.new(name: "fix", ...)
9
+ # result = PgSqlTriggers::SQL::Executor.execute(capsule, actor: current_actor, confirmation: "EXECUTE FIX")
10
+ #
11
+ class Executor
12
+ class << self
13
+ # Executes a SQL capsule with safety checks
14
+ #
15
+ # @param capsule [Capsule] The SQL capsule to execute
16
+ # @param actor [Hash] Information about who is executing the capsule
17
+ # @param confirmation [String, nil] The confirmation text for kill switch
18
+ # @param dry_run [Boolean] If true, only validate without executing
19
+ # @return [Hash] Result of the execution with :success, :message, and :data keys
20
+ def execute(capsule, actor:, confirmation: nil, dry_run: false)
21
+ validate_capsule!(capsule)
22
+
23
+ # Check permissions
24
+ check_permissions!(actor)
25
+
26
+ # Check kill switch
27
+ check_kill_switch!(capsule, actor, confirmation)
28
+
29
+ # Log the execution attempt
30
+ log_execution_attempt(capsule, actor, dry_run)
31
+
32
+ if dry_run
33
+ return {
34
+ success: true,
35
+ message: "Dry run successful. SQL would be executed.",
36
+ data: { checksum: capsule.checksum }
37
+ }
38
+ end
39
+
40
+ # Execute in transaction
41
+ result = execute_in_transaction(capsule, actor)
42
+
43
+ # Update registry after successful execution
44
+ update_registry(capsule) if result[:success]
45
+
46
+ # Log the result
47
+ log_execution_result(capsule, actor, result)
48
+
49
+ result
50
+ rescue StandardError => e
51
+ log_execution_error(capsule, actor, e)
52
+ {
53
+ success: false,
54
+ message: "Execution failed: #{e.message}",
55
+ error: e
56
+ }
57
+ end
58
+
59
+ # Executes a SQL capsule by name from the registry
60
+ #
61
+ # @param capsule_name [String] The name of the capsule to execute
62
+ # @param actor [Hash] Information about who is executing the capsule
63
+ # @param confirmation [String, nil] The confirmation text for kill switch
64
+ # @param dry_run [Boolean] If true, only validate without executing
65
+ # @return [Hash] Result of the execution
66
+ def execute_capsule(capsule_name, actor:, confirmation: nil, dry_run: false)
67
+ capsule = load_capsule_from_registry(capsule_name)
68
+
69
+ unless capsule
70
+ return {
71
+ success: false,
72
+ message: "Capsule '#{capsule_name}' not found in registry"
73
+ }
74
+ end
75
+
76
+ execute(capsule, actor: actor, confirmation: confirmation, dry_run: dry_run)
77
+ end
78
+
79
+ private
80
+
81
+ def validate_capsule!(capsule)
82
+ raise ArgumentError, "Capsule must be a PgSqlTriggers::SQL::Capsule" unless capsule.is_a?(Capsule)
83
+ end
84
+
85
+ def check_permissions!(actor)
86
+ PgSqlTriggers::Permissions.check!(actor, :execute_sql)
87
+ rescue PgSqlTriggers::PermissionError => e
88
+ raise PgSqlTriggers::PermissionError,
89
+ "SQL capsule execution requires Admin role: #{e.message}"
90
+ end
91
+
92
+ def check_kill_switch!(_capsule, actor, confirmation)
93
+ PgSqlTriggers::SQL::KillSwitch.check!(
94
+ operation: :execute_sql_capsule,
95
+ environment: Rails.env,
96
+ confirmation: confirmation,
97
+ actor: actor
98
+ )
99
+ end
100
+
101
+ def execute_in_transaction(capsule, _actor)
102
+ ActiveRecord::Base.transaction do
103
+ result = ActiveRecord::Base.connection.execute(capsule.sql)
104
+
105
+ {
106
+ success: true,
107
+ message: "SQL capsule '#{capsule.name}' executed successfully",
108
+ data: {
109
+ checksum: capsule.checksum,
110
+ rows_affected: result.cmd_tuples || 0
111
+ }
112
+ }
113
+ end
114
+ end
115
+
116
+ def update_registry(capsule)
117
+ # Check if capsule already exists in registry
118
+ registry_entry = PgSqlTriggers::TriggerRegistry.find_or_initialize_by(
119
+ trigger_name: capsule.registry_trigger_name
120
+ )
121
+
122
+ registry_entry.assign_attributes(
123
+ table_name: "manual_sql_execution",
124
+ version: Time.current.to_i,
125
+ checksum: capsule.checksum,
126
+ source: "manual_sql",
127
+ function_body: capsule.sql,
128
+ condition: capsule.purpose,
129
+ environment: capsule.environment,
130
+ enabled: true,
131
+ last_executed_at: Time.current
132
+ )
133
+
134
+ registry_entry.save!
135
+ rescue StandardError => e
136
+ logger&.error "[SQL_CAPSULE] Failed to update registry: #{e.message}"
137
+ # Don't fail the execution if registry update fails
138
+ end
139
+
140
+ def load_capsule_from_registry(capsule_name)
141
+ trigger_name = "sql_capsule_#{capsule_name}"
142
+ registry_entry = PgSqlTriggers::TriggerRegistry.find_by(
143
+ trigger_name: trigger_name,
144
+ source: "manual_sql"
145
+ )
146
+
147
+ return nil unless registry_entry
148
+
149
+ Capsule.new(
150
+ name: capsule_name,
151
+ environment: registry_entry.environment || Rails.env.to_s,
152
+ purpose: registry_entry.condition || "No purpose specified",
153
+ sql: registry_entry.function_body,
154
+ created_at: registry_entry.created_at
155
+ )
156
+ end
157
+
158
+ # Logging methods
159
+
160
+ def log_execution_attempt(capsule, actor, dry_run)
161
+ mode = dry_run ? "DRY_RUN" : "EXECUTE"
162
+ logger&.info "[SQL_CAPSULE] #{mode} ATTEMPT: name=#{capsule.name} " \
163
+ "environment=#{capsule.environment} actor=#{format_actor(actor)}"
164
+ end
165
+
166
+ def log_execution_result(capsule, actor, result)
167
+ status = result[:success] ? "SUCCESS" : "FAILED"
168
+ logger&.info "[SQL_CAPSULE] #{status}: name=#{capsule.name} " \
169
+ "environment=#{capsule.environment} actor=#{format_actor(actor)} " \
170
+ "checksum=#{capsule.checksum}"
171
+ end
172
+
173
+ def log_execution_error(capsule, actor, error)
174
+ # Handle case where capsule might not be valid
175
+ capsule_name = capsule.respond_to?(:name) ? capsule.name : "invalid_capsule"
176
+ environment = capsule.respond_to?(:environment) ? capsule.environment : "unknown"
177
+
178
+ logger&.error "[SQL_CAPSULE] ERROR: name=#{capsule_name} " \
179
+ "environment=#{environment} actor=#{format_actor(actor)} " \
180
+ "error=#{error.class.name} message=#{error.message}"
181
+ end
182
+
183
+ def format_actor(actor)
184
+ return "unknown" if actor.nil?
185
+ return actor.to_s unless actor.is_a?(Hash)
186
+
187
+ "#{actor[:type] || 'unknown'}:#{actor[:id] || 'unknown'}"
188
+ end
189
+
190
+ def logger
191
+ if PgSqlTriggers.respond_to?(:logger) && PgSqlTriggers.logger
192
+ PgSqlTriggers.logger
193
+ elsif defined?(Rails) && Rails.respond_to?(:logger)
194
+ Rails.logger
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -11,5 +11,5 @@
11
11
  # 3. Run: bundle exec rake release
12
12
  # See RELEASE.md for detailed release instructions
13
13
  module PgSqlTriggers
14
- VERSION = "1.1.0"
14
+ VERSION = "1.2.0"
15
15
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_sql_triggers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - samaswin87
8
- autorequire:
7
+ - samaswin
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-12-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: pg
@@ -202,7 +201,9 @@ files:
202
201
  - app/controllers/pg_sql_triggers/dashboard_controller.rb
203
202
  - app/controllers/pg_sql_triggers/generator_controller.rb
204
203
  - app/controllers/pg_sql_triggers/migrations_controller.rb
204
+ - app/controllers/pg_sql_triggers/sql_capsules_controller.rb
205
205
  - app/controllers/pg_sql_triggers/tables_controller.rb
206
+ - app/controllers/pg_sql_triggers/triggers_controller.rb
206
207
  - app/models/pg_sql_triggers/application_record.rb
207
208
  - app/models/pg_sql_triggers/trigger_registry.rb
208
209
  - app/views/layouts/pg_sql_triggers/application.html.erb
@@ -211,8 +212,13 @@ files:
211
212
  - app/views/pg_sql_triggers/generator/preview.html.erb
212
213
  - app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb
213
214
  - app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb
215
+ - app/views/pg_sql_triggers/sql_capsules/new.html.erb
216
+ - app/views/pg_sql_triggers/sql_capsules/show.html.erb
214
217
  - app/views/pg_sql_triggers/tables/index.html.erb
215
218
  - app/views/pg_sql_triggers/tables/show.html.erb
219
+ - app/views/pg_sql_triggers/triggers/_drop_modal.html.erb
220
+ - app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb
221
+ - app/views/pg_sql_triggers/triggers/show.html.erb
216
222
  - config/initializers/pg_sql_triggers.rb
217
223
  - config/routes.rb
218
224
  - db/migrate/20251222000001_create_pg_sql_triggers_tables.rb
@@ -259,6 +265,8 @@ files:
259
265
  - lib/pg_sql_triggers/registry/manager.rb
260
266
  - lib/pg_sql_triggers/registry/validator.rb
261
267
  - lib/pg_sql_triggers/sql.rb
268
+ - lib/pg_sql_triggers/sql/capsule.rb
269
+ - lib/pg_sql_triggers/sql/executor.rb
262
270
  - lib/pg_sql_triggers/sql/kill_switch.rb
263
271
  - lib/pg_sql_triggers/testing.rb
264
272
  - lib/pg_sql_triggers/testing/dry_run.rb
@@ -269,16 +277,15 @@ files:
269
277
  - lib/tasks/trigger_migrations.rake
270
278
  - scripts/generate_coverage_report.rb
271
279
  - sig/pg_sql_triggers.rbs
272
- homepage: https://github.com/samaswin87/pg_sql_triggers
280
+ homepage: https://github.com/samaswin/pg_sql_triggers
273
281
  licenses:
274
282
  - MIT
275
283
  metadata:
276
- homepage_uri: https://github.com/samaswin87/pg_sql_triggers
277
- source_code_uri: https://github.com/samaswin87/pg_sql_triggers
278
- changelog_uri: https://github.com/samaswin87/pg_sql_triggers/blob/main/CHANGELOG.md
279
- github_repo: ssh://github.com/samaswin87/pg_sql_triggers
284
+ homepage_uri: https://github.com/samaswin/pg_sql_triggers
285
+ source_code_uri: https://github.com/samaswin/pg_sql_triggers
286
+ changelog_uri: https://github.com/samaswin/pg_sql_triggers/blob/main/CHANGELOG.md
287
+ github_repo: ssh://github.com/samaswin/pg_sql_triggers
280
288
  rubygems_mfa_required: 'true'
281
- post_install_message:
282
289
  rdoc_options: []
283
290
  require_paths:
284
291
  - lib
@@ -293,8 +300,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
293
300
  - !ruby/object:Gem::Version
294
301
  version: '0'
295
302
  requirements: []
296
- rubygems_version: 3.5.22
297
- signing_key:
303
+ rubygems_version: 4.0.3
298
304
  specification_version: 4
299
305
  summary: A PostgreSQL Trigger Control Plane for Rails
300
306
  test_files: []