rails_console_pro 0.1.2 → 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 (40) 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/jobs_command.rb +212 -0
  14. data/lib/rails_console_pro/commands/profile_command.rb +84 -0
  15. data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
  16. data/lib/rails_console_pro/commands.rb +15 -0
  17. data/lib/rails_console_pro/configuration.rb +39 -0
  18. data/lib/rails_console_pro/format_exporter.rb +8 -0
  19. data/lib/rails_console_pro/global_methods.rb +12 -0
  20. data/lib/rails_console_pro/initializer.rb +23 -0
  21. data/lib/rails_console_pro/model_validator.rb +1 -1
  22. data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
  23. data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
  24. data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
  25. data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
  26. data/lib/rails_console_pro/profile_result.rb +109 -0
  27. data/lib/rails_console_pro/pry_commands.rb +106 -0
  28. data/lib/rails_console_pro/queue_insights_result.rb +110 -0
  29. data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
  30. data/lib/rails_console_pro/services/profile_collector.rb +245 -0
  31. data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
  32. data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
  33. data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
  34. data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
  35. data/lib/rails_console_pro/snippets/single_result.rb +30 -0
  36. data/lib/rails_console_pro/snippets/snippet.rb +112 -0
  37. data/lib/rails_console_pro/snippets.rb +12 -0
  38. data/lib/rails_console_pro/version.rb +1 -1
  39. data/rails_console_pro.gemspec +1 -1
  40. 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
+
@@ -31,11 +31,14 @@ module RailsConsolePro
31
31
  autoload :ExplainResult, "rails_console_pro/explain_result"
32
32
  autoload :StatsResult, "rails_console_pro/stats_result"
33
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"
34
36
  autoload :AssociationNavigator, "rails_console_pro/association_navigator"
35
37
  autoload :Commands, "rails_console_pro/commands"
36
38
  autoload :FormatExporter, "rails_console_pro/format_exporter"
37
39
  autoload :ErrorHandler, "rails_console_pro/error_handler"
38
40
  autoload :Paginator, "rails_console_pro/paginator"
41
+ autoload :Snippets, "rails_console_pro/snippets"
39
42
 
40
43
  module Printers
41
44
  autoload :ActiveRecordPrinter, "rails_console_pro/printers/active_record_printer"
@@ -45,6 +48,10 @@ module RailsConsolePro
45
48
  autoload :ExplainPrinter, "rails_console_pro/printers/explain_printer"
46
49
  autoload :StatsPrinter, "rails_console_pro/printers/stats_printer"
47
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"
48
55
  end
49
56
 
50
57
  # Main dispatcher - optimized with early returns
@@ -88,6 +95,14 @@ module RailsConsolePro
88
95
  return Printers::ExplainPrinter if value.is_a?(ExplainResult)
89
96
  return Printers::StatsPrinter if value.is_a?(StatsResult)
90
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
91
106
 
92
107
  # Fallback to base printer
93
108
  BasePrinter
@@ -139,6 +154,10 @@ require_relative 'services/stats_calculator'
139
154
  require_relative 'services/table_size_calculator'
140
155
  require_relative 'services/index_analyzer'
141
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'
142
161
 
143
162
  # Load command classes (needed by Commands module)
144
163
  require_relative 'commands/base_command'
@@ -147,6 +166,9 @@ require_relative 'commands/explain_command'
147
166
  require_relative 'commands/stats_command'
148
167
  require_relative 'commands/diff_command'
149
168
  require_relative 'commands/export_command'
169
+ require_relative 'commands/snippets_command'
170
+ require_relative 'commands/profile_command'
171
+ require_relative 'commands/jobs_command'
150
172
 
151
173
  # Load Commands module (uses command classes)
152
174
  require_relative 'commands'
@@ -157,6 +179,7 @@ require_relative 'serializers/schema_serializer'
157
179
  require_relative 'serializers/stats_serializer'
158
180
  require_relative 'serializers/explain_serializer'
159
181
  require_relative 'serializers/diff_serializer'
182
+ require_relative 'serializers/profile_serializer'
160
183
  require_relative 'serializers/active_record_serializer'
161
184
  require_relative 'serializers/relation_serializer'
162
185
  require_relative 'serializers/array_serializer'
@@ -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
+