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
@@ -1,8 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Helper method to check kill switch before dangerous operations
4
+ def check_kill_switch!(operation)
5
+ PgSqlTriggers::SQL::KillSwitch.check!(
6
+ operation: operation,
7
+ environment: Rails.env,
8
+ confirmation: ENV.fetch("CONFIRMATION_TEXT", nil),
9
+ actor: { type: "CLI", id: ENV.fetch("USER", "unknown") }
10
+ )
11
+ rescue PgSqlTriggers::KillSwitchError => e
12
+ puts "\n#{e.message}\n"
13
+ exit 1
14
+ end
15
+
3
16
  namespace :trigger do
4
17
  desc "Migrate trigger migrations (options: VERSION=x, VERBOSE=false)"
5
18
  task migrate: :environment do
19
+ check_kill_switch!(:trigger_migrate)
20
+
6
21
  PgSqlTriggers::Migrator.ensure_migrations_table!
7
22
 
8
23
  target_version = ENV["VERSION"]&.to_i
@@ -20,6 +35,8 @@ namespace :trigger do
20
35
 
21
36
  desc "Rollback trigger migrations (specify steps w/ STEP=n)"
22
37
  task rollback: :environment do
38
+ check_kill_switch!(:trigger_rollback)
39
+
23
40
  PgSqlTriggers::Migrator.ensure_migrations_table!
24
41
 
25
42
  steps = ENV["STEP"] ? ENV["STEP"].to_i : 1
@@ -64,6 +81,8 @@ namespace :trigger do
64
81
 
65
82
  desc "Runs the 'up' for a given migration VERSION"
66
83
  task "migrate:up" => :environment do
84
+ check_kill_switch!(:trigger_migrate_up)
85
+
67
86
  version = ENV.fetch("VERSION", nil)
68
87
  raise "VERSION is required" unless version
69
88
 
@@ -74,6 +93,8 @@ namespace :trigger do
74
93
 
75
94
  desc "Runs the 'down' for a given migration VERSION"
76
95
  task "migrate:down" => :environment do
96
+ check_kill_switch!(:trigger_migrate_down)
97
+
77
98
  version = ENV.fetch("VERSION", nil)
78
99
  raise "VERSION is required" unless version
79
100
 
@@ -84,6 +105,8 @@ namespace :trigger do
84
105
 
85
106
  desc "Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x)"
86
107
  task "migrate:redo" => :environment do
108
+ check_kill_switch!(:trigger_migrate_redo)
109
+
87
110
  PgSqlTriggers::Migrator.ensure_migrations_table!
88
111
 
89
112
  if ENV["VERSION"]
@@ -127,6 +150,8 @@ end
127
150
  namespace :db do
128
151
  desc "Migrate the database schema and triggers (options: VERSION=x, VERBOSE=false)"
129
152
  task "migrate:with_triggers" => :environment do
153
+ check_kill_switch!(:db_migrate_with_triggers)
154
+
130
155
  verbose = ENV["VERBOSE"] != "false"
131
156
 
132
157
  puts "Running schema and trigger migrations..." if verbose
@@ -140,6 +165,8 @@ namespace :db do
140
165
 
141
166
  desc "Rollback schema and trigger migrations (specify steps w/ STEP=n)"
142
167
  task "rollback:with_triggers" => :environment do
168
+ check_kill_switch!(:db_rollback_with_triggers)
169
+
143
170
  ENV["STEP"] ? ENV["STEP"].to_i : 1
144
171
 
145
172
  # Determine which type of migration was last run
@@ -171,6 +198,8 @@ namespace :db do
171
198
 
172
199
  desc "Runs the 'up' for a given migration VERSION (schema or trigger)"
173
200
  task "migrate:up:with_triggers" => :environment do
201
+ check_kill_switch!(:db_migrate_up_with_triggers)
202
+
174
203
  version = ENV.fetch("VERSION", nil)
175
204
  raise "VERSION is required" unless version
176
205
 
@@ -201,6 +230,8 @@ namespace :db do
201
230
 
202
231
  desc "Runs the 'down' for a given migration VERSION (schema or trigger)"
203
232
  task "migrate:down:with_triggers" => :environment do
233
+ check_kill_switch!(:db_migrate_down_with_triggers)
234
+
204
235
  version = ENV.fetch("VERSION", nil)
205
236
  raise "VERSION is required" unless version
206
237
 
@@ -228,6 +259,8 @@ namespace :db do
228
259
 
229
260
  desc "Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x)"
230
261
  task "migrate:redo:with_triggers" => :environment do
262
+ check_kill_switch!(:db_migrate_redo_with_triggers)
263
+
231
264
  if ENV["VERSION"]
232
265
  Rake::Task["db:migrate:down:with_triggers"].invoke
233
266
  Rake::Task["db:migrate:up:with_triggers"].invoke
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "pathname"
6
+ require "active_support/core_ext/object/blank"
7
+
8
+ def calculate_file_coverage(lines)
9
+ return 0.0 if lines.blank?
10
+
11
+ relevant_lines = lines.compact
12
+ return 0.0 if relevant_lines.empty?
13
+
14
+ covered = relevant_lines.count { |line| line&.positive? }
15
+ total = relevant_lines.count
16
+
17
+ (covered.to_f / total * 100).round(2)
18
+ end
19
+
20
+ def extract_file_path(full_path)
21
+ # Remove workspace root and get relative path
22
+ workspace_root = Pathname.new(__dir__).parent.realpath
23
+ file_path = Pathname.new(full_path)
24
+
25
+ if file_path.to_s.start_with?(workspace_root.to_s)
26
+ file_path.relative_path_from(workspace_root).to_s
27
+ else
28
+ # Fallback: try to extract relative path
29
+ full_path.sub(%r{.*/(lib|app)/}, '\1/')
30
+ end
31
+ end
32
+
33
+ def parse_coverage_data
34
+ coverage_dir = Pathname.new(__dir__).parent.join("coverage")
35
+ resultset_file = coverage_dir.join(".resultset.json")
36
+ last_run_file = coverage_dir.join(".last_run.json")
37
+
38
+ unless resultset_file.exist?
39
+ puts "Error: Coverage resultset file not found at #{resultset_file}"
40
+ exit 1
41
+ end
42
+
43
+ resultset_data = JSON.parse(File.read(resultset_file))
44
+ last_run_file.exist? ? JSON.parse(File.read(last_run_file)) : {}
45
+
46
+ # Get the first (and usually only) test suite result
47
+ test_suite_key = resultset_data.keys.first
48
+ coverage_data = resultset_data[test_suite_key]["coverage"] || {}
49
+
50
+ file_coverage = {}
51
+ total_lines = 0
52
+ total_covered = 0
53
+
54
+ coverage_data.each do |full_path, file_data|
55
+ next unless file_data["lines"]
56
+
57
+ relative_path = extract_file_path(full_path)
58
+ # Skip files in spec, vendor, etc.
59
+ next if relative_path.include?("/spec/") || relative_path.include?("/vendor/")
60
+
61
+ lines = file_data["lines"]
62
+ coverage_percentage = calculate_file_coverage(lines)
63
+
64
+ # Count relevant lines (non-nil lines)
65
+ relevant_lines = lines.compact
66
+ covered_lines = relevant_lines.count { |line| line&.positive? }
67
+
68
+ file_coverage[relative_path] = {
69
+ percentage: coverage_percentage,
70
+ total_lines: relevant_lines.count,
71
+ covered_lines: covered_lines,
72
+ missed_lines: relevant_lines.count - covered_lines
73
+ }
74
+
75
+ total_lines += relevant_lines.count
76
+ total_covered += covered_lines
77
+ end
78
+
79
+ total_coverage = total_lines.positive? ? (total_covered.to_f / total_lines * 100).round(2) : 0.0
80
+
81
+ {
82
+ file_coverage: file_coverage.sort_by { |_k, v| -v[:percentage] },
83
+ total_coverage: total_coverage,
84
+ total_lines: total_lines,
85
+ total_covered: total_covered
86
+ }
87
+ end
88
+
89
+ def generate_markdown(coverage_info)
90
+ markdown = "# Code Coverage Report\n\n"
91
+ markdown += "**Total Coverage: #{coverage_info[:total_coverage]}%**\n\n"
92
+ markdown += "Covered: #{coverage_info[:total_covered]} / #{coverage_info[:total_lines]} lines\n\n"
93
+ markdown += "---\n\n"
94
+ markdown += "## File Coverage\n\n"
95
+ markdown += "| File | Coverage | Covered Lines | Missed Lines | Total Lines |\n"
96
+ markdown += "|------|----------|---------------|--------------|-------------|\n"
97
+
98
+ coverage_info[:file_coverage].each do |file_path, data|
99
+ status_icon = if data[:percentage] >= 90
100
+ "✅"
101
+ else
102
+ data[:percentage] >= 70 ? "⚠️" : "❌"
103
+ end
104
+ markdown += "| `#{file_path}` | #{data[:percentage]}% #{status_icon} | " \
105
+ "#{data[:covered_lines]} | #{data[:missed_lines]} | #{data[:total_lines]} |\n"
106
+ end
107
+
108
+ markdown += "\n---\n\n"
109
+ markdown += "*Report generated automatically from SimpleCov results*\n"
110
+ markdown += "*To regenerate: Run `bundle exec rspec` and then `ruby scripts/generate_coverage_report.rb`*\n"
111
+
112
+ markdown
113
+ end
114
+
115
+ # Main execution
116
+ begin
117
+ coverage_info = parse_coverage_data
118
+ markdown_content = generate_markdown(coverage_info)
119
+
120
+ output_file = Pathname.new(__dir__).parent.join("COVERAGE.md")
121
+ File.write(output_file, markdown_content)
122
+
123
+ puts "Coverage report generated: #{output_file}"
124
+ puts "Total Coverage: #{coverage_info[:total_coverage]}%"
125
+ rescue StandardError => e
126
+ puts "Error generating coverage report: #{e.message}"
127
+ puts e.backtrace.first(5).join("\n")
128
+ exit 1
129
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_sql_triggers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - samaswin87
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-27 00:00:00.000000000 Z
11
+ date: 2025-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '6.0'
33
+ version: '6.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '6.0'
40
+ version: '6.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: database_cleaner-active_record
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: erb_lint
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.9'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: factory_bot_rails
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -172,9 +186,11 @@ executables: []
172
186
  extensions: []
173
187
  extra_rdoc_files: []
174
188
  files:
189
+ - ".erb_lint.yml"
175
190
  - ".rspec"
176
191
  - ".rubocop.yml"
177
192
  - CHANGELOG.md
193
+ - COVERAGE.md
178
194
  - Goal.md
179
195
  - LICENSE
180
196
  - README.md
@@ -193,10 +209,26 @@ files:
193
209
  - app/views/pg_sql_triggers/dashboard/index.html.erb
194
210
  - app/views/pg_sql_triggers/generator/new.html.erb
195
211
  - app/views/pg_sql_triggers/generator/preview.html.erb
212
+ - app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb
213
+ - app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb
196
214
  - app/views/pg_sql_triggers/tables/index.html.erb
197
215
  - app/views/pg_sql_triggers/tables/show.html.erb
216
+ - config/initializers/pg_sql_triggers.rb
198
217
  - config/routes.rb
199
218
  - db/migrate/20251222000001_create_pg_sql_triggers_tables.rb
219
+ - db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb
220
+ - docs/README.md
221
+ - docs/api-reference.md
222
+ - docs/configuration.md
223
+ - docs/getting-started.md
224
+ - docs/kill-switch.md
225
+ - docs/screenshots/.gitkeep
226
+ - docs/screenshots/Generate Trigger.png
227
+ - docs/screenshots/Triggers Page.png
228
+ - docs/screenshots/kill error.png
229
+ - docs/screenshots/kill modal for migration down.png
230
+ - docs/usage-guide.md
231
+ - docs/web-ui.md
200
232
  - lib/generators/pg_sql_triggers/install_generator.rb
201
233
  - lib/generators/pg_sql_triggers/templates/README
202
234
  - lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb
@@ -207,6 +239,9 @@ files:
207
239
  - lib/pg_sql_triggers.rb
208
240
  - lib/pg_sql_triggers/database_introspection.rb
209
241
  - lib/pg_sql_triggers/drift.rb
242
+ - lib/pg_sql_triggers/drift/db_queries.rb
243
+ - lib/pg_sql_triggers/drift/detector.rb
244
+ - lib/pg_sql_triggers/drift/reporter.rb
210
245
  - lib/pg_sql_triggers/dsl.rb
211
246
  - lib/pg_sql_triggers/dsl/trigger_definition.rb
212
247
  - lib/pg_sql_triggers/engine.rb
@@ -215,12 +250,16 @@ files:
215
250
  - lib/pg_sql_triggers/generator/service.rb
216
251
  - lib/pg_sql_triggers/migration.rb
217
252
  - lib/pg_sql_triggers/migrator.rb
253
+ - lib/pg_sql_triggers/migrator/pre_apply_comparator.rb
254
+ - lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb
255
+ - lib/pg_sql_triggers/migrator/safety_validator.rb
218
256
  - lib/pg_sql_triggers/permissions.rb
219
257
  - lib/pg_sql_triggers/permissions/checker.rb
220
258
  - lib/pg_sql_triggers/registry.rb
221
259
  - lib/pg_sql_triggers/registry/manager.rb
222
260
  - lib/pg_sql_triggers/registry/validator.rb
223
261
  - lib/pg_sql_triggers/sql.rb
262
+ - lib/pg_sql_triggers/sql/kill_switch.rb
224
263
  - lib/pg_sql_triggers/testing.rb
225
264
  - lib/pg_sql_triggers/testing/dry_run.rb
226
265
  - lib/pg_sql_triggers/testing/function_tester.rb
@@ -228,6 +267,7 @@ files:
228
267
  - lib/pg_sql_triggers/testing/syntax_validator.rb
229
268
  - lib/pg_sql_triggers/version.rb
230
269
  - lib/tasks/trigger_migrations.rake
270
+ - scripts/generate_coverage_report.rb
231
271
  - sig/pg_sql_triggers.rbs
232
272
  homepage: https://github.com/samaswin87/pg_sql_triggers
233
273
  licenses:
@@ -246,7 +286,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
246
286
  requirements:
247
287
  - - ">="
248
288
  - !ruby/object:Gem::Version
249
- version: 2.7.0
289
+ version: 3.0.0
250
290
  required_rubygems_version: !ruby/object:Gem::Requirement
251
291
  requirements:
252
292
  - - ">="