QueryWise 0.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 (90) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +45 -0
  3. data/CLOUD_RUN_README.md +263 -0
  4. data/DOCKER_README.md +327 -0
  5. data/Dockerfile +69 -0
  6. data/Dockerfile.cloudrun +76 -0
  7. data/Dockerfile.dev +36 -0
  8. data/GEM_Gemfile +16 -0
  9. data/GEM_README.md +421 -0
  10. data/GEM_Rakefile +10 -0
  11. data/GEM_gitignore +137 -0
  12. data/LICENSE.txt +21 -0
  13. data/PUBLISHING_GUIDE.md +269 -0
  14. data/README.md +392 -0
  15. data/app/controllers/api/v1/analysis_controller.rb +340 -0
  16. data/app/controllers/api/v1/api_keys_controller.rb +83 -0
  17. data/app/controllers/api/v1/base_controller.rb +93 -0
  18. data/app/controllers/api/v1/health_controller.rb +86 -0
  19. data/app/controllers/application_controller.rb +2 -0
  20. data/app/controllers/concerns/.keep +0 -0
  21. data/app/jobs/application_job.rb +7 -0
  22. data/app/mailers/application_mailer.rb +4 -0
  23. data/app/models/app_profile.rb +18 -0
  24. data/app/models/application_record.rb +3 -0
  25. data/app/models/concerns/.keep +0 -0
  26. data/app/models/optimization_suggestion.rb +44 -0
  27. data/app/models/query_analysis.rb +47 -0
  28. data/app/models/query_pattern.rb +55 -0
  29. data/app/services/missing_index_detector_service.rb +244 -0
  30. data/app/services/n_plus_one_detector_service.rb +177 -0
  31. data/app/services/slow_query_analyzer_service.rb +225 -0
  32. data/app/services/sql_parser_service.rb +352 -0
  33. data/app/validators/query_data_validator.rb +96 -0
  34. data/app/views/layouts/mailer.html.erb +13 -0
  35. data/app/views/layouts/mailer.text.erb +1 -0
  36. data/app.yaml +109 -0
  37. data/cloudbuild.yaml +47 -0
  38. data/config/application.rb +32 -0
  39. data/config/boot.rb +4 -0
  40. data/config/cable.yml +17 -0
  41. data/config/cache.yml +16 -0
  42. data/config/credentials.yml.enc +1 -0
  43. data/config/database.yml +69 -0
  44. data/config/deploy.yml +116 -0
  45. data/config/environment.rb +5 -0
  46. data/config/environments/development.rb +70 -0
  47. data/config/environments/production.rb +87 -0
  48. data/config/environments/test.rb +53 -0
  49. data/config/initializers/cors.rb +16 -0
  50. data/config/initializers/filter_parameter_logging.rb +8 -0
  51. data/config/initializers/inflections.rb +16 -0
  52. data/config/locales/en.yml +31 -0
  53. data/config/master.key +1 -0
  54. data/config/puma.rb +41 -0
  55. data/config/puma_cloudrun.rb +48 -0
  56. data/config/queue.yml +18 -0
  57. data/config/recurring.yml +15 -0
  58. data/config/routes.rb +28 -0
  59. data/config/storage.yml +34 -0
  60. data/config.ru +6 -0
  61. data/db/cable_schema.rb +11 -0
  62. data/db/cache_schema.rb +14 -0
  63. data/db/migrate/20250818214709_create_app_profiles.rb +13 -0
  64. data/db/migrate/20250818214731_create_query_analyses.rb +22 -0
  65. data/db/migrate/20250818214740_create_query_patterns.rb +22 -0
  66. data/db/migrate/20250818214805_create_optimization_suggestions.rb +20 -0
  67. data/db/queue_schema.rb +129 -0
  68. data/db/schema.rb +79 -0
  69. data/db/seeds.rb +9 -0
  70. data/init.sql +9 -0
  71. data/lib/query_optimizer_client/client.rb +176 -0
  72. data/lib/query_optimizer_client/configuration.rb +43 -0
  73. data/lib/query_optimizer_client/generators/install_generator.rb +43 -0
  74. data/lib/query_optimizer_client/generators/templates/README +46 -0
  75. data/lib/query_optimizer_client/generators/templates/analysis_job.rb +84 -0
  76. data/lib/query_optimizer_client/generators/templates/initializer.rb +30 -0
  77. data/lib/query_optimizer_client/middleware.rb +126 -0
  78. data/lib/query_optimizer_client/railtie.rb +37 -0
  79. data/lib/query_optimizer_client/tasks.rake +228 -0
  80. data/lib/query_optimizer_client/version.rb +5 -0
  81. data/lib/query_optimizer_client.rb +48 -0
  82. data/lib/tasks/.keep +0 -0
  83. data/public/robots.txt +1 -0
  84. data/query_optimizer_client.gemspec +60 -0
  85. data/script/.keep +0 -0
  86. data/storage/.keep +0 -0
  87. data/storage/development.sqlite3 +0 -0
  88. data/storage/test.sqlite3 +0 -0
  89. data/vendor/.keep +0 -0
  90. metadata +265 -0
@@ -0,0 +1,340 @@
1
+ class Api::V1::AnalysisController < Api::V1::BaseController
2
+ def analyze
3
+ if params[:queries].nil?
4
+ render_error('Missing required parameter: queries')
5
+ return
6
+ end
7
+
8
+ queries_data = params[:queries]
9
+
10
+ # Validate queries using the validator
11
+ validation_errors = QueryDataValidator.validate_queries(queries_data)
12
+ if validation_errors.any?
13
+ render_error('Validation failed', :bad_request, errors: validation_errors)
14
+ return
15
+ end
16
+
17
+ # Create QueryAnalysis records
18
+ query_analyses = []
19
+ queries_data.each do |query_data|
20
+ begin
21
+ analysis = create_query_analysis(query_data)
22
+ query_analyses << analysis if analysis
23
+ rescue => e
24
+ Rails.logger.error "Error creating query analysis: #{e.message}"
25
+ Rails.logger.error e.backtrace.join("\n")
26
+ end
27
+ end
28
+
29
+ if query_analyses.empty?
30
+ render_error('No valid queries provided')
31
+ return
32
+ end
33
+
34
+ # Run analysis
35
+ begin
36
+ analysis_results = perform_analysis(query_analyses)
37
+ rescue => e
38
+ Rails.logger.error "Error performing analysis: #{e.message}"
39
+ Rails.logger.error e.backtrace.join("\n")
40
+ render_error('Analysis failed')
41
+ return
42
+ end
43
+
44
+ # Store optimization suggestions
45
+ begin
46
+ store_suggestions(query_analyses, analysis_results)
47
+ rescue => e
48
+ Rails.logger.error "Failed to store suggestions: #{e.message}"
49
+ Rails.logger.error e.backtrace.join("\n")
50
+ # Continue without storing suggestions for now
51
+ end
52
+
53
+ # Format response
54
+ response_data = format_analysis_response(analysis_results)
55
+
56
+ render_success(response_data, message: "Analyzed #{query_analyses.length} queries")
57
+ end
58
+
59
+ def analyze_ci
60
+ if params[:queries].nil?
61
+ render_error('Missing required parameter: queries')
62
+ return
63
+ end
64
+
65
+ queries_data = params[:queries]
66
+ threshold_score = params[:threshold_score] || 70
67
+
68
+ # Validate queries using the validator
69
+ validation_errors = QueryDataValidator.validate_queries(queries_data)
70
+ if validation_errors.any?
71
+ render_error('Validation failed', :bad_request, errors: validation_errors)
72
+ return
73
+ end
74
+
75
+ # Validate threshold score
76
+ if threshold_score.present? && (!threshold_score.is_a?(Numeric) || threshold_score < 0 || threshold_score > 100)
77
+ render_error('Threshold score must be a number between 0 and 100')
78
+ return
79
+ end
80
+
81
+ # Create temporary QueryAnalysis records (don't persist for CI)
82
+ query_analyses = []
83
+ queries_data.each do |query_data|
84
+ analysis = build_query_analysis(query_data)
85
+ query_analyses << analysis if analysis&.valid?
86
+ end
87
+
88
+ if query_analyses.empty?
89
+ render_error('No valid queries provided')
90
+ return
91
+ end
92
+
93
+ # Run analysis
94
+ analysis_results = perform_analysis(query_analyses)
95
+
96
+ # Calculate CI score
97
+ ci_results = calculate_ci_score(analysis_results, threshold_score)
98
+
99
+ render_success(ci_results)
100
+ end
101
+
102
+ private
103
+
104
+ def create_query_analysis(query_data)
105
+ sql_query = query_data[:sql] || query_data['sql']
106
+ duration_ms = query_data[:duration_ms] || query_data['duration_ms']
107
+
108
+ if sql_query.blank?
109
+ return nil
110
+ end
111
+
112
+ parser = SqlParserService.new(sql_query)
113
+
114
+ # Ensure we have valid parsed data
115
+ unless parser.valid?
116
+ Rails.logger.error "Invalid SQL query: #{sql_query}"
117
+ return nil
118
+ end
119
+
120
+ @current_app_profile.query_analyses.create!(
121
+ sql_query: sql_query,
122
+ duration_ms: duration_ms,
123
+ table_name: parser.primary_table,
124
+ query_type: parser.query_type,
125
+ analyzed_at: Time.current,
126
+ query_hash: parser.query_signature,
127
+ parsed_data: {
128
+ where_columns: parser.where_columns,
129
+ order_by_columns: parser.order_by_columns,
130
+ all_tables: parser.all_tables
131
+ }
132
+ )
133
+ rescue => e
134
+ Rails.logger.error "Failed to create query analysis: #{e.message}"
135
+ Rails.logger.error e.backtrace.join("\n")
136
+ nil
137
+ end
138
+
139
+ def build_query_analysis(query_data)
140
+ sql_query = query_data[:sql] || query_data['sql']
141
+ duration_ms = query_data[:duration_ms] || query_data['duration_ms']
142
+
143
+ return nil if sql_query.blank?
144
+
145
+ parser = SqlParserService.new(sql_query)
146
+
147
+ @current_app_profile.query_analyses.build(
148
+ sql_query: sql_query,
149
+ duration_ms: duration_ms,
150
+ table_name: parser.primary_table,
151
+ query_type: parser.query_type,
152
+ analyzed_at: Time.current,
153
+ query_hash: parser.query_signature,
154
+ parsed_data: {
155
+ where_columns: parser.where_columns,
156
+ order_by_columns: parser.order_by_columns,
157
+ all_tables: parser.all_tables
158
+ }
159
+ )
160
+ rescue => e
161
+ Rails.logger.error "Failed to build query analysis: #{e.message}"
162
+ nil
163
+ end
164
+
165
+ def perform_analysis(query_analyses)
166
+ results = {
167
+ n_plus_one: [],
168
+ slow_queries: [],
169
+ missing_indexes: []
170
+ }
171
+
172
+ # N+1 Detection
173
+ n_plus_one_patterns = NPlusOneDetectorService.detect(query_analyses)
174
+ results[:n_plus_one] = n_plus_one_patterns
175
+
176
+ # Slow Query Analysis
177
+ slow_query_issues = SlowQueryAnalyzerService.analyze(query_analyses)
178
+ results[:slow_queries] = slow_query_issues
179
+
180
+ # Missing Index Detection
181
+ index_suggestions = MissingIndexDetectorService.detect(query_analyses)
182
+ results[:missing_indexes] = index_suggestions
183
+
184
+ results
185
+ end
186
+
187
+ def store_suggestions(query_analyses, analysis_results)
188
+ # Store N+1 suggestions
189
+ analysis_results[:n_plus_one].each do |pattern|
190
+ next unless pattern[:first_query]
191
+
192
+ OptimizationSuggestion.create!(
193
+ query_analysis: pattern[:first_query],
194
+ suggestion_type: 'n_plus_one',
195
+ title: pattern[:suggestion][:title],
196
+ description: pattern[:suggestion][:description],
197
+ sql_suggestion: pattern[:suggestion][:sql_suggestion],
198
+ priority: severity_to_priority(pattern[:severity]),
199
+ metadata: pattern.except(:first_query, :sample_queries)
200
+ )
201
+ end
202
+
203
+ # Store slow query suggestions
204
+ analysis_results[:slow_queries].each do |issue|
205
+ issue[:suggestions].each do |suggestion|
206
+ OptimizationSuggestion.create!(
207
+ query_analysis: issue[:query_analysis],
208
+ suggestion_type: 'slow_query',
209
+ title: suggestion[:title],
210
+ description: suggestion[:description],
211
+ sql_suggestion: suggestion[:sql_example],
212
+ priority: priority_to_number(suggestion[:priority]),
213
+ metadata: suggestion.except(:title, :description, :sql_example)
214
+ )
215
+ end
216
+ end
217
+
218
+ # Store index suggestions (create a representative query analysis if needed)
219
+ analysis_results[:missing_indexes].each do |suggestion|
220
+ # Find a representative query for this table
221
+ representative_query = query_analyses.find { |qa| qa.table_name == suggestion[:table_name] }
222
+ next unless representative_query
223
+
224
+ OptimizationSuggestion.create!(
225
+ query_analysis: representative_query,
226
+ suggestion_type: 'missing_index',
227
+ title: "Add index on #{suggestion[:table_name]}(#{suggestion[:columns].join(', ')})",
228
+ description: suggestion[:description],
229
+ sql_suggestion: suggestion[:sql],
230
+ priority: suggestion[:priority],
231
+ metadata: suggestion.except(:sql, :description)
232
+ )
233
+ end
234
+ rescue => e
235
+ Rails.logger.error "Failed to store suggestions: #{e.message}"
236
+ end
237
+
238
+ def format_analysis_response(results)
239
+ {
240
+ n_plus_one: {
241
+ detected: results[:n_plus_one].any?,
242
+ patterns: results[:n_plus_one].map { |pattern| format_n_plus_one_pattern(pattern) }
243
+ },
244
+ slow_queries: results[:slow_queries].map { |issue| format_slow_query_issue(issue) },
245
+ missing_indexes: results[:missing_indexes].map { |suggestion| format_index_suggestion(suggestion) },
246
+ summary: {
247
+ total_issues: results[:n_plus_one].length + results[:slow_queries].length,
248
+ index_suggestions: results[:missing_indexes].length,
249
+ severity_breakdown: calculate_severity_breakdown(results)
250
+ }
251
+ }
252
+ end
253
+
254
+ def format_n_plus_one_pattern(pattern)
255
+ {
256
+ table: pattern[:table_name],
257
+ column: pattern[:column_name],
258
+ query_count: pattern[:query_count],
259
+ severity: pattern[:severity],
260
+ suggestion: pattern[:suggestion][:rails_suggestion],
261
+ example_code: pattern[:suggestion][:example_code]
262
+ }
263
+ end
264
+
265
+ def format_slow_query_issue(issue)
266
+ {
267
+ sql: issue[:query_analysis].sql_query,
268
+ duration_ms: issue[:duration_ms],
269
+ severity: issue[:severity],
270
+ table: issue[:table_name],
271
+ suggestions: issue[:suggestions].map { |s| s.slice(:title, :description, :recommendation) }
272
+ }
273
+ end
274
+
275
+ def format_index_suggestion(suggestion)
276
+ {
277
+ table: suggestion[:table_name],
278
+ columns: suggestion[:columns],
279
+ type: suggestion[:type],
280
+ reason: suggestion[:reason],
281
+ sql: suggestion[:sql],
282
+ priority: suggestion[:priority],
283
+ impact: suggestion[:impact]
284
+ }
285
+ end
286
+
287
+ def calculate_ci_score(results, threshold)
288
+ total_issues = results[:n_plus_one].length + results[:slow_queries].length
289
+ critical_issues = results[:n_plus_one].count { |p| p[:severity] == 'critical' } +
290
+ results[:slow_queries].count { |i| i[:severity] == 'critical' }
291
+
292
+ # Calculate score (100 = perfect, 0 = terrible)
293
+ score = 100
294
+ score -= total_issues * 5 # -5 points per issue
295
+ score -= critical_issues * 15 # Additional -15 points for critical issues
296
+ score = [score, 0].max # Don't go below 0
297
+
298
+ {
299
+ score: score,
300
+ passed: score >= threshold,
301
+ threshold: threshold,
302
+ issues: {
303
+ total: total_issues,
304
+ critical: critical_issues,
305
+ n_plus_one: results[:n_plus_one].length,
306
+ slow_queries: results[:slow_queries].length
307
+ },
308
+ recommendations: results[:missing_indexes].length
309
+ }
310
+ end
311
+
312
+ def severity_to_priority(severity)
313
+ case severity
314
+ when 'low' then 1
315
+ when 'medium' then 2
316
+ when 'high' then 3
317
+ when 'critical' then 4
318
+ else 2
319
+ end
320
+ end
321
+
322
+ def priority_to_number(priority)
323
+ case priority
324
+ when 'low' then 1
325
+ when 'medium' then 2
326
+ when 'high' then 3
327
+ when 'critical' then 4
328
+ else 2
329
+ end
330
+ end
331
+
332
+ def calculate_severity_breakdown(results)
333
+ breakdown = { low: 0, medium: 0, high: 0, critical: 0 }
334
+
335
+ results[:n_plus_one].each { |p| breakdown[p[:severity].to_sym] += 1 }
336
+ results[:slow_queries].each { |i| breakdown[i[:severity].to_sym] += 1 }
337
+
338
+ breakdown
339
+ end
340
+ end
@@ -0,0 +1,83 @@
1
+ class Api::V1::ApiKeysController < Api::V1::BaseController
2
+ skip_before_action :authenticate_api_key, only: [:create]
3
+
4
+ def create
5
+ if params[:app_name].nil?
6
+ render_error('Missing required parameter: app_name')
7
+ return
8
+ end
9
+
10
+ app_name = params[:app_name]
11
+
12
+ if app_name.blank?
13
+ render_error('App name is required')
14
+ return
15
+ end
16
+
17
+ if app_name.length < 3 || app_name.length > 100
18
+ render_error('App name must be between 3 and 100 characters')
19
+ return
20
+ end
21
+
22
+ # Check if app name already exists
23
+ if AppProfile.exists?(name: app_name)
24
+ render_error('App name already exists')
25
+ return
26
+ end
27
+
28
+ app_profile = AppProfile.new(name: app_name)
29
+ api_key = app_profile.generate_api_key!
30
+
31
+ if app_profile.persisted?
32
+ render_success({
33
+ app_name: app_profile.name,
34
+ api_key: api_key,
35
+ created_at: app_profile.created_at.iso8601
36
+ }, message: 'API key created successfully')
37
+ else
38
+ render_error('Failed to create API key', :internal_server_error)
39
+ end
40
+ rescue ActionController::ParameterMissing => e
41
+ render_error("Missing required parameter: #{e.param}")
42
+ end
43
+
44
+ def show
45
+ render_success({
46
+ app_name: @current_app_profile.name,
47
+ created_at: @current_app_profile.created_at.iso8601,
48
+ last_used: @current_app_profile.updated_at.iso8601,
49
+ total_queries: @current_app_profile.query_analyses.count
50
+ })
51
+ end
52
+
53
+ def regenerate
54
+ begin
55
+ new_api_key = @current_app_profile.generate_api_key!
56
+
57
+ render_success({
58
+ app_name: @current_app_profile.name,
59
+ api_key: new_api_key,
60
+ regenerated_at: Time.current.iso8601
61
+ }, message: 'API key regenerated successfully')
62
+ rescue => e
63
+ Rails.logger.error "Failed to regenerate API key: #{e.message}"
64
+ render_error('Failed to regenerate API key', :internal_server_error)
65
+ end
66
+ end
67
+
68
+ def destroy
69
+ app_name = @current_app_profile.name
70
+
71
+ begin
72
+ @current_app_profile.destroy!
73
+
74
+ render_success({
75
+ app_name: app_name,
76
+ deleted_at: Time.current.iso8601
77
+ }, message: 'API key deleted successfully')
78
+ rescue => e
79
+ Rails.logger.error "Failed to delete API key: #{e.message}"
80
+ render_error('Failed to delete API key', :internal_server_error)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,93 @@
1
+ class Api::V1::BaseController < ApplicationController
2
+ before_action :authenticate_api_key
3
+ before_action :set_default_response_format
4
+ before_action :check_rate_limit
5
+ before_action :update_last_used
6
+
7
+ rescue_from StandardError, with: :handle_standard_error
8
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
9
+ rescue_from ActionController::ParameterMissing, with: :handle_parameter_missing
10
+
11
+ private
12
+
13
+ def authenticate_api_key
14
+ api_key = request.headers['X-API-Key'] || params[:api_key]
15
+
16
+ if api_key.blank?
17
+ render_error('API key is required', :unauthorized)
18
+ return
19
+ end
20
+
21
+ @current_app_profile = AppProfile.find_by(api_key_digest: BCrypt::Password.create(api_key))
22
+
23
+ unless @current_app_profile
24
+ render_error('Invalid API key', :unauthorized)
25
+ return
26
+ end
27
+ end
28
+
29
+ def set_default_response_format
30
+ request.format = :json
31
+ end
32
+
33
+ def render_success(data, status: :ok, message: nil)
34
+ response = {
35
+ success: true,
36
+ data: data
37
+ }
38
+ response[:message] = message if message
39
+
40
+ render json: response, status: status
41
+ end
42
+
43
+ def render_error(message, status = :bad_request, errors: nil)
44
+ response = {
45
+ success: false,
46
+ error: message
47
+ }
48
+ response[:errors] = errors if errors
49
+
50
+ render json: response, status: status
51
+ end
52
+
53
+ def handle_standard_error(exception)
54
+ Rails.logger.error "API Error: #{exception.message}"
55
+ Rails.logger.error exception.backtrace.join("\n")
56
+
57
+ render_error('Internal server error', :internal_server_error)
58
+ end
59
+
60
+ def handle_not_found(exception)
61
+ render_error('Resource not found', :not_found)
62
+ end
63
+
64
+ def handle_parameter_missing(exception)
65
+ render_error("Missing required parameter: #{exception.param}", :bad_request)
66
+ end
67
+
68
+ def check_rate_limit
69
+ return unless @current_app_profile
70
+
71
+ # Simple rate limiting: 1000 requests per hour per API key
72
+ cache_key = "rate_limit:#{@current_app_profile.id}"
73
+ current_count = Rails.cache.read(cache_key) || 0
74
+
75
+ if current_count >= 1000
76
+ render_error('Rate limit exceeded. Maximum 1000 requests per hour.', :too_many_requests)
77
+ return
78
+ end
79
+
80
+ Rails.cache.write(cache_key, current_count + 1, expires_in: 1.hour)
81
+ end
82
+
83
+ def update_last_used
84
+ return unless @current_app_profile
85
+
86
+ # Update the last used timestamp (but not on every request to avoid too many DB writes)
87
+ last_update = Rails.cache.read("last_update:#{@current_app_profile.id}")
88
+ if last_update.nil? || last_update < 5.minutes.ago
89
+ @current_app_profile.touch(:updated_at)
90
+ Rails.cache.write("last_update:#{@current_app_profile.id}", Time.current, expires_in: 5.minutes)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,86 @@
1
+ class Api::V1::HealthController < Api::V1::BaseController
2
+ # Skip authentication for health checks
3
+ skip_before_action :authenticate_api_key
4
+
5
+ def show
6
+ health_data = {
7
+ status: 'ok',
8
+ timestamp: Time.current.iso8601,
9
+ version: '1.0.0',
10
+ services: check_services
11
+ }
12
+
13
+ overall_status = health_data[:services].values.all? { |status| status == 'ok' } ? 'ok' : 'degraded'
14
+ health_data[:status] = overall_status
15
+
16
+ status_code = overall_status == 'ok' ? :ok : :service_unavailable
17
+
18
+ render json: health_data, status: status_code
19
+ end
20
+
21
+ private
22
+
23
+ def check_services
24
+ services = {}
25
+
26
+ # Database connectivity
27
+ services[:database] = check_database
28
+
29
+ # SQL Parser
30
+ services[:sql_parser] = check_sql_parser
31
+
32
+ # Analysis services
33
+ services[:analysis_services] = check_analysis_services
34
+
35
+ services
36
+ end
37
+
38
+ def check_database
39
+ ActiveRecord::Base.connection.execute('SELECT 1')
40
+ 'ok'
41
+ rescue => e
42
+ Rails.logger.error "Database health check failed: #{e.message}"
43
+ 'error'
44
+ end
45
+
46
+ def check_sql_parser
47
+ test_sql = "SELECT id FROM users WHERE email = 'test@example.com'"
48
+ parser = SqlParserService.new(test_sql)
49
+
50
+ if parser.valid? && parser.query_type == 'SELECT' && parser.primary_table == 'users'
51
+ 'ok'
52
+ else
53
+ 'error'
54
+ end
55
+ rescue => e
56
+ Rails.logger.error "SQL Parser health check failed: #{e.message}"
57
+ 'error'
58
+ end
59
+
60
+ def check_analysis_services
61
+ # Test with a simple query analysis
62
+ test_queries = [
63
+ QueryAnalysis.new(
64
+ sql_query: "SELECT * FROM users WHERE id = 1",
65
+ duration_ms: 100,
66
+ table_name: "users",
67
+ query_type: "SELECT",
68
+ analyzed_at: Time.current
69
+ )
70
+ ]
71
+
72
+ # Test N+1 detector
73
+ NPlusOneDetectorService.detect(test_queries)
74
+
75
+ # Test slow query analyzer
76
+ SlowQueryAnalyzerService.analyze(test_queries)
77
+
78
+ # Test missing index detector
79
+ MissingIndexDetectorService.detect(test_queries)
80
+
81
+ 'ok'
82
+ rescue => e
83
+ Rails.logger.error "Analysis services health check failed: #{e.message}"
84
+ 'error'
85
+ end
86
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::API
2
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ # Automatically retry jobs that encountered a deadlock
3
+ # retry_on ActiveRecord::Deadlocked
4
+
5
+ # Most jobs are safe to ignore if the underlying records are no longer available
6
+ # discard_on ActiveJob::DeserializationError
7
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: "from@example.com"
3
+ layout "mailer"
4
+ end
@@ -0,0 +1,18 @@
1
+ class AppProfile < ApplicationRecord
2
+ has_many :query_analyses, dependent: :destroy
3
+ has_many :optimization_suggestions, through: :query_analyses
4
+
5
+ validates :name, presence: true, length: { minimum: 1, maximum: 100 }
6
+ validates :api_key_digest, presence: true, uniqueness: true
7
+
8
+ def self.authenticate_with_api_key(api_key)
9
+ find_by(api_key_digest: BCrypt::Password.create(api_key))
10
+ end
11
+
12
+ def generate_api_key!
13
+ api_key = SecureRandom.hex(32)
14
+ self.api_key_digest = BCrypt::Password.create(api_key)
15
+ save!
16
+ api_key
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ end
File without changes
@@ -0,0 +1,44 @@
1
+ class OptimizationSuggestion < ApplicationRecord
2
+ belongs_to :query_analysis
3
+ has_one :app_profile, through: :query_analysis
4
+
5
+ validates :suggestion_type, presence: true,
6
+ inclusion: { in: %w[n_plus_one slow_query missing_index query_optimization] }
7
+ validates :title, presence: true, length: { maximum: 200 }
8
+ validates :description, presence: true
9
+ validates :priority, presence: true,
10
+ inclusion: { in: 1..4 } # 1=low, 2=medium, 3=high, 4=critical
11
+
12
+ scope :by_type, ->(type) { where(suggestion_type: type) }
13
+ scope :by_priority, ->(priority) { where(priority: priority) }
14
+ scope :high_priority, -> { where(priority: [3, 4]) }
15
+ scope :not_implemented, -> { where(implemented: false) }
16
+ scope :implemented, -> { where(implemented: true) }
17
+
18
+ def priority_label
19
+ case priority
20
+ when 1 then 'Low'
21
+ when 2 then 'Medium'
22
+ when 3 then 'High'
23
+ when 4 then 'Critical'
24
+ end
25
+ end
26
+
27
+ def mark_implemented!
28
+ update!(implemented: true)
29
+ end
30
+
31
+ def self.create_for_analysis(analysis, suggestions_data)
32
+ suggestions_data.map do |suggestion_data|
33
+ create!(
34
+ query_analysis: analysis,
35
+ suggestion_type: suggestion_data[:type],
36
+ title: suggestion_data[:title],
37
+ description: suggestion_data[:description],
38
+ sql_suggestion: suggestion_data[:sql_suggestion],
39
+ priority: suggestion_data[:priority] || 2,
40
+ metadata: suggestion_data[:metadata] || {}
41
+ )
42
+ end
43
+ end
44
+ end