query_guard 0.4.2 → 0.5.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -1
  3. data/DESIGN.md +420 -0
  4. data/INDEX.md +309 -0
  5. data/README.md +579 -30
  6. data/exe/queryguard +23 -0
  7. data/lib/query_guard/action_controller_subscriber.rb +27 -0
  8. data/lib/query_guard/analysis/query_risk_classifier.rb +124 -0
  9. data/lib/query_guard/analysis/risk_detectors.rb +258 -0
  10. data/lib/query_guard/analysis/risk_level.rb +35 -0
  11. data/lib/query_guard/analyzers/base.rb +30 -0
  12. data/lib/query_guard/analyzers/query_count_analyzer.rb +31 -0
  13. data/lib/query_guard/analyzers/query_risk_analyzer.rb +146 -0
  14. data/lib/query_guard/analyzers/registry.rb +57 -0
  15. data/lib/query_guard/analyzers/select_star_analyzer.rb +42 -0
  16. data/lib/query_guard/analyzers/slow_query_analyzer.rb +39 -0
  17. data/lib/query_guard/budget.rb +148 -0
  18. data/lib/query_guard/cli/batch_report_formatter.rb +129 -0
  19. data/lib/query_guard/cli/command.rb +93 -0
  20. data/lib/query_guard/cli/commands/analyze.rb +52 -0
  21. data/lib/query_guard/cli/commands/check.rb +58 -0
  22. data/lib/query_guard/cli/formatter.rb +278 -0
  23. data/lib/query_guard/cli/json_reporter.rb +247 -0
  24. data/lib/query_guard/cli/paged_report_formatter.rb +137 -0
  25. data/lib/query_guard/cli/source_metadata_collector.rb +297 -0
  26. data/lib/query_guard/cli.rb +197 -0
  27. data/lib/query_guard/client.rb +4 -6
  28. data/lib/query_guard/config.rb +145 -6
  29. data/lib/query_guard/core/context.rb +80 -0
  30. data/lib/query_guard/core/finding.rb +162 -0
  31. data/lib/query_guard/core/finding_builders.rb +152 -0
  32. data/lib/query_guard/core/query.rb +40 -0
  33. data/lib/query_guard/explain/adapter_interface.rb +89 -0
  34. data/lib/query_guard/explain/explain_enricher.rb +367 -0
  35. data/lib/query_guard/explain/plan_signals.rb +385 -0
  36. data/lib/query_guard/explain/postgresql_adapter.rb +208 -0
  37. data/lib/query_guard/exporter.rb +124 -0
  38. data/lib/query_guard/fingerprint.rb +96 -0
  39. data/lib/query_guard/middleware.rb +101 -15
  40. data/lib/query_guard/migrations/database_adapter.rb +88 -0
  41. data/lib/query_guard/migrations/migration_analyzer.rb +100 -0
  42. data/lib/query_guard/migrations/migration_risk_detectors.rb +287 -0
  43. data/lib/query_guard/migrations/postgresql_adapter.rb +157 -0
  44. data/lib/query_guard/migrations/table_risk_analyzer.rb +154 -0
  45. data/lib/query_guard/migrations/table_size_resolver.rb +152 -0
  46. data/lib/query_guard/publish.rb +38 -0
  47. data/lib/query_guard/rspec.rb +119 -0
  48. data/lib/query_guard/security.rb +99 -0
  49. data/lib/query_guard/store.rb +38 -0
  50. data/lib/query_guard/subscriber.rb +46 -15
  51. data/lib/query_guard/suggest/index_suggester.rb +176 -0
  52. data/lib/query_guard/suggest/pattern_extractors.rb +137 -0
  53. data/lib/query_guard/trace.rb +106 -0
  54. data/lib/query_guard/uploader/http_uploader.rb +166 -0
  55. data/lib/query_guard/uploader/interface.rb +79 -0
  56. data/lib/query_guard/uploader/no_op_uploader.rb +46 -0
  57. data/lib/query_guard/uploader/registry.rb +37 -0
  58. data/lib/query_guard/uploader/upload_service.rb +80 -0
  59. data/lib/query_guard/version.rb +1 -1
  60. data/lib/query_guard.rb +54 -7
  61. metadata +78 -10
  62. data/.rspec +0 -3
  63. data/Rakefile +0 -21
  64. data/config/initializers/query_guard.rb +0 -9
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryGuard
4
+ module Uploader
5
+ # No-op uploader that discards reports (default behavior).
6
+ #
7
+ # This is the default uploader when no remote API is configured.
8
+ # It silently discards reports, making the upload system completely optional.
9
+ #
10
+ # Use case:
11
+ # - Local development without remote API
12
+ # - CI/CD without SaaS platform integration
13
+ # - Organizations that don't want to send data to external services
14
+ class NoOpUploader < Interface
15
+ def initialize(config = nil)
16
+ @config = config
17
+ end
18
+
19
+ # Always succeeds, does nothing.
20
+ def upload(json_report, metadata = {})
21
+ UploadResult.new(
22
+ success: true,
23
+ uploader_name: name,
24
+ details: { discarded: true, bytes: json_report.bytesize }
25
+ )
26
+ end
27
+
28
+ # Always ready (no configuration needed).
29
+ def ready?
30
+ true
31
+ end
32
+
33
+ def name
34
+ 'no-op'
35
+ end
36
+
37
+ def status
38
+ {
39
+ enabled: false,
40
+ mode: 'no-op',
41
+ description: 'Reports are discarded (upload disabled)'
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryGuard
4
+ module Uploader
5
+ # Registry for selecting and instantiating uploaders.
6
+ #
7
+ # Provides a factory pattern for creating the appropriate uploader
8
+ # based on configuration.
9
+ class Registry
10
+ # Get the appropriate uploader for the given configuration.
11
+ #
12
+ # @param config [QueryGuard::Config] Configuration object
13
+ # @return [Interface] An uploader instance
14
+ def self.for_config(config)
15
+ uploader_type = config.uploader_type&.downcase || 'no-op'
16
+
17
+ case uploader_type
18
+ when 'http'
19
+ HttpUploader.new(config)
20
+ when 'no-op', 'noop', 'none', 'disabled', nil
21
+ NoOpUploader.new(config)
22
+ else
23
+ raise ArgumentError, "Unknown uploader type: #{uploader_type}. " \
24
+ "Supported: 'http', 'no-op' (default)"
25
+ end
26
+ end
27
+
28
+ # List all available uploader types.
29
+ def self.available_uploaders
30
+ {
31
+ 'no-op' => 'Local/no-op mode (default, no upload)',
32
+ 'http' => 'HTTP API upload (for future SaaS platform)'
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryGuard
4
+ module Uploader
5
+ # Service for uploading analysis reports.
6
+ #
7
+ # This is the main public API for uploading reports. It:
8
+ # - Coordinates with configured uploader
9
+ # - Handles errors gracefully
10
+ # - Provides logging/status information
11
+ # - Never blocks or raises exceptions
12
+ #
13
+ # Usage:
14
+ # service = QueryGuard::Uploader::UploadService.new(config)
15
+ # result = service.upload_report(json_string, trace_id: '...')
16
+ # puts result.to_h if result.failed?
17
+ class UploadService
18
+ def initialize(config = nil)
19
+ @config = config || QueryGuard.config
20
+ @uploader = Registry.for_config(@config)
21
+ end
22
+
23
+ # Upload a JSON report from the configured JSON reporter.
24
+ #
25
+ # @param json_report [String] JSON string from JsonReporter#generate
26
+ # @param trace_id [String] Optional trace ID for distributed tracing
27
+ # @param request_id [String] Optional request ID
28
+ # @return [UploadResult] Result object (never raises)
29
+ def upload_report(json_report, trace_id: nil, request_id: nil)
30
+ metadata = {}
31
+ metadata[:trace_id] = trace_id if trace_id
32
+ metadata[:request_id] = request_id if request_id
33
+
34
+ @uploader.upload(json_report, metadata)
35
+ rescue StandardError => e
36
+ # Always return graceful result, never raise
37
+ UploadResult.new(
38
+ success: false,
39
+ uploader_name: @uploader.name,
40
+ error: "Unexpected error: #{e.message}"
41
+ )
42
+ end
43
+
44
+ # Get status information about the current uploader.
45
+ #
46
+ # @return [Hash] Status information
47
+ def status
48
+ {
49
+ uploader: @uploader.name,
50
+ ready: @uploader.ready?,
51
+ details: @uploader.status
52
+ }
53
+ end
54
+
55
+ # Check if uploader is ready and configured.
56
+ #
57
+ # @return [Boolean]
58
+ def ready?
59
+ @uploader.ready?
60
+ end
61
+
62
+ # Get the current uploader instance.
63
+ #
64
+ # @return [Interface] The active uploader
65
+ def uploader
66
+ @uploader
67
+ end
68
+
69
+ # Reconfigure with a new config object.
70
+ #
71
+ # @param config [QueryGuard::Config]
72
+ # @return [self]
73
+ def reconfigure(config)
74
+ @config = config
75
+ @uploader = Registry.for_config(@config)
76
+ self
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QueryGuard
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/query_guard.rb CHANGED
@@ -2,22 +2,63 @@
2
2
  require "active_support"
3
3
  require "active_support/notifications"
4
4
  require "query_guard/version"
5
+ require "query_guard/core/query"
6
+ require "query_guard/core/finding"
7
+ require "query_guard/core/finding_builders"
8
+ require "query_guard/core/context"
9
+ require "query_guard/analysis/risk_level"
10
+ require "query_guard/analysis/risk_detectors"
11
+ require "query_guard/analysis/query_risk_classifier"
12
+ require "query_guard/explain/adapter_interface"
13
+ require "query_guard/explain/postgresql_adapter"
14
+ require "query_guard/explain/plan_signals"
15
+ require "query_guard/suggest/pattern_extractors"
16
+ require "query_guard/suggest/index_suggester"
17
+ require "query_guard/explain/explain_enricher"
18
+ require "query_guard/analyzers/base"
19
+ require "query_guard/analyzers/registry"
20
+ require "query_guard/analyzers/slow_query_analyzer"
21
+ require "query_guard/analyzers/query_count_analyzer"
22
+ require "query_guard/analyzers/select_star_analyzer"
23
+ require "query_guard/analyzers/query_risk_analyzer"
24
+ require "query_guard/migrations/migration_risk_detectors"
25
+ require "query_guard/migrations/migration_analyzer"
26
+ require "query_guard/migrations/database_adapter"
27
+ require "query_guard/migrations/postgresql_adapter"
28
+ require "query_guard/migrations/table_size_resolver"
29
+ require "query_guard/migrations/table_risk_analyzer"
30
+ require "query_guard/budget"
31
+ require "query_guard/fingerprint"
32
+ require "query_guard/trace"
5
33
  require "query_guard/config"
34
+ require "query_guard/store"
35
+ require "query_guard/security"
6
36
  require "query_guard/subscriber"
37
+ require "query_guard/action_controller_subscriber"
7
38
  require "query_guard/middleware"
8
39
  require "query_guard/client"
40
+ require "query_guard/uploader/interface"
41
+ require "query_guard/uploader/no_op_uploader"
42
+ require "query_guard/uploader/http_uploader"
43
+ require "query_guard/uploader/registry"
44
+ require "query_guard/uploader/upload_service"
45
+ require "query_guard/cli/source_metadata_collector"
46
+ require "query_guard/cli/json_reporter"
47
+ require "query_guard/cli/batch_report_formatter"
48
+ require "query_guard/cli/paged_report_formatter"
49
+ require "query_guard/cli/formatter"
50
+ require "query_guard/cli/command"
9
51
 
10
52
  module QueryGuard
11
53
  class << self
12
54
  attr_accessor :client
13
- # Keep config as a normal module ivar; no mattr_*
55
+
14
56
  def config
15
57
  @config ||= Config.new
16
58
  end
17
59
 
18
60
  def configure
19
61
  yield(config)
20
- # Build a reusable HTTP client (whatever your Client class is)
21
62
  @client = Client.new(
22
63
  base_url: config.base_url,
23
64
  api_key: config.api_key,
@@ -28,13 +69,10 @@ module QueryGuard
28
69
  end
29
70
 
30
71
  def install!(app = nil)
31
- # Ensure config exists even if user didn't call configure
32
72
  config
33
-
34
- # Install SQL subscriber once
35
73
  Subscriber.install!(config)
74
+ ActionControllerSubscriber.install!(config)
36
75
 
37
- # Insert middleware
38
76
  if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
39
77
  Rails.application.config.middleware.use(QueryGuard::Middleware, config)
40
78
  elsif app
@@ -43,10 +81,19 @@ module QueryGuard
43
81
 
44
82
  self
45
83
  end
84
+
85
+ def exporter
86
+ @exporter ||= QueryGuard::Exporter.new(config)
87
+ end
88
+
89
+ # Trace a block of code and capture query stats.
90
+ # Returns [result, report] tuple.
91
+ def trace(label, context: {}, &block)
92
+ Trace.trace(label, context: context, &block)
93
+ end
46
94
  end
47
95
  end
48
96
 
49
- # Auto-install for Rails via Railtie
50
97
  if defined?(Rails::Railtie)
51
98
  module QueryGuard
52
99
  class Railtie < Rails::Railtie
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: query_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chitradevi36
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-03 00:00:00.000000000 Z
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -44,25 +44,94 @@ dependencies:
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '8.0'
47
- description: query_guard tracks SQL in Rails requests and warns/raises on excessive
48
- count, slow queries, or SELECT * usage.
47
+ - !ruby/object:Gem::Dependency
48
+ name: rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '5.2'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '8.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '5.2'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '8.0'
67
+ description: Automatically detect risky migration patterns (unsafe column removal,
68
+ locking operations, data loss) before they reach production.
49
69
  email:
50
70
  - chitra.rajaguru123@gmail.com
51
- executables: []
71
+ executables:
72
+ - queryguard
52
73
  extensions: []
53
74
  extra_rdoc_files: []
54
75
  files:
55
- - ".rspec"
56
76
  - CHANGELOG.md
77
+ - DESIGN.md
78
+ - INDEX.md
57
79
  - LICENSE.txt
58
80
  - README.md
59
- - Rakefile
60
- - config/initializers/query_guard.rb
81
+ - exe/queryguard
61
82
  - lib/query_guard.rb
83
+ - lib/query_guard/action_controller_subscriber.rb
84
+ - lib/query_guard/analysis/query_risk_classifier.rb
85
+ - lib/query_guard/analysis/risk_detectors.rb
86
+ - lib/query_guard/analysis/risk_level.rb
87
+ - lib/query_guard/analyzers/base.rb
88
+ - lib/query_guard/analyzers/query_count_analyzer.rb
89
+ - lib/query_guard/analyzers/query_risk_analyzer.rb
90
+ - lib/query_guard/analyzers/registry.rb
91
+ - lib/query_guard/analyzers/select_star_analyzer.rb
92
+ - lib/query_guard/analyzers/slow_query_analyzer.rb
93
+ - lib/query_guard/budget.rb
94
+ - lib/query_guard/cli.rb
95
+ - lib/query_guard/cli/batch_report_formatter.rb
96
+ - lib/query_guard/cli/command.rb
97
+ - lib/query_guard/cli/commands/analyze.rb
98
+ - lib/query_guard/cli/commands/check.rb
99
+ - lib/query_guard/cli/formatter.rb
100
+ - lib/query_guard/cli/json_reporter.rb
101
+ - lib/query_guard/cli/paged_report_formatter.rb
102
+ - lib/query_guard/cli/source_metadata_collector.rb
62
103
  - lib/query_guard/client.rb
63
104
  - lib/query_guard/config.rb
105
+ - lib/query_guard/core/context.rb
106
+ - lib/query_guard/core/finding.rb
107
+ - lib/query_guard/core/finding_builders.rb
108
+ - lib/query_guard/core/query.rb
109
+ - lib/query_guard/explain/adapter_interface.rb
110
+ - lib/query_guard/explain/explain_enricher.rb
111
+ - lib/query_guard/explain/plan_signals.rb
112
+ - lib/query_guard/explain/postgresql_adapter.rb
113
+ - lib/query_guard/exporter.rb
114
+ - lib/query_guard/fingerprint.rb
64
115
  - lib/query_guard/middleware.rb
116
+ - lib/query_guard/migrations/database_adapter.rb
117
+ - lib/query_guard/migrations/migration_analyzer.rb
118
+ - lib/query_guard/migrations/migration_risk_detectors.rb
119
+ - lib/query_guard/migrations/postgresql_adapter.rb
120
+ - lib/query_guard/migrations/table_risk_analyzer.rb
121
+ - lib/query_guard/migrations/table_size_resolver.rb
122
+ - lib/query_guard/publish.rb
123
+ - lib/query_guard/rspec.rb
124
+ - lib/query_guard/security.rb
125
+ - lib/query_guard/store.rb
65
126
  - lib/query_guard/subscriber.rb
127
+ - lib/query_guard/suggest/index_suggester.rb
128
+ - lib/query_guard/suggest/pattern_extractors.rb
129
+ - lib/query_guard/trace.rb
130
+ - lib/query_guard/uploader/http_uploader.rb
131
+ - lib/query_guard/uploader/interface.rb
132
+ - lib/query_guard/uploader/no_op_uploader.rb
133
+ - lib/query_guard/uploader/registry.rb
134
+ - lib/query_guard/uploader/upload_service.rb
66
135
  - lib/query_guard/version.rb
67
136
  - sig/query_guard.rbs
68
137
  homepage: https://github.com/Chitradevi36/query_guard
@@ -91,6 +160,5 @@ requirements: []
91
160
  rubygems_version: 3.2.3
92
161
  signing_key:
93
162
  specification_version: 4
94
- summary: Guardrails for ActiveRecord queries per request (count, slow SQL, SELECT
95
- *).
163
+ summary: Database migration safety analyzer for Rails
96
164
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/Rakefile DELETED
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
9
-
10
- desc "Build gem"
11
- task :build do
12
- sh "gem build query_guard.gemspec"
13
- end
14
-
15
- desc "Release: build and push to RubyGems"
16
- task :release do
17
- version = File.read("lib/query_guard/version.rb")[/VERSION\s*=\s*["'](.+?)["']/, 1]
18
- abort("Version not found") unless version
19
- sh "gem build query_guard.gemspec"
20
- sh "gem push query_guard-#{version}.gem"
21
- end
@@ -1,9 +0,0 @@
1
- QueryGuard.configure do |c|
2
- c.enabled_environments = %i[development test] # Prod is usually off
3
- c.max_queries_per_request = 100 # nil to disable
4
- c.max_duration_ms_per_query = 100.0 # nil to disable
5
- c.block_select_star = false
6
- c.ignored_sql = [/^PRAGMA /i, /^SAVEPOINT/i]
7
- c.raise_on_violation = false # true to raise 500
8
- c.log_prefix = "[QueryGuard]"
9
- end