pghero 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pghero might be problematic. Click here for more details.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +2 -2
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  6. data/app/assets/javascripts/pghero/application.js +9 -0
  7. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  8. data/app/assets/stylesheets/pghero/application.css +54 -2
  9. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  10. data/app/controllers/pg_hero/home_controller.rb +148 -52
  11. data/app/helpers/pg_hero/base_helper.rb +15 -0
  12. data/app/views/layouts/pg_hero/application.html.erb +1 -1
  13. data/app/views/pg_hero/home/_connections_table.html.erb +2 -2
  14. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -7
  15. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  16. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  17. data/app/views/pg_hero/home/connections.html.erb +2 -14
  18. data/app/views/pg_hero/home/explain.html.erb +1 -1
  19. data/app/views/pg_hero/home/index.html.erb +58 -22
  20. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  21. data/app/views/pg_hero/home/maintenance.html.erb +7 -7
  22. data/app/views/pg_hero/home/queries.html.erb +10 -0
  23. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  24. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  25. data/app/views/pg_hero/home/space.html.erb +64 -10
  26. data/config/routes.rb +4 -2
  27. data/guides/Rails.md +28 -1
  28. data/guides/Suggested-Indexes.md +1 -1
  29. data/lib/pghero.rb +25 -36
  30. data/lib/pghero/database.rb +5 -1
  31. data/lib/pghero/methods/basic.rb +78 -13
  32. data/lib/pghero/methods/connections.rb +16 -56
  33. data/lib/pghero/methods/explain.rb +2 -6
  34. data/lib/pghero/methods/indexes.rb +173 -18
  35. data/lib/pghero/methods/kill.rb +2 -2
  36. data/lib/pghero/methods/maintenance.rb +23 -26
  37. data/lib/pghero/methods/queries.rb +1 -23
  38. data/lib/pghero/methods/query_stats.rb +95 -96
  39. data/lib/pghero/methods/{replica.rb → replication.rb} +17 -4
  40. data/lib/pghero/methods/sequences.rb +4 -5
  41. data/lib/pghero/methods/space.rb +101 -8
  42. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  43. data/lib/pghero/methods/system.rb +14 -10
  44. data/lib/pghero/methods/tables.rb +8 -8
  45. data/lib/pghero/methods/users.rb +10 -12
  46. data/lib/pghero/version.rb +1 -1
  47. data/lib/tasks/pghero.rake +1 -1
  48. data/test/basic_test.rb +38 -0
  49. data/test/best_index_test.rb +3 -3
  50. data/test/suggested_indexes_test.rb +0 -2
  51. data/test/test_helper.rb +38 -40
  52. metadata +11 -6
  53. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  54. data/test/explain_test.rb +0 -18
@@ -0,0 +1,9 @@
1
+ <div class="content">
2
+ <h1><%= @relation %></h1>
3
+
4
+ <h1>Size <small>MB</small></h1>
5
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
6
+ <script>
7
+ new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, min: null})
8
+ </script>
9
+ </div>
@@ -0,0 +1,107 @@
1
+ <div class="content">
2
+ <pre><code style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= @query %></code></pre>
3
+ <script>
4
+ highlightQueries()
5
+ </script>
6
+
7
+ <% if @explainable_query %>
8
+ <p>
9
+ <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: @explainable_query}}] : [explain_path(query: @explainable_query), {}] %>
10
+ <%= button_to "Explain", button_path, button_options.merge(form: {target: "_blank"}, class: "btn btn-info") %>
11
+ </p>
12
+ <% end %>
13
+
14
+ <% if @origins && @origins.keys.select { |k| k.length > 0 }.any? %>
15
+ <table style="table-layout: auto;">
16
+ <thead>
17
+ <tr>
18
+ <th colspan="2">
19
+ <div style="float: right;">Approx. Time</div>
20
+ Origin
21
+ </th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% @origins.sort_by { |o, c| [-c, o.to_s] }.each do |origin, count| %>
26
+ <tr>
27
+ <td class="origin" style="width: 90%;">
28
+ <% if origin.length > 0 %>
29
+ <%= origin %>
30
+ <% else %>
31
+ <span class="text-muted">Unknown</span>
32
+ <% end %>
33
+ </td>
34
+ <td style="text-align: right; width: 10%;">
35
+ <% pct = (100.0 * count / @total_count).round %>
36
+ <% if pct == 0 %>
37
+ &lt; 1%
38
+ <% else %>
39
+ <%= pct %>%
40
+ <% end %>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ <% end %>
47
+
48
+ <!-- chart -->
49
+ <% if @chart_data %>
50
+ <h1>Total Time <small>ms</small></h1>
51
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
52
+ <script>
53
+ new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
54
+ </script>
55
+
56
+ <h1>Average Time <small>ms</small></h1>
57
+ <div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
58
+ <script>
59
+ new Chartkick.LineChart("chart-2", <%= json_escape(@chart2_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
60
+ </script>
61
+
62
+ <h1>Calls</h1>
63
+ <div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
64
+ <script>
65
+ new Chartkick.LineChart("chart-3", <%= json_escape(@chart3_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false})
66
+ </script>
67
+ <% else %>
68
+ <p>
69
+ Enable
70
+ <%= link_to "historical query stats", "https://github.com/ankane/pghero", target: "_blank" %>
71
+ to see more details
72
+ </p>
73
+ <% end %>
74
+
75
+ <!-- table info -->
76
+ <% if @tables.any? %>
77
+ <h1>Tables</h1>
78
+ <table>
79
+ <thead>
80
+ <tr>
81
+ <th style="width: 25%;">Name</th>
82
+ <th style="width: 25%;">Rows</th>
83
+ <th>Indexes</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody>
87
+ <% @tables.each do |table| %>
88
+ <tr>
89
+ <td><%= table %></td>
90
+ <td><%= @row_counts[table] %></td>
91
+ <td>
92
+ <ul class="list-normal">
93
+ <% @indexes_by_table[table].to_a.sort_by { |i| [i[:primary] ? 0 : 1, i[:columns]] }.each do |i3| %>
94
+ <li>
95
+ <%= i3[:columns].join(", ") %><% if i3[:using] != "btree" %>
96
+ <%= i3[:using].to_s.upcase %><% end %>
97
+ <% if i3[:primary] %> PRIMARY<% elsif i3[:unique] %> UNIQUE<% end %>
98
+ </li>
99
+ <% end %>
100
+ </ul>
101
+ </td>
102
+ </tr>
103
+ <% end %>
104
+ </tbody>
105
+ </table>
106
+ <% end %>
107
+ </div>
@@ -3,25 +3,79 @@
3
3
 
4
4
  <p>Database Size: <%= @database_size %></p>
5
5
 
6
- <table class="table">
6
+ <% if @system_stats_enabled %>
7
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
8
+ <script>
9
+ new Chartkick.LineChart("chart-1", <%= json_escape(free_space_stats_path.to_json).html_safe %>, {colors: ["#5bc0de"]})
10
+ </script>
11
+ <% end %>
12
+
13
+ <!--
14
+ <% if @index_bloat.any? %>
15
+ <p>Check out <%= link_to "index bloat", index_bloat_path %> for an easy way to reclaim space.</p>
16
+ <% end %>
17
+ -->
18
+
19
+ <% if @unused_indexes.any? %>
20
+ <p>
21
+ <%= pluralize(@unused_indexes.size, "unused index") %>. Remove them
22
+ <% if @show_migrations %>
23
+ <a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
24
+ <% end %>
25
+ for faster writes.
26
+
27
+ <% if @database.replicating? %>
28
+ Check they aren’t used on replicas.
29
+ <% end %>
30
+ </p>
31
+
32
+ <div id="migration" style="display: none;">
33
+ <pre>rails g migration remove_unused_indexes</pre>
34
+ <p>And paste</p>
35
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.sort_by { |q| q[:index] }.each do |query| %>
36
+ remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
37
+ </div>
38
+ <% end %>
39
+
40
+ <table class="table space-table">
7
41
  <thead>
8
42
  <tr>
9
- <th>Relation</th>
10
- <th style="width: 15%;"></th>
11
- <th style="width: 15%;">Size</th>
43
+ <th><%= link_to "Relation", {sort: "name"} %></th>
44
+ <th style="width: 15%;"><%= link_to "Size", {} %></th>
45
+ <% if @space_stats_enabled %>
46
+ <th style="width: 15%;"><%= link_to "#{@days}d Growth", {sort: "growth"} %></th>
47
+ <% end %>
12
48
  </tr>
13
49
  </thead>
14
50
  <tbody>
15
51
  <% @relation_sizes.each do |query| %>
16
52
  <tr>
17
- <td>
18
- <%= query["name"] %>
19
- <% if query["schema"] != "public" %>
20
- <span class="text-muted"><%= query["schema"] %></span>
53
+ <td style="<%= query[:type] == "index" ? "font-style: italic;" : "" %>">
54
+ <span style="word-break: break-all;">
55
+ <% name = query[:relation] || query[:table] %>
56
+ <% if @space_stats_enabled %>
57
+ <%= link_to name, relation_space_path(name), target: "_blank", style: "color: inherit;" %>
58
+ <% else %>
59
+ <%= name %>
60
+ <% end %>
61
+ </span>
62
+ <% if query[:schema] != "public" %>
63
+ <span class="text-muted"><%= query[:schema] %></span>
64
+ <% end %>
65
+ <% if @unused_index_names.include?(query[:name]) %>
66
+ <span class="unused-index">UNUSED</span>
21
67
  <% end %>
22
68
  </td>
23
- <td> <span class="text-muted"><%= query["type"] == "index" ? "index" : "" %></span></td>
24
- <td><%= query["size"] %></td>
69
+ <td><%= query[:size] %></td>
70
+ <% if @space_stats_enabled %>
71
+ <td>
72
+ <% if @growth_bytes_by_relation[query[:relation]] %>
73
+ <% if @growth_bytes_by_relation[query[:relation]] < 0 %>-<% end %><%= PgHero.pretty_size(@growth_bytes_by_relation[query[:relation]].abs) %>
74
+ <% else %>
75
+ <span class="text-muted">Unknown</span>
76
+ <% end %>
77
+ </td>
78
+ <% end %>
25
79
  </tr>
26
80
  <% end %>
27
81
  </tbody>
@@ -1,14 +1,17 @@
1
1
  PgHero::Engine.routes.draw do
2
2
  scope "(:database)", constraints: proc { |req| (PgHero.config["databases"].keys + [nil]).include?(req.params[:database]) } do
3
- get "index_usage", to: "home#index_usage"
4
3
  get "space", to: "home#space"
4
+ get "space/:relation", to: "home#relation_space", as: :relation_space
5
+ get "index_bloat", to: "home#index_bloat"
5
6
  get "live_queries", to: "home#live_queries"
6
7
  get "queries", to: "home#queries"
8
+ get "queries/:query_hash", to: "home#show_query", as: :show_query
7
9
  get "system", to: "home#system"
8
10
  get "cpu_usage", to: "home#cpu_usage"
9
11
  get "connection_stats", to: "home#connection_stats"
10
12
  get "replication_lag_stats", to: "home#replication_lag_stats"
11
13
  get "load_stats", to: "home#load_stats"
14
+ get "free_space_stats", to: "home#free_space_stats"
12
15
  get "explain", to: "home#explain"
13
16
  get "tune", to: "home#tune"
14
17
  get "connections", to: "home#connections"
@@ -23,7 +26,6 @@ PgHero::Engine.routes.draw do
23
26
  # legacy routes
24
27
  get "system_stats" => redirect("system")
25
28
  get "query_stats" => redirect("queries")
26
- get "indexes" => redirect("index_usage")
27
29
 
28
30
  root to: "home#index"
29
31
  end
@@ -19,7 +19,7 @@ Be sure to [secure the dashboard](#security) in production.
19
19
  PgHero can suggest indexes to add. To enable, add to your Gemfile:
20
20
 
21
21
  ```ruby
22
- gem 'pg_query'
22
+ gem 'pg_query', '>= 0.9.0'
23
23
  ```
24
24
 
25
25
  and make sure [query stats](#query-stats) are enabled. Read about how it works [here](Suggested-Indexes.md).
@@ -241,6 +241,33 @@ PgHero.drop_user("ganondorf")
241
241
 
242
242
  ## Upgrading
243
243
 
244
+ ### 2.0.0
245
+
246
+ New features
247
+
248
+ - Query details page
249
+
250
+ Breaking changes
251
+
252
+ - Methods now return symbols for keys instead of strings
253
+ - Methods raise `PgHero::NotEnabled` error when a feature isn’t enabled
254
+ - Requires pg_query 0.9.0+ for suggested indexes
255
+ - Historical query stats require the `pghero_query_stats` table to have `query_hash` and `user` columns
256
+ - Removed `with` option - use:
257
+
258
+ ```ruby
259
+ PgHero.databases[:database2].running_queries
260
+ ```
261
+
262
+ instead of
263
+
264
+ ```ruby
265
+ PgHero.with(:database2) { PgHero.running_queries }
266
+ ```
267
+
268
+ - Removed options from `connection_sources` method
269
+ - Removed `locks` method
270
+
244
271
  ### 1.5.0
245
272
 
246
273
  For query stats grouping by user, create a migration with:
@@ -1,6 +1,6 @@
1
1
  # How PgHero Suggests Indexes
2
2
 
3
- 1. Get the most time-consuming queries from [pg_stat_statements](http://www.postgresql.org/docs/9.3/static/pgstatstatements.html).
3
+ 1. Get the most time-consuming queries from [pg_stat_statements](http://www.postgresql.org/docs/current/static/pgstatstatements.html).
4
4
 
5
5
  2. Parse queries with [pg_query](https://github.com/lfittl/pg_query). Look for a single table with a `WHERE` clause that consists of only `=`, `IN`, `IS NULL` or `IS NOT NULL` and/or an `ORDER BY` clause.
6
6
 
@@ -10,7 +10,7 @@ require "pghero/methods/kill"
10
10
  require "pghero/methods/maintenance"
11
11
  require "pghero/methods/queries"
12
12
  require "pghero/methods/query_stats"
13
- require "pghero/methods/replica"
13
+ require "pghero/methods/replication"
14
14
  require "pghero/methods/sequences"
15
15
  require "pghero/methods/space"
16
16
  require "pghero/methods/suggested_indexes"
@@ -26,6 +26,8 @@ require "pghero/connection"
26
26
  require "pghero/query_stats"
27
27
 
28
28
  module PgHero
29
+ class NotEnabled < StandardError; end
30
+
29
31
  # settings
30
32
  class << self
31
33
  attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :total_connections_threshold, :cache_hit_rate_threshold, :env, :show_migrations
@@ -40,16 +42,16 @@ module PgHero
40
42
 
41
43
  class << self
42
44
  extend Forwardable
43
- def_delegators :current_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autoindex_all, :autovacuum_danger,
45
+ def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
44
46
  :best_index, :blocked_queries, :connection_sources, :connection_stats,
45
47
  :cpu_usage, :create_user, :database_size, :db_instance_identifier, :disable_query_stats, :drop_user,
46
48
  :duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
47
49
  :index_hit_rate, :index_usage, :indexes, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
48
- :locks, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
50
+ :last_stats_reset_time, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
49
51
  :query_stats_available?, :query_stats_enabled?, :query_stats_extension_enabled?, :query_stats_readable?,
50
52
  :rds_stats, :read_iops_stats, :region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
51
- :reset_query_stats, :running_queries, :secret_access_key, :sequence_danger, :sequences, :settings,
52
- :slow_queries, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
53
+ :reset_query_stats, :reset_stats, :running_queries, :secret_access_key, :sequence_danger, :sequences, :settings,
54
+ :slow_queries, :space_growth, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
53
55
  :suggested_indexes_enabled?, :system_stats_enabled?, :table_caching, :table_hit_rate, :table_stats,
54
56
  :total_connections, :transaction_id_danger, :unused_indexes, :unused_tables, :write_iops_stats
55
57
 
@@ -62,16 +64,20 @@ module PgHero
62
64
  end
63
65
 
64
66
  def config
65
- Thread.current[:pghero_config] ||= begin
67
+ @config ||= begin
66
68
  path = "config/pghero.yml"
67
69
 
68
- config = YAML.load(ERB.new(File.read(path)).result) if File.exist?(path)
70
+ config_file_exists = File.exist?(path)
71
+
72
+ config = YAML.load(ERB.new(File.read(path)).result) if config_file_exists
69
73
  config ||= {}
70
74
 
71
75
  if config[env]
72
76
  config[env]
73
77
  elsif config["databases"] # preferred format
74
78
  config
79
+ elsif config_file_exists
80
+ raise "Invalid config file"
75
81
  else
76
82
  {
77
83
  "databases" => {
@@ -89,7 +95,7 @@ module PgHero
89
95
  @databases ||= begin
90
96
  Hash[
91
97
  config["databases"].map do |id, c|
92
- [id, PgHero::Database.new(id, c)]
98
+ [id.to_sym, PgHero::Database.new(id, c)]
93
99
  end
94
100
  ]
95
101
  end
@@ -99,26 +105,6 @@ module PgHero
99
105
  databases.values.first
100
106
  end
101
107
 
102
- def current_database
103
- Thread.current[:pghero_current_database] ||= primary_database
104
- end
105
-
106
- def current_database=(database)
107
- raise "Database not found" unless databases[database.to_s]
108
- Thread.current[:pghero_current_database] = databases[database.to_s]
109
- database
110
- end
111
-
112
- def with(database)
113
- previous_database = current_database
114
- begin
115
- self.current_database = database
116
- yield
117
- ensure
118
- self.current_database = previous_database.id
119
- end
120
- end
121
-
122
108
  def capture_query_stats
123
109
  databases.each do |_, database|
124
110
  database.capture_query_stats
@@ -133,20 +119,23 @@ module PgHero
133
119
  true
134
120
  end
135
121
 
136
- def analyze_all
137
- databases.each do |_, database|
138
- database.analyze_tables
122
+ def analyze_all(**options)
123
+ databases.reject { |_, d| d.replica? }.each do |_, database|
124
+ database.analyze_tables(**options)
139
125
  end
140
126
  true
141
127
  end
142
128
 
143
- # Handles Rails 4 ('t') and Rails 5 (true) values.
144
- def truthy?(value)
145
- value == true || value == 't'
129
+ def autoindex_all(create: false)
130
+ databases.each do |_, database|
131
+ puts "Autoindexing #{database}..."
132
+ database.autoindex(create: create)
133
+ end
134
+ true
146
135
  end
147
136
 
148
- def falsey?(value)
149
- value == false || value == 'f'
137
+ def pretty_size(value)
138
+ ActiveSupport::NumberHelper.number_to_human_size(value, precision: 3)
150
139
  end
151
140
  end
152
141
  end
@@ -8,7 +8,7 @@ module PgHero
8
8
  include Methods::Maintenance
9
9
  include Methods::Queries
10
10
  include Methods::QueryStats
11
- include Methods::Replica
11
+ include Methods::Replication
12
12
  include Methods::Sequences
13
13
  include Methods::Space
14
14
  include Methods::SuggestedIndexes
@@ -55,6 +55,10 @@ module PgHero
55
55
  (config["long_running_query_sec"] || PgHero.config["long_running_query_sec"] || PgHero.long_running_query_sec).to_i
56
56
  end
57
57
 
58
+ def index_bloat_bytes
59
+ (config["index_bloat_bytes"] || PgHero.config["index_bloat_bytes"] || 100.megabytes).to_i
60
+ end
61
+
58
62
  private
59
63
 
60
64
  def connection_model
@@ -4,44 +4,78 @@ module PgHero
4
4
  def settings
5
5
  names =
6
6
  if server_version_num >= 90500
7
- %w(
7
+ %i(
8
8
  max_connections shared_buffers effective_cache_size work_mem
9
9
  maintenance_work_mem min_wal_size max_wal_size checkpoint_completion_target
10
10
  wal_buffers default_statistics_target
11
11
  )
12
12
  else
13
- %w(
13
+ %i(
14
14
  max_connections shared_buffers effective_cache_size work_mem
15
15
  maintenance_work_mem checkpoint_segments checkpoint_completion_target
16
16
  wal_buffers default_statistics_target
17
17
  )
18
18
  end
19
- Hash[names.map { |name| [name, select_all("SHOW #{name}").first[name]] }]
19
+ Hash[names.map { |name| [name, select_one("SHOW #{name}")] }]
20
20
  end
21
21
 
22
22
  def ssl_used?
23
23
  ssl_used = nil
24
- connection_model.transaction do
25
- execute("CREATE EXTENSION IF NOT EXISTS sslinfo")
26
- ssl_used = PgHero.truthy?(select_all("SELECT ssl_is_used()").first["ssl_is_used"])
27
- raise ActiveRecord::Rollback
24
+ with_transaction(rollback: true) do
25
+ begin
26
+ execute("CREATE EXTENSION IF NOT EXISTS sslinfo")
27
+ rescue ActiveRecord::StatementInvalid
28
+ # not superuser
29
+ end
30
+ ssl_used = select_one("SELECT ssl_is_used()")
28
31
  end
29
32
  ssl_used
30
33
  end
31
34
 
32
35
  def database_name
33
- select_all("SELECT current_database()").first["current_database"]
36
+ select_one("SELECT current_database()")
34
37
  end
35
38
 
36
39
  def server_version
37
- select_all("SHOW server_version").first["server_version"]
40
+ @server_version ||= select_one("SHOW server_version")
41
+ end
42
+
43
+ def server_version_num
44
+ @server_version_num ||= select_one("SHOW server_version_num").to_i
45
+ end
46
+
47
+ def quote_ident(value)
48
+ quote_table_name(value)
38
49
  end
39
50
 
40
51
  private
41
52
 
42
- def select_all(sql)
53
+ def select_all(sql, conn = nil)
54
+ conn ||= connection
43
55
  # squish for logs
44
- connection.select_all(squish(sql)).to_a
56
+ result = conn.select_all(squish(sql))
57
+ cast_method = ActiveRecord::VERSION::MAJOR < 5 ? :type_cast : :cast_value
58
+ result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(cast_method, val)] }] }
59
+ end
60
+
61
+ def select_all_stats(sql)
62
+ select_all(sql, stats_connection)
63
+ end
64
+
65
+ def select_all_size(sql)
66
+ result = select_all(sql)
67
+ result.each do |row|
68
+ row[:size] = PgHero.pretty_size(row[:size_bytes])
69
+ end
70
+ result
71
+ end
72
+
73
+ def select_one(sql, conn = nil)
74
+ select_all(sql, conn).first.values.first
75
+ end
76
+
77
+ def select_one_stats(sql)
78
+ select_one(sql, stats_connection)
45
79
  end
46
80
 
47
81
  def execute(sql)
@@ -52,6 +86,16 @@ module PgHero
52
86
  connection_model.connection
53
87
  end
54
88
 
89
+ def stats_connection
90
+ ::PgHero::QueryStats.connection
91
+ end
92
+
93
+ def insert_stats(table, columns, values)
94
+ values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
95
+ columns = columns.map { |v| quote_table_name(v) }.join(",")
96
+ stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
97
+ end
98
+
55
99
  # from ActiveSupport
56
100
  def squish(str)
57
101
  str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
@@ -73,12 +117,33 @@ module PgHero
73
117
  end
74
118
  end
75
119
 
76
- def with_lock_timeout(timeout)
120
+ def with_transaction(lock_timeout: nil, statement_timeout: nil, rollback: false)
77
121
  connection_model.transaction do
78
- select_all "SET LOCAL lock_timeout = #{timeout.to_i}"
122
+ select_all "SET LOCAL statement_timeout = #{statement_timeout.to_i}" if statement_timeout
123
+ select_all "SET LOCAL lock_timeout = #{lock_timeout.to_i}" if lock_timeout
79
124
  yield
125
+ raise ActiveRecord::Rollback if rollback
80
126
  end
81
127
  end
128
+
129
+ def table_exists?(table)
130
+ ["PostgreSQL", "PostGIS"].include?(stats_connection.adapter_name) &&
131
+ select_one_stats(<<-SQL
132
+ SELECT EXISTS (
133
+ SELECT
134
+ 1
135
+ FROM
136
+ pg_catalog.pg_class c
137
+ INNER JOIN
138
+ pg_catalog.pg_namespace n ON n.oid = c.relnamespace
139
+ WHERE
140
+ n.nspname = 'public'
141
+ AND c.relname = #{quote(table)}
142
+ AND c.relkind = 'r'
143
+ )
144
+ SQL
145
+ )
146
+ end
82
147
  end
83
148
  end
84
149
  end