ruby-pg-extras 2.3.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +2 -1
- data/README.md +21 -3
- data/lib/ruby-pg-extras/diagnose_data.rb +228 -0
- data/lib/ruby-pg-extras/diagnose_print.rb +46 -0
- data/lib/ruby-pg-extras/index_info.rb +61 -0
- data/lib/ruby-pg-extras/index_info_print.rb +43 -0
- data/lib/ruby-pg-extras/queries/add_extensions.sql +5 -0
- data/lib/ruby-pg-extras/queries/index_scans.sql +12 -0
- data/lib/ruby-pg-extras/queries/indexes.sql +9 -0
- data/lib/ruby-pg-extras/queries/ssl_used.sql +0 -1
- data/lib/ruby-pg-extras/queries/table_index_scans.sql +7 -0
- data/lib/ruby-pg-extras/queries/tables.sql +3 -0
- data/lib/ruby-pg-extras/queries/unused_indexes.sql +1 -1
- data/lib/ruby-pg-extras/table_info.rb +77 -0
- data/lib/ruby-pg-extras/table_info_print.rb +45 -0
- data/lib/ruby-pg-extras/version.rb +1 -1
- data/lib/ruby-pg-extras.rb +53 -5
- data/ruby-pg-extras-diagnose.png +0 -0
- data/ruby-pg-extras.gemspec +1 -0
- data/spec/diagnose_data_spec.rb +65 -0
- data/spec/diagnose_print_spec.rb +37 -0
- data/spec/index_info_spec.rb +56 -0
- data/spec/smoke_spec.rb +0 -5
- data/spec/spec_helper.rb +8 -0
- data/spec/table_info_spec.rb +78 -0
- metadata +36 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 05d2e5841e02ca2a6ea167296d565a0df5e9a516c9e975589db3bd1b7856a5f7
         | 
| 4 | 
            +
              data.tar.gz: 21a2d1b015d0558f4875f5cf1862b5e63a6e1ecbface59d88439712894716d17
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 0bf1ddceb1d24b718b979abc9dc03c04b94d6500487318e32805c8885f8a244bb548dd7c9dd7f738be7de0fa73f1212493a3a22c431ace69478aed2a38901dc7
         | 
| 7 | 
            +
              data.tar.gz: 628eeea090e2839bc38d512a801f13ab7ef7413eb2c45719171dd9946800e9a98444f81a96d14102da749de85776931a616aee5e06f46a4d4f0469cca0e6cb1c
         | 
    
        data/.circleci/config.yml
    CHANGED
    
    | @@ -31,7 +31,8 @@ jobs: | |
| 31 31 | 
             
                  - checkout
         | 
| 32 32 | 
             
                  - run: gem update --system
         | 
| 33 33 | 
             
                  - run: gem install bundler
         | 
| 34 | 
            -
                  - run: bundle  | 
| 34 | 
            +
                  - run: bundle config set --local path 'vendor/bundle'
         | 
| 35 | 
            +
                  - run: bundle install
         | 
| 35 36 | 
             
                  - run: sudo apt-get update --allow-releaseinfo-change
         | 
| 36 37 | 
             
                  - run: sudo apt install postgresql-client-11
         | 
| 37 38 | 
             
                  - run: dockerize -wait tcp://postgres11:5432 -timeout 1m
         | 
    
        data/README.md
    CHANGED
    
    | @@ -26,7 +26,7 @@ In your Gemfile | |
| 26 26 | 
             
            gem "ruby-pg-extras"
         | 
| 27 27 | 
             
            ```
         | 
| 28 28 |  | 
| 29 | 
            -
             | 
| 29 | 
            +
            `calls` and `outliers` queries require [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) extension.
         | 
| 30 30 |  | 
| 31 31 | 
             
            You can check if it is enabled in your database by running:
         | 
| 32 32 |  | 
| @@ -39,6 +39,12 @@ You should see the similar line in the output: | |
| 39 39 | 
             
            | pg_stat_statements  | 1.7  | 1.7 | track execution statistics of all SQL statements executed |
         | 
| 40 40 | 
             
            ```
         | 
| 41 41 |  | 
| 42 | 
            +
            `ssl_used` requires `sslinfo` extension, and `buffercache_usage`/`buffercache_usage` queries need `pg_buffercache`. You can enable them all by running:
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ```ruby
         | 
| 45 | 
            +
            RubyPGExtras.add_extensions
         | 
| 46 | 
            +
            ```
         | 
| 47 | 
            +
             | 
| 42 48 | 
             
            ## Usage
         | 
| 43 49 |  | 
| 44 50 | 
             
            Gem expects the `ENV['DATABASE_URL']` value in the following format:
         | 
| @@ -92,6 +98,18 @@ RubyPGExtras.long_running_queries(args: { threshold: "200 milliseconds" }) | |
| 92 98 |  | 
| 93 99 | 
             
            ```
         | 
| 94 100 |  | 
| 101 | 
            +
            ## Diagnose report
         | 
| 102 | 
            +
             | 
| 103 | 
            +
            The simplest way to start using pg-extras is to execute a `diagnose` method. It runs a set of checks and prints out a report highlighting areas that may require additional investigation:
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            ```ruby
         | 
| 106 | 
            +
            RubyPGExtras.diagnose
         | 
| 107 | 
            +
            ```
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            
         | 
| 110 | 
            +
             | 
| 111 | 
            +
            Keep reading to learn about methods that `diagnose` uses under the hood.
         | 
| 112 | 
            +
             | 
| 95 113 | 
             
            ## Available methods
         | 
| 96 114 |  | 
| 97 115 | 
             
            ### `cache_hit`
         | 
| @@ -236,7 +254,7 @@ This command displays all the current locks, regardless of their type. | |
| 236 254 |  | 
| 237 255 | 
             
            RubyPGExtras.outliers(args: { limit: 20 })
         | 
| 238 256 |  | 
| 239 | 
            -
                                | 
| 257 | 
            +
                               query                 |    exec_time     | prop_exec_time |   ncalls    | sync_io_time
         | 
| 240 258 | 
             
            -----------------------------------------+------------------+----------------+-------------+--------------
         | 
| 241 259 | 
             
             SELECT * FROM archivable_usage_events.. | 154:39:26.431466 | 72.2%          | 34,211,877  | 00:00:00
         | 
| 242 260 | 
             
             COPY public.archivable_usage_events (.. | 50:38:33.198418  | 23.6%          | 13          | 13:34:21.00108
         | 
| @@ -387,7 +405,7 @@ This command displays the total size of each table and materialized view in the | |
| 387 405 |  | 
| 388 406 | 
             
            ```ruby
         | 
| 389 407 |  | 
| 390 | 
            -
            RubyPGExtras.unused_indexes(args: {  | 
| 408 | 
            +
            RubyPGExtras.unused_indexes(args: { max_scans: 20 })
         | 
| 391 409 |  | 
| 392 410 | 
             
                      table      |                       index                | index_size | index_scans
         | 
| 393 411 | 
             
            ---------------------+--------------------------------------------+------------+-------------
         | 
| @@ -0,0 +1,228 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'filesize'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module RubyPGExtras
         | 
| 6 | 
            +
              class DiagnoseData
         | 
| 7 | 
            +
                PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED = "0.985"
         | 
| 8 | 
            +
                PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED = "0.985"
         | 
| 9 | 
            +
                PG_EXTRAS_UNUSED_INDEXES_MAX_SCANS = 20
         | 
| 10 | 
            +
                PG_EXTRAS_UNUSED_INDEXES_MIN_SIZE_BYTES = Filesize.from("1 MB").to_i # 1000000 bytes
         | 
| 11 | 
            +
                PG_EXTRAS_NULL_INDEXES_MIN_SIZE_MB = 1 # 1 MB
         | 
| 12 | 
            +
                PG_EXTRAS_NULL_MIN_NULL_FRAC_PERCENT = 50 # 50%
         | 
| 13 | 
            +
                PG_EXTRAS_BLOAT_MIN_VALUE = 10
         | 
| 14 | 
            +
                PG_EXTRAS_OUTLIERS_MIN_EXEC_RATIO = 33 # 33%
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def self.call
         | 
| 17 | 
            +
                  new.call
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def call
         | 
| 21 | 
            +
                  [
         | 
| 22 | 
            +
                    :table_cache_hit,
         | 
| 23 | 
            +
                    :index_cache_hit,
         | 
| 24 | 
            +
                    :unused_indexes,
         | 
| 25 | 
            +
                    :null_indexes,
         | 
| 26 | 
            +
                    :bloat,
         | 
| 27 | 
            +
                    :duplicate_indexes
         | 
| 28 | 
            +
                  ].yield_self do |checks|
         | 
| 29 | 
            +
                    extensions_data = query_module.extensions(in_format: :hash)
         | 
| 30 | 
            +
                    pg_stats_enabled = extensions_data.find do |el|
         | 
| 31 | 
            +
                      el.fetch("name") == "pg_stat_statements"
         | 
| 32 | 
            +
                    end.fetch("installed_version", false)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    ssl_info_enabled = extensions_data.find do |el|
         | 
| 35 | 
            +
                      el.fetch("name") == "sslinfo"
         | 
| 36 | 
            +
                    end.fetch("installed_version", false)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    if pg_stats_enabled
         | 
| 39 | 
            +
                      checks = checks.concat([:outliers])
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    if ssl_info_enabled
         | 
| 43 | 
            +
                      checks = checks.concat([:ssl_used])
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    checks
         | 
| 47 | 
            +
                  end.map do |check|
         | 
| 48 | 
            +
                    send(check).merge(check_name: check)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                private
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def query_module
         | 
| 55 | 
            +
                  RubyPGExtras
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def table_cache_hit
         | 
| 59 | 
            +
                  min_expected = ENV.fetch(
         | 
| 60 | 
            +
                    "PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED",
         | 
| 61 | 
            +
                    PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED
         | 
| 62 | 
            +
                  ).to_f
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  table_cache_hit_ratio = query_module.cache_hit(in_format: :hash)[1].fetch("ratio").to_f.round(6)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  if table_cache_hit_ratio > min_expected
         | 
| 67 | 
            +
                    {
         | 
| 68 | 
            +
                      ok: true,
         | 
| 69 | 
            +
                      message: "Table cache hit ratio is correct: #{table_cache_hit_ratio}."
         | 
| 70 | 
            +
                    }
         | 
| 71 | 
            +
                  else
         | 
| 72 | 
            +
                    {
         | 
| 73 | 
            +
                      ok: false,
         | 
| 74 | 
            +
                      message: "Table hit ratio is too low: #{table_cache_hit_ratio}."
         | 
| 75 | 
            +
                    }
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def index_cache_hit
         | 
| 80 | 
            +
                  min_expected = ENV.fetch(
         | 
| 81 | 
            +
                    "PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED",
         | 
| 82 | 
            +
                    PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED
         | 
| 83 | 
            +
                  ).to_f
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  index_cache_hit_ratio = query_module.cache_hit(in_format: :hash)[0].fetch("ratio").to_f.round(6)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  if index_cache_hit_ratio > min_expected
         | 
| 88 | 
            +
                    {
         | 
| 89 | 
            +
                      ok: true,
         | 
| 90 | 
            +
                      message: "Index hit ratio is correct: #{index_cache_hit_ratio}."
         | 
| 91 | 
            +
                    }
         | 
| 92 | 
            +
                  else
         | 
| 93 | 
            +
                    {
         | 
| 94 | 
            +
                      ok: false,
         | 
| 95 | 
            +
                      message: "Index hit ratio is too low: #{index_cache_hit_ratio}."
         | 
| 96 | 
            +
                    }
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def ssl_used
         | 
| 101 | 
            +
                  ssl_connection = query_module.ssl_used(in_format: :hash)[0].fetch("ssl_is_used")
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  if ssl_connection
         | 
| 104 | 
            +
                    {
         | 
| 105 | 
            +
                      ok: true,
         | 
| 106 | 
            +
                      message: "Database client is using a secure SSL connection."
         | 
| 107 | 
            +
                    }
         | 
| 108 | 
            +
                  else
         | 
| 109 | 
            +
                    {
         | 
| 110 | 
            +
                      ok: false,
         | 
| 111 | 
            +
                      message: "Database client is using an unencrypted connection."
         | 
| 112 | 
            +
                    }
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                def unused_indexes
         | 
| 117 | 
            +
                  indexes = query_module.unused_indexes(
         | 
| 118 | 
            +
                    in_format: :hash,
         | 
| 119 | 
            +
                    args: { min_scans: PG_EXTRAS_UNUSED_INDEXES_MAX_SCANS }
         | 
| 120 | 
            +
                  ).select do |i|
         | 
| 121 | 
            +
                    Filesize.from(i.fetch("index_size")).to_i >= PG_EXTRAS_UNUSED_INDEXES_MIN_SIZE_BYTES
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  if indexes.count == 0
         | 
| 125 | 
            +
                    {
         | 
| 126 | 
            +
                      ok: true,
         | 
| 127 | 
            +
                      message: "No unused indexes detected."
         | 
| 128 | 
            +
                    }
         | 
| 129 | 
            +
                  else
         | 
| 130 | 
            +
                    print_indexes = indexes.map do |i|
         | 
| 131 | 
            +
                      "'#{i.fetch('index')}' on '#{i.fetch('table')}' size #{i.fetch('index_size')}"
         | 
| 132 | 
            +
                    end.join(",\n")
         | 
| 133 | 
            +
                    {
         | 
| 134 | 
            +
                      ok: false,
         | 
| 135 | 
            +
                      message: "Unused indexes detected:\n#{print_indexes}"
         | 
| 136 | 
            +
                    }
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def null_indexes
         | 
| 141 | 
            +
                  indexes = query_module.null_indexes(
         | 
| 142 | 
            +
                    in_format: :hash,
         | 
| 143 | 
            +
                    args: { min_relation_size_mb: PG_EXTRAS_NULL_INDEXES_MIN_SIZE_MB }
         | 
| 144 | 
            +
                  ).select do |i|
         | 
| 145 | 
            +
                    i.fetch("null_frac").gsub("%", "").to_f >= PG_EXTRAS_NULL_MIN_NULL_FRAC_PERCENT
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  if indexes.count == 0
         | 
| 149 | 
            +
                    {
         | 
| 150 | 
            +
                      ok: true,
         | 
| 151 | 
            +
                      message: "No null indexes detected."
         | 
| 152 | 
            +
                    }
         | 
| 153 | 
            +
                  else
         | 
| 154 | 
            +
                    print_indexes = indexes.map do |i|
         | 
| 155 | 
            +
                      "'#{i.fetch('index')}' size #{i.fetch('index_size')} null values fraction #{i.fetch('null_frac')}"
         | 
| 156 | 
            +
                    end.join(",\n")
         | 
| 157 | 
            +
                    {
         | 
| 158 | 
            +
                      ok: false,
         | 
| 159 | 
            +
                      message: "Null indexes detected:\n#{print_indexes}"
         | 
| 160 | 
            +
                    }
         | 
| 161 | 
            +
                  end
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def bloat
         | 
| 165 | 
            +
                  bloat_data = query_module.bloat(in_format: :hash).select do |b|
         | 
| 166 | 
            +
                    b.fetch("bloat").to_f >= PG_EXTRAS_BLOAT_MIN_VALUE
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  if bloat_data.count == 0
         | 
| 170 | 
            +
                    {
         | 
| 171 | 
            +
                      ok: true,
         | 
| 172 | 
            +
                      message: "No bloat detected."
         | 
| 173 | 
            +
                    }
         | 
| 174 | 
            +
                  else
         | 
| 175 | 
            +
                    print_bloat = bloat_data.map do |b|
         | 
| 176 | 
            +
                      "'#{b.fetch('object_name')}' bloat #{b.fetch('bloat')} waste #{b.fetch('waste')}"
         | 
| 177 | 
            +
                    end.join(",\n")
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                    {
         | 
| 180 | 
            +
                      ok: false,
         | 
| 181 | 
            +
                      message: "Bloat detected:\n#{print_bloat}"
         | 
| 182 | 
            +
                    }
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                def duplicate_indexes
         | 
| 187 | 
            +
                  indexes = query_module.duplicate_indexes(in_format: :hash)
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  if indexes.count == 0
         | 
| 190 | 
            +
                    {
         | 
| 191 | 
            +
                      ok: true,
         | 
| 192 | 
            +
                      message: "No duplicate indexes detected."
         | 
| 193 | 
            +
                    }
         | 
| 194 | 
            +
                  else
         | 
| 195 | 
            +
                    print_indexes = indexes.map do |i|
         | 
| 196 | 
            +
                      "'#{i.fetch('idx1')}' of size #{i.fetch('size')} is identical to '#{i.fetch('idx2')}'"
         | 
| 197 | 
            +
                    end.join(",\n")
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                    {
         | 
| 200 | 
            +
                      ok: false,
         | 
| 201 | 
            +
                      message: "Duplicate indexes detected:\n#{print_indexes}"
         | 
| 202 | 
            +
                    }
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                def outliers
         | 
| 207 | 
            +
                  queries = query_module.outliers(in_format: :hash).select do |q|
         | 
| 208 | 
            +
                    q.fetch("prop_exec_time").gsub("%", "").to_f >= PG_EXTRAS_OUTLIERS_MIN_EXEC_RATIO
         | 
| 209 | 
            +
                  end
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                  if queries.count == 0
         | 
| 212 | 
            +
                    {
         | 
| 213 | 
            +
                      ok: true,
         | 
| 214 | 
            +
                      message: "No queries using significant execution ratio detected."
         | 
| 215 | 
            +
                    }
         | 
| 216 | 
            +
                  else
         | 
| 217 | 
            +
                    print_queries = queries.map do |q|
         | 
| 218 | 
            +
                      "'#{q.fetch('query').slice(0, 30)}...' called #{q.fetch('ncalls')} times, using #{q.fetch('prop_exec_time')} of total exec time."
         | 
| 219 | 
            +
                    end.join(",\n")
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                    {
         | 
| 222 | 
            +
                      ok: false,
         | 
| 223 | 
            +
                      message: "Queries using significant execution ratio detected:\n#{print_queries}"
         | 
| 224 | 
            +
                    }
         | 
| 225 | 
            +
                  end
         | 
| 226 | 
            +
                end
         | 
| 227 | 
            +
              end
         | 
| 228 | 
            +
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'terminal-table'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module RubyPGExtras
         | 
| 6 | 
            +
              class DiagnosePrint
         | 
| 7 | 
            +
                def self.call(data)
         | 
| 8 | 
            +
                  new.call(data)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def call(data)
         | 
| 12 | 
            +
                  rows = data.sort do |el|
         | 
| 13 | 
            +
                    el.fetch(:ok) ? 1 : -1
         | 
| 14 | 
            +
                  end.map do |el|
         | 
| 15 | 
            +
                    symbol = el.fetch(:ok) ? "√" : "x"
         | 
| 16 | 
            +
                    color = el.fetch(:ok) ? :green : :red
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    [
         | 
| 19 | 
            +
                      colorize("[#{symbol}] - #{el.fetch(:check_name)}", color),
         | 
| 20 | 
            +
                      colorize(el.fetch(:message), color)
         | 
| 21 | 
            +
                    ]
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  puts Terminal::Table.new(
         | 
| 25 | 
            +
                    title: title,
         | 
| 26 | 
            +
                    rows: rows
         | 
| 27 | 
            +
                  )
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                private
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def title
         | 
| 33 | 
            +
                  "ruby-pg-extras - diagnose report"
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def colorize(string, color)
         | 
| 37 | 
            +
                  if color == :red
         | 
| 38 | 
            +
                    "\e[0;31;49m#{string}\e[0m"
         | 
| 39 | 
            +
                  elsif color == :green
         | 
| 40 | 
            +
                    "\e[0;32;49m#{string}\e[0m"
         | 
| 41 | 
            +
                  else
         | 
| 42 | 
            +
                    raise "Unsupported color: #{color}"
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            module RubyPGExtras
         | 
| 2 | 
            +
              class IndexInfo
         | 
| 3 | 
            +
                def self.call(table_name = nil)
         | 
| 4 | 
            +
                  new.call(table_name)
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def call(table_name = nil)
         | 
| 8 | 
            +
                  indexes_data.select do |index_data|
         | 
| 9 | 
            +
                    if table_name == nil
         | 
| 10 | 
            +
                      true
         | 
| 11 | 
            +
                    else
         | 
| 12 | 
            +
                      index_data.fetch("tablename") == table_name
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end.sort_by do |index_data|
         | 
| 15 | 
            +
                    index_data.fetch("tablename")
         | 
| 16 | 
            +
                  end.map do |index_data|
         | 
| 17 | 
            +
                    index_name = index_data.fetch("indexname")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    {
         | 
| 20 | 
            +
                      index_name: index_name,
         | 
| 21 | 
            +
                      table_name: index_data.fetch("tablename"),
         | 
| 22 | 
            +
                      columns: index_data.fetch("columns").split(',').map(&:strip),
         | 
| 23 | 
            +
                      index_size: index_size_data.find do |el|
         | 
| 24 | 
            +
                        el.fetch("name") == index_name
         | 
| 25 | 
            +
                      end.fetch("size", "N/A"),
         | 
| 26 | 
            +
                      index_scans:  index_scans_data.find do |el|
         | 
| 27 | 
            +
                        el.fetch("index") == index_name
         | 
| 28 | 
            +
                      end.fetch("index_scans", "N/A"),
         | 
| 29 | 
            +
                      null_frac: null_indexes_data.find do |el|
         | 
| 30 | 
            +
                        el.fetch("index") == index_name
         | 
| 31 | 
            +
                      end&.fetch("null_frac", "N/A") || "0.00%"
         | 
| 32 | 
            +
                    }
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def index_size_data
         | 
| 37 | 
            +
                  @_index_size_data ||= query_module.index_size(in_format: :hash)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def null_indexes_data
         | 
| 41 | 
            +
                  @_null_indexes_data ||= query_module.null_indexes(
         | 
| 42 | 
            +
                    in_format: :hash,
         | 
| 43 | 
            +
                    args: { min_relation_size_mb: 0 }
         | 
| 44 | 
            +
                  )
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def index_scans_data
         | 
| 48 | 
            +
                  @_index_scans_data ||= query_module.index_scans(in_format: :hash)
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def indexes_data
         | 
| 52 | 
            +
                  @_indexes_data ||= query_module.indexes(in_format: :hash)
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                private
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def query_module
         | 
| 58 | 
            +
                  RubyPGExtras
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
            end
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'terminal-table'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module RubyPGExtras
         | 
| 6 | 
            +
              class IndexInfoPrint
         | 
| 7 | 
            +
                def self.call(data)
         | 
| 8 | 
            +
                  new.call(data)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def call(data)
         | 
| 12 | 
            +
                  rows = data.map do |el|
         | 
| 13 | 
            +
                    [
         | 
| 14 | 
            +
                      el.fetch(:index_name),
         | 
| 15 | 
            +
                      el.fetch(:table_name),
         | 
| 16 | 
            +
                      el.fetch(:columns).join(', '),
         | 
| 17 | 
            +
                      el.fetch(:index_size),
         | 
| 18 | 
            +
                      el.fetch(:index_scans),
         | 
| 19 | 
            +
                      el.fetch(:null_frac)
         | 
| 20 | 
            +
                    ]
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  puts Terminal::Table.new(
         | 
| 24 | 
            +
                    headings: [
         | 
| 25 | 
            +
                      "Index name",
         | 
| 26 | 
            +
                      "Table name",
         | 
| 27 | 
            +
                      "Columns",
         | 
| 28 | 
            +
                      "Index size",
         | 
| 29 | 
            +
                      "Index scans",
         | 
| 30 | 
            +
                      "Null frac"
         | 
| 31 | 
            +
                    ],
         | 
| 32 | 
            +
                    title: title,
         | 
| 33 | 
            +
                    rows: rows
         | 
| 34 | 
            +
                  )
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                private
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def title
         | 
| 40 | 
            +
                  "Index info"
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
| @@ -0,0 +1,12 @@ | |
| 1 | 
            +
            /* Number of scans performed on indexes */
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            SELECT
         | 
| 4 | 
            +
              schemaname,
         | 
| 5 | 
            +
              relname AS table,
         | 
| 6 | 
            +
              indexrelname AS index,
         | 
| 7 | 
            +
              pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
         | 
| 8 | 
            +
              idx_scan as index_scans
         | 
| 9 | 
            +
            FROM pg_stat_user_indexes ui
         | 
| 10 | 
            +
            JOIN pg_index i ON ui.indexrelid = i.indexrelid
         | 
| 11 | 
            +
            ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
         | 
| 12 | 
            +
            pg_relation_size(i.indexrelid) DESC;
         | 
| @@ -11,6 +11,6 @@ SELECT | |
| 11 11 | 
             
              idx_scan as index_scans
         | 
| 12 12 | 
             
            FROM pg_stat_user_indexes ui
         | 
| 13 13 | 
             
            JOIN pg_index i ON ui.indexrelid = i.indexrelid
         | 
| 14 | 
            -
            WHERE NOT indisunique AND idx_scan < %{ | 
| 14 | 
            +
            WHERE NOT indisunique AND idx_scan < %{max_scans} AND pg_relation_size(relid) > 5 * 8192
         | 
| 15 15 | 
             
            ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
         | 
| 16 16 | 
             
            pg_relation_size(i.indexrelid) DESC;
         | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            module RubyPGExtras
         | 
| 2 | 
            +
              class TableInfo
         | 
| 3 | 
            +
                def self.call(table_name = nil)
         | 
| 4 | 
            +
                  new.call(table_name)
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def call(table_name)
         | 
| 8 | 
            +
                  tables_data.select do |table_data|
         | 
| 9 | 
            +
                    if table_name == nil
         | 
| 10 | 
            +
                      true
         | 
| 11 | 
            +
                    else
         | 
| 12 | 
            +
                      table_data.fetch("tablename") == table_name
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end.sort_by do |table_data|
         | 
| 15 | 
            +
                    table_data.fetch("tablename")
         | 
| 16 | 
            +
                  end.map do |table_data|
         | 
| 17 | 
            +
                    table_name = table_data.fetch("tablename")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    {
         | 
| 20 | 
            +
                      table_name: table_name,
         | 
| 21 | 
            +
                      table_size: table_size_data.find do |el|
         | 
| 22 | 
            +
                        el.fetch("name") == table_name
         | 
| 23 | 
            +
                      end.fetch("size", "N/A"),
         | 
| 24 | 
            +
                      table_cache_hit: table_cache_hit_data.find do |el|
         | 
| 25 | 
            +
                        el.fetch("name") == table_name
         | 
| 26 | 
            +
                      end.fetch("ratio", "N/A"),
         | 
| 27 | 
            +
                      indexes_cache_hit: index_cache_hit_data.find do |el|
         | 
| 28 | 
            +
                        el.fetch("name") == table_name
         | 
| 29 | 
            +
                      end.fetch("ratio", "N/A"),
         | 
| 30 | 
            +
                      estimated_rows: records_rank_data.find do |el|
         | 
| 31 | 
            +
                        el.fetch("name") == table_name
         | 
| 32 | 
            +
                      end.fetch("estimated_count", "N/A"),
         | 
| 33 | 
            +
                      sequential_scans: seq_scans_data.find do |el|
         | 
| 34 | 
            +
                        el.fetch("name") == table_name
         | 
| 35 | 
            +
                      end.fetch("count", "N/A"),
         | 
| 36 | 
            +
                      indexes_scans: table_index_scans_data.find do |el|
         | 
| 37 | 
            +
                        el.fetch("name") == table_name
         | 
| 38 | 
            +
                      end.fetch("count", "N/A")
         | 
| 39 | 
            +
                    }
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                private
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def index_cache_hit_data
         | 
| 46 | 
            +
                  @_index_cache_hit_data ||= query_module.index_cache_hit(in_format: :hash)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def table_cache_hit_data
         | 
| 50 | 
            +
                  @_table_cache_hit_data ||= query_module.table_cache_hit(in_format: :hash)
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def table_size_data
         | 
| 54 | 
            +
                  @_table_size_data ||= query_module.table_size(in_format: :hash)
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def records_rank_data
         | 
| 58 | 
            +
                  @_records_rank_data ||= query_module.records_rank(in_format: :hash)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def tables_data
         | 
| 62 | 
            +
                  @_tables_data ||= query_module.tables(in_format: :hash)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def seq_scans_data
         | 
| 66 | 
            +
                  @_seq_scans_data ||= query_module.seq_scans(in_format: :hash)
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def table_index_scans_data
         | 
| 70 | 
            +
                  @_table_index_scans_data ||= query_module.table_index_scans(in_format: :hash)
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def query_module
         | 
| 74 | 
            +
                  RubyPGExtras
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'terminal-table'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module RubyPGExtras
         | 
| 6 | 
            +
              class TableInfoPrint
         | 
| 7 | 
            +
                def self.call(data)
         | 
| 8 | 
            +
                  new.call(data)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def call(data)
         | 
| 12 | 
            +
                  rows = data.map do |el|
         | 
| 13 | 
            +
                    [
         | 
| 14 | 
            +
                      el.fetch(:table_name),
         | 
| 15 | 
            +
                      el.fetch(:table_size),
         | 
| 16 | 
            +
                      el.fetch(:table_cache_hit),
         | 
| 17 | 
            +
                      el.fetch(:indexes_cache_hit),
         | 
| 18 | 
            +
                      el.fetch(:estimated_rows),
         | 
| 19 | 
            +
                      el.fetch(:sequential_scans),
         | 
| 20 | 
            +
                      el.fetch(:indexes_scans)
         | 
| 21 | 
            +
                    ]
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  puts Terminal::Table.new(
         | 
| 25 | 
            +
                    headings: [
         | 
| 26 | 
            +
                      "Table name",
         | 
| 27 | 
            +
                      "Table size",
         | 
| 28 | 
            +
                      "Table cache hit",
         | 
| 29 | 
            +
                      "Indexes cache hit",
         | 
| 30 | 
            +
                      "Estimated rows",
         | 
| 31 | 
            +
                      "Sequentail scans",
         | 
| 32 | 
            +
                      "Indexes scans"
         | 
| 33 | 
            +
                    ],
         | 
| 34 | 
            +
                    title: title,
         | 
| 35 | 
            +
                    rows: rows
         | 
| 36 | 
            +
                  )
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def title
         | 
| 42 | 
            +
                  "Table info"
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
            end
         | 
    
        data/lib/ruby-pg-extras.rb
    CHANGED
    
    | @@ -3,17 +3,23 @@ | |
| 3 3 | 
             
            require 'terminal-table'
         | 
| 4 4 | 
             
            require 'uri'
         | 
| 5 5 | 
             
            require 'pg'
         | 
| 6 | 
            +
            require 'ruby-pg-extras/diagnose_data'
         | 
| 7 | 
            +
            require 'ruby-pg-extras/diagnose_print'
         | 
| 8 | 
            +
            require 'ruby-pg-extras/index_info'
         | 
| 9 | 
            +
            require 'ruby-pg-extras/index_info_print'
         | 
| 10 | 
            +
            require 'ruby-pg-extras/table_info'
         | 
| 11 | 
            +
            require 'ruby-pg-extras/table_info_print'
         | 
| 6 12 |  | 
| 7 13 | 
             
            module RubyPGExtras
         | 
| 8 14 | 
             
              @@database_url = nil
         | 
| 9 15 | 
             
              NEW_PG_STAT_STATEMENTS = "1.8"
         | 
| 10 16 |  | 
| 11 17 | 
             
              QUERIES = %i(
         | 
| 12 | 
            -
                bloat blocking cache_hit db_settings
         | 
| 13 | 
            -
                calls extensions table_cache_hit index_cache_hit
         | 
| 14 | 
            -
                index_size index_usage null_indexes locks all_locks
         | 
| 18 | 
            +
                add_extensions bloat blocking cache_hit db_settings
         | 
| 19 | 
            +
                calls extensions table_cache_hit tables index_cache_hit
         | 
| 20 | 
            +
                indexes index_size index_usage index_scans null_indexes locks all_locks
         | 
| 15 21 | 
             
                long_running_queries mandelbrot outliers
         | 
| 16 | 
            -
                records_rank seq_scans table_indexes_size
         | 
| 22 | 
            +
                records_rank seq_scans table_index_scans table_indexes_size
         | 
| 17 23 | 
             
                table_size total_index_size total_table_size
         | 
| 18 24 | 
             
                unused_indexes duplicate_indexes vacuum_stats kill_all
         | 
| 19 25 | 
             
                pg_stat_statements_reset buffercache_stats
         | 
| @@ -28,7 +34,7 @@ module RubyPGExtras | |
| 28 34 | 
             
                outliers_legacy: { limit: 10 },
         | 
| 29 35 | 
             
                buffercache_stats: { limit: 10 },
         | 
| 30 36 | 
             
                buffercache_usage: { limit: 20 },
         | 
| 31 | 
            -
                unused_indexes: {  | 
| 37 | 
            +
                unused_indexes: { max_scans: 50 },
         | 
| 32 38 | 
             
                null_indexes: { min_relation_size_mb: 10 }
         | 
| 33 39 | 
             
              })
         | 
| 34 40 |  | 
| @@ -67,6 +73,48 @@ module RubyPGExtras | |
| 67 73 | 
             
                )
         | 
| 68 74 | 
             
              end
         | 
| 69 75 |  | 
| 76 | 
            +
              def self.diagnose(in_format: :display_table)
         | 
| 77 | 
            +
                data = RubyPGExtras::DiagnoseData.call
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                if in_format == :display_table
         | 
| 80 | 
            +
                  RubyPGExtras::DiagnosePrint.call(data)
         | 
| 81 | 
            +
                elsif in_format == :hash
         | 
| 82 | 
            +
                  data
         | 
| 83 | 
            +
                elsif in_format == :array
         | 
| 84 | 
            +
                  data.map(&:values)
         | 
| 85 | 
            +
                else
         | 
| 86 | 
            +
                  raise "Invalid 'in_format' argument!"
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
              def self.index_info(args: {}, in_format: :display_table)
         | 
| 91 | 
            +
                data = RubyPGExtras::IndexInfo.call(args[:table_name])
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                if in_format == :display_table
         | 
| 94 | 
            +
                  RubyPGExtras::IndexInfoPrint.call(data)
         | 
| 95 | 
            +
                elsif in_format == :hash
         | 
| 96 | 
            +
                  data
         | 
| 97 | 
            +
                elsif in_format == :array
         | 
| 98 | 
            +
                  data.map(&:values)
         | 
| 99 | 
            +
                else
         | 
| 100 | 
            +
                  raise "Invalid 'in_format' argument!"
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
              def self.table_info(args: {}, in_format: :display_table)
         | 
| 105 | 
            +
                data = RubyPGExtras::TableInfo.call(args[:table_name])
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                if in_format == :display_table
         | 
| 108 | 
            +
                  RubyPGExtras::TableInfoPrint.call(data)
         | 
| 109 | 
            +
                elsif in_format == :hash
         | 
| 110 | 
            +
                  data
         | 
| 111 | 
            +
                elsif in_format == :array
         | 
| 112 | 
            +
                  data.map(&:values)
         | 
| 113 | 
            +
                else
         | 
| 114 | 
            +
                  raise "Invalid 'in_format' argument!"
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
             | 
| 70 118 | 
             
              def self.display_result(result, title:, in_format:)
         | 
| 71 119 | 
             
                case in_format
         | 
| 72 120 | 
             
                when :array
         | 
| Binary file | 
    
        data/ruby-pg-extras.gemspec
    CHANGED
    
    | @@ -16,6 +16,7 @@ Gem::Specification.new do |gem| | |
| 16 16 | 
             
              gem.require_paths = ["lib"]
         | 
| 17 17 | 
             
              gem.license       = "MIT"
         | 
| 18 18 | 
             
              gem.add_dependency "pg"
         | 
| 19 | 
            +
              gem.add_dependency "filesize"
         | 
| 19 20 | 
             
              gem.add_dependency "terminal-table"
         | 
| 20 21 | 
             
              gem.add_development_dependency "rake"
         | 
| 21 22 | 
             
              gem.add_development_dependency "rspec"
         | 
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe RubyPGExtras::DiagnoseData do
         | 
| 6 | 
            +
              subject(:result) do
         | 
| 7 | 
            +
                RubyPGExtras::DiagnoseData.call
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              describe "call" do
         | 
| 11 | 
            +
                context "stubbed cases" do
         | 
| 12 | 
            +
                  before do
         | 
| 13 | 
            +
                    expect(RubyPGExtras).to receive(:unused_indexes) {
         | 
| 14 | 
            +
                      [
         | 
| 15 | 
            +
                        { "table" => "public.plans", "index" => "index_plans_on_payer_id", "index_size" => "16 MB", "index_scans" => 0 },
         | 
| 16 | 
            +
                        { "table" => "public.feedbacks", "index" => "index_feedbacks_on_target_id", "index_size" => "80 kB", "index_scans" => 1 },
         | 
| 17 | 
            +
                        { "table" => "public.channels", "index" => "index_channels_on_slack_id", "index_size" => "56 MB", "index_scans" => 7}
         | 
| 18 | 
            +
                      ]
         | 
| 19 | 
            +
                    }
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    expect(RubyPGExtras).to receive(:null_indexes) {
         | 
| 22 | 
            +
                      [
         | 
| 23 | 
            +
                        { "oid" => 123, "index" => "index_plans_on_payer_id", "index_size" => "16 MB", "unique" => true, "null_frac" => "00.00%", "expected_saving" => "0 kb" },
         | 
| 24 | 
            +
                        { "oid" => 321, "index" => "index_feedbacks_on_target_id", "index_size" => "80 kB", "unique" => true, "null_frac" => "97.00%", "expected_saving" => "77 kb" },
         | 
| 25 | 
            +
                        { "oid" => 231, "index" => "index_channels_on_slack_id", "index_size" => "56 MB", "unique" => true, "null_frac" => "49.99%", "expected_saving" => "28 MB" }
         | 
| 26 | 
            +
                      ]
         | 
| 27 | 
            +
                    }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    expect(RubyPGExtras).to receive(:bloat) {
         | 
| 30 | 
            +
                      [
         | 
| 31 | 
            +
                        { "type" => "table", "schemaname" => "public", "object_name" => "bloated_table_1", "bloat" => 8, "waste" => "0 kb" },
         | 
| 32 | 
            +
                        { "type" => "table", "schemaname" => "public", "object_name" => "bloated_table_2", "bloat" => 8, "waste" => "77 kb" },
         | 
| 33 | 
            +
                        { "type" => "schemaname", "public" => "index_channels_on_slack_id", "object_name" => "bloated_index", "bloat" => 11, "waste" => "28 MB" }
         | 
| 34 | 
            +
                      ]
         | 
| 35 | 
            +
                    }
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    expect(RubyPGExtras).to receive(:duplicate_indexes) {
         | 
| 38 | 
            +
                      [
         | 
| 39 | 
            +
                        { "size" => "128 kb", "idx1" => "users_pkey", "idx2" => "index_users_id" }
         | 
| 40 | 
            +
                      ]
         | 
| 41 | 
            +
                    }
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    expect(RubyPGExtras).to receive(:outliers) {
         | 
| 44 | 
            +
                      [
         | 
| 45 | 
            +
                        { "query" => "SELECT * FROM users WHERE users.age > 20 AND users.height > 160", "exec_time" => "154:39:26.431466", "prop_exec_time" => "72.2%", "ncalls" => "34,211,877", "sync_io_time" => "00:34:19.784318" }
         | 
| 46 | 
            +
                      ]
         | 
| 47 | 
            +
                    }
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  it "works" do
         | 
| 51 | 
            +
                    expect {
         | 
| 52 | 
            +
                      RubyPGExtras::DiagnosePrint.call(result)
         | 
| 53 | 
            +
                    }.not_to raise_error
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                context "real database data" do
         | 
| 58 | 
            +
                  it "works" do
         | 
| 59 | 
            +
                    expect {
         | 
| 60 | 
            +
                      RubyPGExtras::DiagnosePrint.call(result)
         | 
| 61 | 
            +
                    }.not_to raise_error
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe RubyPGExtras::DiagnosePrint do
         | 
| 6 | 
            +
              subject(:print_result) do
         | 
| 7 | 
            +
                RubyPGExtras::DiagnosePrint.call(data)
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              let(:data) do
         | 
| 11 | 
            +
                [
         | 
| 12 | 
            +
                  {
         | 
| 13 | 
            +
                    :check_name => :table_cache_hit,
         | 
| 14 | 
            +
                    :ok => false,
         | 
| 15 | 
            +
                    :message => "Table hit ratio too low: 0.906977."
         | 
| 16 | 
            +
                  },
         | 
| 17 | 
            +
                  {
         | 
| 18 | 
            +
                    :check_name => :index_cache_hit,
         | 
| 19 | 
            +
                    :ok => false,
         | 
| 20 | 
            +
                    :message => "Index hit ratio is too low: 0.818182."
         | 
| 21 | 
            +
                  },
         | 
| 22 | 
            +
                  {
         | 
| 23 | 
            +
                    :check_name => :ssl_used,
         | 
| 24 | 
            +
                    :ok => true,
         | 
| 25 | 
            +
                    :message => "Database client is using a secure SSL connection."
         | 
| 26 | 
            +
                  }
         | 
| 27 | 
            +
                ]
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              describe "call" do
         | 
| 31 | 
            +
                it "works" do
         | 
| 32 | 
            +
                  expect {
         | 
| 33 | 
            +
                    print_result
         | 
| 34 | 
            +
                  }.not_to raise_error
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,56 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe RubyPGExtras::IndexInfo do
         | 
| 6 | 
            +
              subject(:result) do
         | 
| 7 | 
            +
                RubyPGExtras::IndexInfo.call
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              describe "call" do
         | 
| 11 | 
            +
                context "stubbed cases" do
         | 
| 12 | 
            +
                  before do
         | 
| 13 | 
            +
                    expect(RubyPGExtras).to receive(:indexes) {
         | 
| 14 | 
            +
                      [
         | 
| 15 | 
            +
                        { "schemaname" => "public", "indexname" => "index_users_on_api_auth_token", "tablename" => "users", "columns" => "api_auth_token, column2" },
         | 
| 16 | 
            +
                        {"schemaname" => "public", "indexname" => "index_teams_on_slack_id", "tablename" => "teams", "columns" => "slack_id" },
         | 
| 17 | 
            +
                      ]
         | 
| 18 | 
            +
                    }
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    expect(RubyPGExtras).to receive(:index_size) {
         | 
| 21 | 
            +
                      [
         | 
| 22 | 
            +
                        { "name" => "index_users_on_api_auth_token", "size" => "1744 kB" },
         | 
| 23 | 
            +
                        {"name" => "index_teams_on_slack_id", "size" => "500 kB"},
         | 
| 24 | 
            +
                      ]
         | 
| 25 | 
            +
                    }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    expect(RubyPGExtras).to receive(:null_indexes) {
         | 
| 28 | 
            +
                      [
         | 
| 29 | 
            +
                        { "oid" => 16803, "index" => "index_users_on_api_auth_token", "index_size" => "1744 kB", "unique"=>true, "indexed_column" => "api_auth_token", "null_frac" => "25.00%", "expected_saving" => "300 kB" }
         | 
| 30 | 
            +
                      ]
         | 
| 31 | 
            +
                    }
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    expect(RubyPGExtras).to receive(:index_scans) {
         | 
| 34 | 
            +
                      [
         | 
| 35 | 
            +
                        { "schemaname" => "public", "table" => "users", "index" => "index_users_on_api_auth_token", "index_size" => "1744 kB", "index_scans"=> 0 },
         | 
| 36 | 
            +
                        { "schemaname" => "public", "table" => "teams", "index" => "index_teams_on_slack_id", "index_size" => "500 kB", "index_scans"=> 0 }
         | 
| 37 | 
            +
                      ]
         | 
| 38 | 
            +
                    }
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  it "works" do
         | 
| 42 | 
            +
                    expect {
         | 
| 43 | 
            +
                      RubyPGExtras::IndexInfoPrint.call(result)
         | 
| 44 | 
            +
                    }.not_to raise_error
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                context "real data" do
         | 
| 49 | 
            +
                  it "works" do
         | 
| 50 | 
            +
                    expect {
         | 
| 51 | 
            +
                      RubyPGExtras::IndexInfoPrint.call(result)
         | 
| 52 | 
            +
                    }.not_to raise_error
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
            end
         | 
    
        data/spec/smoke_spec.rb
    CHANGED
    
    | @@ -3,11 +3,6 @@ | |
| 3 3 | 
             
            require 'spec_helper'
         | 
| 4 4 |  | 
| 5 5 | 
             
            describe RubyPGExtras do
         | 
| 6 | 
            -
              before(:all) do
         | 
| 7 | 
            -
                RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_buffercache;")
         | 
| 8 | 
            -
                RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;")
         | 
| 9 | 
            -
              end
         | 
| 10 | 
            -
             | 
| 11 6 | 
             
              RubyPGExtras::QUERIES.each do |query_name|
         | 
| 12 7 | 
             
                it "#{query_name} description can be read" do
         | 
| 13 8 | 
             
                  expect do
         | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    | @@ -17,3 +17,11 @@ else | |
| 17 17 | 
             
            end
         | 
| 18 18 |  | 
| 19 19 | 
             
            ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:#{port}/ruby-pg-extras-test"
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            RSpec.configure do |config|
         | 
| 22 | 
            +
              config.before(:suite) do
         | 
| 23 | 
            +
                RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;")
         | 
| 24 | 
            +
                RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_buffercache;")
         | 
| 25 | 
            +
                RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS sslinfo;")
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,78 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe RubyPGExtras::TableInfo do
         | 
| 6 | 
            +
              subject(:result) do
         | 
| 7 | 
            +
                RubyPGExtras::TableInfo.call
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              describe "call" do
         | 
| 11 | 
            +
                context "stubbed cases" do
         | 
| 12 | 
            +
                  before do
         | 
| 13 | 
            +
                    expect(RubyPGExtras).to receive(:tables) {
         | 
| 14 | 
            +
                      [
         | 
| 15 | 
            +
                        { "schemaname" => "public", "tablename" => "users" },
         | 
| 16 | 
            +
                        { "schemaname" => "public", "tablename" => "teams" }
         | 
| 17 | 
            +
                      ]
         | 
| 18 | 
            +
                    }
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    expect(RubyPGExtras).to receive(:table_size) {
         | 
| 21 | 
            +
                      [
         | 
| 22 | 
            +
                        { "name" => "teams", "size" => "25 MB" },
         | 
| 23 | 
            +
                        {"name" => "users", "size" => "250 MB"},
         | 
| 24 | 
            +
                      ]
         | 
| 25 | 
            +
                    }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    expect(RubyPGExtras).to receive(:index_cache_hit) {
         | 
| 28 | 
            +
                      [
         | 
| 29 | 
            +
                        { "name" => "teams", "ratio" => "0.98" },
         | 
| 30 | 
            +
                        { "name" => "users", "ratio" => "0.999" },
         | 
| 31 | 
            +
                      ]
         | 
| 32 | 
            +
                    }
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    expect(RubyPGExtras).to receive(:table_cache_hit) {
         | 
| 35 | 
            +
                      [
         | 
| 36 | 
            +
                        { "name" => "teams", "ratio" => "0.88" },
         | 
| 37 | 
            +
                        { "name" => "users", "ratio" => "0.899" },
         | 
| 38 | 
            +
                      ]
         | 
| 39 | 
            +
                    }
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    expect(RubyPGExtras).to receive(:records_rank) {
         | 
| 42 | 
            +
                      [
         | 
| 43 | 
            +
                        { "name" => "teams", "estimated_count" => "358" },
         | 
| 44 | 
            +
                        { "name" => "users", "estimated_count" => "8973" },
         | 
| 45 | 
            +
                      ]
         | 
| 46 | 
            +
                    }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    expect(RubyPGExtras).to receive(:seq_scans) {
         | 
| 49 | 
            +
                      [
         | 
| 50 | 
            +
                        { "name" => "teams", "count" => "0" },
         | 
| 51 | 
            +
                        { "name" => "users", "count" => "409328" },
         | 
| 52 | 
            +
                      ]
         | 
| 53 | 
            +
                    }
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    expect(RubyPGExtras).to receive(:table_index_scans) {
         | 
| 56 | 
            +
                      [
         | 
| 57 | 
            +
                        { "name" => "teams", "count" => "8579" },
         | 
| 58 | 
            +
                        { "name" => "users", "count" => "0" },
         | 
| 59 | 
            +
                      ]
         | 
| 60 | 
            +
                    }
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  it "works" do
         | 
| 64 | 
            +
                    expect {
         | 
| 65 | 
            +
                      RubyPGExtras::TableInfoPrint.call(result)
         | 
| 66 | 
            +
                    }.not_to raise_error
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                context "real data" do
         | 
| 71 | 
            +
                  it "works" do
         | 
| 72 | 
            +
                    expect {
         | 
| 73 | 
            +
                      RubyPGExtras::TableInfoPrint.call(result)
         | 
| 74 | 
            +
                    }.not_to raise_error
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: ruby-pg-extras
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 2. | 
| 4 | 
            +
              version: 3.2.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - pawurb
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2021- | 
| 11 | 
            +
            date: 2021-12-15 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: pg
         | 
| @@ -24,6 +24,20 @@ dependencies: | |
| 24 24 | 
             
                - - ">="
         | 
| 25 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 26 | 
             
                    version: '0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: filesize
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - ">="
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '0'
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - ">="
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '0'
         | 
| 27 41 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 42 | 
             
              name: terminal-table
         | 
| 29 43 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -83,6 +97,11 @@ files: | |
| 83 97 | 
             
            - Rakefile
         | 
| 84 98 | 
             
            - docker-compose.yml.sample
         | 
| 85 99 | 
             
            - lib/ruby-pg-extras.rb
         | 
| 100 | 
            +
            - lib/ruby-pg-extras/diagnose_data.rb
         | 
| 101 | 
            +
            - lib/ruby-pg-extras/diagnose_print.rb
         | 
| 102 | 
            +
            - lib/ruby-pg-extras/index_info.rb
         | 
| 103 | 
            +
            - lib/ruby-pg-extras/index_info_print.rb
         | 
| 104 | 
            +
            - lib/ruby-pg-extras/queries/add_extensions.sql
         | 
| 86 105 | 
             
            - lib/ruby-pg-extras/queries/all_locks.sql
         | 
| 87 106 | 
             
            - lib/ruby-pg-extras/queries/bloat.sql
         | 
| 88 107 | 
             
            - lib/ruby-pg-extras/queries/blocking.sql
         | 
| @@ -95,8 +114,10 @@ files: | |
| 95 114 | 
             
            - lib/ruby-pg-extras/queries/duplicate_indexes.sql
         | 
| 96 115 | 
             
            - lib/ruby-pg-extras/queries/extensions.sql
         | 
| 97 116 | 
             
            - lib/ruby-pg-extras/queries/index_cache_hit.sql
         | 
| 117 | 
            +
            - lib/ruby-pg-extras/queries/index_scans.sql
         | 
| 98 118 | 
             
            - lib/ruby-pg-extras/queries/index_size.sql
         | 
| 99 119 | 
             
            - lib/ruby-pg-extras/queries/index_usage.sql
         | 
| 120 | 
            +
            - lib/ruby-pg-extras/queries/indexes.sql
         | 
| 100 121 | 
             
            - lib/ruby-pg-extras/queries/kill_all.sql
         | 
| 101 122 | 
             
            - lib/ruby-pg-extras/queries/locks.sql
         | 
| 102 123 | 
             
            - lib/ruby-pg-extras/queries/long_running_queries.sql
         | 
| @@ -109,16 +130,25 @@ files: | |
| 109 130 | 
             
            - lib/ruby-pg-extras/queries/seq_scans.sql
         | 
| 110 131 | 
             
            - lib/ruby-pg-extras/queries/ssl_used.sql
         | 
| 111 132 | 
             
            - lib/ruby-pg-extras/queries/table_cache_hit.sql
         | 
| 133 | 
            +
            - lib/ruby-pg-extras/queries/table_index_scans.sql
         | 
| 112 134 | 
             
            - lib/ruby-pg-extras/queries/table_indexes_size.sql
         | 
| 113 135 | 
             
            - lib/ruby-pg-extras/queries/table_size.sql
         | 
| 136 | 
            +
            - lib/ruby-pg-extras/queries/tables.sql
         | 
| 114 137 | 
             
            - lib/ruby-pg-extras/queries/total_index_size.sql
         | 
| 115 138 | 
             
            - lib/ruby-pg-extras/queries/total_table_size.sql
         | 
| 116 139 | 
             
            - lib/ruby-pg-extras/queries/unused_indexes.sql
         | 
| 117 140 | 
             
            - lib/ruby-pg-extras/queries/vacuum_stats.sql
         | 
| 141 | 
            +
            - lib/ruby-pg-extras/table_info.rb
         | 
| 142 | 
            +
            - lib/ruby-pg-extras/table_info_print.rb
         | 
| 118 143 | 
             
            - lib/ruby-pg-extras/version.rb
         | 
| 144 | 
            +
            - ruby-pg-extras-diagnose.png
         | 
| 119 145 | 
             
            - ruby-pg-extras.gemspec
         | 
| 146 | 
            +
            - spec/diagnose_data_spec.rb
         | 
| 147 | 
            +
            - spec/diagnose_print_spec.rb
         | 
| 148 | 
            +
            - spec/index_info_spec.rb
         | 
| 120 149 | 
             
            - spec/smoke_spec.rb
         | 
| 121 150 | 
             
            - spec/spec_helper.rb
         | 
| 151 | 
            +
            - spec/table_info_spec.rb
         | 
| 122 152 | 
             
            homepage: http://github.com/pawurb/ruby-pg-extras
         | 
| 123 153 | 
             
            licenses:
         | 
| 124 154 | 
             
            - MIT
         | 
| @@ -143,5 +173,9 @@ signing_key: | |
| 143 173 | 
             
            specification_version: 4
         | 
| 144 174 | 
             
            summary: Ruby PostgreSQL performance database insights
         | 
| 145 175 | 
             
            test_files:
         | 
| 176 | 
            +
            - spec/diagnose_data_spec.rb
         | 
| 177 | 
            +
            - spec/diagnose_print_spec.rb
         | 
| 178 | 
            +
            - spec/index_info_spec.rb
         | 
| 146 179 | 
             
            - spec/smoke_spec.rb
         | 
| 147 180 | 
             
            - spec/spec_helper.rb
         | 
| 181 | 
            +
            - spec/table_info_spec.rb
         |