pg_sql_triggers 1.1.1 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +56 -0
- data/COVERAGE.md +32 -28
- data/README.md +29 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
- data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
- data/config/routes.rb +9 -0
- data/docs/api-reference.md +252 -4
- data/docs/web-ui.md +82 -17
- data/lib/pg_sql_triggers/registry/manager.rb +28 -13
- data/lib/pg_sql_triggers/registry.rb +41 -0
- data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
- data/lib/pg_sql_triggers/sql/executor.rb +200 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- metadata +10 -1
data/docs/web-ui.md
CHANGED
|
@@ -93,9 +93,56 @@ Available actions depend on trigger state and your permissions:
|
|
|
93
93
|
- **Enable/Disable**: Toggle trigger activation
|
|
94
94
|
- **Apply**: Apply generated trigger definition
|
|
95
95
|
- **Drop**: Remove trigger from database (Admin only)
|
|
96
|
+
- **Re-Execute**: Drop and recreate trigger from registry definition (Admin only)
|
|
96
97
|
- **View SQL**: See the trigger's SQL definition
|
|
97
98
|
- **View Diff**: Compare DSL vs database state
|
|
98
99
|
|
|
100
|
+
### Drop Trigger
|
|
101
|
+
|
|
102
|
+
The drop action permanently removes a trigger from the database and registry.
|
|
103
|
+
|
|
104
|
+
1. Navigate to the trigger detail page
|
|
105
|
+
2. Click the "Drop Trigger" button
|
|
106
|
+
3. A modal will appear requiring:
|
|
107
|
+
- **Reason**: Explanation for dropping the trigger (required for audit trail)
|
|
108
|
+
- **Confirmation**: In protected environments, type the exact confirmation text shown
|
|
109
|
+
4. Review the warning message
|
|
110
|
+
5. Click "Drop Trigger" to confirm
|
|
111
|
+
|
|
112
|
+
**Important Notes**:
|
|
113
|
+
- This action is **irreversible** - the trigger will be permanently removed
|
|
114
|
+
- Requires **Admin** permission level
|
|
115
|
+
- Protected by kill switch in production environments
|
|
116
|
+
- Reason is logged for compliance and audit purposes
|
|
117
|
+
- The trigger is removed from both the database and the registry
|
|
118
|
+
|
|
119
|
+
### Re-Execute Trigger
|
|
120
|
+
|
|
121
|
+
The re-execute action fixes drifted triggers by dropping and recreating them from the registry definition.
|
|
122
|
+
|
|
123
|
+
1. Navigate to the trigger detail page
|
|
124
|
+
2. If the trigger is drifted, you'll see a drift warning
|
|
125
|
+
3. Click the "Re-Execute" button
|
|
126
|
+
4. A modal will appear showing:
|
|
127
|
+
- **Drift Comparison**: Differences between database state and registry definition
|
|
128
|
+
- **Reason Field**: Explanation for re-executing (required for audit trail)
|
|
129
|
+
- **Confirmation**: In protected environments, type the exact confirmation text shown
|
|
130
|
+
5. Review the drift differences to understand what will change
|
|
131
|
+
6. Click "Re-Execute Trigger" to confirm
|
|
132
|
+
|
|
133
|
+
**What Happens**:
|
|
134
|
+
1. Current trigger is dropped from the database
|
|
135
|
+
2. New trigger is created using the registry definition (function_body, events, timing, condition)
|
|
136
|
+
3. Registry is updated with execution timestamp
|
|
137
|
+
4. Operation is logged with reason and actor information
|
|
138
|
+
|
|
139
|
+
**Important Notes**:
|
|
140
|
+
- Requires **Admin** permission level
|
|
141
|
+
- Protected by kill switch in production environments
|
|
142
|
+
- Reason is logged for compliance and audit purposes
|
|
143
|
+
- Executes in a database transaction (rolls back on error)
|
|
144
|
+
- Best used to fix triggers that have drifted from their DSL definition
|
|
145
|
+
|
|
99
146
|
## Migration Management
|
|
100
147
|
|
|
101
148
|
The Web UI provides full migration management capabilities.
|
|
@@ -159,33 +206,51 @@ After each migration action:
|
|
|
159
206
|
|
|
160
207
|
## SQL Capsules
|
|
161
208
|
|
|
162
|
-
SQL Capsules provide emergency escape hatches for executing SQL directly.
|
|
209
|
+
SQL Capsules provide emergency escape hatches for executing SQL directly with comprehensive safety checks and audit logging.
|
|
163
210
|
|
|
164
211
|
### When to Use SQL Capsules
|
|
165
212
|
|
|
166
213
|
Use SQL Capsules for:
|
|
167
214
|
- Emergency fixes in production
|
|
168
|
-
-
|
|
215
|
+
- Critical data corrections
|
|
169
216
|
- Testing SQL functions
|
|
170
217
|
- Debugging trigger behavior
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
- One-off database operations
|
|
219
|
+
|
|
220
|
+
### Creating and Executing SQL Capsules
|
|
221
|
+
|
|
222
|
+
1. Navigate to "SQL Capsules" → "New SQL Capsule"
|
|
223
|
+
2. Fill in the capsule form:
|
|
224
|
+
- **Name**: Unique identifier (alphanumeric, underscores, hyphens only)
|
|
225
|
+
- **Environment**: Target environment (e.g., production, staging)
|
|
226
|
+
- **Purpose**: Detailed explanation of what the SQL does and why (required for audit trail)
|
|
227
|
+
- **SQL**: The SQL statement(s) to execute
|
|
228
|
+
3. Click "Create and Execute" or "Save for Later"
|
|
229
|
+
4. Review the capsule details on the confirmation page
|
|
230
|
+
5. In protected environments, enter confirmation text when prompted
|
|
231
|
+
6. Click "Execute" to run the SQL
|
|
232
|
+
7. Review the execution results
|
|
233
|
+
|
|
234
|
+
### Viewing Capsule History
|
|
235
|
+
|
|
236
|
+
1. Navigate to "SQL Capsules" → "History"
|
|
237
|
+
2. View list of previously executed capsules with:
|
|
238
|
+
- Name and purpose
|
|
239
|
+
- Environment and timestamp
|
|
240
|
+
- SQL checksum
|
|
241
|
+
- Execution status
|
|
242
|
+
3. Click on a capsule to view details
|
|
243
|
+
4. Re-execute historical capsules if needed
|
|
182
244
|
|
|
183
245
|
### Safety Features
|
|
184
246
|
|
|
185
|
-
- **
|
|
186
|
-
- **
|
|
187
|
-
- **
|
|
188
|
-
- **
|
|
247
|
+
- **Admin Permission Required**: Only Admin users can create and execute SQL capsules
|
|
248
|
+
- **Production Protection**: Requires typed confirmation in protected environments
|
|
249
|
+
- **Kill Switch Integration**: All executions are protected by kill switch
|
|
250
|
+
- **Comprehensive Logging**: All operations logged with actor, timestamp, and checksum
|
|
251
|
+
- **Transactional Execution**: SQL runs in a transaction and rolls back on error
|
|
252
|
+
- **Registry Storage**: All capsules are stored in the registry with checksums
|
|
253
|
+
- **Purpose Tracking**: Required purpose field ensures all executions are documented
|
|
189
254
|
|
|
190
255
|
### Example SQL Capsules
|
|
191
256
|
|
|
@@ -33,8 +33,13 @@ module PgSqlTriggers
|
|
|
33
33
|
trigger_name = definition.name
|
|
34
34
|
|
|
35
35
|
# Use cached lookup if available to avoid N+1 queries during trigger file loading
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Explicitly check cache first to avoid query in some Ruby versions where ||= may evaluate RHS
|
|
37
|
+
existing = if _registry_cache.key?(trigger_name)
|
|
38
|
+
_registry_cache[trigger_name]
|
|
39
|
+
else
|
|
40
|
+
_registry_cache[trigger_name] =
|
|
41
|
+
PgSqlTriggers::TriggerRegistry.find_by(trigger_name: trigger_name)
|
|
42
|
+
end
|
|
38
43
|
|
|
39
44
|
# Calculate checksum using field-concatenation (consistent with TriggerRegistry model)
|
|
40
45
|
checksum = calculate_checksum(definition)
|
|
@@ -51,17 +56,27 @@ module PgSqlTriggers
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
if existing
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
# Check if attributes have actually changed to avoid unnecessary queries
|
|
60
|
+
attributes_changed = attributes.any? do |key, value|
|
|
61
|
+
existing.send(key) != value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if attributes_changed
|
|
65
|
+
begin
|
|
66
|
+
existing.update!(attributes)
|
|
67
|
+
# Update cache with the modified record (reload to get fresh data)
|
|
68
|
+
reloaded = existing.reload
|
|
69
|
+
_registry_cache[trigger_name] = reloaded
|
|
70
|
+
reloaded
|
|
71
|
+
rescue ActiveRecord::RecordNotFound
|
|
72
|
+
# Cached record was deleted, create a new one
|
|
73
|
+
new_record = PgSqlTriggers::TriggerRegistry.create!(attributes)
|
|
74
|
+
_registry_cache[trigger_name] = new_record
|
|
75
|
+
new_record
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
# No changes, return cached record without any queries
|
|
79
|
+
existing
|
|
65
80
|
end
|
|
66
81
|
else
|
|
67
82
|
new_record = PgSqlTriggers::TriggerRegistry.create!(attributes)
|
|
@@ -32,5 +32,46 @@ module PgSqlTriggers
|
|
|
32
32
|
def self.validate!
|
|
33
33
|
Validator.validate!
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# Console APIs for trigger operations
|
|
37
|
+
# These methods provide a convenient interface for managing triggers from the Rails console
|
|
38
|
+
|
|
39
|
+
def self.enable(trigger_name, actor:, confirmation: nil)
|
|
40
|
+
check_permission!(actor, :enable_trigger)
|
|
41
|
+
trigger = find_trigger!(trigger_name)
|
|
42
|
+
trigger.enable!(confirmation: confirmation)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.disable(trigger_name, actor:, confirmation: nil)
|
|
46
|
+
check_permission!(actor, :disable_trigger)
|
|
47
|
+
trigger = find_trigger!(trigger_name)
|
|
48
|
+
trigger.disable!(confirmation: confirmation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.drop(trigger_name, actor:, reason:, confirmation: nil)
|
|
52
|
+
check_permission!(actor, :drop_trigger)
|
|
53
|
+
trigger = find_trigger!(trigger_name)
|
|
54
|
+
trigger.drop!(reason: reason, confirmation: confirmation, actor: actor)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.re_execute(trigger_name, actor:, reason:, confirmation: nil)
|
|
58
|
+
check_permission!(actor, :drop_trigger) # Re-execute requires same permission as drop
|
|
59
|
+
trigger = find_trigger!(trigger_name)
|
|
60
|
+
trigger.re_execute!(reason: reason, confirmation: confirmation, actor: actor)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Private helper methods
|
|
64
|
+
|
|
65
|
+
def self.find_trigger!(trigger_name)
|
|
66
|
+
PgSqlTriggers::TriggerRegistry.find_by!(trigger_name: trigger_name)
|
|
67
|
+
rescue ActiveRecord::RecordNotFound
|
|
68
|
+
raise ArgumentError, "Trigger '#{trigger_name}' not found in registry"
|
|
69
|
+
end
|
|
70
|
+
private_class_method :find_trigger!
|
|
71
|
+
|
|
72
|
+
def self.check_permission!(actor, action)
|
|
73
|
+
PgSqlTriggers::Permissions.check!(actor, action)
|
|
74
|
+
end
|
|
75
|
+
private_class_method :check_permission!
|
|
35
76
|
end
|
|
36
77
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module PgSqlTriggers
|
|
6
|
+
module SQL
|
|
7
|
+
# Capsule represents a named SQL capsule with environment declaration and purpose
|
|
8
|
+
# Used for emergency operations and manual SQL execution
|
|
9
|
+
#
|
|
10
|
+
# @example Creating a SQL capsule
|
|
11
|
+
# capsule = PgSqlTriggers::SQL::Capsule.new(
|
|
12
|
+
# name: "fix_user_permissions",
|
|
13
|
+
# environment: "production",
|
|
14
|
+
# purpose: "Emergency fix for user permission issue",
|
|
15
|
+
# sql: "UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';"
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class Capsule
|
|
19
|
+
attr_reader :name, :environment, :purpose, :sql, :created_at
|
|
20
|
+
|
|
21
|
+
# @param name [String] The name of the SQL capsule
|
|
22
|
+
# @param environment [String] The environment this capsule is intended for
|
|
23
|
+
# @param purpose [String] Description of what this capsule does and why
|
|
24
|
+
# @param sql [String] The SQL to execute
|
|
25
|
+
# @param created_at [Time, nil] The timestamp when the capsule was created (defaults to now)
|
|
26
|
+
def initialize(name:, environment:, purpose:, sql:, created_at: nil)
|
|
27
|
+
@name = name
|
|
28
|
+
@environment = environment
|
|
29
|
+
@purpose = purpose
|
|
30
|
+
@sql = sql
|
|
31
|
+
@created_at = created_at || Time.current
|
|
32
|
+
validate!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Calculates the checksum of the SQL content
|
|
36
|
+
# @return [String] The SHA256 checksum of the SQL
|
|
37
|
+
def checksum
|
|
38
|
+
@checksum ||= Digest::SHA256.hexdigest(sql.to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts the capsule to a hash suitable for storage
|
|
42
|
+
# @return [Hash] The capsule data
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
name: name,
|
|
46
|
+
environment: environment,
|
|
47
|
+
purpose: purpose,
|
|
48
|
+
sql: sql,
|
|
49
|
+
checksum: checksum,
|
|
50
|
+
created_at: created_at
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the registry trigger name for this capsule
|
|
55
|
+
# SQL capsules are stored in the registry with a special naming pattern
|
|
56
|
+
# @return [String] The trigger name for registry storage
|
|
57
|
+
def registry_trigger_name
|
|
58
|
+
"sql_capsule_#{name}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate!
|
|
64
|
+
errors = []
|
|
65
|
+
errors << "Name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
|
66
|
+
errors << "Environment cannot be blank" if environment.nil? || environment.to_s.strip.empty?
|
|
67
|
+
errors << "Purpose cannot be blank" if purpose.nil? || purpose.to_s.strip.empty?
|
|
68
|
+
errors << "SQL cannot be blank" if sql.nil? || sql.to_s.strip.empty?
|
|
69
|
+
|
|
70
|
+
# Validate name format (alphanumeric, underscores, hyphens only)
|
|
71
|
+
unless name.to_s.match?(/\A[a-z0-9_-]+\z/i)
|
|
72
|
+
errors << "Name must contain only letters, numbers, underscores, and hyphens"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise ArgumentError, "Invalid capsule: #{errors.join(', ')}" if errors.any?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -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
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_sql_triggers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- samaswin
|
|
@@ -201,7 +201,9 @@ files:
|
|
|
201
201
|
- app/controllers/pg_sql_triggers/dashboard_controller.rb
|
|
202
202
|
- app/controllers/pg_sql_triggers/generator_controller.rb
|
|
203
203
|
- app/controllers/pg_sql_triggers/migrations_controller.rb
|
|
204
|
+
- app/controllers/pg_sql_triggers/sql_capsules_controller.rb
|
|
204
205
|
- app/controllers/pg_sql_triggers/tables_controller.rb
|
|
206
|
+
- app/controllers/pg_sql_triggers/triggers_controller.rb
|
|
205
207
|
- app/models/pg_sql_triggers/application_record.rb
|
|
206
208
|
- app/models/pg_sql_triggers/trigger_registry.rb
|
|
207
209
|
- app/views/layouts/pg_sql_triggers/application.html.erb
|
|
@@ -210,8 +212,13 @@ files:
|
|
|
210
212
|
- app/views/pg_sql_triggers/generator/preview.html.erb
|
|
211
213
|
- app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb
|
|
212
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
|
|
213
217
|
- app/views/pg_sql_triggers/tables/index.html.erb
|
|
214
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
|
|
215
222
|
- config/initializers/pg_sql_triggers.rb
|
|
216
223
|
- config/routes.rb
|
|
217
224
|
- db/migrate/20251222000001_create_pg_sql_triggers_tables.rb
|
|
@@ -258,6 +265,8 @@ files:
|
|
|
258
265
|
- lib/pg_sql_triggers/registry/manager.rb
|
|
259
266
|
- lib/pg_sql_triggers/registry/validator.rb
|
|
260
267
|
- lib/pg_sql_triggers/sql.rb
|
|
268
|
+
- lib/pg_sql_triggers/sql/capsule.rb
|
|
269
|
+
- lib/pg_sql_triggers/sql/executor.rb
|
|
261
270
|
- lib/pg_sql_triggers/sql/kill_switch.rb
|
|
262
271
|
- lib/pg_sql_triggers/testing.rb
|
|
263
272
|
- lib/pg_sql_triggers/testing/dry_run.rb
|