pghero 3.1.0 → 3.7.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -1
  3. data/LICENSE.txt +1 -1
  4. data/README.md +2 -2
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
  6. data/app/assets/javascripts/pghero/application.js +27 -12
  7. data/app/assets/javascripts/pghero/chartkick.js +834 -764
  8. data/app/assets/javascripts/pghero/highlight.min.js +440 -0
  9. data/app/assets/javascripts/pghero/jquery.js +318 -197
  10. data/app/assets/javascripts/pghero/nouislider.js +676 -1066
  11. data/app/assets/stylesheets/pghero/application.css +108 -2
  12. data/app/assets/stylesheets/pghero/nouislider.css +4 -10
  13. data/app/controllers/pg_hero/home_controller.rb +53 -16
  14. data/app/helpers/pg_hero/home_helper.rb +3 -3
  15. data/app/views/layouts/pg_hero/application.html.erb +3 -3
  16. data/app/views/pg_hero/home/_connections_table.html.erb +1 -1
  17. data/app/views/pg_hero/home/_live_queries_table.html.erb +8 -8
  18. data/app/views/pg_hero/home/_queries_table.html.erb +5 -5
  19. data/app/views/pg_hero/home/_query_stats_slider.html.erb +8 -8
  20. data/app/views/pg_hero/home/_suggested_index.html.erb +6 -5
  21. data/app/views/pg_hero/home/connections.html.erb +12 -12
  22. data/app/views/pg_hero/home/explain.html.erb +2 -2
  23. data/app/views/pg_hero/home/index.html.erb +22 -20
  24. data/app/views/pg_hero/home/index_bloat.html.erb +6 -6
  25. data/app/views/pg_hero/home/maintenance.html.erb +3 -3
  26. data/app/views/pg_hero/home/queries.html.erb +7 -5
  27. data/app/views/pg_hero/home/relation_space.html.erb +4 -4
  28. data/app/views/pg_hero/home/show_query.html.erb +35 -31
  29. data/app/views/pg_hero/home/space.html.erb +50 -46
  30. data/app/views/pg_hero/home/system.html.erb +18 -18
  31. data/app/views/pg_hero/home/tune.html.erb +2 -2
  32. data/lib/generators/pghero/query_stats_generator.rb +1 -0
  33. data/lib/generators/pghero/space_stats_generator.rb +1 -0
  34. data/lib/pghero/database.rb +2 -2
  35. data/lib/pghero/engine.rb +4 -3
  36. data/lib/pghero/methods/basic.rb +26 -31
  37. data/lib/pghero/methods/connections.rb +4 -4
  38. data/lib/pghero/methods/constraints.rb +1 -1
  39. data/lib/pghero/methods/explain.rb +4 -3
  40. data/lib/pghero/methods/indexes.rb +8 -8
  41. data/lib/pghero/methods/kill.rb +1 -1
  42. data/lib/pghero/methods/maintenance.rb +3 -3
  43. data/lib/pghero/methods/queries.rb +2 -2
  44. data/lib/pghero/methods/query_stats.rb +34 -24
  45. data/lib/pghero/methods/replication.rb +2 -2
  46. data/lib/pghero/methods/sequences.rb +10 -5
  47. data/lib/pghero/methods/settings.rb +8 -1
  48. data/lib/pghero/methods/space.rb +20 -14
  49. data/lib/pghero/methods/suggested_indexes.rb +14 -7
  50. data/lib/pghero/methods/system.rb +12 -6
  51. data/lib/pghero/methods/tables.rb +4 -5
  52. data/lib/pghero/version.rb +1 -1
  53. data/lib/pghero.rb +35 -36
  54. data/lib/tasks/pghero.rake +11 -1
  55. data/licenses/LICENSE-chart.js.txt +1 -1
  56. data/licenses/LICENSE-date-fns.txt +21 -20
  57. data/licenses/LICENSE-kurkle-color.txt +9 -0
  58. metadata +8 -11
  59. data/app/assets/javascripts/pghero/highlight.pack.js +0 -2
@@ -31,21 +31,26 @@ module PgHero
31
31
  end
32
32
 
33
33
  def quote_ident(value)
34
- connection.quote_column_name(value)
34
+ with_connection { |c| c.quote_column_name(value) }
35
35
  end
36
36
 
37
37
  private
38
38
 
39
- def select_all(sql, conn: nil, query_columns: [])
40
- conn ||= connection
39
+ def select_all(sql, stats: false, query_columns: [])
40
+ with_connection(stats: stats) do |conn|
41
+ select_all_leased(sql, conn: conn, query_columns: query_columns)
42
+ end
43
+ end
44
+
45
+ def select_all_leased(sql, conn:, query_columns:)
41
46
  # squish for logs
42
47
  retries = 0
43
48
  begin
44
49
  result = conn.select_all(add_source(squish(sql)))
45
- if ActiveRecord::VERSION::STRING.to_f >= 6.1
46
- result = result.map(&:symbolize_keys)
50
+ if ActiveRecord::VERSION::MAJOR >= 8
51
+ result = result.to_a.map(&:symbolize_keys)
47
52
  else
48
- result = result.map { |row| row.to_h { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] } }
53
+ result = result.map(&:symbolize_keys)
49
54
  end
50
55
  if filter_data
51
56
  query_columns.each do |column|
@@ -81,7 +86,7 @@ module PgHero
81
86
  end
82
87
 
83
88
  def select_all_stats(sql, **options)
84
- select_all(sql, **options, conn: stats_connection)
89
+ select_all(sql, **options, stats: true)
85
90
  end
86
91
 
87
92
  def select_all_size(sql)
@@ -92,35 +97,21 @@ module PgHero
92
97
  result
93
98
  end
94
99
 
95
- def select_one(sql, conn: nil)
96
- select_all(sql, conn: conn).first.values.first
97
- end
98
-
99
- def select_one_stats(sql)
100
- select_one(sql, conn: stats_connection)
100
+ def select_one(sql)
101
+ select_all(sql).first.values.first
101
102
  end
102
103
 
103
104
  def execute(sql)
104
- connection.execute(add_source(sql))
105
+ with_connection { |c| c.execute(add_source(sql)) }
105
106
  end
106
107
 
107
- def connection
108
- connection_model.connection
108
+ def with_connection(stats: false, &block)
109
+ model = stats ? ::PgHero::Stats : connection_model
110
+ model.connection_pool.with_connection(&block)
109
111
  end
110
112
 
111
- def stats_connection
112
- ::PgHero::Stats.connection
113
- end
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
113
  def squish(str)
123
- str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
114
+ str.to_s.squish
124
115
  end
125
116
 
126
117
  def add_source(sql)
@@ -128,11 +119,15 @@ module PgHero
128
119
  end
129
120
 
130
121
  def quote(value)
131
- connection.quote(value)
122
+ with_connection { |c| c.quote(value) }
132
123
  end
133
124
 
134
125
  def quote_table_name(value)
135
- connection.quote_table_name(value)
126
+ with_connection { |c| c.quote_table_name(value) }
127
+ end
128
+
129
+ def quote_column_name(value)
130
+ with_connection { |c| c.quote_column_name(value) }
136
131
  end
137
132
 
138
133
  def unquote(part)
@@ -153,7 +148,7 @@ module PgHero
153
148
  end
154
149
 
155
150
  def table_exists?(table)
156
- stats_connection.table_exists?(table)
151
+ with_connection(stats: true) { |c| c.table_exists?(table) }
157
152
  end
158
153
  end
159
154
  end
@@ -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
@@ -57,7 +57,7 @@ module PgHero
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,
@@ -9,10 +9,10 @@ module PgHero
9
9
 
10
10
  # use transaction for safety
11
11
  with_transaction(statement_timeout: (explain_timeout_sec * 1000).round, rollback: true) do
12
- if (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
12
+ if (sql.delete_suffix(";").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
13
13
  raise ActiveRecord::StatementInvalid, "Unsafe statement"
14
14
  end
15
- explanation = select_all("EXPLAIN #{sql}").map { |v| v[:"QUERY PLAN"] }.join("\n")
15
+ explanation = execute("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
16
16
  end
17
17
 
18
18
  explanation
@@ -20,11 +20,12 @@ module PgHero
20
20
 
21
21
  # TODO rename to explain in 4.0
22
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")
23
+ def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, generic_plan: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
24
24
  options = []
25
25
  add_explain_option(options, "ANALYZE", analyze)
26
26
  add_explain_option(options, "VERBOSE", verbose)
27
27
  add_explain_option(options, "SETTINGS", settings)
28
+ add_explain_option(options, "GENERIC_PLAN", generic_plan)
28
29
  add_explain_option(options, "COSTS", costs)
29
30
  add_explain_option(options, "BUFFERS", buffers)
30
31
  add_explain_option(options, "WAL", wal)
@@ -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
@@ -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,
@@ -162,19 +162,20 @@ module PgHero
162
162
  end
163
163
  end
164
164
 
165
- def clean_query_stats
166
- 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
167
168
  end
168
169
 
169
170
  def slow_queries(query_stats: nil, **options)
170
- query_stats ||= self.query_stats(options)
171
+ query_stats ||= self.query_stats(**options)
171
172
  query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
172
173
  end
173
174
 
174
- def query_hash_stats(query_hash, user: nil)
175
+ def query_hash_stats(query_hash, user: nil, current: false)
175
176
  if historical_query_stats_enabled? && supports_query_hash?
176
177
  start_at = 24.hours.ago
177
- select_all_stats <<-SQL
178
+ stats = select_all_stats <<~SQL
178
179
  SELECT
179
180
  captured_at,
180
181
  total_time / 1000 / 60 AS total_minutes,
@@ -191,6 +192,15 @@ module PgHero
191
192
  ORDER BY
192
193
  1 ASC
193
194
  SQL
195
+ if current
196
+ captured_at = Time.current
197
+ current_stats = current_query_stats(query_hash: query_hash, user: user, origin: true)
198
+ current_stats.each do |r|
199
+ r[:captured_at] = captured_at
200
+ end
201
+ stats += current_stats
202
+ end
203
+ stats
194
204
  else
195
205
  raise NotEnabled, "Query hash stats not enabled"
196
206
  end
@@ -199,12 +209,12 @@ module PgHero
199
209
  private
200
210
 
201
211
  # http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
202
- def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil)
212
+ def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil, user: nil, origin: false)
203
213
  if query_stats_enabled?
204
214
  limit ||= 100
205
215
  sort ||= "total_minutes"
206
216
  total_time = server_version_num >= 130000 ? "(total_plan_time + total_exec_time)" : "total_time"
207
- query = <<-SQL
217
+ query = <<~SQL
208
218
  WITH query_stats AS (
209
219
  SELECT
210
220
  LEFT(query, 10000) AS query,
@@ -223,10 +233,12 @@ module PgHero
223
233
  calls > 0 AND
224
234
  pg_database.datname = #{database ? quote(database) : "current_database()"}
225
235
  #{query_hash ? "AND queryid = #{quote(query_hash)}" : nil}
236
+ #{user ? "AND rolname = #{quote(user)}" : nil}
226
237
  )
227
238
  SELECT
228
239
  query,
229
240
  query AS explainable_query,
241
+ #{origin ? "(SELECT regexp_matches(query, '.*/\\*(.+?)\\*/'))[1] AS origin," : nil}
230
242
  query_hash,
231
243
  query_stats.user,
232
244
  total_minutes,
@@ -237,7 +249,7 @@ module PgHero
237
249
  FROM
238
250
  query_stats
239
251
  ORDER BY
240
- #{quote_table_name(sort)} DESC
252
+ #{quote_column_name(sort)} DESC
241
253
  LIMIT #{limit.to_i}
242
254
  SQL
243
255
 
@@ -253,7 +265,7 @@ module PgHero
253
265
  def historical_query_stats(sort: nil, start_at: nil, end_at: nil, query_hash: nil)
254
266
  if historical_query_stats_enabled?
255
267
  sort ||= "total_minutes"
256
- query = <<-SQL
268
+ query = <<~SQL
257
269
  WITH query_stats AS (
258
270
  SELECT
259
271
  #{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
@@ -286,7 +298,7 @@ module PgHero
286
298
  FROM
287
299
  query_stats
288
300
  ORDER BY
289
- #{quote_table_name(sort)} DESC
301
+ #{quote_column_name(sort)} DESC
290
302
  LIMIT 100
291
303
  SQL
292
304
 
@@ -310,14 +322,14 @@ module PgHero
310
322
  all_queries_total_minutes: stats2.sum { |s| s[:all_queries_total_minutes] }
311
323
  }
312
324
  value[:total_percent] = value[:total_minutes] * 100.0 / value[:all_queries_total_minutes]
313
- value[:explainable_query] = stats2.map { |s| s[:explainable_query] }.select { |q| q && explainable?(q) }.first
325
+ value[:explainable_query] = stats2.map { |s| s[:explainable_query] }.find { |q| q && explainable?(q) }
314
326
  query_stats << value
315
327
  end
316
328
  query_stats
317
329
  end
318
330
 
319
331
  def explainable?(query)
320
- query =~ /select/i && !query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i
332
+ query =~ /select/i && (server_version_num >= 160000 || (!query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i))
321
333
  end
322
334
 
323
335
  # removes comments
@@ -329,19 +341,17 @@ module PgHero
329
341
  def insert_query_stats(db_id, db_query_stats, now)
330
342
  values =
331
343
  db_query_stats.map do |qs|
332
- [
333
- db_id,
334
- qs[:query],
335
- qs[:total_minutes] * 60 * 1000,
336
- qs[:calls],
337
- now,
338
- supports_query_hash? ? qs[:query_hash] : nil,
339
- qs[:user]
340
- ]
344
+ {
345
+ database: db_id,
346
+ query: qs[:query],
347
+ total_time: qs[:total_minutes] * 60 * 1000,
348
+ calls: qs[:calls],
349
+ captured_at: now,
350
+ query_hash: supports_query_hash? ? qs[:query_hash] : nil,
351
+ user: qs[:user]
352
+ }
341
353
  end
342
-
343
- columns = %w[database query total_time calls captured_at query_hash user]
344
- insert_stats("pghero_query_stats", columns, values)
354
+ PgHero::QueryStats.insert_all!(values)
345
355
  end
346
356
  end
347
357
  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,
@@ -39,14 +39,19 @@ module PgHero
39
39
 
40
40
  add_sequence_attributes(sequences)
41
41
 
42
- sequences.select { |s| s[:readable] }.each_slice(1024) do |slice|
43
- sql = slice.map { |s| "SELECT last_value FROM #{quote_ident(s[:schema])}.#{quote_ident(s[:sequence])}" }.join(" UNION ALL ")
42
+ last_value = {}
43
+ sequences.select { |s| s[:readable] }.map { |s| [s[:schema], s[:sequence]] }.uniq.each_slice(1024) do |slice|
44
+ sql = slice.map { |s| "SELECT last_value FROM #{quote_ident(s[0])}.#{quote_ident(s[1])}" }.join(" UNION ALL ")
44
45
 
45
46
  select_all(sql).zip(slice) do |row, seq|
46
- seq[:last_value] = row[:last_value]
47
+ last_value[seq] = row[:last_value]
47
48
  end
48
49
  end
49
50
 
51
+ sequences.select { |s| s[:readable] }.each do |seq|
52
+ seq[:last_value] = last_value[[seq[:schema], seq[:sequence]]]
53
+ end
54
+
50
55
  # use to_s for unparsable sequences
51
56
  sequences.sort_by { |s| s[:sequence].to_s }
52
57
  end
@@ -84,7 +89,7 @@ module PgHero
84
89
  # also adds schema if missing
85
90
  def add_sequence_attributes(sequences)
86
91
  # fetch data
87
- sequence_attributes = select_all <<-SQL
92
+ sequence_attributes = select_all <<~SQL
88
93
  SELECT
89
94
  n.nspname AS schema,
90
95
  c.relname AS sequence,
@@ -3,7 +3,14 @@ module PgHero
3
3
  module Settings
4
4
  def settings
5
5
  names =
6
- if server_version_num >= 90500
6
+ if server_version_num >= 100000
7
+ %i(
8
+ max_connections shared_buffers effective_cache_size maintenance_work_mem
9
+ checkpoint_completion_target wal_buffers default_statistics_target
10
+ random_page_cost effective_io_concurrency work_mem huge_pages
11
+ min_wal_size max_wal_size
12
+ )
13
+ elsif server_version_num >= 90500
7
14
  %i(
8
15
  max_connections shared_buffers effective_cache_size work_mem
9
16
  maintenance_work_mem min_wal_size max_wal_size checkpoint_completion_target
@@ -6,11 +6,11 @@ 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,
13
- CASE WHEN c.relkind = 'r' THEN 'table' ELSE 'index' END AS type,
13
+ CASE c.relkind WHEN 'r' THEN 'table' WHEN 'm' then 'matview' ELSE 'index' END AS type,
14
14
  pg_table_size(c.oid) AS size_bytes
15
15
  FROM
16
16
  pg_class c
@@ -19,7 +19,7 @@ module PgHero
19
19
  WHERE
20
20
  n.nspname NOT IN ('pg_catalog', 'information_schema')
21
21
  AND n.nspname !~ '^pg_toast'
22
- AND c.relkind IN ('r', 'i')
22
+ AND c.relkind IN ('r', 'm', 'i')
23
23
  ORDER BY
24
24
  pg_table_size(c.oid) DESC,
25
25
  2 ASC
@@ -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,
@@ -52,7 +52,7 @@ module PgHero
52
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,
@@ -95,7 +95,7 @@ module PgHero
95
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?
@@ -46,7 +46,7 @@ module PgHero
46
46
  indexes += existing_columns["brin"][index[:table]]
47
47
  end
48
48
 
49
- covering_index = indexes.find { |e| index_covers?(e.map { |v| v.sub(/ inet_ops\z/, "") }, index[:columns]) }
49
+ covering_index = indexes.find { |e| index_covers?(e.map { |v| v.delete_suffix(" inet_ops") }, index[:columns]) }
50
50
  if covering_index
51
51
  best_index[:covering_index] = covering_index
52
52
  best_index[:explanation] = "Covered by index on (#{covering_index.join(", ")})"
@@ -79,7 +79,9 @@ module PgHero
79
79
  suggested_indexes.each do |index|
80
80
  p index
81
81
  if create
82
- connection.execute("CREATE INDEX CONCURRENTLY ON #{quote_table_name(index[:table])} (#{index[:columns].map { |c| quote_table_name(c) }.join(",")})")
82
+ with_connection do |connection|
83
+ connection.execute("CREATE INDEX CONCURRENTLY ON #{quote_table_name(index[:table])} (#{index[:columns].map { |c| quote_column_name(c) }.join(",")})")
84
+ end
83
85
  end
84
86
  end
85
87
  end
@@ -194,6 +196,7 @@ module PgHero
194
196
  end
195
197
 
196
198
  def best_index_structure(statement)
199
+ return {error: "Empty statement"} if statement.to_s.empty?
197
200
  return {error: "Too large"} if statement.to_s.length > 10000
198
201
 
199
202
  begin
@@ -286,27 +289,31 @@ module PgHero
286
289
  else
287
290
  raise "Not Implemented"
288
291
  end
289
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr.name.first.string.str)
290
- [{column: aexpr.lexpr.column_ref.fields.last.string.str, op: aexpr.name.first.string.str}]
292
+ elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr.name.first.string.send(str_method))
293
+ [{column: aexpr.lexpr.column_ref.fields.last.string.send(str_method), op: aexpr.name.first.string.send(str_method)}]
291
294
  elsif tree.null_test
292
295
  op = tree.null_test.nulltesttype == :IS_NOT_NULL ? "not_null" : "null"
293
- [{column: tree.null_test.arg.column_ref.fields.last.string.str, op: op}]
296
+ [{column: tree.null_test.arg.column_ref.fields.last.string.send(str_method), op: op}]
294
297
  else
295
298
  raise "Not Implemented"
296
299
  end
297
300
  end
298
301
 
302
+ def str_method
303
+ @str_method ||= Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("4") ? :sval : :str
304
+ end
305
+
299
306
  def parse_sort(sort_clause)
300
307
  sort_clause.map do |v|
301
308
  {
302
- column: v.sort_by.node.column_ref.fields.last.string.str,
309
+ column: v.sort_by.node.column_ref.fields.last.string.send(str_method),
303
310
  direction: v.sort_by.sortby_dir == :SORTBY_DESC ? "desc" : "asc"
304
311
  }
305
312
  end
306
313
  end
307
314
 
308
315
  def column_stats(schema: nil, table: nil)
309
- select_all <<-SQL
316
+ select_all <<~SQL
310
317
  SELECT
311
318
  schemaname AS schema,
312
319
  tablename AS table,