pghero 1.5.3 → 2.0.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +75 -0
  4. data/Gemfile +1 -1
  5. data/README.md +8 -14
  6. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  7. data/app/assets/javascripts/pghero/application.js +9 -0
  8. data/app/assets/javascripts/pghero/chartkick.js +511 -152
  9. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  10. data/app/assets/stylesheets/pghero/application.css +66 -2
  11. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  12. data/app/controllers/pg_hero/home_controller.rb +166 -63
  13. data/app/helpers/pg_hero/base_helper.rb +15 -0
  14. data/app/views/layouts/pg_hero/application.html.erb +4 -3
  15. data/app/views/pg_hero/home/_connections_table.html.erb +2 -9
  16. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -8
  17. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  18. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  19. data/app/views/pg_hero/home/connections.html.erb +19 -1
  20. data/app/views/pg_hero/home/explain.html.erb +1 -1
  21. data/app/views/pg_hero/home/index.html.erb +125 -66
  22. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  23. data/app/views/pg_hero/home/live_queries.html.erb +3 -1
  24. data/app/views/pg_hero/home/maintenance.html.erb +11 -7
  25. data/app/views/pg_hero/home/queries.html.erb +10 -0
  26. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  27. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  28. data/app/views/pg_hero/home/space.html.erb +64 -10
  29. data/app/views/pg_hero/home/system.html.erb +16 -4
  30. data/config/routes.rb +4 -2
  31. data/guides/Linux.md +20 -0
  32. data/guides/Rails.md +30 -4
  33. data/guides/Suggested-Indexes.md +1 -1
  34. data/lib/generators/pghero/config_generator.rb +13 -0
  35. data/lib/generators/pghero/query_stats_generator.rb +7 -1
  36. data/lib/generators/pghero/space_stats_generator.rb +7 -1
  37. data/lib/generators/pghero/templates/config.yml +23 -0
  38. data/lib/generators/pghero/templates/query_stats.rb +1 -1
  39. data/lib/generators/pghero/templates/space_stats.rb +1 -1
  40. data/lib/pghero/database.rb +6 -2
  41. data/lib/pghero/engine.rb +10 -7
  42. data/lib/pghero/methods/basic.rb +83 -11
  43. data/lib/pghero/methods/connections.rb +16 -56
  44. data/lib/pghero/methods/explain.rb +3 -5
  45. data/lib/pghero/methods/indexes.rb +179 -21
  46. data/lib/pghero/methods/kill.rb +2 -2
  47. data/lib/pghero/methods/maintenance.rb +34 -20
  48. data/lib/pghero/methods/queries.rb +28 -45
  49. data/lib/pghero/methods/query_stats.rb +97 -91
  50. data/lib/pghero/methods/{replica.rb → replication.rb} +20 -4
  51. data/lib/pghero/methods/sequences.rb +4 -5
  52. data/lib/pghero/methods/space.rb +101 -8
  53. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  54. data/lib/pghero/methods/system.rb +23 -13
  55. data/lib/pghero/methods/tables.rb +9 -9
  56. data/lib/pghero/methods/users.rb +10 -12
  57. data/lib/pghero/version.rb +1 -1
  58. data/lib/pghero.rb +34 -36
  59. data/lib/tasks/pghero.rake +5 -0
  60. data/test/basic_test.rb +38 -0
  61. data/test/best_index_test.rb +3 -3
  62. data/test/suggested_indexes_test.rb +0 -2
  63. data/test/test_helper.rb +38 -40
  64. metadata +14 -7
  65. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  66. data/test/explain_test.rb +0 -18
@@ -2,29 +2,29 @@ module PgHero
2
2
  module Methods
3
3
  module SuggestedIndexes
4
4
  def suggested_indexes_enabled?
5
- defined?(PgQuery) && query_stats_enabled?
5
+ defined?(PgQuery) && Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("0.9.0") && query_stats_enabled?
6
6
  end
7
7
 
8
8
  # TODO clean this mess
9
- def suggested_indexes_by_query(options = {})
9
+ def suggested_indexes_by_query(queries: nil, query_stats: nil, indexes: nil)
10
10
  best_indexes = {}
11
11
 
12
12
  if suggested_indexes_enabled?
13
13
  # get most time-consuming queries
14
- queries = options[:queries] || (options[:query_stats] || query_stats(historical: true, start_at: 24.hours.ago)).map { |qs| qs["query"] }
14
+ queries ||= (query_stats || self.query_stats(historical: true, start_at: 24.hours.ago)).map { |qs| qs[:query] }
15
15
 
16
16
  # get best indexes for queries
17
17
  best_indexes = best_index_helper(queries)
18
18
 
19
19
  if best_indexes.any?
20
20
  existing_columns = Hash.new { |hash, key| hash[key] = Hash.new { |hash2, key2| hash2[key2] = [] } }
21
- indexes = self.indexes
22
- indexes.group_by { |g| g["using"] }.each do |group, inds|
21
+ indexes ||= self.indexes
22
+ indexes.group_by { |g| g[:using] }.each do |group, inds|
23
23
  inds.each do |i|
24
- existing_columns[group][i["table"]] << i["columns"]
24
+ existing_columns[group][i[:table]] << i[:columns]
25
25
  end
26
26
  end
27
- indexes_by_table = indexes.group_by { |i| i["table"] }
27
+ indexes_by_table = indexes.group_by { |i| i[:table] }
28
28
 
29
29
  best_indexes.each do |_query, best_index|
30
30
  if best_index[:found]
@@ -38,15 +38,17 @@ module PgHero
38
38
  end
39
39
  end
40
40
  end
41
+ else
42
+ raise NotEnabled, "Suggested indexes not enabled"
41
43
  end
42
44
 
43
45
  best_indexes
44
46
  end
45
47
 
46
- def suggested_indexes(options = {})
48
+ def suggested_indexes(suggested_indexes_by_query: nil, **options)
47
49
  indexes = []
48
50
 
49
- (options[:suggested_indexes_by_query] || suggested_indexes_by_query(options)).select { |_s, i| i[:found] && !i[:covering_index] }.group_by { |_s, i| i[:index] }.each do |index, group|
51
+ (suggested_indexes_by_query || self.suggested_indexes_by_query(options)).select { |_s, i| i[:found] && !i[:covering_index] }.group_by { |_s, i| i[:index] }.each do |index, group|
50
52
  details = {}
51
53
  group.map(&:second).each do |g|
52
54
  details = details.except(:index).deep_merge(g)
@@ -57,25 +59,16 @@ module PgHero
57
59
  indexes.sort_by { |i| [i[:table], i[:columns]] }
58
60
  end
59
61
 
60
- def autoindex(options = {})
62
+ def autoindex(create: false)
61
63
  suggested_indexes.each do |index|
62
64
  p index
63
- if options[:create]
65
+ if create
64
66
  connection.execute("CREATE INDEX CONCURRENTLY ON #{quote_table_name(index[:table])} (#{index[:columns].map { |c| quote_table_name(c) }.join(",")})")
65
67
  end
66
68
  end
67
69
  end
68
70
 
69
- def autoindex_all(options = {})
70
- config["databases"].keys.each do |database|
71
- with(database) do
72
- puts "Autoindexing #{database}..."
73
- autoindex(options)
74
- end
75
- end
76
- end
77
-
78
- def best_index(statement, _options = {})
71
+ def best_index(statement)
79
72
  best_index_helper([statement])[statement]
80
73
  end
81
74
 
@@ -95,8 +88,8 @@ module PgHero
95
88
  # TODO get schema from query structure, then try search path
96
89
  schema = connection_model.connection_config[:schema] || "public"
97
90
  if tables.any?
98
- row_stats = Hash[table_stats(table: tables, schema: schema).map { |i| [i["table"], i["reltuples"]] }]
99
- col_stats = column_stats(table: tables, schema: schema).group_by { |i| i["table"] }
91
+ row_stats = Hash[table_stats(table: tables, schema: schema).map { |i| [i[:table], i[:estimated_rows]] }]
92
+ col_stats = column_stats(table: tables, schema: schema).group_by { |i| i[:table] }
100
93
  end
101
94
 
102
95
  # find best index based on query structure and column stats
@@ -117,7 +110,7 @@ module PgHero
117
110
  total_rows = row_stats[table].to_i
118
111
  index[:rows] = total_rows
119
112
 
120
- ranks = Hash[col_stats[table].to_a.map { |r| [r["column"], r] }]
113
+ ranks = Hash[col_stats[table].to_a.map { |r| [r[:column], r] }]
121
114
  columns = (where + sort).map { |c| c[:column] }.uniq
122
115
 
123
116
  if columns.any?
@@ -188,9 +181,7 @@ module PgHero
188
181
  return {error: "Too large"} if statement.to_s.length > 10000
189
182
 
190
183
  begin
191
- parsed_statement = PgQuery.parse(statement)
192
- v2 = parsed_statement.respond_to?(:tree)
193
- tree = v2 ? parsed_statement.tree : parsed_statement.parsetree
184
+ tree = PgQuery.parse(statement).tree
194
185
  rescue PgQuery::ParseError
195
186
  return {error: "Parse error"}
196
187
  end
@@ -201,27 +192,23 @@ module PgHero
201
192
  unless table
202
193
  error =
203
194
  case tree.keys.first
204
- when "InsertStmt", "INSERT INTO"
195
+ when "InsertStmt"
205
196
  "INSERT statement"
206
- when "VariableSetStmt", "SET"
197
+ when "VariableSetStmt"
207
198
  "SET statement"
208
199
  when "SelectStmt"
209
200
  if (tree["SelectStmt"]["fromClause"].first["JoinExpr"] rescue false)
210
201
  "JOIN not supported yet"
211
202
  end
212
- when "SELECT"
213
- if (tree["SELECT"]["fromClause"].first["JOINEXPR"] rescue false)
214
- "JOIN not supported yet"
215
- end
216
203
  end
217
204
  return {error: error || "Unknown structure"}
218
205
  end
219
206
 
220
207
  select = tree.values.first
221
- where = (select["whereClause"] ? parse_where(select["whereClause"], v2) : []) rescue nil
208
+ where = (select["whereClause"] ? parse_where(select["whereClause"]) : []) rescue nil
222
209
  return {error: "Unknown structure"} unless where
223
210
 
224
- sort = (select["sortClause"] ? parse_sort(select["sortClause"], v2) : []) rescue []
211
+ sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue []
225
212
 
226
213
  {table: table, where: where, sort: sort}
227
214
  end
@@ -235,22 +222,22 @@ module PgHero
235
222
  def row_estimates(stats, total_rows, rows_left, op)
236
223
  case op
237
224
  when "null"
238
- rows_left * stats["null_frac"].to_f
225
+ rows_left * stats[:null_frac].to_f
239
226
  when "not_null"
240
- rows_left * (1 - stats["null_frac"].to_f)
227
+ rows_left * (1 - stats[:null_frac].to_f)
241
228
  else
242
- rows_left *= (1 - stats["null_frac"].to_f)
229
+ rows_left *= (1 - stats[:null_frac].to_f)
243
230
  ret =
244
- if stats["n_distinct"].to_f == 0
231
+ if stats[:n_distinct].to_f == 0
245
232
  0
246
- elsif stats["n_distinct"].to_f < 0
233
+ elsif stats[:n_distinct].to_f < 0
247
234
  if total_rows > 0
248
- (-1 / stats["n_distinct"].to_f) * (rows_left / total_rows.to_f)
235
+ (-1 / stats[:n_distinct].to_f) * (rows_left / total_rows.to_f)
249
236
  else
250
237
  0
251
238
  end
252
239
  else
253
- rows_left / stats["n_distinct"].to_f
240
+ rows_left / stats[:n_distinct].to_f
254
241
  end
255
242
 
256
243
  case op
@@ -272,85 +259,39 @@ module PgHero
272
259
  tree["DeleteStmt"]["relation"]["RangeVar"]["relname"]
273
260
  when "UpdateStmt"
274
261
  tree["UpdateStmt"]["relation"]["RangeVar"]["relname"]
275
- when "SELECT"
276
- tree["SELECT"]["fromClause"].first["RANGEVAR"]["relname"]
277
- when "DELETE FROM"
278
- tree["DELETE FROM"]["relation"]["RANGEVAR"]["relname"]
279
- when "UPDATE"
280
- tree["UPDATE"]["relation"]["RANGEVAR"]["relname"]
281
262
  end
282
263
  end
283
264
 
284
265
  # TODO capture values
285
- def parse_where(tree, v2 = false)
286
- if v2
287
- aexpr = tree["A_Expr"]
266
+ def parse_where(tree)
267
+ aexpr = tree["A_Expr"]
288
268
 
289
- if tree["BoolExpr"]
290
- if tree["BoolExpr"]["boolop"] == 0
291
- tree["BoolExpr"]["args"].flat_map { |v| parse_where(v, v2) }
292
- else
293
- raise "Not Implemented"
294
- end
295
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first["String"]["str"])
296
- [{column: aexpr["lexpr"]["ColumnRef"]["fields"].last["String"]["str"], op: aexpr["name"].first["String"]["str"]}]
297
- elsif tree["NullTest"]
298
- op = tree["NullTest"]["nulltesttype"] == 1 ? "not_null" : "null"
299
- [{column: tree["NullTest"]["arg"]["ColumnRef"]["fields"].last["String"]["str"], op: op}]
269
+ if tree["BoolExpr"]
270
+ if tree["BoolExpr"]["boolop"] == 0
271
+ tree["BoolExpr"]["args"].flat_map { |v| parse_where(v) }
300
272
  else
301
273
  raise "Not Implemented"
302
274
  end
275
+ elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first["String"]["str"])
276
+ [{column: aexpr["lexpr"]["ColumnRef"]["fields"].last["String"]["str"], op: aexpr["name"].first["String"]["str"]}]
277
+ elsif tree["NullTest"]
278
+ op = tree["NullTest"]["nulltesttype"] == 1 ? "not_null" : "null"
279
+ [{column: tree["NullTest"]["arg"]["ColumnRef"]["fields"].last["String"]["str"], op: op}]
303
280
  else
304
- aexpr = tree["AEXPR"] || tree[nil]
305
-
306
- if tree["BOOLEXPR"]
307
- if tree["BOOLEXPR"]["boolop"] == 0
308
- tree["BOOLEXPR"]["args"].flat_map { |v| parse_where(v) }
309
- else
310
- raise "Not Implemented"
311
- end
312
- elsif tree["AEXPR AND"]
313
- left = parse_where(tree["AEXPR AND"]["lexpr"])
314
- right = parse_where(tree["AEXPR AND"]["rexpr"])
315
- if left && right
316
- left + right
317
- else
318
- raise "Not Implemented"
319
- end
320
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first)
321
- [{column: aexpr["lexpr"]["COLUMNREF"]["fields"].last, op: aexpr["name"].first}]
322
- elsif tree["AEXPR IN"] && ["=", "<>"].include?(tree["AEXPR IN"]["name"].first)
323
- [{column: tree["AEXPR IN"]["lexpr"]["COLUMNREF"]["fields"].last, op: tree["AEXPR IN"]["name"].first}]
324
- elsif tree["NULLTEST"]
325
- op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
326
- [{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
327
- else
328
- raise "Not Implemented"
329
- end
281
+ raise "Not Implemented"
330
282
  end
331
283
  end
332
284
 
333
- def parse_sort(sort_clause, v2)
334
- if v2
335
- sort_clause.map do |v|
336
- {
337
- column: v["SortBy"]["node"]["ColumnRef"]["fields"].last["String"]["str"],
338
- direction: v["SortBy"]["sortby_dir"] == 2 ? "desc" : "asc"
339
- }
340
- end
341
- else
342
- sort_clause.map do |v|
343
- {
344
- column: v["SORTBY"]["node"]["COLUMNREF"]["fields"].last,
345
- direction: v["SORTBY"]["sortby_dir"] == 2 ? "desc" : "asc"
346
- }
347
- end
285
+ def parse_sort(sort_clause)
286
+ sort_clause.map do |v|
287
+ {
288
+ column: v["SortBy"]["node"]["ColumnRef"]["fields"].last["String"]["str"],
289
+ direction: v["SortBy"]["sortby_dir"] == 2 ? "desc" : "asc"
290
+ }
348
291
  end
349
292
  end
350
293
 
351
- def column_stats(options = {})
352
- schema = options[:schema]
353
- tables = options[:table] ? Array(options[:table]) : nil
294
+ def column_stats(schema: nil, table: nil)
354
295
  select_all <<-SQL
355
296
  SELECT
356
297
  schemaname AS schema,
@@ -361,8 +302,8 @@ module PgHero
361
302
  FROM
362
303
  pg_stats
363
304
  WHERE
364
- #{tables ? "tablename IN (#{tables.map { |t| quote(t) }.join(", ")})" : "1 = 1"}
365
- AND schemaname = #{quote(schema)}
305
+ schemaname = #{quote(schema)}
306
+ #{table ? "AND tablename IN (#{Array(table).map { |t| quote(t) }.join(", ")})" : ""}
366
307
  ORDER BY
367
308
  1, 2, 3
368
309
  SQL
@@ -1,38 +1,48 @@
1
1
  module PgHero
2
2
  module Methods
3
3
  module System
4
- def cpu_usage(options = {})
4
+ def cpu_usage(**options)
5
5
  rds_stats("CPUUtilization", options)
6
6
  end
7
7
 
8
- def connection_stats(options = {})
8
+ def connection_stats(**options)
9
9
  rds_stats("DatabaseConnections", options)
10
10
  end
11
11
 
12
- def replication_lag_stats(options = {})
12
+ def replication_lag_stats(**options)
13
13
  rds_stats("ReplicaLag", options)
14
14
  end
15
15
 
16
- def read_iops_stats(options = {})
16
+ def read_iops_stats(**options)
17
17
  rds_stats("ReadIOPS", options)
18
18
  end
19
19
 
20
- def write_iops_stats(options = {})
20
+ def write_iops_stats(**options)
21
21
  rds_stats("WriteIOPS", options)
22
22
  end
23
23
 
24
- def rds_stats(metric_name, options = {})
24
+ def free_space_stats(**options)
25
+ rds_stats("FreeStorageSpace", options)
26
+ end
27
+
28
+ def rds_stats(metric_name, duration: nil, period: nil, offset: nil)
25
29
  if system_stats_enabled?
30
+ aws_options = {region: region}
31
+ if access_key_id
32
+ aws_options[:access_key_id] = access_key_id
33
+ aws_options[:secret_access_key] = secret_access_key
34
+ end
35
+
26
36
  client =
27
37
  if defined?(Aws)
28
- Aws::CloudWatch::Client.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region)
38
+ Aws::CloudWatch::Client.new(aws_options)
29
39
  else
30
- AWS::CloudWatch.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region).client
40
+ AWS::CloudWatch.new(aws_options).client
31
41
  end
32
42
 
33
- duration = (options[:duration] || 1.hour).to_i
34
- period = (options[:period] || 1.minute).to_i
35
- offset = (options[:offset] || 0).to_i
43
+ duration = (duration || 1.hour).to_i
44
+ period = (period || 1.minute).to_i
45
+ offset = (offset || 0).to_i
36
46
 
37
47
  end_time = (Time.now - offset)
38
48
  # ceil period
@@ -53,12 +63,12 @@ module PgHero
53
63
  end
54
64
  data
55
65
  else
56
- {}
66
+ raise NotEnabled, "System stats not enabled"
57
67
  end
58
68
  end
59
69
 
60
70
  def system_stats_enabled?
61
- !!((defined?(Aws) || defined?(AWS)) && access_key_id && secret_access_key && db_instance_identifier)
71
+ !!((defined?(Aws) || defined?(AWS)) && db_instance_identifier)
62
72
  end
63
73
 
64
74
  def access_key_id
@@ -2,18 +2,19 @@ module PgHero
2
2
  module Methods
3
3
  module Tables
4
4
  def table_hit_rate
5
- select_all(<<-SQL
5
+ select_one(<<-SQL
6
6
  SELECT
7
7
  sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) AS rate
8
8
  FROM
9
9
  pg_statio_user_tables
10
10
  SQL
11
- ).first["rate"].to_f
11
+ )
12
12
  end
13
13
 
14
14
  def table_caching
15
15
  select_all <<-SQL
16
16
  SELECT
17
+ schemaname AS schema,
17
18
  relname AS table,
18
19
  CASE WHEN heap_blks_hit + heap_blks_read = 0 THEN
19
20
  0
@@ -32,7 +33,7 @@ module PgHero
32
33
  SELECT
33
34
  schemaname AS schema,
34
35
  relname AS table,
35
- n_live_tup rows_in_table
36
+ n_live_tup AS estimated_rows
36
37
  FROM
37
38
  pg_stat_user_tables
38
39
  WHERE
@@ -43,22 +44,21 @@ module PgHero
43
44
  SQL
44
45
  end
45
46
 
46
- def table_stats(options = {})
47
- schema = options[:schema]
48
- tables = options[:table] ? Array(options[:table]) : nil
47
+ def table_stats(schema: nil, table: nil)
49
48
  select_all <<-SQL
50
49
  SELECT
51
50
  nspname AS schema,
52
51
  relname AS table,
53
- reltuples::bigint
52
+ reltuples::bigint AS estimated_rows,
53
+ pg_total_relation_size(pg_class.oid) AS size_bytes
54
54
  FROM
55
55
  pg_class
56
56
  INNER JOIN
57
57
  pg_namespace ON pg_namespace.oid = pg_class.relnamespace
58
58
  WHERE
59
59
  relkind = 'r'
60
- AND nspname = #{quote(schema)}
61
- #{tables ? "AND relname IN (#{tables.map { |t| quote(t) }.join(", ")})" : nil}
60
+ #{schema ? "AND nspname = #{quote(schema)}" : nil}
61
+ #{table ? "AND relname IN (#{Array(table).map { |t| quote(t) }.join(", ")})" : nil}
62
62
  ORDER BY
63
63
  1, 2
64
64
  SQL
@@ -1,10 +1,9 @@
1
1
  module PgHero
2
2
  module Methods
3
3
  module Users
4
- def create_user(user, options = {})
5
- password = options[:password] || random_password
6
- schema = options[:schema] || "public"
7
- database = options[:database] || connection_model.connection_config[:database]
4
+ def create_user(user, password: nil, schema: "public", database: nil, readonly: false, tables: nil)
5
+ password ||= random_password
6
+ database ||= connection_model.connection_config[:database]
8
7
 
9
8
  commands =
10
9
  [
@@ -12,16 +11,16 @@ module PgHero
12
11
  "GRANT CONNECT ON DATABASE #{database} TO #{user}",
13
12
  "GRANT USAGE ON SCHEMA #{schema} TO #{user}"
14
13
  ]
15
- if options[:readonly]
16
- if options[:tables]
17
- commands.concat table_grant_commands("SELECT", options[:tables], user)
14
+ if readonly
15
+ if tables
16
+ commands.concat table_grant_commands("SELECT", tables, user)
18
17
  else
19
18
  commands << "GRANT SELECT ON ALL TABLES IN SCHEMA #{schema} TO #{user}"
20
19
  commands << "ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema} GRANT SELECT ON TABLES TO #{user}"
21
20
  end
22
21
  else
23
- if options[:tables]
24
- commands.concat table_grant_commands("ALL PRIVILEGES", options[:tables], user)
22
+ if tables
23
+ commands.concat table_grant_commands("ALL PRIVILEGES", tables, user)
25
24
  else
26
25
  commands << "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA #{schema} TO #{user}"
27
26
  commands << "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA #{schema} TO #{user}"
@@ -40,9 +39,8 @@ module PgHero
40
39
  {password: password}
41
40
  end
42
41
 
43
- def drop_user(user, options = {})
44
- schema = options[:schema] || "public"
45
- database = options[:database] || connection_model.connection_config[:database]
42
+ def drop_user(user, schema: "public", database: nil)
43
+ database ||= connection_model.connection_config[:database]
46
44
 
47
45
  # thanks shiftb
48
46
  commands =
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.5.3"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/pghero.rb CHANGED
@@ -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, :autoindex, :autoindex_all, :autovacuum_danger,
44
- :best_index, :blocked_queries, :capture_query_stats, :connection_sources, :connection_stats,
45
+ def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
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,14 +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 =
69
- (YAML.load(ERB.new(File.read(path)).result)[env] 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
73
+ config ||= {}
70
74
 
71
- if config
75
+ if config[env]
76
+ config[env]
77
+ elsif config["databases"] # preferred format
72
78
  config
79
+ elsif config_file_exists
80
+ raise "Invalid config file"
73
81
  else
74
82
  {
75
83
  "databases" => {
@@ -87,7 +95,7 @@ module PgHero
87
95
  @databases ||= begin
88
96
  Hash[
89
97
  config["databases"].map do |id, c|
90
- [id, PgHero::Database.new(id, c)]
98
+ [id.to_sym, PgHero::Database.new(id, c)]
91
99
  end
92
100
  ]
93
101
  end
@@ -97,26 +105,6 @@ module PgHero
97
105
  databases.values.first
98
106
  end
99
107
 
100
- def current_database
101
- Thread.current[:pghero_current_database] ||= primary_database
102
- end
103
-
104
- def current_database=(database)
105
- raise "Database not found" unless databases[database.to_s]
106
- Thread.current[:pghero_current_database] = databases[database.to_s]
107
- database
108
- end
109
-
110
- def with(database)
111
- previous_database = current_database
112
- begin
113
- self.current_database = database
114
- yield
115
- ensure
116
- self.current_database = previous_database.id
117
- end
118
- end
119
-
120
108
  def capture_query_stats
121
109
  databases.each do |_, database|
122
110
  database.capture_query_stats
@@ -131,13 +119,23 @@ module PgHero
131
119
  true
132
120
  end
133
121
 
134
- # Handles Rails 4 ('t') and Rails 5 (true) values.
135
- def truthy?(value)
136
- value == true || value == 't'
122
+ def analyze_all(**options)
123
+ databases.reject { |_, d| d.replica? }.each do |_, database|
124
+ database.analyze_tables(**options)
125
+ end
126
+ true
127
+ end
128
+
129
+ def autoindex_all(create: false)
130
+ databases.each do |_, database|
131
+ puts "Autoindexing #{database}..."
132
+ database.autoindex(create: create)
133
+ end
134
+ true
137
135
  end
138
136
 
139
- def falsey?(value)
140
- value == false || value == 'f'
137
+ def pretty_size(value)
138
+ ActiveSupport::NumberHelper.number_to_human_size(value, precision: 3)
141
139
  end
142
140
  end
143
141
  end
@@ -9,6 +9,11 @@ namespace :pghero do
9
9
  PgHero.capture_space_stats
10
10
  end
11
11
 
12
+ desc "analyze tables"
13
+ task analyze: :environment do
14
+ PgHero.analyze_all(verbose: true, min_size: ENV["MIN_SIZE_GB"].to_f.gigabytes)
15
+ end
16
+
12
17
  desc "autoindex"
13
18
  task autoindex: :environment do
14
19
  PgHero.autoindex_all(create: true)
@@ -0,0 +1,38 @@
1
+ require_relative "test_helper"
2
+
3
+ class BasicTest < Minitest::Test
4
+ def setup
5
+ City.delete_all
6
+ end
7
+
8
+ def test_explain
9
+ City.create!
10
+ PgHero.explain("ANALYZE DELETE FROM cities")
11
+ assert_equal 1, City.count
12
+ end
13
+
14
+ def test_explain_multiple_statements
15
+ City.create!
16
+ assert_raises(ActiveRecord::StatementInvalid) { PgHero.explain("ANALYZE DELETE FROM cities; DELETE FROM cities; COMMIT") }
17
+ end
18
+
19
+ def test_analyze_tables
20
+ assert PgHero.analyze_tables
21
+ end
22
+
23
+ def test_relation_sizes
24
+ assert PgHero.relation_sizes
25
+ end
26
+
27
+ def test_transaction_id_danger
28
+ assert PgHero.transaction_id_danger(threshold: 10000000000)
29
+ end
30
+
31
+ def test_autovacuum_danger
32
+ assert PgHero.autovacuum_danger
33
+ end
34
+
35
+ def test_databases
36
+ assert PgHero.databases[:primary].running_queries
37
+ end
38
+ end