Class: TalkToYourApp::Plugins::Db::Tools::Query

Inherits:
Tool
  • Object
show all
Defined in:
lib/talk_to_your_app/plugins/db/tools/query.rb

Overview

Runs a read-only SQL query on the declared :replica_readonly connection and renders the rows as JSON, plain text, or an HTML table. The query runs inside a transaction with a per-query statement timeout, so a slow query cannot poison the connection pool. Writes are rejected by the underlying read-only database role — the gem does not parse SQL.

Constant Summary collapse

DEFAULT_TIMEOUT_MS =
30_000
CONNECTION =
:replica_readonly

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Tool

argument, arguments, connection, default_arguments, description, dispatch, input_schema_hash, invoke, name, normalize_response, to_mcp_definition, to_mcp_tool

Class Method Details

.timeout_statement(adapter_name, ms) ⇒ Object

The SQL that arms a per-query timeout for an adapter, or nil when the adapter has no per-statement timeout. Pure (no connection) so it is unit-testable without each database installed.

Postgres -> transaction-local `statement_timeout` (unwound with the txn).
MySQL    -> session `max_execution_time` (ms), bounding read-only SELECTs.
SQLite / MariaDB / others -> none (documented in the README).


63
64
65
66
67
68
# File 'lib/talk_to_your_app/plugins/db/tools/query.rb', line 63

def self.timeout_statement(adapter_name, ms)
  case adapter_name
  when /postgres/i      then "SET LOCAL statement_timeout = #{ms.to_i}"
  when /mysql|trilogy/i then "SET SESSION max_execution_time = #{ms.to_i}"
  end
end

Instance Method Details

#call(args, ctx) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/talk_to_your_app/plugins/db/tools/query.rb', line 27

def call(args, ctx)
  format = args[:format] || "json"
  result = ctx.connection do |conn|
    conn.transaction do
      apply_statement_timeout(conn, timeout_ms)
      begin
        conn.exec_query(args[:sql])
      ensure
        clear_statement_timeout(conn)
      end
    end
  end
  render(result, format)
rescue ActiveRecord::QueryCanceled, ActiveRecord::StatementTimeout => e
  error("Query exceeded the #{timeout_ms}ms statement timeout: #{e.message}")
rescue ActiveRecord::ReadOnlyError => e
  # Rails' connected_to(role: :reading) blocks writes before they reach
  # the database; the read-only DB role is the backstop behind it.
  error("Write rejected: this connection is read-only. #{e.message}")
rescue ActiveRecord::StatementInvalid => e
  error("Query failed: #{e.message}")
end