Class: QueryGuard::Explain::PostgreSQLAdapter

Inherits:
AdapterInterface show all
Defined in:
lib/query_guard/explain/postgresql_adapter.rb

Overview

PostgreSQL-specific EXPLAIN plan adapter. Executes EXPLAIN (FORMAT JSON, ANALYZE) for PostgreSQL queries.

Features:

  • Safely executes EXPLAIN with ANALYZE for actual metrics
  • Returns well-structured JSON plan data
  • Fails gracefully if connection unavailable
  • Filters out unsafe query patterns
  • Configurable timeout and ANALYZE flag

Example:

adapter = PostgreSQLAdapter.new(ActiveRecord::Base.connection)
plan = adapter.get_plan("SELECT * FROM users WHERE status = 'active'")

Note: Requires postgres adapter (pg gem) with access to query execution

Constant Summary collapse

DEFAULT_EXPLAIN_TIMEOUT =

seconds

5.0

Instance Method Summary collapse

Constructor Details

#initialize(connection, options = {}) ⇒ PostgreSQLAdapter

Initialize PostgreSQL adapter

Parameters:

  • connection (PG::Connection or ActiveRecord Adapter) —

    PostgreSQL connection

  • options (Hash) (defaults to: {}) —

    Configuration options @option options [Float] :timeout Explain query timeout (default: 5.0s) @option options [Boolean] :use_analyze Whether to use ANALYZE (default: false for safety) @option options [Logger] :logger Optional logger instance for debugging @option options [Boolean] :validate_connection Check connection is valid on init (default: true)



33
34
35
36
37
38
39
40
41
# File 'lib/query_guard/explain/postgresql_adapter.rb', line 33

def initialize(connection, options = {})
  super(connection)
  @timeout = options[:timeout] || DEFAULT_EXPLAIN_TIMEOUT
  @use_analyze = options.fetch(:use_analyze, false)
  @logger = options[:logger]
  @validate_connection = options.fetch(:validate_connection, true)

  validate_connection! if @validate_connection
end

Instance Method Details

#can_explain?(sql) ⇒ Boolean

Check if query can be safely explained

Parameters:

  • sql (String) —

    SQL query

Returns:

  • (Boolean) —

    True if query is safe to EXPLAIN



79
80
81
82
83
84
85
86
87
# File 'lib/query_guard/explain/postgresql_adapter.rb', line 79

def can_explain?(sql)
  normalized = sql.strip.upcase
  # Only EXPLAIN SELECT, WITH (CTE), and simple UPDATE/DELETE
  return false if normalized.start_with?("PRAGMA", "BEGIN", "COMMIT")
  return false if normalized.start_with?("DROP", "ALTER", "CREATE", "TRUNCATE")
  return false if normalized.include?("RETURNING") && !normalized.start_with?("SELECT")

  true
end

#engine_name ⇒ Symbol

Engine identifier

Returns:

  • (Symbol)


92
93
94
# File 'lib/query_guard/explain/postgresql_adapter.rb', line 92

def engine_name
  :postgresql
end

#get_plan(sql, options = {}) ⇒ Hash

Execute EXPLAIN and return parsed JSON plan

Parameters:

  • sql (String) —

    SQL query to analyze

  • options (Hash) (defaults to: {}) —

    Override default options

Returns:

  • (Hash) —

    Parsed EXPLAIN plan JSON

Raises:



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/query_guard/explain/postgresql_adapter.rb', line 49

def get_plan(sql, options = {})
  unless can_explain?(sql)
    error_msg = "Cannot EXPLAIN this query type: #{sql.strip[0..50]}..."
    log_warn(error_msg)
    raise UnsupportedQueryError, error_msg
  end

  use_analyze = options.fetch(:use_analyze, @use_analyze)
  explain_sql = build_explain_query(sql, use_analyze)

  log_debug("Executing EXPLAIN query", use_analyze: use_analyze)
  plan_json = execute_explain(explain_sql)
  JSON.parse(plan_json)
rescue JSON::ParserError => e
  error_msg = "Failed to parse EXPLAIN output: #{e.message}"
  log_warn(error_msg)
  raise PlanParseError, error_msg
rescue Timeout::Error => e
  error_msg = "EXPLAIN query timed out after #{@timeout}s"
  log_warn(error_msg)
  raise TimeoutError, error_msg
rescue StandardError => e
  log_warn("EXPLAIN execution failed: #{e.message}")
  raise AdapterError, "EXPLAIN execution failed: #{e.message}"
end

#server_version ⇒ String

Get version of PostgreSQL server

Returns:

  • (String) —

    PostgreSQL version



99
100
101
102
103
# File 'lib/query_guard/explain/postgresql_adapter.rb', line 99

def server_version
  execute_query("SELECT version()").first.first
rescue StandardError => e
  raise ConnectionError, "Cannot connect to PostgreSQL: #{e.message}"
end