Class: RailsVitals::MCP::Tools::ExplainQuery

Inherits:
Base
  • Object
show all
Defined in:
lib/rails_vitals/mcp/tools/explain_query.rb

Constant Summary collapse

TOOL_NAME =
"railsvitals_explain_query"
DESCRIPTION =
<<~DESC.strip
  Runs EXPLAIN ANALYZE on a SELECT query and returns the execution plan summary:
  total cost, actual execution time, rows examined, detected warnings (Seq Scan,
  Sort without index, large Nested Loop), and deterministic fix suggestions with
  migration hints. Only SELECT statements are accepted — any SQL containing DML
  keywords (INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER) is rejected, including
  CTEs. Use this to investigate a specific slow query from railsvitals_get_slow_queries.

  When presenting results, always lead with the interpretation field as a plain-English
  verdict. Then highlight each warning by name and explain what it means for this specific
  query (table name, rows scanned). For each suggestion, show the body explanation first,
  then the migration snippet in a code block, then the generator command if present.
  Avoid listing raw numbers without context — translate total_cost and rows_examined into
  plain language (e.g. "scanned 50,000 rows to return 3" instead of "rows_examined: 50000").
DESC
INPUT_SCHEMA =
{
  type: "object",
  required: [ "sql" ],
  properties: {
    sql: {
      type: "string",
      description: "The SELECT query to explain. Must not contain INSERT, UPDATE, DELETE, DROP, TRUNCATE, or ALTER — including inside CTEs."
    }
  }
}.freeze
DML_PATTERN =
/\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE)\b/i.freeze

Instance Method Summary collapse

Methods inherited from Base

definition, tool_name

Instance Method Details

#call(params) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/rails_vitals/mcp/tools/explain_query.rb', line 36

def call(params)
  sql = params[:sql] || params["sql"]

  return missing_sql_response if sql.nil? || sql.strip.empty?
  return rejected_sql_response(sql) if dml_present?(sql)

  result = Analyzers::ExplainAnalyzer.analyze(sql.strip)

  return error_response(result.error) if result.error

  {
    sql: result.sql,
    total_cost: result.total_cost,
    actual_time_ms: result.actual_time_ms,
    rows_examined: result.rows_examined,
    warnings: serialize_warnings(result.warnings),
    suggestions: serialize_suggestions(result.suggestions),
    interpretation: result.interpretation
  }
end