rails_console_pro 0.1.1 → 0.1.3

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +261 -240
  3. data/CHANGELOG.md +4 -0
  4. data/QUICK_START.md +8 -0
  5. data/README.md +16 -0
  6. data/docs/FORMATTING.md +5 -0
  7. data/docs/MODEL_STATISTICS.md +4 -0
  8. data/docs/OBJECT_DIFFING.md +6 -0
  9. data/docs/PROFILING.md +91 -0
  10. data/docs/QUEUE_INSIGHTS.md +82 -0
  11. data/docs/SCHEMA_INSPECTION.md +5 -0
  12. data/docs/SNIPPETS.md +71 -0
  13. data/lib/rails_console_pro/commands/base_command.rb +1 -1
  14. data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
  15. data/lib/rails_console_pro/commands/profile_command.rb +84 -0
  16. data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
  17. data/lib/rails_console_pro/commands.rb +15 -0
  18. data/lib/rails_console_pro/configuration.rb +39 -0
  19. data/lib/rails_console_pro/format_exporter.rb +8 -0
  20. data/lib/rails_console_pro/global_methods.rb +12 -0
  21. data/lib/rails_console_pro/initializer.rb +29 -4
  22. data/lib/rails_console_pro/model_validator.rb +1 -1
  23. data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
  24. data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
  25. data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
  26. data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
  27. data/lib/rails_console_pro/profile_result.rb +109 -0
  28. data/lib/rails_console_pro/pry_commands.rb +106 -0
  29. data/lib/rails_console_pro/queue_insights_result.rb +110 -0
  30. data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
  31. data/lib/rails_console_pro/services/profile_collector.rb +245 -0
  32. data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
  33. data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
  34. data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
  35. data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
  36. data/lib/rails_console_pro/snippets/single_result.rb +30 -0
  37. data/lib/rails_console_pro/snippets/snippet.rb +112 -0
  38. data/lib/rails_console_pro/snippets.rb +12 -0
  39. data/lib/rails_console_pro/version.rb +1 -1
  40. data/rails_console_pro.gemspec +1 -1
  41. metadata +26 -8
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Commands
5
+ # Command for managing, searching, and listing reusable console snippets
6
+ class SnippetsCommand < BaseCommand
7
+ DEFAULT_LIST_LIMIT = 10
8
+
9
+ def execute(action = :list, *args, **kwargs, &block)
10
+ return disabled_message unless enabled?
11
+
12
+ action = infer_action(action, args)
13
+ case action
14
+ when :list
15
+ list_snippets(**kwargs)
16
+ when :search
17
+ search_snippets(*args, **kwargs)
18
+ when :add, :create
19
+ add_snippet(*args, **kwargs, &block)
20
+ when :show
21
+ show_snippet(*args)
22
+ when :delete, :remove
23
+ delete_snippet(*args)
24
+ when :favorite
25
+ toggle_favorite(*args, value: true)
26
+ when :unfavorite
27
+ toggle_favorite(*args, value: false)
28
+ else
29
+ pastel.red("Unknown snippets action: #{action}")
30
+ end
31
+ rescue ArgumentError => e
32
+ pastel.red("Snippets error: #{e.message}")
33
+ end
34
+
35
+ private
36
+
37
+ def enabled?
38
+ RailsConsolePro.config.enabled && RailsConsolePro.config.snippets_command_enabled
39
+ end
40
+
41
+ def disabled_message
42
+ pastel.yellow('Snippets command is disabled. Enable via RailsConsolePro.configure { |c| c.snippets_command_enabled = true }')
43
+ end
44
+
45
+ def infer_action(action, args)
46
+ return action.to_sym if action.respond_to?(:to_sym)
47
+ return :search if args.any?
48
+
49
+ :list
50
+ end
51
+
52
+ def list_snippets(limit: DEFAULT_LIST_LIMIT, favorites: false)
53
+ snippets = repository.all(limit: limit)
54
+ snippets = snippets.select(&:favorite?) if favorites
55
+
56
+ Snippets::CollectionResult.new(
57
+ snippets: snippets,
58
+ limit: limit,
59
+ total_count: repository.all.size,
60
+ tags: favorites ? ['favorite'] : []
61
+ )
62
+ end
63
+
64
+ def search_snippets(term = nil, tags: nil, limit: DEFAULT_LIST_LIMIT)
65
+ search_result = repository.search(term: term, tags: tags, limit: limit)
66
+ snippets = search_result[:results]
67
+ Snippets::CollectionResult.new(
68
+ snippets: snippets,
69
+ query: term,
70
+ tags: Array(tags),
71
+ limit: limit,
72
+ total_count: search_result[:total_count]
73
+ )
74
+ end
75
+
76
+ def add_snippet(body = nil, id: nil, tags: nil, description: nil, favorite: false, metadata: nil, &block)
77
+ snippet_body = extract_body(body, &block)
78
+
79
+ snippet = repository.add(
80
+ body: snippet_body,
81
+ id: id,
82
+ tags: tags,
83
+ description: description,
84
+ favorite: favorite,
85
+ metadata: metadata
86
+ )
87
+
88
+ Snippets::SingleResult.new(snippet: snippet, action: :add, created: true)
89
+ end
90
+
91
+ def show_snippet(id)
92
+ snippet = repository.find(id)
93
+ return pastel.yellow("No snippet found for '#{id}'") unless snippet
94
+
95
+ Snippets::SingleResult.new(snippet: snippet, action: :show, created: false)
96
+ end
97
+
98
+ def delete_snippet(id)
99
+ if repository.delete(id)
100
+ pastel.green("Removed snippet '#{id}'")
101
+ else
102
+ pastel.yellow("No snippet found for '#{id}'")
103
+ end
104
+ end
105
+
106
+ def toggle_favorite(id, value:)
107
+ snippet = repository.update(id, favorite: value)
108
+ return pastel.yellow("No snippet found for '#{id}'") unless snippet
109
+
110
+ status = value ? 'marked as favorite' : 'removed from favorites'
111
+ message = pastel.green("Snippet '#{id}' #{status}.")
112
+ Snippets::SingleResult.new(
113
+ snippet: snippet,
114
+ action: value ? :favorite : :unfavorite,
115
+ created: false,
116
+ message: message
117
+ )
118
+ end
119
+
120
+ def extract_body(body, &block)
121
+ content = if block
122
+ result = block.call
123
+ result.respond_to?(:join) ? result.join("\n") : result.to_s
124
+ else
125
+ body
126
+ end
127
+
128
+ raise ArgumentError, 'Snippet body is required' if content.nil? || content.to_s.strip.empty?
129
+
130
+ content.to_s
131
+ end
132
+
133
+ def repository
134
+ @repository ||= RailsConsolePro::Services::SnippetRepository.new(
135
+ store_path: RailsConsolePro.config.snippet_store_path
136
+ )
137
+ end
138
+ end
139
+ end
140
+ end
141
+
@@ -30,5 +30,20 @@ module RailsConsolePro
30
30
  def diff(object1, object2)
31
31
  DiffCommand.new.execute(object1, object2)
32
32
  end
33
+
34
+ # Profiling command
35
+ def profile(target = nil, *args, **kwargs, &block)
36
+ ProfileCommand.new.execute(target, *args, **kwargs, &block)
37
+ end
38
+
39
+ # Queue insights command
40
+ def jobs(options = {})
41
+ JobsCommand.new.execute(options)
42
+ end
43
+
44
+ # Snippets command
45
+ def snippets(action = :list, *args, **kwargs, &block)
46
+ SnippetsCommand.new.execute(action, *args, **kwargs, &block)
47
+ end
33
48
  end
34
49
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tmpdir'
4
+ require 'pathname'
5
+
3
6
  module RailsConsolePro
4
7
  # Configuration class for Enhanced Console Printer
5
8
  class Configuration
@@ -45,10 +48,14 @@ module RailsConsolePro
45
48
  attr_accessor :navigate_command_enabled
46
49
  attr_accessor :stats_command_enabled
47
50
  attr_accessor :diff_command_enabled
51
+ attr_accessor :snippets_command_enabled
52
+ attr_accessor :profile_command_enabled
53
+ attr_accessor :queue_command_enabled
48
54
  attr_accessor :active_record_printer_enabled
49
55
  attr_accessor :relation_printer_enabled
50
56
  attr_accessor :collection_printer_enabled
51
57
  attr_accessor :export_enabled
58
+ attr_accessor :snippet_store_path
52
59
 
53
60
  # Color customization
54
61
  attr_accessor :color_scheme
@@ -76,6 +83,11 @@ module RailsConsolePro
76
83
  attr_accessor :stats_large_table_threshold
77
84
  attr_accessor :stats_skip_distinct_threshold
78
85
 
86
+ # Profiling settings
87
+ attr_accessor :profile_slow_query_threshold
88
+ attr_accessor :profile_duplicate_query_threshold
89
+ attr_accessor :profile_max_saved_queries
90
+
79
91
  def initialize
80
92
  # Default feature toggles - all enabled
81
93
  @enabled = true
@@ -84,10 +96,14 @@ module RailsConsolePro
84
96
  @navigate_command_enabled = true
85
97
  @stats_command_enabled = true
86
98
  @diff_command_enabled = true
99
+ @snippets_command_enabled = true
100
+ @profile_command_enabled = true
101
+ @queue_command_enabled = true
87
102
  @active_record_printer_enabled = true
88
103
  @relation_printer_enabled = true
89
104
  @collection_printer_enabled = true
90
105
  @export_enabled = true
106
+ @snippet_store_path = default_snippet_store_path
91
107
 
92
108
  # Default color scheme
93
109
  @color_scheme = :dark
@@ -138,6 +154,11 @@ module RailsConsolePro
138
154
  # Default stats calculation settings
139
155
  @stats_large_table_threshold = 10_000 # Consider table large if it has 10k+ records
140
156
  @stats_skip_distinct_threshold = 10_000 # Skip distinct count for tables with 10k+ records
157
+
158
+ # Default profiling settings
159
+ @profile_slow_query_threshold = 100.0 # milliseconds
160
+ @profile_duplicate_query_threshold = 2
161
+ @profile_max_saved_queries = 10
141
162
  end
142
163
 
143
164
  # Set color scheme (dark, light, or custom)
@@ -190,6 +211,7 @@ module RailsConsolePro
190
211
  @navigate_command_enabled = false
191
212
  @stats_command_enabled = false
192
213
  @diff_command_enabled = false
214
+ @queue_command_enabled = false
193
215
  @active_record_printer_enabled = false
194
216
  @relation_printer_enabled = false
195
217
  @collection_printer_enabled = false
@@ -204,6 +226,7 @@ module RailsConsolePro
204
226
  @navigate_command_enabled = true
205
227
  @stats_command_enabled = true
206
228
  @diff_command_enabled = true
229
+ @queue_command_enabled = true
207
230
  @active_record_printer_enabled = true
208
231
  @relation_printer_enabled = true
209
232
  @collection_printer_enabled = true
@@ -214,6 +237,22 @@ module RailsConsolePro
214
237
  def reset
215
238
  initialize
216
239
  end
240
+
241
+ private
242
+
243
+ def default_snippet_store_path
244
+ base_path =
245
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
246
+ Rails.root.join('tmp', 'rails_console_pro')
247
+ else
248
+ File.expand_path(File.join(Dir.respond_to?(:pwd) ? Dir.pwd : Dir.tmpdir, 'tmp', 'rails_console_pro'))
249
+ end
250
+
251
+ base_path = Pathname.new(base_path) unless base_path.is_a?(Pathname)
252
+ (base_path + 'snippets.yml').to_s
253
+ rescue StandardError
254
+ File.join(Dir.tmpdir, 'rails_console_pro', 'snippets.yml')
255
+ end
217
256
  end
218
257
  end
219
258
 
@@ -81,6 +81,8 @@ module RailsConsolePro
81
81
  serialize_diff_result(data)
82
82
  when ExplainResult
83
83
  serialize_explain_result(data)
84
+ when ProfileResult
85
+ serialize_profile_result(data)
84
86
  when ActiveRecord::Base
85
87
  serialize_active_record(data)
86
88
  when ActiveRecord::Relation
@@ -131,6 +133,10 @@ module RailsConsolePro
131
133
  Serializers::DiffSerializer.serialize(result, self)
132
134
  end
133
135
 
136
+ def serialize_profile_result(result)
137
+ Serializers::ProfileSerializer.serialize(result, self)
138
+ end
139
+
134
140
  def serialize_active_record(record)
135
141
  Serializers::ActiveRecordSerializer.serialize(record, self)
136
142
  end
@@ -388,6 +394,8 @@ module RailsConsolePro
388
394
  "Diff Comparison: #{data.object1_type} vs #{data.object2_type}"
389
395
  when ExplainResult
390
396
  "SQL Explain Analysis"
397
+ when ProfileResult
398
+ "Profile: #{data.label || 'Session'}"
391
399
  when ActiveRecord::Base
392
400
  "#{data.class.name} ##{data.id}"
393
401
  when ActiveRecord::Relation
@@ -40,3 +40,15 @@ def diff(object1, object2)
40
40
  RailsConsolePro::Commands.diff(object1, object2)
41
41
  end
42
42
 
43
+ def profile(target = nil, *args, **kwargs, &block)
44
+ RailsConsolePro::Commands.profile(target, *args, **kwargs, &block)
45
+ end
46
+
47
+ def jobs(options = {})
48
+ RailsConsolePro::Commands.jobs(options)
49
+ end
50
+
51
+ def snippets(action = :list, *args, **kwargs, &block)
52
+ RailsConsolePro::Commands.snippets(action, *args, **kwargs, &block)
53
+ end
54
+
@@ -8,6 +8,9 @@ module RailsConsolePro
8
8
 
9
9
  # Singleton Pastel instance
10
10
  PASTEL = Pastel.new(enabled: TTY::Color.color?)
11
+
12
+ # Require ColorHelper early since it's used throughout
13
+ require_relative 'color_helper'
11
14
 
12
15
  # Configuration instance
13
16
  def config
@@ -22,18 +25,20 @@ module RailsConsolePro
22
25
 
23
26
  # Autoload all components
24
27
  autoload :Configuration, "rails_console_pro/configuration"
25
- autoload :ColorHelper, "rails_console_pro/color_helper"
26
28
  autoload :BasePrinter, "rails_console_pro/base_printer"
27
29
  autoload :ModelValidator, "rails_console_pro/model_validator"
28
30
  autoload :SchemaInspectorResult, "rails_console_pro/schema_inspector_result"
29
31
  autoload :ExplainResult, "rails_console_pro/explain_result"
30
32
  autoload :StatsResult, "rails_console_pro/stats_result"
31
33
  autoload :DiffResult, "rails_console_pro/diff_result"
34
+ autoload :ProfileResult, "rails_console_pro/profile_result"
35
+ autoload :QueueInsightsResult, "rails_console_pro/queue_insights_result"
32
36
  autoload :AssociationNavigator, "rails_console_pro/association_navigator"
33
37
  autoload :Commands, "rails_console_pro/commands"
34
38
  autoload :FormatExporter, "rails_console_pro/format_exporter"
35
39
  autoload :ErrorHandler, "rails_console_pro/error_handler"
36
40
  autoload :Paginator, "rails_console_pro/paginator"
41
+ autoload :Snippets, "rails_console_pro/snippets"
37
42
 
38
43
  module Printers
39
44
  autoload :ActiveRecordPrinter, "rails_console_pro/printers/active_record_printer"
@@ -43,6 +48,10 @@ module RailsConsolePro
43
48
  autoload :ExplainPrinter, "rails_console_pro/printers/explain_printer"
44
49
  autoload :StatsPrinter, "rails_console_pro/printers/stats_printer"
45
50
  autoload :DiffPrinter, "rails_console_pro/printers/diff_printer"
51
+ autoload :ProfilePrinter, "rails_console_pro/printers/profile_printer"
52
+ autoload :SnippetCollectionPrinter, "rails_console_pro/printers/snippet_collection_printer"
53
+ autoload :SnippetPrinter, "rails_console_pro/printers/snippet_printer"
54
+ autoload :QueueInsightsPrinter, "rails_console_pro/printers/queue_insights_printer"
46
55
  end
47
56
 
48
57
  # Main dispatcher - optimized with early returns
@@ -57,7 +66,7 @@ module RailsConsolePro
57
66
  rescue => e
58
67
  # Show error in development to help debug
59
68
  if Rails.env.development? || ENV['RAILS_CONSOLE_PRO_DEBUG']
60
- pastel = ColorHelper.pastel
69
+ pastel = PASTEL
61
70
  output.puts pastel.red.bold("💥 RailsConsolePro Error: #{e.class}: #{e.message}")
62
71
  output.puts pastel.dim(e.backtrace.first(5).join("\n"))
63
72
  end
@@ -86,6 +95,14 @@ module RailsConsolePro
86
95
  return Printers::ExplainPrinter if value.is_a?(ExplainResult)
87
96
  return Printers::StatsPrinter if value.is_a?(StatsResult)
88
97
  return Printers::DiffPrinter if value.is_a?(DiffResult)
98
+ return Printers::ProfilePrinter if value.is_a?(ProfileResult)
99
+ return Printers::QueueInsightsPrinter if value.is_a?(QueueInsightsResult)
100
+ if defined?(Snippets::CollectionResult) && value.is_a?(Snippets::CollectionResult)
101
+ return Printers::SnippetCollectionPrinter
102
+ end
103
+ if defined?(Snippets::SingleResult) && value.is_a?(Snippets::SingleResult)
104
+ return Printers::SnippetPrinter
105
+ end
89
106
 
90
107
  # Fallback to base printer
91
108
  BasePrinter
@@ -105,7 +122,7 @@ module RailsConsolePro
105
122
  end
106
123
 
107
124
  def handle_error(output, error, value, pry_instance)
108
- pastel = ColorHelper.pastel
125
+ pastel = PASTEL
109
126
  output.puts pastel.red.bold("💥 #{error.class}: #{error.message}")
110
127
  output.puts pastel.dim(error.backtrace.first(3).join("\n"))
111
128
  default_print(output, value, pry_instance)
@@ -137,6 +154,10 @@ require_relative 'services/stats_calculator'
137
154
  require_relative 'services/table_size_calculator'
138
155
  require_relative 'services/index_analyzer'
139
156
  require_relative 'services/column_stats_calculator'
157
+ require_relative 'services/profile_collector'
158
+ require_relative 'services/snippet_repository'
159
+ require_relative 'services/queue_action_service'
160
+ require_relative 'services/queue_insight_fetcher'
140
161
 
141
162
  # Load command classes (needed by Commands module)
142
163
  require_relative 'commands/base_command'
@@ -145,6 +166,9 @@ require_relative 'commands/explain_command'
145
166
  require_relative 'commands/stats_command'
146
167
  require_relative 'commands/diff_command'
147
168
  require_relative 'commands/export_command'
169
+ require_relative 'commands/snippets_command'
170
+ require_relative 'commands/profile_command'
171
+ require_relative 'commands/jobs_command'
148
172
 
149
173
  # Load Commands module (uses command classes)
150
174
  require_relative 'commands'
@@ -155,6 +179,7 @@ require_relative 'serializers/schema_serializer'
155
179
  require_relative 'serializers/stats_serializer'
156
180
  require_relative 'serializers/explain_serializer'
157
181
  require_relative 'serializers/diff_serializer'
182
+ require_relative 'serializers/profile_serializer'
158
183
  require_relative 'serializers/active_record_serializer'
159
184
  require_relative 'serializers/relation_serializer'
160
185
  require_relative 'serializers/array_serializer'
@@ -169,7 +194,7 @@ end
169
194
 
170
195
  # Print welcome message if enabled (only for Pry)
171
196
  if RailsConsolePro.config.show_welcome_message && defined?(Pry)
172
- pastel = ColorHelper.pastel
197
+ pastel = RailsConsolePro::PASTEL
173
198
  puts pastel.bright_green("🚀 Rails Console Pro Loaded!")
174
199
  puts pastel.cyan("📊 Use `schema ModelName`, `explain Query`, `stats ModelName`, `diff obj1, obj2`, or `navigate ModelName`")
175
200
  puts pastel.dim("💾 Export support: Use `.to_json`, `.to_yaml`, `.to_html`, or `.export_to_file` on any result")
@@ -27,7 +27,7 @@ module RailsConsolePro
27
27
  # Check if model is abstract
28
28
  def abstract_class?(model_class)
29
29
  return false unless valid_model?(model_class)
30
- model_class.abstract_class?
30
+ !!model_class.abstract_class?
31
31
  end
32
32
 
33
33
  # Check if model uses Single Table Inheritance (STI)
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Printers
5
+ # Printer for profile results
6
+ class ProfilePrinter < BasePrinter
7
+ def print
8
+ print_header
9
+ print_summary
10
+ print_sql_breakdown
11
+ print_cache_breakdown
12
+ print_instantiation_info
13
+ print_slow_queries
14
+ print_duplicate_queries
15
+ print_query_samples
16
+ print_result_preview
17
+ print_error_info
18
+ print_footer
19
+ end
20
+
21
+ private
22
+
23
+ def print_header
24
+ label = value.label || 'Profiling Session'
25
+ header_color = config.get_color(:header)
26
+ border_length = config.header_width
27
+
28
+ output.puts bold_color(header_color, '═' * border_length)
29
+ output.puts bold_color(header_color, "🧪 PROFILE: #{label}")
30
+ output.puts bold_color(header_color, '═' * border_length)
31
+ end
32
+
33
+ def print_summary
34
+ output.puts bold_color(config.get_color(:warning), "\n⏱ Execution Summary:")
35
+ output.puts " #{summary_line('Total time', format_ms(value.duration_ms), config.get_color(:success))}"
36
+ output.puts " #{summary_line('SQL time', format_ms(value.total_sql_duration_ms), config.get_color(:attribute_value_numeric))}"
37
+ sql_ratio = ratio(value.total_sql_duration_ms, value.duration_ms)
38
+ output.puts color(config.get_color(:info), " (#{sql_ratio}% of total time spent in SQL)") if sql_ratio
39
+ end
40
+
41
+ def print_sql_breakdown
42
+ output.puts bold_color(config.get_color(:warning), "\n🗂 Query Breakdown:")
43
+ output.puts " #{summary_line('Total queries', value.query_count, config.get_color(:attribute_value_numeric))}"
44
+ output.puts " #{summary_line('Read queries', value.read_query_count, config.get_color(:attribute_value_string))}"
45
+ output.puts " #{summary_line('Write queries', value.write_query_count, config.get_color(:attribute_value_numeric))}"
46
+ output.puts " #{summary_line('Cached queries', value.cached_query_count, config.get_color(:info))}"
47
+ end
48
+
49
+ def print_cache_breakdown
50
+ return unless value.cache_activity?
51
+
52
+ output.puts bold_color(config.get_color(:warning), "\n🧮 Cache Activity:")
53
+ output.puts " #{summary_line('Cache hits', value.cache_hits, config.get_color(:success))}"
54
+ output.puts " #{summary_line('Cache misses', value.cache_misses, config.get_color(:error))}"
55
+ output.puts " #{summary_line('Cache writes', value.cache_writes, config.get_color(:attribute_value_numeric))}"
56
+ end
57
+
58
+ def print_instantiation_info
59
+ return if value.instantiation_count.zero?
60
+
61
+ output.puts bold_color(config.get_color(:warning), "\n📦 Records Instantiated:")
62
+ output.puts " #{summary_line('ActiveRecord objects', value.instantiation_count, config.get_color(:attribute_value_numeric))}"
63
+ end
64
+
65
+ def print_slow_queries
66
+ return unless value.slow_queries?
67
+
68
+ output.puts bold_color(config.get_color(:warning), "\n🐢 Slow Queries (#{config.profile_slow_query_threshold}ms+):")
69
+ value.slow_queries.each_with_index do |query, index|
70
+ print_query_item(index + 1, query, highlight: config.get_color(:error))
71
+ end
72
+ end
73
+
74
+ def print_duplicate_queries
75
+ return unless value.duplicate_queries?
76
+
77
+ output.puts bold_color(config.get_color(:warning), "\n♻️ Possible N+1 Queries:")
78
+ value.duplicate_queries.each_with_index do |duplicate, index|
79
+ sql_preview = truncate_sql(duplicate.sql)
80
+ duration = format_ms(duplicate.total_duration_ms)
81
+ output.puts " #{index + 1}. #{bold_color(config.get_color(:error), "#{duplicate.count}x")} "\
82
+ "#{color(config.get_color(:attribute_value_string), sql_preview)} "\
83
+ "#{color(config.get_color(:info), "(#{duration} total)")} "\
84
+ "#{color(config.get_color(:warning), "[#{duplicate.fingerprint.hash}]")}"
85
+ end
86
+ end
87
+
88
+ def print_query_samples
89
+ return unless value.query_samples?
90
+
91
+ output.puts bold_color(config.get_color(:warning), "\n🔍 Query Samples:")
92
+ value.query_samples.each_with_index do |query, index|
93
+ print_query_item(index + 1, query)
94
+ end
95
+ end
96
+
97
+ def print_result_preview
98
+ preview = format_result_preview(value.result)
99
+ output.puts bold_color(config.get_color(:warning), "\n🧾 Result Preview:")
100
+ output.puts " #{preview}"
101
+ end
102
+
103
+ def print_error_info
104
+ return unless value.error?
105
+
106
+ error = value.error
107
+ output.puts bold_color(config.get_color(:error), "\n⚠️ Error Raised During Profiling:")
108
+ output.puts " #{color(config.get_color(:error), "#{error.class}: #{error.message}")}"
109
+ backtrace = Array(error.backtrace).first(3)
110
+ backtrace.each do |line|
111
+ output.puts color(config.get_color(:info), " ↳ #{line}")
112
+ end
113
+ end
114
+
115
+ def print_footer
116
+ footer_color = config.get_color(:footer)
117
+ output.puts bold_color(footer_color, "\n" + '═' * config.header_width)
118
+ if value.started_at && value.finished_at
119
+ output.puts color(:dim, "Started: #{value.started_at.strftime('%Y-%m-%d %H:%M:%S')}")
120
+ output.puts color(:dim, "Finished: #{value.finished_at.strftime('%Y-%m-%d %H:%M:%S')}")
121
+ end
122
+ end
123
+
124
+ def summary_line(label, value, color_value)
125
+ "#{label.ljust(18)} #{bold_color(color_value, value.to_s)}"
126
+ end
127
+
128
+ def format_ms(duration_ms)
129
+ return '0.00 ms' if duration_ms.nil?
130
+
131
+ format('%.2f ms', duration_ms)
132
+ end
133
+
134
+ def ratio(value, total)
135
+ return nil if total.to_f.zero?
136
+
137
+ ((value.to_f / total.to_f) * 100).round(2)
138
+ end
139
+
140
+ def truncate_sql(sql)
141
+ sql.to_s.gsub(/\s+/, ' ').strip.truncate(120)
142
+ end
143
+
144
+ def print_query_item(index, query, highlight: config.get_color(:attribute_value_numeric))
145
+ duration = format_ms(query.duration_ms)
146
+ cached_label = query.cached ? color(config.get_color(:info), '[cache] ') : ''
147
+ name_label = query.name.to_s.empty? ? '' : color(config.get_color(:attribute_key), "(#{query.name}) ")
148
+ binds = format_binds(query.binds)
149
+
150
+ output.puts " #{index}. #{bold_color(highlight, duration)} #{cached_label}#{name_label}"\
151
+ "#{color(config.get_color(:attribute_value_string), truncate_sql(query.sql))}"
152
+ output.puts " #{color(:dim, binds)}" if binds
153
+ end
154
+
155
+ def format_binds(binds)
156
+ return nil if binds.nil? || binds.empty?
157
+
158
+ "binds: #{binds.map { |b| b.nil? ? 'nil' : b.inspect }.join(', ')}"
159
+ end
160
+
161
+ def format_result_preview(result)
162
+ case result
163
+ when NilClass
164
+ color(config.get_color(:attribute_value_nil), 'nil')
165
+ when Array
166
+ color(config.get_color(:attribute_value_numeric), "#{result.class.name} (#{result.size} items)")
167
+ when ActiveRecord::Relation
168
+ color(config.get_color(:attribute_value_numeric), "#{result.klass.name} relation (#{result.count} records)")
169
+ when ActiveRecord::Base
170
+ color(config.get_color(:attribute_value_string), "#{result.class.name}##{result.id || 'new'}")
171
+ else
172
+ color(config.get_color(:attribute_value_string), result.inspect.truncate(120))
173
+ end
174
+ rescue => e
175
+ color(config.get_color(:error), "Error previewing result: #{e.message}")
176
+ end
177
+ end
178
+ end
179
+ end
180
+