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,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ # Value object for query builder results
5
+ class QueryBuilderResult
6
+ attr_reader :relation, :sql, :explain_result, :statistics, :model_class
7
+
8
+ def initialize(relation:, sql:, explain_result: nil, statistics: {}, model_class: nil)
9
+ @relation = relation
10
+ @sql = sql
11
+ @explain_result = explain_result
12
+ @statistics = statistics
13
+ @model_class = model_class || (relation.respond_to?(:klass) ? relation.klass : nil)
14
+ end
15
+
16
+ def analyze
17
+ return self if explain_result
18
+ return self if sql.nil? # Can't analyze if SQL generation failed
19
+
20
+ explain_cmd = Commands::ExplainCommand.new
21
+ @explain_result = explain_cmd.execute(relation)
22
+ self
23
+ end
24
+
25
+ def execute
26
+ return nil if sql.nil?
27
+ relation.load
28
+ end
29
+
30
+ def to_a
31
+ return [] if sql.nil?
32
+ relation.to_a
33
+ end
34
+
35
+ def count
36
+ return 0 if sql.nil?
37
+ relation.count
38
+ end
39
+
40
+ def exists?
41
+ return false if sql.nil?
42
+ relation.exists?
43
+ end
44
+
45
+ # Export to JSON
46
+ def to_json(pretty: true)
47
+ FormatExporter.to_json(self, pretty: pretty)
48
+ end
49
+
50
+ # Export to YAML
51
+ def to_yaml
52
+ FormatExporter.to_yaml(self)
53
+ end
54
+
55
+ # Export to HTML
56
+ def to_html(style: :default)
57
+ FormatExporter.to_html(self, title: "Query Builder: #{model_class.name}", style: style)
58
+ end
59
+
60
+ # Export to file
61
+ def export_to_file(file_path, format: nil)
62
+ FormatExporter.export_to_file(self, file_path, format: format)
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Serializers
5
+ # Serializer for CompareResult objects
6
+ class CompareSerializer < BaseSerializer
7
+ def serialize(compare_result)
8
+ result = {
9
+ timestamp: compare_result.timestamp&.iso8601,
10
+ total_strategies: compare_result.comparisons.size,
11
+ fastest: compare_result.fastest_name,
12
+ slowest: compare_result.slowest_name,
13
+ performance_ratio: compare_result.performance_ratio,
14
+ has_errors: compare_result.has_errors?,
15
+ error_count: compare_result.error_count,
16
+ total_queries: compare_result.total_queries,
17
+ comparisons: serialize_comparisons(compare_result.comparisons)
18
+ }
19
+ result[:winner] = serialize_comparison(compare_result.winner) if compare_result.winner
20
+ result
21
+ end
22
+
23
+ private
24
+
25
+ def serialize_comparisons(comparisons)
26
+ Array(comparisons).map { |c| serialize_comparison(c) }
27
+ end
28
+
29
+ def serialize_comparison(comparison)
30
+ return nil unless comparison
31
+
32
+ {
33
+ name: comparison.name,
34
+ duration_ms: comparison.duration_ms,
35
+ query_count: comparison.query_count,
36
+ memory_usage_kb: comparison.memory_usage_kb,
37
+ error: serialize_error(comparison.error),
38
+ sql_queries: serialize_sql_queries(comparison.sql_queries),
39
+ result: serialize_data(comparison.result)
40
+ }
41
+ end
42
+
43
+ def serialize_sql_queries(queries)
44
+ Array(queries).map do |query|
45
+ {
46
+ sql: query[:sql],
47
+ duration_ms: query[:duration_ms],
48
+ name: query[:name],
49
+ cached: query[:cached] || false
50
+ }
51
+ end
52
+ end
53
+
54
+ def serialize_error(error)
55
+ return nil unless error
56
+
57
+ {
58
+ class: error.class.name,
59
+ message: error.message,
60
+ backtrace: Array(error.backtrace).first(10)
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Serializers
5
+ # Serializer for introspection results
6
+ class IntrospectSerializer < BaseSerializer
7
+ def serialize(result)
8
+ {
9
+ 'type' => 'introspection',
10
+ 'model' => result.model.name,
11
+ 'callbacks' => serialize_callbacks(result.callbacks),
12
+ 'enums' => serialize_enums(result.enums),
13
+ 'concerns' => serialize_concerns(result.concerns),
14
+ 'scopes' => serialize_scopes(result.scopes),
15
+ 'validations' => serialize_validations(result.validations),
16
+ 'lifecycle_hooks' => serialize_data(result.lifecycle_hooks),
17
+ 'timestamp' => result.timestamp.iso8601,
18
+ 'has_callbacks' => result.has_callbacks?,
19
+ 'has_enums' => result.has_enums?,
20
+ 'has_concerns' => result.has_concerns?,
21
+ 'has_scopes' => result.has_scopes?,
22
+ 'has_validations' => result.has_validations?
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def serialize_callbacks(callbacks)
29
+ callbacks.transform_values do |chain|
30
+ chain.map do |callback|
31
+ {
32
+ 'name' => callback[:name].to_s,
33
+ 'kind' => callback[:kind].to_s,
34
+ 'if' => serialize_condition(callback[:if]),
35
+ 'unless' => serialize_condition(callback[:unless])
36
+ }.compact
37
+ end
38
+ end
39
+ end
40
+
41
+ def serialize_enums(enums)
42
+ enums.transform_values do |data|
43
+ {
44
+ 'mapping' => data[:mapping],
45
+ 'values' => data[:values],
46
+ 'type' => data[:type].to_s
47
+ }
48
+ end
49
+ end
50
+
51
+ def serialize_concerns(concerns)
52
+ concerns.map do |concern|
53
+ {
54
+ 'name' => concern[:name],
55
+ 'type' => concern[:type].to_s,
56
+ 'location' => serialize_location(concern[:location])
57
+ }.compact
58
+ end
59
+ end
60
+
61
+ def serialize_scopes(scopes)
62
+ scopes.transform_keys(&:to_s).transform_values do |data|
63
+ {
64
+ 'sql' => data[:sql],
65
+ 'values' => serialize_data(data[:values]),
66
+ 'conditions' => data[:conditions]
67
+ }
68
+ end
69
+ end
70
+
71
+ def serialize_validations(validations)
72
+ validations.transform_keys(&:to_s).transform_values do |validators|
73
+ validators.map do |validator|
74
+ {
75
+ 'type' => validator[:type],
76
+ 'attributes' => validator[:attributes].map(&:to_s),
77
+ 'options' => serialize_data(validator[:options]),
78
+ 'conditions' => serialize_data(validator[:conditions])
79
+ }
80
+ end
81
+ end
82
+ end
83
+
84
+ def serialize_condition(condition)
85
+ return nil if condition.nil? || condition.empty?
86
+ condition.map(&:to_s)
87
+ end
88
+
89
+ def serialize_location(location)
90
+ return nil if location.nil?
91
+ {
92
+ 'file' => location[:file],
93
+ 'line' => location[:line]
94
+ }
95
+ end
96
+ end
97
+ end
98
+ end
99
+
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Serializers
5
+ # Serializer for QueryBuilderResult objects
6
+ class QueryBuilderSerializer < BaseSerializer
7
+ def serialize(query_builder_result)
8
+ {
9
+ model_class: query_builder_result.model_class.name,
10
+ sql: query_builder_result.sql,
11
+ statistics: query_builder_result.statistics,
12
+ explain_result: serialize_explain(query_builder_result.explain_result)
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def serialize_explain(explain_result)
19
+ return nil unless explain_result
20
+
21
+ if defined?(Serializers::ExplainSerializer)
22
+ Serializers::ExplainSerializer.serialize(explain_result, exporter)
23
+ else
24
+ {
25
+ sql: explain_result.sql,
26
+ execution_time: explain_result.execution_time,
27
+ indexes_used: explain_result.indexes_used,
28
+ recommendations: explain_result.recommendations
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,420 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Services
5
+ # Service to collect model introspection data
6
+ class IntrospectionCollector
7
+ attr_reader :model_class
8
+
9
+ def initialize(model_class)
10
+ @model_class = model_class
11
+ end
12
+
13
+ # Collect all introspection data
14
+ def collect
15
+ {
16
+ callbacks: collect_callbacks,
17
+ enums: collect_enums,
18
+ concerns: collect_concerns,
19
+ scopes: collect_scopes,
20
+ validations: collect_validations,
21
+ lifecycle_hooks: collect_lifecycle_hooks
22
+ }
23
+ end
24
+
25
+ # Collect all callbacks with their order
26
+ def collect_callbacks
27
+ return {} unless model_class.respond_to?(:_commit_callbacks)
28
+
29
+ callback_types = [
30
+ :before_validation, :after_validation,
31
+ :before_save, :around_save, :after_save,
32
+ :before_create, :around_create, :after_create,
33
+ :before_update, :around_update, :after_update,
34
+ :before_destroy, :around_destroy, :after_destroy,
35
+ :after_commit, :after_rollback,
36
+ :after_find, :after_initialize,
37
+ :after_touch
38
+ ]
39
+
40
+ callbacks = {}
41
+ callback_types.each do |type|
42
+ chain = get_callback_chain(type)
43
+ next if chain.empty?
44
+
45
+ callbacks[type] = chain
46
+ end
47
+
48
+ callbacks
49
+ end
50
+
51
+ # Collect all enums
52
+ def collect_enums
53
+ return {} unless model_class.respond_to?(:defined_enums)
54
+
55
+ model_class.defined_enums.transform_values do |mapping|
56
+ {
57
+ mapping: mapping,
58
+ values: mapping.keys,
59
+ type: detect_enum_type(mapping)
60
+ }
61
+ end
62
+ rescue => e
63
+ {}
64
+ end
65
+
66
+ # Collect all concerns and modules
67
+ def collect_concerns
68
+ return [] unless model_class.respond_to?(:ancestors)
69
+
70
+ concerns = []
71
+ model_class.ancestors.each do |ancestor|
72
+ next if ancestor == model_class
73
+ next if [ActiveRecord::Base, Object, BasicObject, Kernel].include?(ancestor)
74
+ next if ancestor.name.nil? || ancestor.name.empty?
75
+ next if ancestor.name.start_with?('ActiveRecord::', 'ActiveSupport::')
76
+
77
+ # Check if it's a concern or module
78
+ is_concern = ancestor.respond_to?(:included_modules) &&
79
+ ancestor.included_modules.include?(ActiveSupport::Concern)
80
+
81
+ concerns << {
82
+ name: ancestor.name,
83
+ type: is_concern ? :concern : (ancestor.is_a?(Class) ? :class : :module),
84
+ location: source_location_for(ancestor)
85
+ }
86
+ end
87
+
88
+ concerns.uniq { |c| c[:name] }
89
+ rescue => e
90
+ []
91
+ end
92
+
93
+ # Collect all scopes with their SQL
94
+ def collect_scopes
95
+ return {} unless model_class.respond_to?(:scope_attributes?)
96
+
97
+ scopes = {}
98
+
99
+ # Get all singleton methods that might be scopes
100
+ scope_methods = model_class.methods(false) - ActiveRecord::Base.methods(false)
101
+
102
+ scope_methods.each do |method_name|
103
+ next if method_name.to_s.start_with?('_')
104
+
105
+ begin
106
+ # Try to call the scope and get its SQL
107
+ scope_result = model_class.public_send(method_name)
108
+
109
+ if scope_result.is_a?(ActiveRecord::Relation)
110
+ scopes[method_name] = {
111
+ sql: scope_result.to_sql,
112
+ values: extract_scope_values(scope_result),
113
+ conditions: extract_scope_conditions(scope_result)
114
+ }
115
+ end
116
+ rescue ArgumentError, NameError, NoMethodError
117
+ # Skip if it requires arguments or is not a scope
118
+ next
119
+ rescue => e
120
+ # Skip problematic scopes
121
+ next
122
+ end
123
+ end
124
+
125
+ scopes
126
+ rescue => e
127
+ {}
128
+ end
129
+
130
+ # Collect all validations
131
+ def collect_validations
132
+ return [] unless model_class.respond_to?(:validators)
133
+
134
+ validations = []
135
+
136
+ model_class.validators.each do |validator|
137
+ attributes = validator.attributes rescue [:unknown]
138
+
139
+ validations << {
140
+ type: validator.class.name.demodulize,
141
+ attributes: attributes,
142
+ options: extract_validator_options(validator),
143
+ conditions: extract_validator_conditions(validator)
144
+ }
145
+ end
146
+
147
+ # Group by attribute for better organization
148
+ validations.group_by { |v| v[:attributes].first }
149
+ rescue => e
150
+ []
151
+ end
152
+
153
+ # Collect lifecycle hooks summary
154
+ def collect_lifecycle_hooks
155
+ {
156
+ callbacks_count: count_callbacks,
157
+ validations_count: count_validations,
158
+ has_observers: has_observers?,
159
+ has_state_machine: has_state_machine?
160
+ }
161
+ rescue => e
162
+ {}
163
+ end
164
+
165
+ # Find where a method is defined
166
+ def method_source_location(method_name)
167
+ return nil unless model_class.respond_to?(method_name)
168
+
169
+ method = if model_class.respond_to?(method_name)
170
+ model_class.method(method_name)
171
+ else
172
+ return nil
173
+ end
174
+
175
+ location = method.source_location
176
+ return nil unless location
177
+
178
+ {
179
+ file: location[0],
180
+ line: location[1],
181
+ owner: method.owner.name,
182
+ type: determine_method_type(method.owner)
183
+ }
184
+ rescue => e
185
+ nil
186
+ end
187
+
188
+ private
189
+
190
+ # Get callback chain for a specific type
191
+ def get_callback_chain(type)
192
+ chain_method = "_#{type}_callbacks"
193
+ return [] unless model_class.respond_to?(chain_method)
194
+
195
+ callback_chain = model_class.send(chain_method)
196
+ return [] unless callback_chain.respond_to?(:each)
197
+
198
+ callbacks = []
199
+ callback_chain.each do |callback|
200
+ next unless callback.respond_to?(:filter)
201
+
202
+ filter_name = extract_callback_name(callback)
203
+ next if filter_name.to_s.empty?
204
+
205
+ callback_kind = begin
206
+ callback.kind
207
+ rescue
208
+ :unknown
209
+ end
210
+
211
+ callbacks << {
212
+ name: filter_name,
213
+ kind: callback_kind,
214
+ if: extract_callback_condition(callback, :if),
215
+ unless: extract_callback_condition(callback, :unless)
216
+ }
217
+ end
218
+
219
+ callbacks
220
+ rescue => e
221
+ []
222
+ end
223
+
224
+ # Extract callback name
225
+ def extract_callback_name(callback)
226
+ filter = callback.filter
227
+
228
+ case filter
229
+ when Symbol, String
230
+ filter
231
+ when Proc
232
+ "<Proc>"
233
+ else
234
+ if filter.respond_to?(:name)
235
+ filter.name
236
+ else
237
+ filter.class.name
238
+ end
239
+ end
240
+ rescue => e
241
+ :unknown
242
+ end
243
+
244
+ # Extract callback condition
245
+ def extract_callback_condition(callback, type)
246
+ return nil unless callback.respond_to?(type)
247
+
248
+ conditions = callback.send(type)
249
+ return nil if conditions.empty?
250
+
251
+ conditions.map do |condition|
252
+ case condition
253
+ when Symbol, String
254
+ condition
255
+ when Proc
256
+ "<Proc>"
257
+ else
258
+ condition.class.name
259
+ end
260
+ end
261
+ rescue => e
262
+ nil
263
+ end
264
+
265
+ # Detect enum type (integer or string)
266
+ def detect_enum_type(mapping)
267
+ return :integer if mapping.values.first.is_a?(Integer)
268
+ return :string if mapping.values.first.is_a?(String)
269
+ :unknown
270
+ end
271
+
272
+ # Extract scope values
273
+ def extract_scope_values(scope)
274
+ return {} unless scope.respond_to?(:values)
275
+
276
+ values = scope.values
277
+ {
278
+ where: values[:where]&.to_s || nil,
279
+ order: values[:order]&.to_s || nil,
280
+ limit: values[:limit],
281
+ offset: values[:offset],
282
+ includes: values[:includes]&.to_s || nil,
283
+ joins: values[:joins]&.to_s || nil
284
+ }.compact
285
+ rescue => e
286
+ {}
287
+ end
288
+
289
+ # Extract scope conditions
290
+ def extract_scope_conditions(scope)
291
+ return [] unless scope.respond_to?(:where_clause)
292
+
293
+ predicates = scope.where_clause.send(:predicates) rescue []
294
+ predicates.map(&:to_s).compact
295
+ rescue => e
296
+ []
297
+ end
298
+
299
+ # Extract validator options
300
+ def extract_validator_options(validator)
301
+ options = {}
302
+
303
+ # Common validation options
304
+ [:allow_nil, :allow_blank, :on, :strict, :message].each do |opt|
305
+ options[opt] = validator.options[opt] if validator.options.key?(opt)
306
+ end
307
+
308
+ # Type-specific options
309
+ case validator
310
+ when ActiveModel::Validations::LengthValidator
311
+ [:minimum, :maximum, :in, :within, :is].each do |opt|
312
+ options[opt] = validator.options[opt] if validator.options.key?(opt)
313
+ end
314
+ when ActiveModel::Validations::NumericalityValidator
315
+ [:greater_than, :greater_than_or_equal_to, :less_than, :less_than_or_equal_to,
316
+ :equal_to, :odd, :even, :only_integer].each do |opt|
317
+ options[opt] = validator.options[opt] if validator.options.key?(opt)
318
+ end
319
+ when ActiveRecord::Validations::UniquenessValidator
320
+ options[:scope] = validator.options[:scope] if validator.options.key?(:scope)
321
+ end
322
+
323
+ options
324
+ rescue => e
325
+ {}
326
+ end
327
+
328
+ # Extract validator conditions
329
+ def extract_validator_conditions(validator)
330
+ conditions = {}
331
+
332
+ [:if, :unless].each do |cond|
333
+ next unless validator.options.key?(cond)
334
+
335
+ value = validator.options[cond]
336
+ conditions[cond] = case value
337
+ when Symbol, String
338
+ value
339
+ when Proc
340
+ "<Proc>"
341
+ else
342
+ value.class.name
343
+ end
344
+ end
345
+
346
+ conditions
347
+ rescue => e
348
+ {}
349
+ end
350
+
351
+ # Count all callbacks
352
+ def count_callbacks
353
+ collect_callbacks.values.flatten.count
354
+ rescue => e
355
+ 0
356
+ end
357
+
358
+ # Count all validations
359
+ def count_validations
360
+ model_class.validators.count
361
+ rescue => e
362
+ 0
363
+ end
364
+
365
+ # Check if model has observers
366
+ def has_observers?
367
+ # In modern Rails, observers are deprecated
368
+ # This checks for any observer-like patterns
369
+ return false unless defined?(ActiveRecord::Observer)
370
+
371
+ ActiveRecord::Observer.descendants.any? { |obs| obs.observed_classes.include?(model_class) }
372
+ rescue => e
373
+ false
374
+ end
375
+
376
+ # Check if model has state machine
377
+ def has_state_machine?
378
+ # Check for common state machine gems
379
+ model_class.respond_to?(:state_machines) || # state_machines gem
380
+ model_class.respond_to?(:aasm_states) || # aasm gem
381
+ model_class.respond_to?(:workflow_spec) # workflow gem
382
+ rescue => e
383
+ false
384
+ end
385
+
386
+ # Get source location for a module/class
387
+ def source_location_for(klass)
388
+ return nil if klass.name.nil?
389
+
390
+ # Try to find where it's defined
391
+ methods = klass.instance_methods(false)
392
+ return nil if methods.empty?
393
+
394
+ method = klass.instance_method(methods.first)
395
+ location = method.source_location
396
+
397
+ return nil unless location
398
+
399
+ {
400
+ file: location[0],
401
+ line: location[1]
402
+ }
403
+ rescue => e
404
+ nil
405
+ end
406
+
407
+ # Determine method type (model, concern, gem, etc.)
408
+ def determine_method_type(owner)
409
+ return :model if owner == model_class
410
+ return :concern if owner.name&.end_with?('Concern')
411
+ return :gem if owner.name&.start_with?('ActiveRecord::', 'ActiveSupport::')
412
+ return :parent if owner < ActiveRecord::Base && owner != ActiveRecord::Base
413
+ :module
414
+ rescue => e
415
+ :unknown
416
+ end
417
+ end
418
+ end
419
+ end
420
+
@@ -10,3 +10,4 @@ end
10
10
 
11
11
 
12
12
 
13
+
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsConsolePro
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
6
6