rails_console_pro 0.1.3 → 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.
- checksums.yaml +4 -4
- data/.rspec_status +259 -232
- data/CHANGELOG.md +3 -0
- data/QUICK_START.md +9 -0
- data/README.md +27 -0
- data/docs/MODEL_INTROSPECTION.md +371 -0
- data/docs/QUERY_BUILDER.md +385 -0
- data/lib/rails_console_pro/commands/compare_command.rb +151 -0
- data/lib/rails_console_pro/commands/introspect_command.rb +220 -0
- data/lib/rails_console_pro/commands/query_builder_command.rb +43 -0
- data/lib/rails_console_pro/commands.rb +15 -0
- data/lib/rails_console_pro/compare_result.rb +81 -0
- data/lib/rails_console_pro/configuration.rb +12 -0
- data/lib/rails_console_pro/format_exporter.rb +24 -0
- data/lib/rails_console_pro/global_methods.rb +12 -0
- data/lib/rails_console_pro/initializer.rb +18 -1
- data/lib/rails_console_pro/introspect_result.rb +101 -0
- data/lib/rails_console_pro/printers/compare_printer.rb +138 -0
- data/lib/rails_console_pro/printers/introspect_printer.rb +282 -0
- data/lib/rails_console_pro/printers/query_builder_printer.rb +81 -0
- data/lib/rails_console_pro/query_builder.rb +197 -0
- data/lib/rails_console_pro/query_builder_result.rb +66 -0
- data/lib/rails_console_pro/serializers/compare_serializer.rb +66 -0
- data/lib/rails_console_pro/serializers/introspect_serializer.rb +99 -0
- data/lib/rails_console_pro/serializers/query_builder_serializer.rb +35 -0
- data/lib/rails_console_pro/services/introspection_collector.rb +420 -0
- data/lib/rails_console_pro/snippets/collection_result.rb +1 -0
- data/lib/rails_console_pro/snippets.rb +1 -0
- data/lib/rails_console_pro/version.rb +1 -1
- metadata +17 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for query comparison results
|
|
6
|
+
class ComparePrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
print_header
|
|
9
|
+
print_summary
|
|
10
|
+
print_comparisons
|
|
11
|
+
print_winner
|
|
12
|
+
print_footer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def print_header
|
|
18
|
+
header_color = config.get_color(:header)
|
|
19
|
+
output.puts bold_color(header_color, "═" * config.header_width)
|
|
20
|
+
output.puts bold_color(header_color, "⚖️ QUERY COMPARISON")
|
|
21
|
+
output.puts bold_color(header_color, "═" * config.header_width)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def print_summary
|
|
25
|
+
output.puts bold_color(config.get_color(:warning), "\n📊 Summary:")
|
|
26
|
+
output.puts " Total Strategies: #{color(config.get_color(:attribute_value_numeric), value.comparisons.size)}"
|
|
27
|
+
output.puts " Fastest: #{color(config.get_color(:success), value.fastest_name || 'N/A')}"
|
|
28
|
+
output.puts " Slowest: #{color(config.get_color(:error), value.slowest_name || 'N/A')}"
|
|
29
|
+
|
|
30
|
+
if value.performance_ratio
|
|
31
|
+
ratio = value.performance_ratio
|
|
32
|
+
ratio_color = ratio > 2 ? config.get_color(:error) : config.get_color(:warning)
|
|
33
|
+
output.puts " Performance Ratio: #{color(ratio_color, "#{ratio}x slower")}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if value.has_errors?
|
|
37
|
+
output.puts " Errors: #{color(config.get_color(:error), value.error_count.to_s)}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def print_comparisons
|
|
42
|
+
output.puts bold_color(config.get_color(:warning), "\n📈 Detailed Results:")
|
|
43
|
+
|
|
44
|
+
sorted = value.comparisons.sort_by { |c| c.duration_ms || Float::INFINITY }
|
|
45
|
+
|
|
46
|
+
sorted.each_with_index do |comparison, index|
|
|
47
|
+
print_comparison(comparison, index + 1, sorted.size)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def print_comparison(comparison, rank, total)
|
|
52
|
+
is_winner = comparison == value.fastest
|
|
53
|
+
rank_color = is_winner ? config.get_color(:success) : config.get_color(:attribute_value_string)
|
|
54
|
+
|
|
55
|
+
output.puts "\n #{color(rank_color, "##{rank}")} #{bold_color(rank_color, comparison.name)}"
|
|
56
|
+
|
|
57
|
+
if comparison.error
|
|
58
|
+
output.puts " #{color(config.get_color(:error), "❌ Error: #{comparison.error.class} - #{comparison.error.message}")}"
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Duration
|
|
63
|
+
duration_str = format_duration(comparison.duration_ms)
|
|
64
|
+
output.puts " ⏱️ Duration: #{duration_str}"
|
|
65
|
+
|
|
66
|
+
# Query count
|
|
67
|
+
query_color = comparison.query_count > 10 ? config.get_color(:error) :
|
|
68
|
+
comparison.query_count > 1 ? config.get_color(:warning) :
|
|
69
|
+
config.get_color(:success)
|
|
70
|
+
output.puts " 🔢 Queries: #{color(query_color, comparison.query_count.to_s)}"
|
|
71
|
+
|
|
72
|
+
# Memory usage
|
|
73
|
+
if comparison.memory_usage_kb && comparison.memory_usage_kb > 0
|
|
74
|
+
memory_str = format_memory(comparison.memory_usage_kb)
|
|
75
|
+
output.puts " 💾 Memory: #{color(config.get_color(:info), memory_str)}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# SQL queries preview
|
|
79
|
+
if comparison.sql_queries&.any?
|
|
80
|
+
preview_count = [comparison.sql_queries.size, 3].min
|
|
81
|
+
output.puts " 📝 SQL Queries (#{comparison.sql_queries.size} total):"
|
|
82
|
+
comparison.sql_queries.first(preview_count).each_with_index do |query_info, idx|
|
|
83
|
+
sql_preview = truncate_sql(query_info[:sql], 60)
|
|
84
|
+
duration = query_info[:duration_ms]
|
|
85
|
+
cached = query_info[:cached] ? " (cached)" : ""
|
|
86
|
+
output.puts " #{idx + 1}. #{color(config.get_color(:attribute_value_string), sql_preview)} #{color(config.get_color(:border), "(#{duration}ms#{cached})")}"
|
|
87
|
+
end
|
|
88
|
+
if comparison.sql_queries.size > preview_count
|
|
89
|
+
output.puts " ... and #{comparison.sql_queries.size - preview_count} more"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def print_winner
|
|
95
|
+
return unless value.winner
|
|
96
|
+
|
|
97
|
+
output.puts bold_color(config.get_color(:success), "\n🏆 Winner: #{value.winner.name}")
|
|
98
|
+
if value.performance_ratio && value.performance_ratio > 1
|
|
99
|
+
output.puts " #{color(config.get_color(:info), "This strategy is #{value.performance_ratio.round(1)}x faster than the slowest")}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def print_footer
|
|
104
|
+
footer_color = config.get_color(:footer)
|
|
105
|
+
output.puts bold_color(footer_color, "═" * config.header_width)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def format_duration(ms)
|
|
109
|
+
return color(config.get_color(:attribute_value_nil), "N/A") unless ms
|
|
110
|
+
|
|
111
|
+
case ms
|
|
112
|
+
when 0...10
|
|
113
|
+
color(config.get_color(:success), "#{ms.round(2)}ms")
|
|
114
|
+
when 10...100
|
|
115
|
+
color(config.get_color(:warning), "#{ms.round(2)}ms")
|
|
116
|
+
else
|
|
117
|
+
color(config.get_color(:error), "#{ms.round(2)}ms")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_memory(kb)
|
|
122
|
+
if kb < 1024
|
|
123
|
+
"#{kb.round(2)} KB"
|
|
124
|
+
elsif kb < 1024 * 1024
|
|
125
|
+
"#{(kb / 1024.0).round(2)} MB"
|
|
126
|
+
else
|
|
127
|
+
"#{(kb / (1024.0 * 1024.0)).round(2)} GB"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def truncate_sql(sql, max_length)
|
|
132
|
+
return sql if sql.length <= max_length
|
|
133
|
+
"#{sql[0, max_length - 3]}..."
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for model introspection results
|
|
6
|
+
class IntrospectPrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
print_header
|
|
9
|
+
print_lifecycle_summary
|
|
10
|
+
print_callbacks
|
|
11
|
+
print_validations
|
|
12
|
+
print_enums
|
|
13
|
+
print_scopes
|
|
14
|
+
print_concerns
|
|
15
|
+
print_footer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def print_header
|
|
21
|
+
model = value.model
|
|
22
|
+
header_color = config.get_color(:header)
|
|
23
|
+
output.puts bold_color(header_color, "═" * config.header_width)
|
|
24
|
+
output.puts bold_color(header_color, "🔍 MODEL INTROSPECTION: #{model.name}")
|
|
25
|
+
output.puts bold_color(header_color, "═" * config.header_width)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def print_lifecycle_summary
|
|
29
|
+
hooks = value.lifecycle_hooks
|
|
30
|
+
return if hooks.empty?
|
|
31
|
+
|
|
32
|
+
output.puts bold_color(config.get_color(:warning), "\n📊 Lifecycle Summary:")
|
|
33
|
+
|
|
34
|
+
output.puts " Callbacks: #{bold_color(config.get_color(:attribute_value_numeric), hooks[:callbacks_count].to_s)}"
|
|
35
|
+
output.puts " Validations: #{bold_color(config.get_color(:attribute_value_numeric), hooks[:validations_count].to_s)}"
|
|
36
|
+
|
|
37
|
+
if hooks[:has_state_machine]
|
|
38
|
+
output.puts " #{bold_color(config.get_color(:success), '✓')} Has state machine"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if hooks[:has_observers]
|
|
42
|
+
output.puts " #{bold_color(config.get_color(:success), '✓')} Has observers"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print_callbacks
|
|
47
|
+
return unless value.has_callbacks?
|
|
48
|
+
|
|
49
|
+
output.puts bold_color(config.get_color(:warning), "\n🔔 Callbacks:")
|
|
50
|
+
|
|
51
|
+
value.callbacks.each do |type, chain|
|
|
52
|
+
next if chain.empty?
|
|
53
|
+
|
|
54
|
+
type_color = callback_type_color(type)
|
|
55
|
+
output.puts "\n #{bold_color(type_color, type.to_s)} (#{chain.size}):"
|
|
56
|
+
|
|
57
|
+
chain.each_with_index do |callback, index|
|
|
58
|
+
print_callback(callback, index + 1)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def print_callback(callback, index)
|
|
64
|
+
name_color = config.get_color(:attribute_key)
|
|
65
|
+
output.print " #{color(:dim, "#{index}.")} #{bold_color(name_color, callback[:name])}"
|
|
66
|
+
|
|
67
|
+
# Print conditions
|
|
68
|
+
conditions = []
|
|
69
|
+
if callback[:if]
|
|
70
|
+
conditions << "#{color(config.get_color(:success), 'if')}: #{callback[:if].join(', ')}"
|
|
71
|
+
end
|
|
72
|
+
if callback[:unless]
|
|
73
|
+
conditions << "#{color(config.get_color(:error), 'unless')}: #{callback[:unless].join(', ')}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
unless conditions.empty?
|
|
77
|
+
output.print " #{color(:dim, '(')}#{conditions.join(', ')}#{color(:dim, ')')}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
output.puts
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def print_validations
|
|
84
|
+
return unless value.has_validations?
|
|
85
|
+
|
|
86
|
+
output.puts bold_color(config.get_color(:warning), "\n✅ Validations:")
|
|
87
|
+
|
|
88
|
+
value.validations.each do |attribute, validators|
|
|
89
|
+
next if validators.empty?
|
|
90
|
+
|
|
91
|
+
attr_color = config.get_color(:attribute_key)
|
|
92
|
+
output.puts "\n #{bold_color(attr_color, attribute.to_s)}:"
|
|
93
|
+
|
|
94
|
+
validators.each do |validator|
|
|
95
|
+
print_validator(validator)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def print_validator(validator)
|
|
101
|
+
validator_color = get_validator_color(validator[:type])
|
|
102
|
+
output.print " #{bold_color(validator_color, validator[:type])}"
|
|
103
|
+
|
|
104
|
+
# Print options
|
|
105
|
+
unless validator[:options].empty?
|
|
106
|
+
opts = validator[:options].map do |key, val|
|
|
107
|
+
"#{key}: #{format_validator_value(val)}"
|
|
108
|
+
end.join(', ')
|
|
109
|
+
output.print " #{color(:dim, '(')}#{opts}#{color(:dim, ')')}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Print conditions
|
|
113
|
+
unless validator[:conditions].empty?
|
|
114
|
+
conds = validator[:conditions].map do |key, val|
|
|
115
|
+
"#{key}: #{val}"
|
|
116
|
+
end.join(', ')
|
|
117
|
+
output.print " [#{color(config.get_color(:info), conds)}]"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
output.puts
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def print_enums
|
|
124
|
+
return unless value.has_enums?
|
|
125
|
+
|
|
126
|
+
output.puts bold_color(config.get_color(:warning), "\n🔢 Enums:")
|
|
127
|
+
|
|
128
|
+
value.enums.each do |name, data|
|
|
129
|
+
enum_color = config.get_color(:attribute_key)
|
|
130
|
+
type_badge = format_enum_type_badge(data[:type])
|
|
131
|
+
|
|
132
|
+
output.puts "\n #{bold_color(enum_color, name)} #{type_badge}:"
|
|
133
|
+
|
|
134
|
+
# Print values in a compact format
|
|
135
|
+
values = data[:values].map { |v| color(config.get_color(:success), v) }
|
|
136
|
+
output.puts " #{values.join(', ')}"
|
|
137
|
+
|
|
138
|
+
# Show mapping preview for first few
|
|
139
|
+
if data[:mapping].size <= 5
|
|
140
|
+
mapping = data[:mapping].map do |k, v|
|
|
141
|
+
"#{color(config.get_color(:attribute_key), k)} => #{color(config.get_color(:attribute_value_numeric), v)}"
|
|
142
|
+
end.join(', ')
|
|
143
|
+
output.puts " #{color(:dim, "Mapping: #{mapping}")}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def print_scopes
|
|
149
|
+
return unless value.has_scopes?
|
|
150
|
+
|
|
151
|
+
output.puts bold_color(config.get_color(:warning), "\n🔭 Scopes:")
|
|
152
|
+
|
|
153
|
+
value.scopes.each do |name, data|
|
|
154
|
+
scope_color = config.get_color(:attribute_key)
|
|
155
|
+
output.puts "\n #{bold_color(scope_color, name.to_s)}:"
|
|
156
|
+
|
|
157
|
+
# Print SQL with syntax highlighting
|
|
158
|
+
sql = data[:sql]
|
|
159
|
+
output.puts " #{color(:dim, '└─ SQL:')} #{color(config.get_color(:info), truncate_sql(sql))}"
|
|
160
|
+
|
|
161
|
+
# Print scope values if interesting
|
|
162
|
+
unless data[:values].empty?
|
|
163
|
+
data[:values].each do |key, value|
|
|
164
|
+
next if value.nil? || value.to_s.empty?
|
|
165
|
+
output.puts " #{color(:dim, '└─')} #{color(config.get_color(:attribute_key), key.to_s)}: #{format_value(value)}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def print_concerns
|
|
172
|
+
return unless value.has_concerns?
|
|
173
|
+
|
|
174
|
+
output.puts bold_color(config.get_color(:warning), "\n📦 Concerns & Modules:")
|
|
175
|
+
|
|
176
|
+
# Group by type
|
|
177
|
+
by_type = value.concerns.group_by { |c| c[:type] }
|
|
178
|
+
|
|
179
|
+
[:concern, :class, :module].each do |type|
|
|
180
|
+
items = by_type[type]
|
|
181
|
+
next unless items && items.any?
|
|
182
|
+
|
|
183
|
+
type_label = type.to_s.capitalize.pluralize
|
|
184
|
+
output.puts "\n #{bold_color(config.get_color(:info), type_label)}:"
|
|
185
|
+
|
|
186
|
+
items.each do |concern|
|
|
187
|
+
print_concern(concern)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def print_concern(concern)
|
|
193
|
+
name_color = config.get_color(:attribute_key)
|
|
194
|
+
type_badge = format_concern_badge(concern[:type])
|
|
195
|
+
|
|
196
|
+
output.print " #{type_badge} #{bold_color(name_color, concern[:name])}"
|
|
197
|
+
|
|
198
|
+
if concern[:location]
|
|
199
|
+
file = concern[:location][:file]
|
|
200
|
+
line = concern[:location][:line]
|
|
201
|
+
# Show relative path if possible
|
|
202
|
+
display_path = file.include?(Rails.root.to_s) ? file.sub(Rails.root.to_s + '/', '') : file rescue file
|
|
203
|
+
output.print " #{color(:dim, "#{display_path}:#{line}")}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
output.puts
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def print_footer
|
|
210
|
+
footer_color = config.get_color(:footer)
|
|
211
|
+
output.puts bold_color(footer_color, "\n" + "═" * config.header_width)
|
|
212
|
+
output.puts color(:dim, "Generated: #{value.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
213
|
+
|
|
214
|
+
# Print helpful tips
|
|
215
|
+
output.puts "\n#{color(config.get_color(:info), '💡 Tip:')} Use #{color(:cyan, 'introspect Model, :callbacks')} to see only callbacks"
|
|
216
|
+
output.puts " Use #{color(:cyan, 'introspect Model, :method_name')} to find where a method is defined"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Helper methods
|
|
220
|
+
def callback_type_color(type)
|
|
221
|
+
case type
|
|
222
|
+
when :before_validation, :before_save, :before_create, :before_update, :before_destroy
|
|
223
|
+
:yellow
|
|
224
|
+
when :after_validation, :after_save, :after_create, :after_update, :after_destroy, :after_commit
|
|
225
|
+
:green
|
|
226
|
+
when :around_save, :around_create, :around_update, :around_destroy
|
|
227
|
+
:cyan
|
|
228
|
+
else
|
|
229
|
+
:blue
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def get_validator_color(validator_type)
|
|
234
|
+
colors = config.validator_colors
|
|
235
|
+
colors[validator_type] || config.get_color(:attribute_key)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def format_validator_value(val)
|
|
239
|
+
case val
|
|
240
|
+
when Array
|
|
241
|
+
val.map { |v| color(config.get_color(:success), v.to_s) }.join(', ')
|
|
242
|
+
when Range
|
|
243
|
+
"#{val.first}..#{val.last}"
|
|
244
|
+
when Regexp
|
|
245
|
+
color(config.get_color(:info), val.inspect)
|
|
246
|
+
else
|
|
247
|
+
format_value(val)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def format_enum_type_badge(type)
|
|
252
|
+
case type
|
|
253
|
+
when :integer
|
|
254
|
+
color(config.get_color(:attribute_value_numeric), '[Integer]')
|
|
255
|
+
when :string
|
|
256
|
+
color(config.get_color(:attribute_value_string), '[String]')
|
|
257
|
+
else
|
|
258
|
+
color(:dim, '[Unknown]')
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def format_concern_badge(type)
|
|
263
|
+
case type
|
|
264
|
+
when :concern
|
|
265
|
+
bold_color(config.get_color(:success), '●')
|
|
266
|
+
when :class
|
|
267
|
+
bold_color(config.get_color(:info), '▪')
|
|
268
|
+
when :module
|
|
269
|
+
bold_color(config.get_color(:warning), '○')
|
|
270
|
+
else
|
|
271
|
+
color(:dim, '·')
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def truncate_sql(sql, max_length = 80)
|
|
276
|
+
return sql if sql.length <= max_length
|
|
277
|
+
"#{sql[0...max_length]}..."
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for query builder results
|
|
6
|
+
class QueryBuilderPrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
print_header
|
|
9
|
+
print_error if has_error?
|
|
10
|
+
print_query unless has_error?
|
|
11
|
+
print_statistics
|
|
12
|
+
print_explain if value.explain_result
|
|
13
|
+
print_footer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def print_header
|
|
19
|
+
header_color = config.get_color(:header)
|
|
20
|
+
output.puts bold_color(header_color, "═" * config.header_width)
|
|
21
|
+
output.puts bold_color(header_color, "🔧 QUERY BUILDER: #{value.model_class.name}")
|
|
22
|
+
output.puts bold_color(header_color, "═" * config.header_width)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def print_query
|
|
26
|
+
output.puts bold_color(config.get_color(:warning), "\n📝 Generated SQL:")
|
|
27
|
+
output.puts color(config.get_color(:attribute_value_string), value.sql)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def print_statistics
|
|
31
|
+
return if value.statistics.empty?
|
|
32
|
+
|
|
33
|
+
output.puts bold_color(config.get_color(:warning), "\n📊 Statistics:")
|
|
34
|
+
value.statistics.each do |key, val|
|
|
35
|
+
next if key == "SQL" # Already shown above
|
|
36
|
+
key_color = config.get_color(:attribute_key)
|
|
37
|
+
output.puts " #{bold_color(key_color, key.to_s.ljust(20))} #{color(config.get_color(:attribute_value_string), val.to_s)}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def print_explain
|
|
42
|
+
return unless value.explain_result
|
|
43
|
+
|
|
44
|
+
output.puts bold_color(config.get_color(:warning), "\n🔬 Query Analysis:")
|
|
45
|
+
|
|
46
|
+
explain_printer = ExplainPrinter.new(output, value.explain_result, nil)
|
|
47
|
+
explain_printer.print
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def print_footer
|
|
51
|
+
footer_color = config.get_color(:footer)
|
|
52
|
+
output.puts bold_color(footer_color, "═" * config.header_width)
|
|
53
|
+
output.puts color(config.get_color(:info), "\n💡 Tip: Use .execute to run the query, or .to_a to get results") unless has_error?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def has_error?
|
|
57
|
+
value.statistics && value.statistics["Error"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def print_error
|
|
61
|
+
error_msg = value.statistics["Error"]
|
|
62
|
+
output.puts bold_color(config.get_color(:error), "\n❌ Query Error:")
|
|
63
|
+
output.puts color(config.get_color(:error), " #{error_msg}")
|
|
64
|
+
|
|
65
|
+
# Provide helpful hints for common errors
|
|
66
|
+
if error_msg.include?("polymorphic")
|
|
67
|
+
output.puts color(config.get_color(:warning), "\n💡 Tip: Polymorphic associations have limitations:")
|
|
68
|
+
output.puts color(config.get_color(:info), " - For eager loading: use 'preload' instead of 'includes'")
|
|
69
|
+
output.puts color(config.get_color(:info), " - For joins: filter by polymorphic columns (channel_id, channel_type)")
|
|
70
|
+
output.puts color(config.get_color(:info), " - Or use raw SQL joins: joins(\"INNER JOIN ...\")")
|
|
71
|
+
elsif error_msg.include?("ambiguous")
|
|
72
|
+
output.puts color(config.get_color(:warning), "\n💡 Tip: When joining tables, qualify column names with table names.")
|
|
73
|
+
output.puts color(config.get_color(:info), " Example: where('table_name.column_name > ?', value)")
|
|
74
|
+
elsif error_msg.include?("ActiveRecord") || error_msg.include?("association")
|
|
75
|
+
output.puts color(config.get_color(:warning), "\n💡 Tip: Check that associations are defined correctly in your model.")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# DSL for building and analyzing ActiveRecord queries
|
|
5
|
+
class QueryBuilder
|
|
6
|
+
def initialize(model_class)
|
|
7
|
+
unless model_class.respond_to?(:all)
|
|
8
|
+
raise ArgumentError, "#{model_class} is not an ActiveRecord model"
|
|
9
|
+
end
|
|
10
|
+
@model_class = model_class
|
|
11
|
+
@relation = model_class.all
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Chainable query methods
|
|
15
|
+
def where(*args, **kwargs)
|
|
16
|
+
@relation = @relation.where(*args, **kwargs)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def includes(*args)
|
|
21
|
+
@relation = @relation.includes(*args)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def preload(*args)
|
|
26
|
+
@relation = @relation.preload(*args)
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def eager_load(*args)
|
|
31
|
+
@relation = @relation.eager_load(*args)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def joins(*args)
|
|
36
|
+
@relation = @relation.joins(*args)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def left_joins(*args)
|
|
41
|
+
@relation = @relation.left_joins(*args)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def left_outer_joins(*args)
|
|
46
|
+
@relation = @relation.left_outer_joins(*args)
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def select(*args)
|
|
51
|
+
@relation = @relation.select(*args)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def order(*args)
|
|
56
|
+
@relation = @relation.order(*args)
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def limit(value)
|
|
61
|
+
@relation = @relation.limit(value)
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def offset(value)
|
|
66
|
+
@relation = @relation.offset(value)
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def group(*args)
|
|
71
|
+
@relation = @relation.group(*args)
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def having(*args)
|
|
76
|
+
@relation = @relation.having(*args)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def distinct(value = true)
|
|
81
|
+
@relation = @relation.distinct(value)
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def readonly(value = true)
|
|
86
|
+
@relation = @relation.readonly(value)
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def lock(locks = true)
|
|
91
|
+
@relation = @relation.lock(locks)
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def reorder(*args)
|
|
96
|
+
@relation = @relation.reorder(*args)
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def reverse_order
|
|
101
|
+
@relation = @relation.reverse_order
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unscope(*args)
|
|
106
|
+
@relation = @relation.unscope(*args)
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def rewhere(conditions)
|
|
111
|
+
@relation = @relation.rewhere(conditions)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Analyze the query (returns QueryBuilderResult with explain)
|
|
116
|
+
def analyze
|
|
117
|
+
begin
|
|
118
|
+
sql = @relation.to_sql
|
|
119
|
+
statistics = build_statistics
|
|
120
|
+
result = QueryBuilderResult.new(
|
|
121
|
+
relation: @relation,
|
|
122
|
+
sql: sql,
|
|
123
|
+
statistics: statistics,
|
|
124
|
+
model_class: @model_class
|
|
125
|
+
)
|
|
126
|
+
result.analyze
|
|
127
|
+
rescue => e
|
|
128
|
+
# Return a result with error information
|
|
129
|
+
QueryBuilderResult.new(
|
|
130
|
+
relation: @relation,
|
|
131
|
+
sql: nil,
|
|
132
|
+
statistics: { "Model" => @model_class.name, "Table" => @model_class.table_name, "Error" => e.message },
|
|
133
|
+
model_class: @model_class
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Build the query and return result without explain
|
|
139
|
+
def build
|
|
140
|
+
begin
|
|
141
|
+
sql = @relation.to_sql
|
|
142
|
+
statistics = build_statistics
|
|
143
|
+
QueryBuilderResult.new(
|
|
144
|
+
relation: @relation,
|
|
145
|
+
sql: sql,
|
|
146
|
+
statistics: statistics,
|
|
147
|
+
model_class: @model_class
|
|
148
|
+
)
|
|
149
|
+
rescue => e
|
|
150
|
+
# Return a result with error information
|
|
151
|
+
QueryBuilderResult.new(
|
|
152
|
+
relation: @relation,
|
|
153
|
+
sql: nil,
|
|
154
|
+
statistics: { "Model" => @model_class.name, "Table" => @model_class.table_name, "Error" => e.message },
|
|
155
|
+
model_class: @model_class
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Execute the query and return the relation
|
|
161
|
+
def execute
|
|
162
|
+
@relation
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Delegate other methods to the relation
|
|
166
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
167
|
+
if @relation.respond_to?(method_name)
|
|
168
|
+
result = @relation.public_send(method_name, *args, **kwargs, &block)
|
|
169
|
+
# If result is a relation, return self for chaining
|
|
170
|
+
if result.is_a?(ActiveRecord::Relation) && result.klass == @model_class
|
|
171
|
+
@relation = result
|
|
172
|
+
self
|
|
173
|
+
else
|
|
174
|
+
result
|
|
175
|
+
end
|
|
176
|
+
else
|
|
177
|
+
super
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
182
|
+
@relation.respond_to?(method_name, include_private) || super
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def build_statistics
|
|
188
|
+
{
|
|
189
|
+
"Model" => @model_class.name,
|
|
190
|
+
"Table" => @model_class.table_name,
|
|
191
|
+
"SQL" => @relation.to_sql
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|