rails_pulse 0.1.2 → 0.1.4

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -20
  3. data/Rakefile +169 -86
  4. data/app/assets/images/rails_pulse/dashboard.png +0 -0
  5. data/app/assets/images/rails_pulse/request.png +0 -0
  6. data/app/assets/stylesheets/rails_pulse/application.css +28 -5
  7. data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
  8. data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
  9. data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
  10. data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
  11. data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
  12. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
  13. data/app/controllers/concerns/zoom_range_concern.rb +31 -0
  14. data/app/controllers/rails_pulse/application_controller.rb +5 -1
  15. data/app/controllers/rails_pulse/queries_controller.rb +49 -10
  16. data/app/controllers/rails_pulse/requests_controller.rb +46 -20
  17. data/app/controllers/rails_pulse/routes_controller.rb +40 -1
  18. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
  19. data/app/helpers/rails_pulse/chart_helper.rb +16 -8
  20. data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
  21. data/app/javascript/rails_pulse/application.js +34 -3
  22. data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
  23. data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
  24. data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
  25. data/app/javascript/rails_pulse/controllers/index_controller.js +249 -11
  26. data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
  27. data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
  28. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
  29. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
  30. data/app/models/rails_pulse/queries/cards/average_query_times.rb +20 -20
  31. data/app/models/rails_pulse/queries/cards/execution_rate.rb +58 -14
  32. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +14 -9
  33. data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
  34. data/app/models/rails_pulse/query.rb +46 -0
  35. data/app/models/rails_pulse/request.rb +1 -1
  36. data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
  37. data/app/models/rails_pulse/requests/tables/index.rb +77 -0
  38. data/app/models/rails_pulse/routes/cards/average_response_times.rb +18 -20
  39. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +14 -9
  40. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +14 -9
  41. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +29 -13
  42. data/app/models/rails_pulse/routes/tables/index.rb +4 -2
  43. data/app/models/rails_pulse/summary.rb +7 -7
  44. data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
  45. data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
  46. data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
  47. data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
  48. data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
  49. data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +154 -0
  50. data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
  51. data/app/services/rails_pulse/query_analysis_service.rb +125 -0
  52. data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
  53. data/app/views/layouts/rails_pulse/application.html.erb +0 -2
  54. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
  55. data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
  56. data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
  57. data/app/views/rails_pulse/components/_metric_card.html.erb +28 -5
  58. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
  59. data/app/views/rails_pulse/components/_panel.html.erb +1 -1
  60. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
  61. data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
  62. data/app/views/rails_pulse/dashboard/index.html.erb +2 -2
  63. data/app/views/rails_pulse/operations/show.html.erb +17 -15
  64. data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
  65. data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
  66. data/app/views/rails_pulse/queries/_analysis_results.html.erb +117 -0
  67. data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
  68. data/app/views/rails_pulse/queries/_show_table.html.erb +34 -6
  69. data/app/views/rails_pulse/queries/_table.html.erb +4 -8
  70. data/app/views/rails_pulse/queries/index.html.erb +48 -51
  71. data/app/views/rails_pulse/queries/show.html.erb +56 -52
  72. data/app/views/rails_pulse/requests/_operations.html.erb +30 -43
  73. data/app/views/rails_pulse/requests/_table.html.erb +31 -18
  74. data/app/views/rails_pulse/requests/index.html.erb +55 -50
  75. data/app/views/rails_pulse/requests/show.html.erb +0 -2
  76. data/app/views/rails_pulse/routes/_requests_table.html.erb +39 -0
  77. data/app/views/rails_pulse/routes/_table.html.erb +4 -10
  78. data/app/views/rails_pulse/routes/index.html.erb +49 -52
  79. data/app/views/rails_pulse/routes/show.html.erb +6 -8
  80. data/config/initializers/rails_charts_csp_patch.rb +32 -40
  81. data/config/routes.rb +5 -1
  82. data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
  83. data/db/rails_pulse_schema.rb +10 -1
  84. data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +81 -0
  85. data/lib/generators/rails_pulse/install_generator.rb +75 -18
  86. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +72 -2
  87. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +23 -0
  88. data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
  89. data/lib/generators/rails_pulse/upgrade_generator.rb +226 -0
  90. data/lib/rails_pulse/engine.rb +21 -0
  91. data/lib/rails_pulse/version.rb +1 -1
  92. data/lib/tasks/rails_pulse.rake +27 -8
  93. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  94. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  95. data/public/rails-pulse-assets/rails-pulse.js +53 -53
  96. data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
  97. metadata +25 -6
  98. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  99. data/app/assets/images/rails_pulse/routes.png +0 -0
  100. data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
  101. data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
@@ -0,0 +1,241 @@
1
+ # Detects N+1 query patterns by analyzing query repetition within request cycles.
2
+ # Groups operations by time windows and identifies repetitive single-record lookups that suggest missing eager loading.
3
+ module RailsPulse
4
+ module Analysis
5
+ class NPlusOneDetector < BaseAnalyzer
6
+ REQUEST_GROUPING_WINDOW = 0.1.seconds
7
+ REPETITION_THRESHOLD = 3
8
+
9
+ def analyze
10
+ return default_result if operations.empty?
11
+
12
+ analysis = {
13
+ is_likely_n_plus_one: false,
14
+ confidence_score: 0,
15
+ evidence: [],
16
+ suggested_fixes: [],
17
+ execution_patterns: {}
18
+ }
19
+
20
+ # Group operations by request cycles
21
+ request_groups = group_operations_by_request
22
+
23
+ # Analyze patterns within each request
24
+ request_groups.each do |group|
25
+ pattern_analysis = analyze_request_pattern(group)
26
+
27
+ if pattern_analysis[:repetitive_queries]
28
+ analysis[:is_likely_n_plus_one] = true
29
+ analysis[:confidence_score] += pattern_analysis[:confidence]
30
+ analysis[:evidence].concat(pattern_analysis[:evidence])
31
+ analysis[:suggested_fixes].concat(pattern_analysis[:fixes])
32
+ end
33
+ end
34
+
35
+ # Normalize confidence score
36
+ analysis[:confidence_score] = [ analysis[:confidence_score], 100 ].min
37
+
38
+ # Add execution pattern analysis
39
+ analysis[:execution_patterns] = analyze_execution_patterns
40
+
41
+ # Generate ActiveRecord-specific suggestions
42
+ if analysis[:is_likely_n_plus_one]
43
+ analysis[:suggested_fixes].concat(generate_activerecord_fixes)
44
+ end
45
+
46
+ analysis
47
+ end
48
+
49
+ private
50
+
51
+ def default_result
52
+ {
53
+ is_likely_n_plus_one: false,
54
+ confidence_score: 0,
55
+ evidence: [],
56
+ suggested_fixes: [],
57
+ execution_patterns: {}
58
+ }
59
+ end
60
+
61
+ def group_operations_by_request
62
+ groups = []
63
+ current_group = []
64
+
65
+ sorted_operations = operations.sort_by(&:occurred_at)
66
+
67
+ sorted_operations.each do |operation|
68
+ if current_group.empty? || time_within_window?(operation, current_group.last)
69
+ current_group << operation
70
+ else
71
+ groups << current_group if current_group.size > 1
72
+ current_group = [ operation ]
73
+ end
74
+ end
75
+
76
+ groups << current_group if current_group.size > 1
77
+ groups
78
+ end
79
+
80
+ def time_within_window?(operation, last_operation)
81
+ (operation.occurred_at - last_operation.occurred_at) < REQUEST_GROUPING_WINDOW
82
+ end
83
+
84
+ def analyze_request_pattern(operations_group)
85
+ analysis = {
86
+ repetitive_queries: false,
87
+ confidence: 0,
88
+ evidence: [],
89
+ fixes: []
90
+ }
91
+
92
+ # Look for repeated similar queries
93
+ normalized_queries = operations_group.map { |op| normalize_sql_for_pattern_detection(op.label) }
94
+ query_counts = normalized_queries.tally
95
+
96
+ query_counts.each do |normalized_query, count|
97
+ next unless count >= REPETITION_THRESHOLD
98
+
99
+ analysis[:repetitive_queries] = true
100
+ analysis[:confidence] += count * 10 # Higher repetition = higher confidence
101
+
102
+ analysis[:evidence] << {
103
+ type: "repetitive_query",
104
+ description: "Query executed #{count} times in single request",
105
+ query_pattern: normalized_query,
106
+ occurrences: count
107
+ }
108
+
109
+ # Detect specific N+1 patterns
110
+ if single_record_lookup_pattern?(normalized_query)
111
+ analysis[:evidence] << {
112
+ type: "single_record_lookup",
113
+ description: "Single record lookup pattern suggests missing eager loading",
114
+ query_pattern: normalized_query
115
+ }
116
+
117
+ analysis[:fixes] << {
118
+ type: "eager_loading",
119
+ description: "Use includes() or preload() to load associated records",
120
+ code_example: detect_association_from_query(normalized_query)
121
+ }
122
+ end
123
+ end
124
+
125
+ analysis
126
+ end
127
+
128
+ def single_record_lookup_pattern?(normalized_query)
129
+ normalized_query.match?(/SELECT.*FROM.*WHERE.*=\s*\?/i)
130
+ end
131
+
132
+ def analyze_execution_patterns
133
+ return {} if operations.empty?
134
+
135
+ {
136
+ total_executions: operations.count,
137
+ time_span_minutes: time_span_in_minutes,
138
+ executions_per_minute: calculate_executions_per_minute,
139
+ peak_execution_periods: find_peak_execution_periods,
140
+ common_execution_contexts: extract_execution_contexts
141
+ }
142
+ end
143
+
144
+ def time_span_in_minutes
145
+ return 0 if operations.count < 2
146
+ ((operations.last.occurred_at - operations.first.occurred_at) / 1.minute).round(2)
147
+ end
148
+
149
+ def calculate_executions_per_minute
150
+ return 0 if operations.count < 2
151
+
152
+ time_span = operations.last.occurred_at - operations.first.occurred_at
153
+ return operations.count if time_span <= 0
154
+
155
+ (operations.count / (time_span / 1.minute)).round(2)
156
+ end
157
+
158
+ def find_peak_execution_periods
159
+ # Group by 5-minute windows and find peaks
160
+ windows = operations.group_by { |op| (op.occurred_at.to_i / 300) * 300 }
161
+ return [] if windows.empty?
162
+
163
+ avg_per_window = windows.values.sum(&:count).to_f / windows.count
164
+
165
+ windows.select { |_, ops| ops.count > avg_per_window * 1.5 }.map do |timestamp, ops|
166
+ {
167
+ period: Time.at(timestamp).strftime("%Y-%m-%d %H:%M"),
168
+ executions: ops.count,
169
+ above_average_by: ((ops.count - avg_per_window) / avg_per_window * 100).round(1)
170
+ }
171
+ end
172
+ end
173
+
174
+ def extract_execution_contexts
175
+ contexts = operations.filter_map(&:codebase_location).compact
176
+ return {} if contexts.empty?
177
+
178
+ # Extract controller/model patterns
179
+ controller_actions = extract_controller_actions(contexts)
180
+ model_methods = extract_model_methods(contexts)
181
+
182
+ {
183
+ controller_actions: controller_actions.tally,
184
+ model_methods: model_methods.tally,
185
+ unique_locations: contexts.uniq.count,
186
+ total_contexts: contexts.count
187
+ }
188
+ end
189
+
190
+ def extract_controller_actions(contexts)
191
+ contexts.filter_map do |context|
192
+ match = context.match(%r{app/controllers/(.+?)#(.+)})
193
+ "#{match[1]}##{match[2]}" if match
194
+ end
195
+ end
196
+
197
+ def extract_model_methods(contexts)
198
+ contexts.filter_map do |context|
199
+ match = context.match(%r{app/models/(.+?)\.rb.*in `(.+?)'})
200
+ "#{match[1]}.#{match[2]}" if match
201
+ end
202
+ end
203
+
204
+ def generate_activerecord_fixes
205
+ [
206
+ {
207
+ type: "includes",
208
+ description: "Use includes() to eager load associations",
209
+ code_example: "User.includes(:posts).where(active: true)"
210
+ },
211
+ {
212
+ type: "preload",
213
+ description: "Use preload() when you don't need to query on associations",
214
+ code_example: "User.preload(:posts).limit(10)"
215
+ },
216
+ {
217
+ type: "joins",
218
+ description: "Use joins() when you only need to filter, not access associated data",
219
+ code_example: "User.joins(:posts).where(posts: { published: true })"
220
+ }
221
+ ]
222
+ end
223
+
224
+ def detect_association_from_query(normalized_query)
225
+ # Try to extract table/column info to suggest specific associations
226
+ match = normalized_query.match(/from\s+(\w+).*where\s+(\w+)\s*=/)
227
+ return "Model.includes(:association)" unless match
228
+
229
+ table = match[1]
230
+ column = match[2]
231
+
232
+ if column.end_with?("_id")
233
+ association = column.gsub("_id", "")
234
+ return "#{table.classify}.includes(:#{association})"
235
+ end
236
+
237
+ "Model.includes(:association)"
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,154 @@
1
+ # Analyzes SQL query structure and complexity.
2
+ # Detects query type, table joins, WHERE complexity, and common anti-patterns like SELECT * or missing LIMIT.
3
+ module RailsPulse
4
+ module Analysis
5
+ class QueryCharacteristicsAnalyzer < BaseAnalyzer
6
+ def analyze
7
+ {
8
+ query_type: detect_query_type,
9
+ table_count: count_tables,
10
+ join_count: count_joins,
11
+ where_clause_complexity: analyze_where_complexity,
12
+ has_subqueries: has_subqueries?,
13
+ has_limit: has_limit?,
14
+ has_order_by: has_order_by?,
15
+ has_group_by: has_group_by?,
16
+ has_having: has_having?,
17
+ has_distinct: has_distinct?,
18
+ has_aggregations: has_aggregations?,
19
+ estimated_complexity: calculate_complexity_score,
20
+ pattern_issues: detect_pattern_issues
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def detect_query_type
27
+ case sql.strip.upcase
28
+ when /^SELECT/ then "SELECT"
29
+ when /^INSERT/ then "INSERT"
30
+ when /^UPDATE/ then "UPDATE"
31
+ when /^DELETE/ then "DELETE"
32
+ when /^CREATE/ then "CREATE"
33
+ when /^DROP/ then "DROP"
34
+ when /^ALTER/ then "ALTER"
35
+ else "UNKNOWN"
36
+ end
37
+ end
38
+
39
+ def count_tables
40
+ tables = []
41
+
42
+ # Match FROM clause with various table name formats
43
+ # Handles: table_name, schema.table, "quoted_table", `backtick_table`
44
+ tables.concat(sql.scan(/FROM\s+(?:`([^`]+)`|"([^"]+)"|'([^']+)'|(\w+(?:\.\w+)?))/i).flatten.compact)
45
+
46
+ # Match JOIN clauses (INNER JOIN, LEFT JOIN, etc.)
47
+ tables.concat(sql.scan(/(?:INNER\s+|LEFT\s+|RIGHT\s+|FULL\s+|CROSS\s+)?JOIN\s+(?:`([^`]+)`|"([^"]+)"|'([^']+)'|(\w+(?:\.\w+)?))/i).flatten.compact)
48
+
49
+ # Remove schema prefixes for uniqueness check (schema.table -> table)
50
+ normalized_tables = tables.map { |table| table.split(".").last }
51
+ normalized_tables.uniq.length
52
+ end
53
+
54
+ def count_joins
55
+ sql.scan(/\bJOIN\b/i).length
56
+ end
57
+
58
+ def analyze_where_complexity
59
+ where_clause = extract_where_clause
60
+ return 0 unless where_clause
61
+
62
+ condition_count = where_clause.scan(/\bAND\b|\bOR\b/i).length + 1
63
+ function_count = where_clause.scan(/\w+\s*\(/).length
64
+
65
+ condition_count + (function_count * 2)
66
+ end
67
+
68
+ def has_subqueries?
69
+ sql.include?("(SELECT")
70
+ end
71
+
72
+ def has_limit?
73
+ sql.match?(/\bLIMIT\s+\d+/i)
74
+ end
75
+
76
+ def has_order_by?
77
+ sql.include?("ORDER BY")
78
+ end
79
+
80
+ def has_group_by?
81
+ sql.include?("GROUP BY")
82
+ end
83
+
84
+ def has_having?
85
+ sql.include?("HAVING")
86
+ end
87
+
88
+ def has_distinct?
89
+ sql.include?("DISTINCT")
90
+ end
91
+
92
+ def has_aggregations?
93
+ sql.match?(/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i)
94
+ end
95
+
96
+ def calculate_complexity_score
97
+ score = 0
98
+ score += count_tables * 2
99
+ score += count_joins * 3
100
+ score += analyze_where_complexity
101
+ score += sql.scan(/\bUNION\b/i).length * 4
102
+ score += sql.scan(/\(SELECT/i).length * 5
103
+ score
104
+ end
105
+
106
+ def detect_pattern_issues
107
+ issues = []
108
+
109
+ # Missing WHERE clause on SELECT
110
+ if sql.match?(/^SELECT.*FROM.*(?!WHERE)/i) && !has_limit?
111
+ issues << {
112
+ type: "missing_where_clause",
113
+ severity: "warning",
114
+ description: "SELECT query without WHERE clause may return excessive data",
115
+ impact: "Performance degradation from full table scans"
116
+ }
117
+ end
118
+
119
+ # SELECT * usage
120
+ if sql.include?("SELECT *")
121
+ issues << {
122
+ type: "select_star",
123
+ severity: "info",
124
+ description: "Using SELECT * may retrieve unnecessary columns",
125
+ impact: "Increased memory usage and network transfer"
126
+ }
127
+ end
128
+
129
+ # Missing LIMIT on potentially large results
130
+ if sql.match?(/^SELECT.*FROM.*WHERE/i) && !has_limit? && !sql.include?("COUNT")
131
+ issues << {
132
+ type: "missing_limit",
133
+ severity: "warning",
134
+ description: "Query may return large result sets without LIMIT",
135
+ impact: "Memory exhaustion and slow response times"
136
+ }
137
+ end
138
+
139
+ # Complex WHERE clauses
140
+ where_clause = extract_where_clause
141
+ if where_clause && where_clause.scan(/\bAND\b|\bOR\b/i).length > 5
142
+ issues << {
143
+ type: "complex_where_clause",
144
+ severity: "warning",
145
+ description: "Complex WHERE clause with many conditions",
146
+ impact: "Difficult to optimize and maintain"
147
+ }
148
+ end
149
+
150
+ issues
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,217 @@
1
+ # Consolidates analysis results into prioritized, actionable optimization suggestions.
2
+ # Combines insights from all analyzers and categorizes suggestions by impact and implementation complexity.
3
+ module RailsPulse
4
+ module Analysis
5
+ class SuggestionGenerator
6
+ attr_reader :analysis_results
7
+
8
+ def initialize(analysis_results)
9
+ @analysis_results = analysis_results
10
+ end
11
+
12
+ def generate
13
+ suggestions = []
14
+
15
+ # Suggestions from pattern issues
16
+ suggestions.concat(generate_issue_suggestions)
17
+
18
+ # Suggestions from index recommendations
19
+ suggestions.concat(generate_index_suggestions)
20
+
21
+ # Suggestions from N+1 analysis
22
+ suggestions.concat(generate_n_plus_one_suggestions)
23
+
24
+ # Suggestions from query characteristics
25
+ suggestions.concat(generate_query_characteristic_suggestions)
26
+
27
+ # Suggestions from explain plan issues
28
+ suggestions.concat(generate_explain_plan_suggestions)
29
+
30
+ # Prioritize and deduplicate suggestions
31
+ prioritize_suggestions(suggestions)
32
+ end
33
+
34
+ private
35
+
36
+ def generate_issue_suggestions
37
+ issues = analysis_results.dig(:query_characteristics, :pattern_issues) || []
38
+
39
+ issues.map do |issue|
40
+ case issue[:type]
41
+ when "select_star"
42
+ {
43
+ type: "optimization",
44
+ action: "Specify only needed columns instead of SELECT *",
45
+ benefit: "Reduced memory usage and faster data transfer",
46
+ priority: "medium",
47
+ category: "sql_optimization"
48
+ }
49
+ when "missing_limit"
50
+ {
51
+ type: "optimization",
52
+ action: "Add LIMIT clause to prevent large result sets",
53
+ benefit: "Controlled memory usage and faster response times",
54
+ priority: "high",
55
+ category: "sql_optimization"
56
+ }
57
+ when "missing_where_clause"
58
+ {
59
+ type: "optimization",
60
+ action: "Add WHERE clause to filter results",
61
+ benefit: "Avoid full table scans and reduce data transfer",
62
+ priority: "high",
63
+ category: "sql_optimization"
64
+ }
65
+ when "complex_where_clause"
66
+ {
67
+ type: "refactoring",
68
+ action: "Simplify WHERE clause by breaking into multiple queries or using views",
69
+ benefit: "Easier maintenance and potentially better performance",
70
+ priority: "medium",
71
+ category: "code_quality"
72
+ }
73
+ end
74
+ end.compact
75
+ end
76
+
77
+ def generate_index_suggestions
78
+ recommendations = analysis_results[:index_recommendations] || []
79
+
80
+ recommendations.map do |rec|
81
+ {
82
+ type: "index",
83
+ action: "Add #{rec[:type]} index: #{rec[:migration_code]}",
84
+ benefit: rec[:estimated_benefit],
85
+ priority: rec[:priority],
86
+ migration_code: rec[:migration_code],
87
+ table: rec[:table],
88
+ columns: rec[:columns],
89
+ category: "database_optimization"
90
+ }
91
+ end
92
+ end
93
+
94
+ def generate_n_plus_one_suggestions
95
+ n_plus_one = analysis_results[:n_plus_one_analysis] || {}
96
+ return [] unless n_plus_one[:is_likely_n_plus_one]
97
+
98
+ suggestions = n_plus_one[:suggested_fixes] || []
99
+
100
+ suggestions.map do |fix|
101
+ {
102
+ type: "n_plus_one",
103
+ action: fix[:description],
104
+ benefit: "Eliminate N+1 queries and reduce database load",
105
+ priority: "high",
106
+ code_example: fix[:code_example],
107
+ confidence: n_plus_one[:confidence_score],
108
+ category: "performance_critical"
109
+ }
110
+ end
111
+ end
112
+
113
+ def generate_query_characteristic_suggestions
114
+ stats = analysis_results[:query_characteristics] || {}
115
+ suggestions = []
116
+
117
+ if stats[:join_count] && stats[:join_count] > 3
118
+ suggestions << {
119
+ type: "optimization",
120
+ action: "Review if all #{stats[:join_count]} JOINs are necessary",
121
+ benefit: "Simplified query execution and better performance",
122
+ priority: "medium",
123
+ category: "sql_optimization"
124
+ }
125
+ end
126
+
127
+ if stats[:estimated_complexity] && stats[:estimated_complexity] > 10
128
+ suggestions << {
129
+ type: "refactoring",
130
+ action: "Consider breaking complex query (complexity: #{stats[:estimated_complexity]}) into smaller parts",
131
+ benefit: "Easier maintenance and potentially better performance",
132
+ priority: "medium",
133
+ category: "code_quality"
134
+ }
135
+ end
136
+
137
+ if stats[:has_subqueries] && stats[:join_count] && stats[:join_count] > 1
138
+ suggestions << {
139
+ type: "optimization",
140
+ action: "Consider converting subqueries to JOINs for better performance",
141
+ benefit: "More efficient query execution in most databases",
142
+ priority: "medium",
143
+ category: "sql_optimization"
144
+ }
145
+ end
146
+
147
+ suggestions
148
+ end
149
+
150
+ def generate_explain_plan_suggestions
151
+ explain_issues = analysis_results.dig(:explain_plan, :issues) || []
152
+
153
+ explain_issues.map do |issue|
154
+ case issue[:type]
155
+ when "sequential_scan"
156
+ {
157
+ type: "index",
158
+ action: "Consider adding database indexes for WHERE clause columns",
159
+ benefit: "Dramatically faster query execution",
160
+ priority: "high",
161
+ category: "database_optimization"
162
+ }
163
+ when "temporary_table"
164
+ {
165
+ type: "optimization",
166
+ action: "Optimize query to avoid temporary tables and filesort operations",
167
+ benefit: "Reduced memory usage and faster execution",
168
+ priority: "medium",
169
+ category: "sql_optimization"
170
+ }
171
+ when "high_cost_operation"
172
+ {
173
+ type: "optimization",
174
+ action: "Review query execution plan for high-cost operations",
175
+ benefit: "Identify specific bottlenecks for targeted optimization",
176
+ priority: "high",
177
+ category: "performance_critical"
178
+ }
179
+ when "where_without_index"
180
+ {
181
+ type: "index",
182
+ action: "Add indexes to support WHERE clause conditions",
183
+ benefit: "Eliminate row-by-row filtering during query execution",
184
+ priority: "high",
185
+ category: "database_optimization"
186
+ }
187
+ end
188
+ end.compact
189
+ end
190
+
191
+ def prioritize_suggestions(suggestions)
192
+ # Remove duplicates based on action
193
+ unique_suggestions = suggestions.uniq { |s| s[:action] }
194
+
195
+ # Sort by priority and category
196
+ unique_suggestions.sort_by do |suggestion|
197
+ priority_score = case suggestion[:priority]
198
+ when "high" then 3
199
+ when "medium" then 2
200
+ when "low" then 1
201
+ else 0
202
+ end
203
+
204
+ category_score = case suggestion[:category]
205
+ when "performance_critical" then 4
206
+ when "database_optimization" then 3
207
+ when "sql_optimization" then 2
208
+ when "code_quality" then 1
209
+ else 0
210
+ end
211
+
212
+ [ -priority_score, -category_score, suggestion[:action] ]
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end