pghero 2.8.3 → 3.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0ef604cc8c9d99ad4c990aff2b2ee6686dcfe8ba43e1b57a5babab2a4f3613d
4
- data.tar.gz: d1d6e6b002e0f0ba4c574ecf9cdc4b6953ea89e03215f05529a217fac1228f8e
3
+ metadata.gz: 47c3b581297903d9228470aeaaf0d292037a607a972f7555c40aca2384db8c70
4
+ data.tar.gz: a6f3cba27dd2034de0a24e855c6047966548d7253ead84d6a19395c0d23609dc
5
5
  SHA512:
6
- metadata.gz: 6999c79d2cf479bce29e06e7dda24e635e95173281ee7231c9af1c777871df3d8b271e38b4adb4baa8ca508aaeeb2f07f8c61ae2cf66ca475e1dc0b6603d34e6
7
- data.tar.gz: 12fa2e5b15cb1150c51e4076a6c1c68bf55a429de833ed9809a92c46d18a945766a08b348bf4398d893521cd20e297b9c97b3418c0565a259a3c01a077645252
6
+ metadata.gz: '08dd24651f52597743f937956dffca4784589ed9075e2c03cb3319e3d9cc73a71d2eb3409a37e3823b53e23cd1940a50739e95e5357b441ec4b942a6a7a2346c'
7
+ data.tar.gz: ca28bc463f7095cebd7e09907aa1e5c1eccd0ab32b288b0963da05344bfcaa292353d8e284636c6d8a642e36514c43419f916bedcfeebe1caca62cdfad3dd3d2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 3.0.0 (2022-09-13)
2
+
3
+ - Changed `capture_query_stats` to only reset stats for current database in Postgres 12+
4
+ - Changed `reset_query_stats` to only reset stats for current database (use `reset_instance_query_stats` to reset stats for entire instance)
5
+ - Added `visualize_url` option to config
6
+ - Removed `access_key_id`, `secret_access_key`, `region`, and `db_instance_identifier` methods (use `aws_` prefixed methods instead)
7
+ - Dropped support for Linux packages for EOL versions
8
+ - Dropped support for Ruby < 2.7 and Rails < 6
9
+ - Dropped support for pg_query < 2
10
+ - Dropped support for aws-sdk < 2
11
+
1
12
  ## 2.8.3 (2022-05-01)
2
13
 
3
14
  - Added support for `google-apis-monitoring_v3`
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2021 Andrew Kane, 2008-2014 Heroku (initial queries)
1
+ Copyright (c) 2014-2022 Andrew Kane, 2008-2014 Heroku (initial queries)
2
2
 
3
3
  MIT License
4
4
 
@@ -84,7 +84,7 @@ module PgHero
84
84
  @space_stats_enabled = @database.space_stats_enabled? && !@only_tables
85
85
  if @space_stats_enabled
86
86
  space_growth = @database.space_growth(days: @days, relation_sizes: @relation_sizes)
87
- @growth_bytes_by_relation = Hash[ space_growth.map { |r| [[r[:schema], r[:relation]], r[:growth_bytes]] } ]
87
+ @growth_bytes_by_relation = space_growth.to_h { |r| [[r[:schema], r[:relation]], r[:growth_bytes]] }
88
88
  if params[:sort] == "growth"
89
89
  @relation_sizes.sort_by! { |r| s = @growth_bytes_by_relation[[r[:schema], r[:relation]]]; [s ? 0 : 1, -s.to_i, r[:schema], r[:relation]] }
90
90
  end
@@ -184,7 +184,7 @@ module PgHero
184
184
  @chart2_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:average_time].round(1)] }, library: chart_library_options}]
185
185
  @chart3_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:calls]] }, library: chart_library_options}]
186
186
 
187
- @origins = Hash[query_hash_stats.group_by { |r| r[:origin].to_s }.map { |k, v| [k, v.size] }]
187
+ @origins = query_hash_stats.group_by { |r| r[:origin].to_s }.to_h { |k, v| [k, v.size] }
188
188
  @total_count = query_hash_stats.size
189
189
  end
190
190
 
@@ -192,11 +192,11 @@ module PgHero
192
192
  @tables.sort!
193
193
 
194
194
  if @tables.any?
195
- @row_counts = Hash[@database.table_stats(table: @tables).map { |i| [i[:table], i[:estimated_rows]] }]
195
+ @row_counts = @database.table_stats(table: @tables).to_h { |i| [i[:table], i[:estimated_rows]] }
196
196
  @indexes_by_table = @database.indexes.group_by { |i| i[:table] }
197
197
  end
198
198
  else
199
- render_text "Unknown query"
199
+ render_text "Unknown query", status: :not_found
200
200
  end
201
201
  end
202
202
 
@@ -217,9 +217,9 @@ module PgHero
217
217
  @period = (params[:period] || 60.seconds).to_i
218
218
 
219
219
  if @duration / @period > 1440
220
- render_text "Too many data points"
220
+ render_text "Too many data points", status: :bad_request
221
221
  elsif @period % 60 != 0
222
- render_text "Period must be a multiple of 60"
222
+ render_text "Period must be a multiple of 60", status: :bad_request
223
223
  end
224
224
  end
225
225
 
@@ -369,7 +369,11 @@ module PgHero
369
369
  end
370
370
 
371
371
  def reset_query_stats
372
- @database.reset_query_stats
372
+ if @database.server_version_num >= 120000
373
+ @database.reset_query_stats
374
+ else
375
+ @database.reset_instance_query_stats
376
+ end
373
377
  redirect_backward notice: "Query stats reset"
374
378
  rescue ActiveRecord::StatementInvalid
375
379
  redirect_backward alert: "The database user does not have permission to reset query stats"
@@ -445,12 +449,13 @@ module PgHero
445
449
  end
446
450
 
447
451
  def check_api
448
- render_text "No support for Rails API. See https://github.com/pghero/pghero for a standalone app." if Rails.application.config.try(:api_only)
452
+ if Rails.application.config.try(:api_only)
453
+ render_text "No support for Rails API. See https://github.com/pghero/pghero for a standalone app.", status: :internal_server_error
454
+ end
449
455
  end
450
456
 
451
- # TODO return error status code
452
- def render_text(message)
453
- render plain: message
457
+ def render_text(message, status:)
458
+ render plain: message, status: status
454
459
  end
455
460
 
456
461
  def ensure_query_stats
@@ -12,7 +12,7 @@
12
12
 
13
13
  <% if @explanation %>
14
14
  <% if @visualize %>
15
- <p>Paste the output below into the <%= link_to "Postgres Explain Visualizer", "https://tatiyants.com/pev/#/plans/new", target: "_blank" %></p>
15
+ <p>Paste the output below into the <%= link_to "explain visualizer", PgHero.visualize_url, target: "_blank" %></p>
16
16
  <% end %>
17
17
  <pre><code><%= @explanation %></code></pre>
18
18
  <% unless @visualize %>
@@ -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
@@ -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|
@@ -53,7 +53,7 @@ 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
@@ -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
@@ -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
@@ -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
@@ -49,7 +49,7 @@ 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
55
  stats = select_all_stats <<-SQL
@@ -92,7 +92,7 @@ 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
98
  stats = select_all_stats <<-SQL
@@ -2,7 +2,7 @@ module PgHero
2
2
  module Methods
3
3
  module SuggestedIndexes
4
4
  def suggested_indexes_enabled?
5
- defined?(PgQuery) && Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("0.9.0") && query_stats_enabled?
5
+ defined?(PgQuery) && Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("2") && query_stats_enabled?
6
6
  end
7
7
 
8
8
  # TODO clean this mess
@@ -104,7 +104,7 @@ module PgHero
104
104
  # TODO get schema from query structure, then try search path
105
105
  schema = PgHero.connection_config(connection_model)[:schema] || "public"
106
106
  if tables.any?
107
- row_stats = Hash[table_stats(table: tables, schema: schema).map { |i| [i[:table], i[:estimated_rows]] }]
107
+ row_stats = table_stats(table: tables, schema: schema).to_h { |i| [i[:table], i[:estimated_rows]] }
108
108
  col_stats = column_stats(table: tables, schema: schema).group_by { |i| i[:table] }
109
109
  end
110
110
 
@@ -126,7 +126,7 @@ module PgHero
126
126
  total_rows = row_stats[table].to_i
127
127
  index[:rows] = total_rows
128
128
 
129
- ranks = Hash[col_stats[table].to_a.map { |r| [r[:column], r] }]
129
+ ranks = col_stats[table].to_a.to_h { |r| [r[:column], r] }
130
130
  columns = (where + sort).map { |c| c[:column] }.uniq
131
131
 
132
132
  if columns.any?
@@ -135,7 +135,7 @@ module PgHero
135
135
  sort = sort.first(first_desc + 1) if first_desc
136
136
  where = where.sort_by { |c| [row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]), c[:column]] } + sort
137
137
 
138
- index[:row_estimates] = Hash[where.map { |c| ["#{c[:column]} (#{c[:op] || "sort"})", row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }]
138
+ index[:row_estimates] = where.to_h { |c| ["#{c[:column]} (#{c[:op] || "sort"})", row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }
139
139
 
140
140
  # no index needed if less than 500 rows
141
141
  if total_rows >= 500
@@ -202,68 +202,33 @@ module PgHero
202
202
  return {error: "Parse error"}
203
203
  end
204
204
 
205
- if PgQuery::VERSION.to_i >= 2
206
- return {error: "Unknown structure"} unless tree.stmts.size == 1
207
-
208
- tree = tree.stmts.first.stmt
209
-
210
- table = parse_table_v2(tree) rescue nil
211
- unless table
212
- error =
213
- case tree.node
214
- when :insert_stmt
215
- "INSERT statement"
216
- when :variable_set_stmt
217
- "SET statement"
218
- when :select_stmt
219
- if (tree.select_stmt.from_clause.first.join_expr rescue false)
220
- "JOIN not supported yet"
221
- end
222
- end
223
- return {error: error || "Unknown structure"}
224
- end
225
-
226
- select = tree[tree.node.to_s]
227
- where = (select.where_clause ? parse_where_v2(select.where_clause) : []) rescue nil
228
- return {error: "Unknown structure"} unless where
229
-
230
- sort = (select.sort_clause ? parse_sort_v2(select.sort_clause) : []) rescue []
231
-
232
- {table: table, where: where, sort: sort}
233
- else
234
- # TODO remove support for pg_query < 2 in PgHero 3.0
235
-
236
- return {error: "Unknown structure"} unless tree.size == 1
237
-
238
- tree = tree.first
239
-
240
- # pg_query 1.0.0
241
- tree = tree["RawStmt"]["stmt"] if tree["RawStmt"]
242
-
243
- table = parse_table(tree) rescue nil
244
- unless table
245
- error =
246
- case tree.keys.first
247
- when "InsertStmt"
248
- "INSERT statement"
249
- when "VariableSetStmt"
250
- "SET statement"
251
- when "SelectStmt"
252
- if (tree["SelectStmt"]["fromClause"].first["JoinExpr"] rescue false)
253
- "JOIN not supported yet"
254
- end
205
+ return {error: "Unknown structure"} unless tree.stmts.size == 1
206
+
207
+ tree = tree.stmts.first.stmt
208
+
209
+ table = parse_table(tree) rescue nil
210
+ unless table
211
+ error =
212
+ case tree.node
213
+ when :insert_stmt
214
+ "INSERT statement"
215
+ when :variable_set_stmt
216
+ "SET statement"
217
+ when :select_stmt
218
+ if (tree.select_stmt.from_clause.first.join_expr rescue false)
219
+ "JOIN not supported yet"
255
220
  end
256
- return {error: error || "Unknown structure"}
257
- end
221
+ end
222
+ return {error: error || "Unknown structure"}
223
+ end
258
224
 
259
- select = tree.values.first
260
- where = (select["whereClause"] ? parse_where(select["whereClause"]) : []) rescue nil
261
- return {error: "Unknown structure"} unless where
225
+ select = tree[tree.node.to_s]
226
+ where = (select.where_clause ? parse_where(select.where_clause) : []) rescue nil
227
+ return {error: "Unknown structure"} unless where
262
228
 
263
- sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue []
229
+ sort = (select.sort_clause ? parse_sort(select.sort_clause) : []) rescue []
264
230
 
265
- {table: table, where: where, sort: sort}
266
- end
231
+ {table: table, where: where, sort: sort}
267
232
  end
268
233
 
269
234
  # TODO better row estimation
@@ -300,7 +265,7 @@ module PgHero
300
265
  end
301
266
  end
302
267
 
303
- def parse_table_v2(tree)
268
+ def parse_table(tree)
304
269
  case tree.node
305
270
  when :select_stmt
306
271
  tree.select_stmt.from_clause.first.range_var.relname
@@ -311,24 +276,13 @@ module PgHero
311
276
  end
312
277
  end
313
278
 
314
- def parse_table(tree)
315
- case tree.keys.first
316
- when "SelectStmt"
317
- tree["SelectStmt"]["fromClause"].first["RangeVar"]["relname"]
318
- when "DeleteStmt"
319
- tree["DeleteStmt"]["relation"]["RangeVar"]["relname"]
320
- when "UpdateStmt"
321
- tree["UpdateStmt"]["relation"]["RangeVar"]["relname"]
322
- end
323
- end
324
-
325
279
  # TODO capture values
326
- def parse_where_v2(tree)
280
+ def parse_where(tree)
327
281
  aexpr = tree.a_expr
328
282
 
329
283
  if tree.bool_expr
330
284
  if tree.bool_expr.boolop == :AND_EXPR
331
- tree.bool_expr.args.flat_map { |v| parse_where_v2(v) }
285
+ tree.bool_expr.args.flat_map { |v| parse_where(v) }
332
286
  else
333
287
  raise "Not Implemented"
334
288
  end
@@ -342,27 +296,7 @@ module PgHero
342
296
  end
343
297
  end
344
298
 
345
- # TODO capture values
346
- def parse_where(tree)
347
- aexpr = tree["A_Expr"]
348
-
349
- if tree["BoolExpr"]
350
- if tree["BoolExpr"]["boolop"] == 0
351
- tree["BoolExpr"]["args"].flat_map { |v| parse_where(v) }
352
- else
353
- raise "Not Implemented"
354
- end
355
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first["String"]["str"])
356
- [{column: aexpr["lexpr"]["ColumnRef"]["fields"].last["String"]["str"], op: aexpr["name"].first["String"]["str"]}]
357
- elsif tree["NullTest"]
358
- op = tree["NullTest"]["nulltesttype"] == 1 ? "not_null" : "null"
359
- [{column: tree["NullTest"]["arg"]["ColumnRef"]["fields"].last["String"]["str"], op: op}]
360
- else
361
- raise "Not Implemented"
362
- end
363
- end
364
-
365
- def parse_sort_v2(sort_clause)
299
+ def parse_sort(sort_clause)
366
300
  sort_clause.map do |v|
367
301
  {
368
302
  column: v.sort_by.node.column_ref.fields.last.string.str,
@@ -371,15 +305,6 @@ module PgHero
371
305
  end
372
306
  end
373
307
 
374
- def parse_sort(sort_clause)
375
- sort_clause.map do |v|
376
- {
377
- column: v["SortBy"]["node"]["ColumnRef"]["fields"].last["String"]["str"],
378
- direction: v["SortBy"]["sortby_dir"] == 2 ? "desc" : "asc"
379
- }
380
- end
381
- end
382
-
383
308
  def column_stats(schema: nil, table: nil)
384
309
  select_all <<-SQL
385
310
  SELECT
@@ -5,9 +5,8 @@ module PgHero
5
5
  !system_stats_provider.nil?
6
6
  end
7
7
 
8
- # TODO remove defined checks in 3.0
9
8
  def system_stats_provider
10
- if aws_db_instance_identifier && (defined?(Aws) || defined?(AWS))
9
+ if aws_db_instance_identifier
11
10
  :aws
12
11
  elsif gcp_database_id
13
12
  :gcp
@@ -42,18 +41,13 @@ module PgHero
42
41
 
43
42
  def rds_stats(metric_name, duration: nil, period: nil, offset: nil, series: false)
44
43
  if system_stats_enabled?
45
- aws_options = {region: region}
46
- if access_key_id
47
- aws_options[:access_key_id] = access_key_id
48
- aws_options[:secret_access_key] = secret_access_key
44
+ aws_options = {region: aws_region}
45
+ if aws_access_key_id
46
+ aws_options[:access_key_id] = aws_access_key_id
47
+ aws_options[:secret_access_key] = aws_secret_access_key
49
48
  end
50
49
 
51
- client =
52
- if defined?(Aws)
53
- Aws::CloudWatch::Client.new(aws_options)
54
- else
55
- AWS::CloudWatch.new(aws_options).client
56
- end
50
+ client = Aws::CloudWatch::Client.new(aws_options)
57
51
 
58
52
  duration = (duration || 1.hour).to_i
59
53
  period = (period || 1.minute).to_i
@@ -2,11 +2,15 @@ module PgHero
2
2
  module Methods
3
3
  module Users
4
4
  # documented as unsafe to pass user input
5
- # TODO quote in 3.0, but still not officially supported
5
+ # identifiers are now quoted, but still not officially supported
6
6
  def create_user(user, password: nil, schema: "public", database: nil, readonly: false, tables: nil)
7
7
  password ||= random_password
8
8
  database ||= PgHero.connection_config(connection_model)[:database]
9
9
 
10
+ user = quote_ident(user)
11
+ schema = quote_ident(schema)
12
+ database = quote_ident(database)
13
+
10
14
  commands =
11
15
  [
12
16
  "CREATE ROLE #{user} LOGIN PASSWORD #{quote(password)}",
@@ -42,10 +46,14 @@ module PgHero
42
46
  end
43
47
 
44
48
  # documented as unsafe to pass user input
45
- # TODO quote in 3.0, but still not officially supported
49
+ # identifiers are now quoted, but still not officially supported
46
50
  def drop_user(user, schema: "public", database: nil)
47
51
  database ||= PgHero.connection_config(connection_model)[:database]
48
52
 
53
+ user = quote_ident(user)
54
+ schema = quote_ident(schema)
55
+ database = quote_ident(database)
56
+
49
57
  # thanks shiftb
50
58
  commands =
51
59
  [
@@ -77,9 +85,9 @@ module PgHero
77
85
  SecureRandom.base64(40).delete("+/=")[0...24]
78
86
  end
79
87
 
80
- def table_grant_commands(privilege, tables, user)
88
+ def table_grant_commands(privilege, tables, quoted_user)
81
89
  tables.map do |table|
82
- "GRANT #{privilege} ON TABLE #{table} TO #{user}"
90
+ "GRANT #{privilege} ON TABLE #{quote_ident(table)} TO #{quoted_user}"
83
91
  end
84
92
  end
85
93
  end
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "2.8.3"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/pghero.rb CHANGED
@@ -53,15 +53,15 @@ module PgHero
53
53
 
54
54
  class << self
55
55
  extend Forwardable
56
- def_delegators :primary_database, :access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
56
+ def_delegators :primary_database, :aws_access_key_id, :analyze, :analyze_tables, :autoindex, :autovacuum_danger,
57
57
  :best_index, :blocked_queries, :connections, :connection_sources, :connection_states, :connection_stats,
58
- :cpu_usage, :create_user, :database_size, :db_instance_identifier, :disable_query_stats, :drop_user,
58
+ :cpu_usage, :create_user, :database_size, :aws_db_instance_identifier, :disable_query_stats, :drop_user,
59
59
  :duplicate_indexes, :enable_query_stats, :explain, :historical_query_stats_enabled?, :index_caching,
60
60
  :index_hit_rate, :index_usage, :indexes, :invalid_constraints, :invalid_indexes, :kill, :kill_all, :kill_long_running_queries,
61
61
  :last_stats_reset_time, :long_running_queries, :maintenance_info, :missing_indexes, :query_stats,
62
62
  :query_stats_available?, :query_stats_enabled?, :query_stats_extension_enabled?, :query_stats_readable?,
63
- :rds_stats, :read_iops_stats, :region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
64
- :reset_query_stats, :reset_stats, :running_queries, :secret_access_key, :sequence_danger, :sequences, :settings,
63
+ :rds_stats, :read_iops_stats, :aws_region, :relation_sizes, :replica?, :replication_lag, :replication_lag_stats,
64
+ :reset_query_stats, :reset_stats, :running_queries, :aws_secret_access_key, :sequence_danger, :sequences, :settings,
65
65
  :slow_queries, :space_growth, :ssl_used?, :stats_connection, :suggested_indexes, :suggested_indexes_by_query,
66
66
  :suggested_indexes_enabled?, :system_stats_enabled?, :table_caching, :table_hit_rate, :table_stats,
67
67
  :total_connections, :transaction_id_danger, :unused_indexes, :unused_tables, :write_iops_stats
@@ -86,12 +86,22 @@ module PgHero
86
86
  @password ||= config["password"] || ENV["PGHERO_PASSWORD"]
87
87
  end
88
88
 
89
+ # config pattern for https://github.com/ankane/pghero/issues/424
89
90
  def stats_database_url
90
- @stats_database_url ||= config["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
91
+ @stats_database_url ||= (file_config || {})["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
92
+ end
93
+
94
+ def visualize_url
95
+ @visualize_url ||= config["visualize_url"] || ENV["PGHERO_VISUALIZE_URL"] || "https://tatiyants.com/pev/#/plans/new"
91
96
  end
92
97
 
93
98
  def config
94
- @config ||= begin
99
+ @config ||= file_config || default_config
100
+ end
101
+
102
+ # private
103
+ def file_config
104
+ unless defined?(@file_config)
95
105
  require "erb"
96
106
  require "yaml"
97
107
 
@@ -102,40 +112,48 @@ module PgHero
102
112
  config = YAML.load(ERB.new(File.read(path)).result) if config_file_exists
103
113
  config ||= {}
104
114
 
105
- if config[env]
106
- config[env]
107
- elsif config["databases"] # preferred format
108
- config
109
- elsif config_file_exists
110
- raise "Invalid config file"
111
- else
112
- databases = {}
113
-
114
- if !ENV["PGHERO_DATABASE_URL"] && spec_supported?
115
- ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas_key => true).each do |db|
116
- databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
117
- end
115
+ @file_config =
116
+ if config[env]
117
+ config[env]
118
+ elsif config["databases"] # preferred format
119
+ config
120
+ elsif config_file_exists
121
+ raise "Invalid config file"
122
+ else
123
+ nil
118
124
  end
125
+ end
119
126
 
120
- if databases.empty?
121
- databases["primary"] = {
122
- "url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
123
- }
124
- end
127
+ @file_config
128
+ end
125
129
 
126
- if databases.size == 1
127
- databases.values.first.merge!(
128
- "db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"],
129
- "gcp_database_id" => ENV["PGHERO_GCP_DATABASE_ID"],
130
- "azure_resource_id" => ENV["PGHERO_AZURE_RESOURCE_ID"]
131
- )
132
- end
130
+ # private
131
+ def default_config
132
+ databases = {}
133
133
 
134
- {
135
- "databases" => databases
136
- }
134
+ unless ENV["PGHERO_DATABASE_URL"]
135
+ ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas_key => true).each do |db|
136
+ databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
137
137
  end
138
138
  end
139
+
140
+ if databases.empty?
141
+ databases["primary"] = {
142
+ "url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
143
+ }
144
+ end
145
+
146
+ if databases.size == 1
147
+ databases.values.first.merge!(
148
+ "aws_db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"],
149
+ "gcp_database_id" => ENV["PGHERO_GCP_DATABASE_ID"],
150
+ "azure_resource_id" => ENV["PGHERO_AZURE_RESOURCE_ID"]
151
+ )
152
+ end
153
+
154
+ {
155
+ "databases" => databases
156
+ }
139
157
  end
140
158
 
141
159
  # ensure we only have one copy of databases
@@ -206,11 +224,6 @@ module PgHero
206
224
  end
207
225
  end
208
226
 
209
- # private
210
- def spec_supported?
211
- ActiveRecord::VERSION::MAJOR >= 6
212
- end
213
-
214
227
  # private
215
228
  def connection_config(model)
216
229
  ActiveRecord::VERSION::STRING.to_f >= 6.1 ? model.connection_db_config.configuration_hash : model.connection_config
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pghero
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.3
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-02 00:00:00.000000000 Z
11
+ date: 2022-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,16 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: '6'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
26
+ version: '6'
27
27
  description:
28
- email: andrew@chartkick.com
28
+ email: andrew@ankane.org
29
29
  executables: []
30
30
  extensions: []
31
31
  extra_rdoc_files: []
@@ -116,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
116
  requirements:
117
117
  - - ">="
118
118
  - !ruby/object:Gem::Version
119
- version: '2.4'
119
+ version: '2.7'
120
120
  required_rubygems_version: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ">="