Module: Profiler::ExplainRunner
- Defined in:
- lib/profiler/explain_runner.rb
Overview
Shared service for running EXPLAIN ANALYZE on a stored query. Used by both the HTTP API controller and the MCP explain_query tool.
Class Method Summary collapse
- .build_explain_statement(sql, adapter) ⇒ Object
- .reconstruct_sql(sql, binds, conn, adapter) ⇒ Object
-
.run(profile_token, query_index) ⇒ Hash
{ result:, format: “json”|“text”, adapter: String } or raises ArgumentError / RuntimeError.
Class Method Details
.build_explain_statement(sql, adapter) ⇒ Object
72 73 74 75 76 77 78 79 80 |
# File 'lib/profiler/explain_runner.rb', line 72 def self.build_explain_statement(sql, adapter) if adapter.include?("postgresql") ["EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) #{sql}", "json"] elsif adapter.include?("mysql") ["EXPLAIN FORMAT=JSON #{sql}", "json"] else ["EXPLAIN QUERY PLAN #{sql}", "text"] end end |
.reconstruct_sql(sql, binds, conn, adapter) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/profiler/explain_runner.rb', line 53 def self.reconstruct_sql(sql, binds, conn, adapter) return sql if binds.empty? if adapter.include?("postgresql") result = sql.dup binds.each_with_index do |value, i| result = result.gsub("$#{i + 1}", conn.quote(value)) end result else # MySQL / SQLite: replace ? sequentially result = sql.dup binds.each do |value| result = result.sub("?", conn.quote(value)) end result end end |
.run(profile_token, query_index) ⇒ Hash
Returns { result:, format: “json”|“text”, adapter: String } or raises ArgumentError / RuntimeError.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/profiler/explain_runner.rb', line 11 def self.run(profile_token, query_index) unless Profiler.configuration.enabled raise SecurityError, "EXPLAIN is only available when the profiler is enabled" end profile = Profiler.storage.load(profile_token) raise ArgumentError, "Profile not found: #{profile_token}" unless profile db_data = profile.collector_data("database") raise ArgumentError, "No database data in this profile" unless db_data && db_data["queries"] queries = db_data["queries"] query_index = query_index.to_i unless query_index >= 0 && query_index < queries.size raise ArgumentError, "Query index #{query_index} out of range (0..#{queries.size - 1})" end query = queries[query_index] sql = query["sql"].to_s binds = Array(query["binds"]) conn = ActiveRecord::Base.connection adapter = conn.adapter_name.downcase full_sql = reconstruct_sql(sql, binds, conn, adapter) explain_sql, format = build_explain_statement(full_sql, adapter) rows = conn.exec_query(explain_sql, "EXPLAIN").to_a result = if format == "json" # PostgreSQL / MySQL return JSON in rows[0]["QUERY PLAN"] or rows[0]["EXPLAIN"] raw = rows.first&.values&.first.to_s JSON.parse(raw) rescue raw else rows.map { |r| r.values.join("\t") }.join("\n") end { result: result, format: format, adapter: adapter } end |