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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +259 -232
  3. data/CHANGELOG.md +3 -0
  4. data/QUICK_START.md +9 -0
  5. data/README.md +27 -0
  6. data/docs/MODEL_INTROSPECTION.md +371 -0
  7. data/docs/QUERY_BUILDER.md +385 -0
  8. data/lib/rails_console_pro/commands/compare_command.rb +151 -0
  9. data/lib/rails_console_pro/commands/introspect_command.rb +220 -0
  10. data/lib/rails_console_pro/commands/query_builder_command.rb +43 -0
  11. data/lib/rails_console_pro/commands.rb +15 -0
  12. data/lib/rails_console_pro/compare_result.rb +81 -0
  13. data/lib/rails_console_pro/configuration.rb +12 -0
  14. data/lib/rails_console_pro/format_exporter.rb +24 -0
  15. data/lib/rails_console_pro/global_methods.rb +12 -0
  16. data/lib/rails_console_pro/initializer.rb +18 -1
  17. data/lib/rails_console_pro/introspect_result.rb +101 -0
  18. data/lib/rails_console_pro/printers/compare_printer.rb +138 -0
  19. data/lib/rails_console_pro/printers/introspect_printer.rb +282 -0
  20. data/lib/rails_console_pro/printers/query_builder_printer.rb +81 -0
  21. data/lib/rails_console_pro/query_builder.rb +197 -0
  22. data/lib/rails_console_pro/query_builder_result.rb +66 -0
  23. data/lib/rails_console_pro/serializers/compare_serializer.rb +66 -0
  24. data/lib/rails_console_pro/serializers/introspect_serializer.rb +99 -0
  25. data/lib/rails_console_pro/serializers/query_builder_serializer.rb +35 -0
  26. data/lib/rails_console_pro/services/introspection_collector.rb +420 -0
  27. data/lib/rails_console_pro/snippets/collection_result.rb +1 -0
  28. data/lib/rails_console_pro/snippets.rb +1 -0
  29. data/lib/rails_console_pro/version.rb +1 -1
  30. 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
+