Class: QueryGuard::Migrations::PostgreSQLAdapter

Inherits:
DatabaseAdapter show all
Defined in:
lib/query_guard/migrations/postgresql_adapter.rb

Overview

PostgreSQL-specific metadata adapter

Queries PostgreSQL system catalog to estimate table sizes and determine lock risk based on row count and write activity.

Row count thresholds (from PostgreSQL best practices):

  • < 1M rows: Low lock risk, fast schema changes
  • 1M - 10M rows: Medium lock risk, monitor lock times
  • 10M - 100M rows: High lock risk, use concurrent operations
  • 100M rows: Critical risk, requires special handling

Constant Summary collapse

LOCK_THRESHOLDS =

Lock duration thresholds (milliseconds) Based on typical PostgreSQL behavior

{
  low: 1_000_000,        # < 1M rows
  medium: 10_000_000,    # 1M - 10M rows
  high: 100_000_000,     # 10M - 100M rows
  critical: Float::INFINITY  # > 100M rows
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(connection, schema: "public", cache: true) ⇒ PostgreSQLAdapter

Initialize PostgreSQL adapter

Parameters:

  • connection (Object) —

    Active database connection (responds to exec_query)

  • schema (String) (defaults to: "public") —

    Schema to query (default: public)

  • cache (Hash) (defaults to: true) —

    a customizable set of options

Options Hash (cache:):

  • Whether (Boolean) —

    to cache row count queries (default: true)



30
31
32
33
34
35
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 30

def initialize(connection, schema: "public", cache: true)
  @connection = connection
  @schema = schema
  @cache_enabled = cache
  @cache = {} if cache
end

Instance Method Details

#clear_cache ⇒ Object

Clear the row count cache

Call this if you've made schema changes and want fresh stats.



152
153
154
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 152

def clear_cache
  @cache.clear if @cache_enabled && @cache
end

#connected? ⇒ Boolean

Check if connection is healthy

Returns:

  • (Boolean) —

    True if we can query the database



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 137

def connected?
  return false if @connection.nil?

  begin
    @connection.exec_query("SELECT 1 LIMIT 1")
    true
  rescue StandardError => e
    warn "Database connection check failed: #{e.message}"
    false
  end
end

#estimate_lock_risk(table_name) ⇒ Symbol?

Estimate lock risk based on table size

Larger tables take longer to lock and rewrite, increasing risk of production impact.

Parameters:

  • table_name (String, Symbol) —

    Table name

Returns:

  • (Symbol, nil) —

    Risk level: :low, :medium, :high, :critical



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 76

def estimate_lock_risk(table_name)
  rows = estimate_table_rows(table_name)
  return nil if rows.nil?

  case rows
  when 0...LOCK_THRESHOLDS[:low]
    :low
  when LOCK_THRESHOLDS[:low]...LOCK_THRESHOLDS[:medium]
    :medium
  when LOCK_THRESHOLDS[:medium]...LOCK_THRESHOLDS[:high]
    :high
  else
    :critical
  end
end

#estimate_table_rows(table_name) ⇒ Integer?

Estimate row count using PostgreSQL statistics

Uses pg_class.reltuples for estimated row count. Falls back to COUNT(*) if stats unavailable.

Parameters:

  • table_name (String, Symbol) —

    Table name

Returns:

  • (Integer, nil) —

    Estimated rows, or nil if unavailable



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 44

def estimate_table_rows(table_name)
  return nil unless connected?
  return @cache[table_name.to_s] if @cache_enabled && @cache.key?(table_name.to_s)

  begin
    # Use PostgreSQL statistics (faster, less accurate)
    result = @connection.exec_query(
      "SELECT reltuples::bigint as estimated_rows FROM pg_class
       WHERE relname = $1 AND relnamespace = 
         (SELECT oid FROM pg_namespace WHERE nspname = $2)
       LIMIT 1",
      "GetTableRows",
      [[table_name.to_s, :string], [@schema, :string]]
    )

    rows = result.rows.dig(0, 0)&.to_i
    @cache[table_name.to_s] = rows if @cache_enabled
    rows
  rescue StandardError => e
    # Fail gracefully - log if possible, return nil
    warn "Failed to get row count for #{table_name}: #{e.message}"
    nil
  end
end

#list_tables ⇒ Array<String>

List all tables in schema

Returns:

  • (Array<String>) —

    Table names



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 116

def list_tables
  return [] unless connected?

  begin
    result = @connection.exec_query(
      "SELECT table_name FROM information_schema.tables
       WHERE table_schema = $1
       ORDER BY table_name",
      "ListTables",
      [[@schema, :string]]
    )
    result.rows.flatten
  rescue StandardError => e
    warn "Failed to list tables: #{e.message}"
    []
  end
end

#table_exists?(table_name) ⇒ Boolean

Check if table exists

Parameters:

  • table_name (String, Symbol) —

    Table name

Returns:

  • (Boolean) —

    True if table exists in schema



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/query_guard/migrations/postgresql_adapter.rb', line 96

def table_exists?(table_name)
  return false unless connected?

  begin
    result = @connection.exec_query(
      "SELECT 1 FROM information_schema.tables
       WHERE table_schema = $1 AND table_name = $2 LIMIT 1",
      "CheckTableExists",
      [[@schema, :string], [table_name.to_s, :string]]
    )
    result.rows.any?
  rescue StandardError => e
    warn "Failed to check if table exists #{table_name}: #{e.message}"
    false
  end
end