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,385 @@
1
+ # Query Builder & Comparator
2
+
3
+ Compare different query strategies and build optimized ActiveRecord queries interactively.
4
+
5
+ ## Query Comparison
6
+
7
+ Compare multiple query approaches side-by-side to find the optimal strategy.
8
+
9
+ ### Basic Usage
10
+
11
+ ```ruby
12
+ # Compare different query strategies
13
+ compare do |c|
14
+ c.run("Eager loading") { User.includes(:posts).to_a }
15
+ c.run("N+1") { User.all.map(&:posts) }
16
+ c.run("Select specific") { User.select(:id, :email).to_a }
17
+ end
18
+ ```
19
+
20
+ ### What Gets Compared
21
+
22
+ The comparison tracks:
23
+ - **Execution Time**: Wall-clock time in milliseconds
24
+ - **Query Count**: Number of SQL queries executed
25
+ - **Memory Usage**: Memory consumption (platform-dependent)
26
+ - **SQL Queries**: All SQL queries with their execution times
27
+ - **Errors**: Any exceptions that occur during execution
28
+
29
+ ### Example Output
30
+
31
+ ```
32
+ ═══════════════════════════════════════════════════════════
33
+ ⚖️ QUERY COMPARISON
34
+ ═══════════════════════════════════════════════════════════
35
+
36
+ 📊 Summary:
37
+ Total Strategies: 3
38
+ Fastest: Eager loading
39
+ Slowest: N+1
40
+ Performance Ratio: 5.2x slower
41
+
42
+ 📈 Detailed Results:
43
+
44
+ #1 Eager loading
45
+ ⏱️ Duration: 45.23ms
46
+ 🔢 Queries: 2
47
+ 💾 Memory: 1.2 MB
48
+ 📝 SQL Queries (2 total):
49
+ 1. SELECT "users".* FROM "users" (12.5ms)
50
+ 2. SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) (32.7ms)
51
+
52
+ #2 Select specific
53
+ ⏱️ Duration: 28.15ms
54
+ 🔢 Queries: 1
55
+ 💾 Memory: 0.8 MB
56
+ 📝 SQL Queries (1 total):
57
+ 1. SELECT "users"."id", "users"."email" FROM "users" (28.1ms)
58
+
59
+ #3 N+1
60
+ ⏱️ Duration: 234.67ms
61
+ 🔢 Queries: 101
62
+ 💾 Memory: 2.5 MB
63
+ 📝 SQL Queries (101 total):
64
+ 1. SELECT "users".* FROM "users" (15.2ms)
65
+ 2. SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1 (2.1ms)
66
+ ... and 99 more
67
+
68
+ 🏆 Winner: Select specific
69
+ This strategy is 8.3x faster than the slowest
70
+ ```
71
+
72
+ ### Advanced Comparison
73
+
74
+ ```ruby
75
+ # Compare complex scenarios
76
+ compare do |c|
77
+ c.run("With joins") do
78
+ User.joins(:posts)
79
+ .where(posts: { published: true })
80
+ .distinct
81
+ .to_a
82
+ end
83
+
84
+ c.run("With includes") do
85
+ User.includes(:posts)
86
+ .where(posts: { published: true })
87
+ .to_a
88
+ end
89
+
90
+ c.run("Subquery") do
91
+ User.where(id: Post.published.select(:user_id)).to_a
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Error Handling
97
+
98
+ ```ruby
99
+ # Comparisons continue even if one strategy fails
100
+ compare do |c|
101
+ c.run("Valid query") { User.all.to_a }
102
+ c.run("Invalid query") { User.where(nonexistent: true).to_a }
103
+ c.run("Another valid") { User.limit(10).to_a }
104
+ end
105
+ # Shows errors for failed strategies but continues with others
106
+ ```
107
+
108
+ ### Export Comparison Results
109
+
110
+ ```ruby
111
+ result = compare do |c|
112
+ c.run("Strategy 1") { User.includes(:posts).to_a }
113
+ c.run("Strategy 2") { User.all.map(&:posts) }
114
+ end
115
+
116
+ # Export to JSON
117
+ result.to_json
118
+ result.export_to_file('comparison.json')
119
+
120
+ # Export to YAML
121
+ result.to_yaml
122
+ result.export_to_file('comparison.yaml')
123
+
124
+ # Export to HTML
125
+ result.to_html
126
+ result.export_to_file('comparison.html')
127
+ ```
128
+
129
+ ## Interactive Query Builder
130
+
131
+ Build and analyze ActiveRecord queries using a fluent DSL.
132
+
133
+ ### Basic Usage
134
+
135
+ ```ruby
136
+ # Build a query
137
+ query User do
138
+ where(active: true)
139
+ includes(:posts)
140
+ order(:created_at)
141
+ limit(10)
142
+ end
143
+ ```
144
+
145
+ ### Analyze Query Performance
146
+
147
+ ```ruby
148
+ # Get SQL + explain plan
149
+ query User do
150
+ where(active: true)
151
+ includes(:posts)
152
+ order(:created_at)
153
+ limit(10)
154
+ end.analyze
155
+ ```
156
+
157
+ This shows:
158
+ - Generated SQL query
159
+ - Query execution plan (EXPLAIN)
160
+ - Index usage
161
+ - Performance recommendations
162
+ - Statistics
163
+
164
+ ### Example Output
165
+
166
+ ```
167
+ ═══════════════════════════════════════════════════════════
168
+ 🔧 QUERY BUILDER: User
169
+ ═══════════════════════════════════════════════════════════
170
+
171
+ 📝 Generated SQL:
172
+ SELECT "users".* FROM "users"
173
+ WHERE "users"."active" = $1
174
+ ORDER BY "users"."created_at" ASC
175
+ LIMIT $2
176
+
177
+ 📊 Statistics:
178
+ Model User
179
+ Table users
180
+
181
+ 🔬 Query Analysis:
182
+ ═══════════════════════════════════════════════════════════
183
+ 🔬 SQL EXPLAIN ANALYSIS
184
+ ═══════════════════════════════════════════════════════════
185
+
186
+ 📝 Query:
187
+ SELECT "users".* FROM "users" WHERE "users"."active" = $1 ORDER BY "users"."created_at" ASC LIMIT $2
188
+
189
+ ⏱️ Execution Time: 12.45ms
190
+
191
+ 📊 Query Plan:
192
+ ✅ Index Scan using index_users_on_active on users
193
+ Index Cond: (active = true)
194
+ Sort Key: created_at
195
+ Rows: 10
196
+
197
+ 🔍 Index Analysis:
198
+ ✅ Indexes used:
199
+ • index_users_on_active
200
+
201
+ 💡 Recommendations:
202
+ • Query is optimized with index usage
203
+ ```
204
+
205
+ ### Available Query Methods
206
+
207
+ The query builder supports all ActiveRecord::Relation methods:
208
+
209
+ ```ruby
210
+ query User do
211
+ # Filtering
212
+ where(active: true)
213
+ where("created_at > ?", 1.week.ago)
214
+ where.not(deleted: true)
215
+
216
+ # Associations
217
+ includes(:posts, :comments)
218
+ joins(:posts)
219
+ left_joins(:profile)
220
+
221
+ # Selection
222
+ select(:id, :email, :name)
223
+ distinct
224
+
225
+ # Ordering
226
+ order(:created_at)
227
+ order(created_at: :desc)
228
+ order("created_at DESC, name ASC")
229
+
230
+ # Pagination
231
+ limit(10)
232
+ offset(20)
233
+
234
+ # Grouping
235
+ group(:status)
236
+ having("COUNT(*) > ?", 5)
237
+
238
+ # Other
239
+ readonly
240
+ lock
241
+ end
242
+ ```
243
+
244
+ ### Execute the Query
245
+
246
+ ```ruby
247
+ # Build and execute
248
+ result = query User do
249
+ where(active: true)
250
+ limit(10)
251
+ end
252
+
253
+ # Execute and get results
254
+ result.execute # Returns the relation
255
+ result.to_a # Returns array of records
256
+ result.count # Returns count
257
+ result.exists? # Returns boolean
258
+ ```
259
+
260
+ ### Chain Methods
261
+
262
+ ```ruby
263
+ # You can chain methods naturally
264
+ query User do
265
+ where(active: true)
266
+ includes(:posts)
267
+ order(:created_at)
268
+ limit(10)
269
+ end.analyze.to_a
270
+ ```
271
+
272
+ ### Without Block
273
+
274
+ ```ruby
275
+ # Build query programmatically
276
+ builder = query User
277
+ builder.where(active: true)
278
+ builder.includes(:posts)
279
+ builder.analyze
280
+ ```
281
+
282
+ ### Export Query Builder Results
283
+
284
+ ```ruby
285
+ result = query User do
286
+ where(active: true)
287
+ includes(:posts)
288
+ end.analyze
289
+
290
+ # Export to JSON
291
+ result.to_json
292
+ result.export_to_file('query.json')
293
+
294
+ # Export to YAML
295
+ result.to_yaml
296
+ result.export_to_file('query.yaml')
297
+
298
+ # Export to HTML
299
+ result.to_html
300
+ result.export_to_file('query.html')
301
+ ```
302
+
303
+ ## Use Cases
304
+
305
+ ### Finding N+1 Problems
306
+
307
+ ```ruby
308
+ compare do |c|
309
+ c.run("N+1 Problem") do
310
+ User.all.map { |u| u.posts.count }
311
+ end
312
+
313
+ c.run("Eager Loading") do
314
+ User.includes(:posts).map { |u| u.posts.count }
315
+ end
316
+
317
+ c.run("Counter Cache") do
318
+ User.select(:id, :posts_count).map(&:posts_count)
319
+ end
320
+ end
321
+ ```
322
+
323
+ ### Optimizing Complex Queries
324
+
325
+ ```ruby
326
+ # Compare different approaches to the same problem
327
+ compare do |c|
328
+ c.run("Multiple Includes") do
329
+ User.includes(:posts, :comments, :profile).to_a
330
+ end
331
+
332
+ c.run("Nested Includes") do
333
+ User.includes(posts: :comments).to_a
334
+ end
335
+
336
+ c.run("Preload") do
337
+ User.preload(:posts, :comments, :profile).to_a
338
+ end
339
+ end
340
+ ```
341
+
342
+ ### Testing Query Performance
343
+
344
+ ```ruby
345
+ # Build and analyze before deploying
346
+ query User do
347
+ joins(:posts)
348
+ .where(posts: { published: true })
349
+ .group(:id)
350
+ .having("COUNT(posts.id) > ?", 5)
351
+ .order("COUNT(posts.id) DESC")
352
+ .limit(10)
353
+ end.analyze
354
+ ```
355
+
356
+ ## Features
357
+
358
+ - **Side-by-Side Comparison**: Compare multiple query strategies simultaneously
359
+ - **Performance Metrics**: Track execution time, query count, and memory usage
360
+ - **SQL Analysis**: See all SQL queries executed with their timings
361
+ - **Error Resilience**: Failed strategies don't stop the comparison
362
+ - **Fluent DSL**: Chain query methods naturally
363
+ - **Query Analysis**: Integrated EXPLAIN plan analysis
364
+ - **Export Support**: Export results to JSON, YAML, or HTML
365
+ - **Winner Detection**: Automatically identifies the fastest strategy
366
+
367
+ ## Tips
368
+
369
+ 1. **Warm up the database**: Run queries once before comparing to avoid cold cache effects
370
+ 2. **Use realistic data**: Test with production-like data volumes
371
+ 3. **Compare apples to apples**: Ensure all strategies return the same data
372
+ 4. **Check query count**: Lower query count usually means better performance
373
+ 5. **Review SQL queries**: Look at the actual SQL to understand what's happening
374
+ 6. **Use analyze**: Always use `.analyze` to see the execution plan
375
+
376
+ ## Configuration
377
+
378
+ ```ruby
379
+ # Disable query builder
380
+ RailsConsolePro.configure do |c|
381
+ c.query_builder_command_enabled = false
382
+ c.compare_command_enabled = false
383
+ end
384
+ ```
385
+
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Commands
5
+ # Command for comparing different query strategies
6
+ class CompareCommand < BaseCommand
7
+ def execute(&block)
8
+ return disabled_message unless enabled?
9
+ return pastel.red('No block provided') unless block_given?
10
+
11
+ comparator = Comparator.new(config)
12
+ comparator.compare(&block)
13
+ rescue => e
14
+ RailsConsolePro::ErrorHandler.handle(e, context: :compare)
15
+ end
16
+
17
+ private
18
+
19
+ def enabled?
20
+ RailsConsolePro.config.enabled && RailsConsolePro.config.compare_command_enabled
21
+ end
22
+
23
+ def disabled_message
24
+ pastel.yellow('Compare command is disabled. Enable it via RailsConsolePro.configure { |c| c.compare_command_enabled = true }')
25
+ end
26
+
27
+ def config
28
+ RailsConsolePro.config
29
+ end
30
+ end
31
+
32
+ # Internal comparator that runs and measures query strategies
33
+ class Comparator
34
+ SQL_EVENT = 'sql.active_record'
35
+ IGNORED_SQL_NAMES = %w[SCHEMA CACHE EXPLAIN TRANSACTION].freeze
36
+
37
+ attr_reader :config
38
+
39
+ def initialize(config = RailsConsolePro.config)
40
+ @config = config
41
+ end
42
+
43
+ def compare(&block)
44
+ runner = Runner.new(config)
45
+ runner.instance_eval(&block)
46
+ runner.build_result
47
+ end
48
+
49
+ # Internal runner that collects comparison data
50
+ class Runner
51
+ attr_reader :config, :comparisons
52
+
53
+ def initialize(config)
54
+ @config = config
55
+ @comparisons = []
56
+ end
57
+
58
+ def run(name, &block)
59
+ return unless block_given?
60
+
61
+ comparison = execute_comparison(name, block)
62
+ @comparisons << comparison
63
+ comparison
64
+ end
65
+
66
+ def build_result
67
+ winner = @comparisons.reject { |c| c.error }.min_by { |c| c.duration_ms || Float::INFINITY }
68
+ CompareResult.new(comparisons: @comparisons, winner: winner)
69
+ end
70
+
71
+ private
72
+
73
+ def execute_comparison(name, block)
74
+ sql_queries = []
75
+ error = nil
76
+ result = nil
77
+
78
+ subscription = subscribe_to_sql_events(sql_queries)
79
+
80
+ wall_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ memory_before = memory_usage
82
+
83
+ begin
84
+ result = block.call
85
+ rescue => e
86
+ error = e
87
+ ensure
88
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall_start) * 1000.0).round(2)
89
+ memory_after = memory_usage
90
+ memory_usage_kb = memory_after - memory_before
91
+
92
+ ActiveSupport::Notifications.unsubscribe(subscription) if subscription
93
+
94
+ # Get query_count from collected queries
95
+ query_count = sql_queries.size
96
+ end
97
+
98
+ CompareResult::Comparison.new(
99
+ name: name.to_s,
100
+ duration_ms: duration_ms,
101
+ query_count: query_count,
102
+ result: result,
103
+ error: error,
104
+ sql_queries: sql_queries.dup,
105
+ memory_usage_kb: memory_usage_kb
106
+ )
107
+ end
108
+
109
+ def subscribe_to_sql_events(sql_queries)
110
+ ActiveSupport::Notifications.subscribe(SQL_EVENT) do |*args|
111
+ event = ActiveSupport::Notifications::Event.new(*args)
112
+ payload = event.payload
113
+ sql = payload[:sql].to_s
114
+ name = payload[:name].to_s
115
+
116
+ next if sql.empty?
117
+ next if IGNORED_SQL_NAMES.any? { |ignored| name.start_with?(ignored) }
118
+ next if sql =~ /\A\s*(BEGIN|COMMIT|ROLLBACK)/i
119
+
120
+ sql_queries << {
121
+ sql: sql,
122
+ duration_ms: event.duration.round(2),
123
+ name: name,
124
+ cached: payload[:cached] || false
125
+ }
126
+ end
127
+ end
128
+
129
+ def memory_usage
130
+ # Try to get memory usage if available (works on Linux)
131
+ if defined?(RSS) && Process.respond_to?(:memory)
132
+ Process.memory / 1024.0 # Convert to KB
133
+ elsif File.exist?('/proc/self/status')
134
+ # Linux proc filesystem
135
+ status = File.read('/proc/self/status')
136
+ if match = status.match(/VmRSS:\s+(\d+)\s+kB/)
137
+ match[1].to_f
138
+ else
139
+ 0.0
140
+ end
141
+ else
142
+ 0.0
143
+ end
144
+ rescue
145
+ 0.0
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+