pghero 2.8.3 → 3.3.3

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE.txt +1 -1
  4. data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
  5. data/app/assets/javascripts/pghero/application.js +13 -12
  6. data/app/assets/javascripts/pghero/chartkick.js +834 -764
  7. data/app/assets/javascripts/pghero/highlight.min.js +440 -0
  8. data/app/assets/javascripts/pghero/jquery.js +318 -197
  9. data/app/assets/javascripts/pghero/nouislider.js +676 -1066
  10. data/app/assets/stylesheets/pghero/application.css +8 -2
  11. data/app/assets/stylesheets/pghero/nouislider.css +4 -10
  12. data/app/controllers/pg_hero/home_controller.rb +103 -37
  13. data/app/helpers/pg_hero/home_helper.rb +2 -2
  14. data/app/views/layouts/pg_hero/application.html.erb +4 -2
  15. data/app/views/pg_hero/home/_query_stats_slider.html.erb +6 -6
  16. data/app/views/pg_hero/home/connections.html.erb +6 -6
  17. data/app/views/pg_hero/home/explain.html.erb +4 -2
  18. data/app/views/pg_hero/home/index.html.erb +3 -1
  19. data/app/views/pg_hero/home/queries.html.erb +4 -2
  20. data/app/views/pg_hero/home/relation_space.html.erb +1 -1
  21. data/app/views/pg_hero/home/show_query.html.erb +17 -13
  22. data/app/views/pg_hero/home/space.html.erb +44 -40
  23. data/app/views/pg_hero/home/system.html.erb +6 -6
  24. data/lib/generators/pghero/query_stats_generator.rb +1 -0
  25. data/lib/generators/pghero/space_stats_generator.rb +1 -0
  26. data/lib/generators/pghero/templates/config.yml.tt +6 -0
  27. data/lib/pghero/database.rb +0 -7
  28. data/lib/pghero/engine.rb +1 -1
  29. data/lib/pghero/methods/basic.rb +6 -9
  30. data/lib/pghero/methods/connections.rb +5 -5
  31. data/lib/pghero/methods/constraints.rb +1 -1
  32. data/lib/pghero/methods/explain.rb +34 -0
  33. data/lib/pghero/methods/indexes.rb +8 -8
  34. data/lib/pghero/methods/kill.rb +1 -1
  35. data/lib/pghero/methods/maintenance.rb +4 -4
  36. data/lib/pghero/methods/queries.rb +2 -2
  37. data/lib/pghero/methods/query_stats.rb +28 -29
  38. data/lib/pghero/methods/replication.rb +2 -2
  39. data/lib/pghero/methods/sequences.rb +3 -3
  40. data/lib/pghero/methods/settings.rb +1 -1
  41. data/lib/pghero/methods/space.rb +20 -14
  42. data/lib/pghero/methods/suggested_indexes.rb +40 -110
  43. data/lib/pghero/methods/system.rb +16 -16
  44. data/lib/pghero/methods/tables.rb +4 -5
  45. data/lib/pghero/methods/users.rb +12 -4
  46. data/lib/pghero/version.rb +1 -1
  47. data/lib/pghero.rb +90 -65
  48. data/lib/tasks/pghero.rake +11 -1
  49. data/licenses/LICENSE-chart.js.txt +1 -1
  50. data/licenses/LICENSE-date-fns.txt +21 -20
  51. data/licenses/LICENSE-kurkle-color.txt +9 -0
  52. metadata +9 -8
  53. data/app/assets/javascripts/pghero/highlight.pack.js +0 -2
@@ -1,34 +1,34 @@
1
1
  <div class="content">
2
2
  <p id="periods">
3
3
  <% @periods.each do |name, options| %>
4
- <%= link_to name, system_path(options) %>
4
+ <%= link_to name, system_path(params: options) %>
5
5
  <% end %>
6
6
  </p>
7
- <% path_options = {duration: @duration, period: @period} %>
7
+ <% path_options = {params: {duration: @duration, period: @period}} %>
8
8
 
9
9
  <h1>CPU</h1>
10
10
  <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
11
11
  <script>
12
- new Chartkick.LineChart("chart-1", <%= json_escape(cpu_usage_path(path_options).to_json).html_safe %>, {max: 100, colors: ["#5bc0de"], suffix: "%", library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
12
+ new Chartkick.LineChart("chart-1", <%= pghero_js_value(cpu_usage_path(path_options)) %>, {max: 100, colors: ["#5bc0de"], suffix: "%", library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
13
13
  </script>
14
14
 
15
15
  <h1>Load</h1>
16
16
  <div id="chart-2" class="chart" style="margin-bottom: 20px;">Loading...</div>
17
17
  <script>
18
- new Chartkick.LineChart("chart-2", <%= json_escape(load_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de", "#d9534f"], library: {plugins: {tooltip: {intersect: false, mode: "nearest"}}}})
18
+ new Chartkick.LineChart("chart-2", <%= pghero_js_value(load_stats_path(path_options)) %>, {colors: ["#5bc0de", "#d9534f"], library: {plugins: {tooltip: {intersect: false, mode: "nearest"}}}})
19
19
  </script>
20
20
 
21
21
  <h1>Connections</h1>
22
22
  <div id="chart-3" class="chart" style="margin-bottom: 20px;">Loading...</div>
23
23
  <script>
24
- new Chartkick.LineChart("chart-3", <%= json_escape(connection_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"], library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
24
+ new Chartkick.LineChart("chart-3", <%= pghero_js_value(connection_stats_path(path_options)) %>, {colors: ["#5bc0de"], library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
25
25
  </script>
26
26
 
27
27
  <% if @database.replica? %>
28
28
  <h1>Replication Lag</h1>
29
29
  <div id="chart-4" class="chart" style="margin-bottom: 20px;">Loading...</div>
30
30
  <script>
31
- new Chartkick.LineChart("chart-4", <%= json_escape(replication_lag_stats_path(path_options).to_json).html_safe %>, {colors: ["#5bc0de"], library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
31
+ new Chartkick.LineChart("chart-4", <%= pghero_js_value(replication_lag_stats_path(path_options)) %>, {colors: ["#5bc0de"], library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
32
32
  </script>
33
33
  <% end %>
34
34
  </div>
@@ -1,3 +1,4 @@
1
+ require "rails/generators"
1
2
  require "rails/generators/active_record"
2
3
 
3
4
  module Pghero
@@ -1,3 +1,4 @@
1
+ require "rails/generators"
1
2
  require "rails/generators/active_record"
2
3
 
3
4
  module Pghero
@@ -24,9 +24,15 @@ databases:
24
24
  # Minimum connections for high connections warning
25
25
  # total_connections_threshold: 500
26
26
 
27
+ # Explain functionality
28
+ # explain: true / false / analyze
29
+
27
30
  # Statement timeout for explain
28
31
  # explain_timeout_sec: 10
29
32
 
33
+ # Visualize URL for explain
34
+ # visualize_url: https://...
35
+
30
36
  # Time zone (defaults to app time zone)
31
37
  # time_zone: "Pacific Time (US & Canada)"
32
38
 
@@ -118,12 +118,6 @@ module PgHero
118
118
  @filter_data
119
119
  end
120
120
 
121
- # TODO remove in next major version
122
- alias_method :access_key_id, :aws_access_key_id
123
- alias_method :secret_access_key, :aws_secret_access_key
124
- alias_method :region, :aws_region
125
- alias_method :db_instance_identifier, :aws_db_instance_identifier
126
-
127
121
  private
128
122
 
129
123
  # check adapter lazily
@@ -148,7 +142,6 @@ module PgHero
148
142
 
149
143
  # resolve spec
150
144
  if !url && config["spec"]
151
- raise Error, "Spec requires Rails 6+" unless PgHero.spec_supported?
152
145
  config_options = {env_name: PgHero.env, PgHero.spec_name_key => config["spec"], PgHero.include_replicas_key => true}
153
146
  resolved = ActiveRecord::Base.configurations.configs_for(**config_options)
154
147
  raise Error, "Spec not found: #{config["spec"]}" unless resolved
data/lib/pghero/engine.rb CHANGED
@@ -5,7 +5,7 @@ module PgHero
5
5
  initializer "pghero", group: :all do |app|
6
6
  # check if Rails api mode
7
7
  if app.config.respond_to?(:assets)
8
- if defined?(Sprockets) && Sprockets::VERSION >= "4"
8
+ if defined?(Sprockets) && Sprockets::VERSION.to_i >= 4
9
9
  app.config.assets.precompile << "pghero/application.js"
10
10
  app.config.assets.precompile << "pghero/application.css"
11
11
  app.config.assets.precompile << "pghero/favicon.png"
@@ -45,7 +45,7 @@ module PgHero
45
45
  if ActiveRecord::VERSION::STRING.to_f >= 6.1
46
46
  result = result.map(&:symbolize_keys)
47
47
  else
48
- result = result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] }] }
48
+ result = result.map { |row| row.to_h { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] } }
49
49
  end
50
50
  if filter_data
51
51
  query_columns.each do |column|
@@ -112,15 +112,8 @@ module PgHero
112
112
  ::PgHero::Stats.connection
113
113
  end
114
114
 
115
- def insert_stats(table, columns, values)
116
- values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
117
- columns = columns.map { |v| quote_table_name(v) }.join(",")
118
- stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
119
- end
120
-
121
- # from ActiveSupport
122
115
  def squish(str)
123
- str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
116
+ str.to_s.squish
124
117
  end
125
118
 
126
119
  def add_source(sql)
@@ -135,6 +128,10 @@ module PgHero
135
128
  connection.quote_table_name(value)
136
129
  end
137
130
 
131
+ def quote_column_name(value)
132
+ connection.quote_column_name(value)
133
+ end
134
+
138
135
  def unquote(part)
139
136
  if part && part.start_with?('"')
140
137
  part[1..-2]
@@ -3,7 +3,7 @@ module PgHero
3
3
  module Connections
4
4
  def connections
5
5
  if server_version_num >= 90500
6
- select_all <<-SQL
6
+ select_all <<~SQL
7
7
  SELECT
8
8
  pg_stat_activity.pid,
9
9
  datname AS database,
@@ -20,7 +20,7 @@ module PgHero
20
20
  pg_stat_activity.pid
21
21
  SQL
22
22
  else
23
- select_all <<-SQL
23
+ select_all <<~SQL
24
24
  SELECT
25
25
  pid,
26
26
  datname AS database,
@@ -41,7 +41,7 @@ module PgHero
41
41
  end
42
42
 
43
43
  def connection_states
44
- states = select_all <<-SQL
44
+ states = select_all <<~SQL
45
45
  SELECT
46
46
  state,
47
47
  COUNT(*) AS connections
@@ -53,11 +53,11 @@ module PgHero
53
53
  2 DESC, 1
54
54
  SQL
55
55
 
56
- Hash[states.map { |s| [s[:state], s[:connections]] }]
56
+ states.to_h { |s| [s[:state], s[:connections]] }
57
57
  end
58
58
 
59
59
  def connection_sources
60
- select_all <<-SQL
60
+ select_all <<~SQL
61
61
  SELECT
62
62
  datname AS database,
63
63
  usename AS user,
@@ -4,7 +4,7 @@ module PgHero
4
4
  # referenced fields can be nil
5
5
  # as not all constraints are foreign keys
6
6
  def invalid_constraints
7
- select_all <<-SQL
7
+ select_all <<~SQL
8
8
  SELECT
9
9
  nsp.nspname AS schema,
10
10
  rel.relname AS table,
@@ -1,6 +1,8 @@
1
1
  module PgHero
2
2
  module Methods
3
3
  module Explain
4
+ # TODO remove in 4.0
5
+ # note: this method is not affected by the explain option
4
6
  def explain(sql)
5
7
  sql = squish(sql)
6
8
  explanation = nil
@@ -16,6 +18,23 @@ module PgHero
16
18
  explanation
17
19
  end
18
20
 
21
+ # TODO rename to explain in 4.0
22
+ # note: this method is not affected by the explain option
23
+ def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
24
+ options = []
25
+ add_explain_option(options, "ANALYZE", analyze)
26
+ add_explain_option(options, "VERBOSE", verbose)
27
+ add_explain_option(options, "SETTINGS", settings)
28
+ add_explain_option(options, "COSTS", costs)
29
+ add_explain_option(options, "BUFFERS", buffers)
30
+ add_explain_option(options, "WAL", wal)
31
+ add_explain_option(options, "TIMING", timing)
32
+ add_explain_option(options, "SUMMARY", summary)
33
+ options << "FORMAT #{explain_format(format)}"
34
+
35
+ explain("(#{options.join(", ")}) #{sql}")
36
+ end
37
+
19
38
  private
20
39
 
21
40
  def explain_safe?
@@ -24,6 +43,21 @@ module PgHero
24
43
  rescue ActiveRecord::StatementInvalid
25
44
  true
26
45
  end
46
+
47
+ def add_explain_option(options, name, value)
48
+ unless value.nil?
49
+ options << "#{name}#{value ? "" : " FALSE"}"
50
+ end
51
+ end
52
+
53
+ # important! validate format to prevent injection
54
+ def explain_format(format)
55
+ if ["text", "xml", "json", "yaml"].include?(format)
56
+ format.upcase
57
+ else
58
+ raise ArgumentError, "Unknown format"
59
+ end
60
+ end
27
61
  end
28
62
  end
29
63
  end
@@ -2,7 +2,7 @@ module PgHero
2
2
  module Methods
3
3
  module Indexes
4
4
  def index_hit_rate
5
- select_one <<-SQL
5
+ select_one <<~SQL
6
6
  SELECT
7
7
  (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate
8
8
  FROM
@@ -11,7 +11,7 @@ module PgHero
11
11
  end
12
12
 
13
13
  def index_caching
14
- select_all <<-SQL
14
+ select_all <<~SQL
15
15
  SELECT
16
16
  schemaname AS schema,
17
17
  relname AS table,
@@ -29,7 +29,7 @@ module PgHero
29
29
  end
30
30
 
31
31
  def index_usage
32
- select_all <<-SQL
32
+ select_all <<~SQL
33
33
  SELECT
34
34
  schemaname AS schema,
35
35
  relname AS table,
@@ -47,7 +47,7 @@ module PgHero
47
47
  end
48
48
 
49
49
  def missing_indexes
50
- select_all <<-SQL
50
+ select_all <<~SQL
51
51
  SELECT
52
52
  schemaname AS schema,
53
53
  relname AS table,
@@ -69,7 +69,7 @@ module PgHero
69
69
  end
70
70
 
71
71
  def unused_indexes(max_scans: 50, across: [])
72
- result = select_all_size <<-SQL
72
+ result = select_all_size <<~SQL
73
73
  SELECT
74
74
  schemaname AS schema,
75
75
  relname AS table,
@@ -104,7 +104,7 @@ module PgHero
104
104
  end
105
105
 
106
106
  def last_stats_reset_time
107
- select_one <<-SQL
107
+ select_one <<~SQL
108
108
  SELECT
109
109
  pg_stat_get_db_stat_reset_time(oid) AS reset_time
110
110
  FROM
@@ -126,7 +126,7 @@ module PgHero
126
126
  # TODO parse array properly
127
127
  # https://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql
128
128
  def indexes
129
- indexes = select_all(<<-SQL
129
+ indexes = select_all(<<~SQL
130
130
  SELECT
131
131
  schemaname AS schema,
132
132
  t.relname AS table,
@@ -186,7 +186,7 @@ module PgHero
186
186
  # thanks @jberkus and @mbanck
187
187
  def index_bloat(min_size: nil)
188
188
  min_size ||= index_bloat_bytes
189
- select_all <<-SQL
189
+ select_all <<~SQL
190
190
  WITH btree_index_atts AS (
191
191
  SELECT
192
192
  nspname, relname, reltuples, relpages, indrelid, relam,
@@ -11,7 +11,7 @@ module PgHero
11
11
  end
12
12
 
13
13
  def kill_all
14
- select_all <<-SQL
14
+ select_all <<~SQL
15
15
  SELECT
16
16
  pg_terminate_backend(pid)
17
17
  FROM
@@ -1,7 +1,7 @@
1
1
  module PgHero
2
2
  module Methods
3
3
  module Maintenance
4
- # https://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND
4
+ # https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND
5
5
  # "the system will shut down and refuse to start any new transactions
6
6
  # once there are fewer than 1 million transactions left until wraparound"
7
7
  # warn when 10,000,000 transactions left
@@ -9,7 +9,7 @@ module PgHero
9
9
  max_value = max_value.to_i
10
10
  threshold = threshold.to_i
11
11
 
12
- select_all <<-SQL
12
+ select_all <<~SQL
13
13
  SELECT
14
14
  n.nspname AS schema,
15
15
  c.relname AS table,
@@ -35,7 +35,7 @@ module PgHero
35
35
 
36
36
  def vacuum_progress
37
37
  if server_version_num >= 90600
38
- select_all <<-SQL
38
+ select_all <<~SQL
39
39
  SELECT
40
40
  pid,
41
41
  phase
@@ -50,7 +50,7 @@ module PgHero
50
50
  end
51
51
 
52
52
  def maintenance_info
53
- select_all <<-SQL
53
+ select_all <<~SQL
54
54
  SELECT
55
55
  schemaname AS schema,
56
56
  relname AS table,
@@ -2,7 +2,7 @@ module PgHero
2
2
  module Methods
3
3
  module Queries
4
4
  def running_queries(min_duration: nil, all: false)
5
- query = <<-SQL
5
+ query = <<~SQL
6
6
  SELECT
7
7
  pid,
8
8
  state,
@@ -36,7 +36,7 @@ module PgHero
36
36
  # from https://wiki.postgresql.org/wiki/Lock_Monitoring
37
37
  # and https://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
38
38
  def blocked_queries
39
- query = <<-SQL
39
+ query = <<~SQL
40
40
  SELECT
41
41
  COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
42
42
  blockeda.pid AS blocked_pid,
@@ -56,10 +56,9 @@ module PgHero
56
56
  true
57
57
  end
58
58
 
59
- # TODO scope by database in PgHero 3.0
60
- # (add database: database_name to options)
61
59
  def reset_query_stats(**options)
62
- reset_instance_query_stats(**options)
60
+ raise PgHero::Error, "Use reset_instance_query_stats to pass database" if options.delete(:database)
61
+ reset_instance_query_stats(**options, database: database_name)
63
62
  end
64
63
 
65
64
  # resets query stats for the entire instance
@@ -121,7 +120,7 @@ module PgHero
121
120
  server_version_num >= 90400
122
121
  end
123
122
 
124
- # resetting query stats will reset across the entire Postgres instance
123
+ # resetting query stats will reset across the entire Postgres instance in Postgres < 12
125
124
  # this is problematic if multiple PgHero databases use the same Postgres instance
126
125
  #
127
126
  # to get around this, we capture queries for every Postgres database before we
@@ -147,16 +146,15 @@ module PgHero
147
146
  # nothing to do
148
147
  return if query_stats.empty?
149
148
 
150
- # use mapping, not query stats here
151
- # TODO add option for this, and make default in PgHero 3.0
152
- if false # mapping.size == 1 && server_version_num >= 120000
149
+ # reset individual databases for Postgres 12+ instance
150
+ if server_version_num >= 120000
153
151
  query_stats.each do |db_id, db_query_stats|
154
- if reset_query_stats(database: mapping[db_id], raise_errors: raise_errors)
152
+ if reset_instance_query_stats(database: mapping[db_id], raise_errors: raise_errors)
155
153
  insert_query_stats(db_id, db_query_stats, now)
156
154
  end
157
155
  end
158
156
  else
159
- if reset_query_stats(raise_errors: raise_errors)
157
+ if reset_instance_query_stats(raise_errors: raise_errors)
160
158
  query_stats.each do |db_id, db_query_stats|
161
159
  insert_query_stats(db_id, db_query_stats, now)
162
160
  end
@@ -164,8 +162,9 @@ module PgHero
164
162
  end
165
163
  end
166
164
 
167
- def clean_query_stats
168
- PgHero::QueryStats.where(database: id).where("captured_at < ?", 14.days.ago).delete_all
165
+ def clean_query_stats(before: nil)
166
+ before ||= 14.days.ago
167
+ PgHero::QueryStats.where(database: id).where("captured_at < ?", before).delete_all
169
168
  end
170
169
 
171
170
  def slow_queries(query_stats: nil, **options)
@@ -173,10 +172,11 @@ module PgHero
173
172
  query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
174
173
  end
175
174
 
175
+ # TODO option to include current period
176
176
  def query_hash_stats(query_hash, user: nil)
177
177
  if historical_query_stats_enabled? && supports_query_hash?
178
178
  start_at = 24.hours.ago
179
- select_all_stats <<-SQL
179
+ select_all_stats <<~SQL
180
180
  SELECT
181
181
  captured_at,
182
182
  total_time / 1000 / 60 AS total_minutes,
@@ -206,7 +206,7 @@ module PgHero
206
206
  limit ||= 100
207
207
  sort ||= "total_minutes"
208
208
  total_time = server_version_num >= 130000 ? "(total_plan_time + total_exec_time)" : "total_time"
209
- query = <<-SQL
209
+ query = <<~SQL
210
210
  WITH query_stats AS (
211
211
  SELECT
212
212
  LEFT(query, 10000) AS query,
@@ -228,6 +228,7 @@ module PgHero
228
228
  )
229
229
  SELECT
230
230
  query,
231
+ query AS explainable_query,
231
232
  query_hash,
232
233
  query_stats.user,
233
234
  total_minutes,
@@ -238,14 +239,14 @@ module PgHero
238
239
  FROM
239
240
  query_stats
240
241
  ORDER BY
241
- #{quote_table_name(sort)} DESC
242
+ #{quote_column_name(sort)} DESC
242
243
  LIMIT #{limit.to_i}
243
244
  SQL
244
245
 
245
246
  # we may be able to skip query_columns
246
247
  # in more recent versions of Postgres
247
248
  # as pg_stat_statements should be already normalized
248
- select_all(query, query_columns: [:query])
249
+ select_all(query, query_columns: [:query, :explainable_query])
249
250
  else
250
251
  raise NotEnabled, "Query stats not enabled"
251
252
  end
@@ -254,7 +255,7 @@ module PgHero
254
255
  def historical_query_stats(sort: nil, start_at: nil, end_at: nil, query_hash: nil)
255
256
  if historical_query_stats_enabled?
256
257
  sort ||= "total_minutes"
257
- query = <<-SQL
258
+ query = <<~SQL
258
259
  WITH query_stats AS (
259
260
  SELECT
260
261
  #{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
@@ -287,7 +288,7 @@ module PgHero
287
288
  FROM
288
289
  query_stats
289
290
  ORDER BY
290
- #{quote_table_name(sort)} DESC
291
+ #{quote_column_name(sort)} DESC
291
292
  LIMIT 100
292
293
  SQL
293
294
 
@@ -330,19 +331,17 @@ module PgHero
330
331
  def insert_query_stats(db_id, db_query_stats, now)
331
332
  values =
332
333
  db_query_stats.map do |qs|
333
- [
334
- db_id,
335
- qs[:query],
336
- qs[:total_minutes] * 60 * 1000,
337
- qs[:calls],
338
- now,
339
- supports_query_hash? ? qs[:query_hash] : nil,
340
- qs[:user]
341
- ]
334
+ {
335
+ database: db_id,
336
+ query: qs[:query],
337
+ total_time: qs[:total_minutes] * 60 * 1000,
338
+ calls: qs[:calls],
339
+ captured_at: now,
340
+ query_hash: supports_query_hash? ? qs[:query_hash] : nil,
341
+ user: qs[:user]
342
+ }
342
343
  end
343
-
344
- columns = %w[database query total_time calls captured_at query_hash user]
345
- insert_stats("pghero_query_stats", columns, values)
344
+ PgHero::QueryStats.insert_all!(values)
346
345
  end
347
346
  end
348
347
  end
@@ -18,7 +18,7 @@ module PgHero
18
18
  "pg_last_xlog_receive_location() = pg_last_xlog_replay_location()"
19
19
  end
20
20
 
21
- select_one <<-SQL
21
+ select_one <<~SQL
22
22
  SELECT
23
23
  CASE
24
24
  WHEN NOT pg_is_in_recovery() OR #{lag_condition} THEN 0
@@ -32,7 +32,7 @@ module PgHero
32
32
  def replication_slots
33
33
  if server_version_num >= 90400
34
34
  with_feature_support(:replication_slots, []) do
35
- select_all <<-SQL
35
+ select_all <<~SQL
36
36
  SELECT
37
37
  slot_name,
38
38
  database,
@@ -7,7 +7,7 @@ module PgHero
7
7
  # it's what information_schema.columns uses
8
8
  # also, exclude temporary tables to prevent error
9
9
  # when accessing across sessions
10
- sequences = select_all <<-SQL
10
+ sequences = select_all <<~SQL
11
11
  SELECT
12
12
  n.nspname AS table_schema,
13
13
  c.relname AS table,
@@ -84,7 +84,7 @@ module PgHero
84
84
  # also adds schema if missing
85
85
  def add_sequence_attributes(sequences)
86
86
  # fetch data
87
- sequence_attributes = select_all <<-SQL
87
+ sequence_attributes = select_all <<~SQL
88
88
  SELECT
89
89
  n.nspname AS schema,
90
90
  c.relname AS sequence,
@@ -115,7 +115,7 @@ module PgHero
115
115
  end
116
116
 
117
117
  # then populate attributes
118
- readable = Hash[sequence_attributes.map { |s| [[s[:schema], s[:sequence]], s[:readable]] }]
118
+ readable = sequence_attributes.to_h { |s| [[s[:schema], s[:sequence]], s[:readable]] }
119
119
  sequences.each do |sequence|
120
120
  sequence[:readable] = readable[[sequence[:schema], sequence[:sequence]]] || false
121
121
  end
@@ -30,7 +30,7 @@ module PgHero
30
30
  private
31
31
 
32
32
  def fetch_settings(names)
33
- Hash[names.map { |name| [name, select_one("SHOW #{name}")] }]
33
+ names.to_h { |name| [name, select_one("SHOW #{name}")] }
34
34
  end
35
35
  end
36
36
  end
@@ -6,7 +6,7 @@ module PgHero
6
6
  end
7
7
 
8
8
  def relation_sizes
9
- select_all_size <<-SQL
9
+ select_all_size <<~SQL
10
10
  SELECT
11
11
  n.nspname AS schema,
12
12
  c.relname AS relation,
@@ -27,7 +27,7 @@ module PgHero
27
27
  end
28
28
 
29
29
  def table_sizes
30
- select_all_size <<-SQL
30
+ select_all_size <<~SQL
31
31
  SELECT
32
32
  n.nspname AS schema,
33
33
  c.relname AS table,
@@ -49,10 +49,10 @@ module PgHero
49
49
  def space_growth(days: 7, relation_sizes: nil)
50
50
  if space_stats_enabled?
51
51
  relation_sizes ||= self.relation_sizes
52
- sizes = Hash[ relation_sizes.map { |r| [[r[:schema], r[:relation]], r[:size_bytes]] } ]
52
+ sizes = relation_sizes.to_h { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }
53
53
  start_at = days.days.ago
54
54
 
55
- stats = select_all_stats <<-SQL
55
+ stats = select_all_stats <<~SQL
56
56
  WITH t AS (
57
57
  SELECT
58
58
  schema,
@@ -92,10 +92,10 @@ module PgHero
92
92
  def relation_space_stats(relation, schema: "public")
93
93
  if space_stats_enabled?
94
94
  relation_sizes ||= self.relation_sizes
95
- sizes = Hash[ relation_sizes.map { |r| [[r[:schema], r[:relation]], r[:size_bytes]] } ]
95
+ sizes = relation_sizes.map { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }.to_h
96
96
  start_at = 30.days.ago
97
97
 
98
- stats = select_all_stats <<-SQL
98
+ stats = select_all_stats <<~SQL
99
99
  SELECT
100
100
  captured_at,
101
101
  size AS size_bytes
@@ -121,16 +121,22 @@ module PgHero
121
121
 
122
122
  def capture_space_stats
123
123
  now = Time.now
124
- columns = %w(database schema relation size captured_at)
125
- values = []
126
- relation_sizes.each do |rs|
127
- values << [id, rs[:schema], rs[:relation], rs[:size_bytes].to_i, now]
128
- end
129
- insert_stats("pghero_space_stats", columns, values) if values.any?
124
+ values =
125
+ relation_sizes.map do |rs|
126
+ {
127
+ database: id,
128
+ schema: rs[:schema],
129
+ relation: rs[:relation],
130
+ size: rs[:size_bytes].to_i,
131
+ captured_at: now
132
+ }
133
+ end
134
+ PgHero::SpaceStats.insert_all!(values) if values.any?
130
135
  end
131
136
 
132
- def clean_space_stats
133
- PgHero::SpaceStats.where(database: id).where("captured_at < ?", 90.days.ago).delete_all
137
+ def clean_space_stats(before: nil)
138
+ before ||= 90.days.ago
139
+ PgHero::SpaceStats.where(database: id).where("captured_at < ?", before).delete_all
134
140
  end
135
141
 
136
142
  def space_stats_enabled?