pghero 2.6.0 → 2.7.4

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.

@@ -508,3 +508,7 @@ body {
508
508
  .duplicate-indexes pre {
509
509
  margin-top: 10px;
510
510
  }
511
+
512
+ .no-outline:focus {
513
+ outline: none;
514
+ }
@@ -1,4 +1,4 @@
1
- /*! nouislider - 14.0.3 - 10/10/2019 */
1
+ /*! nouislider - 14.6.1 - 8/17/2020 */
2
2
  /* Functional styling;
3
3
  * These styles are required for noUiSlider to function.
4
4
  * You don't need to change these rules to apply your design.
@@ -18,7 +18,6 @@
18
18
  }
19
19
  .noUi-target {
20
20
  position: relative;
21
- direction: ltr;
22
21
  }
23
22
  .noUi-base,
24
23
  .noUi-connects {
@@ -39,7 +38,7 @@
39
38
  position: absolute;
40
39
  z-index: 1;
41
40
  top: 0;
42
- left: 0;
41
+ right: 0;
43
42
  -ms-transform-origin: 0 0;
44
43
  -webkit-transform-origin: 0 0;
45
44
  -webkit-transform-style: preserve-3d;
@@ -56,9 +55,9 @@
56
55
  }
57
56
  /* Offset direction
58
57
  */
59
- html:not([dir="rtl"]) .noUi-horizontal .noUi-origin {
60
- left: auto;
61
- right: 0;
58
+ .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin {
59
+ left: 0;
60
+ right: auto;
62
61
  }
63
62
  /* Give origins 0 height/width so they don't interfere with clicking the
64
63
  * connect elements.
@@ -94,7 +93,7 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-origin {
94
93
  .noUi-horizontal .noUi-handle {
95
94
  width: 34px;
96
95
  height: 28px;
97
- left: -17px;
96
+ right: -17px;
98
97
  top: -6px;
99
98
  }
100
99
  .noUi-vertical {
@@ -103,12 +102,12 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-origin {
103
102
  .noUi-vertical .noUi-handle {
104
103
  width: 28px;
105
104
  height: 34px;
106
- left: -6px;
105
+ right: -6px;
107
106
  top: -17px;
108
107
  }
109
- html:not([dir="rtl"]) .noUi-horizontal .noUi-handle {
110
- right: -17px;
111
- left: auto;
108
+ .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle {
109
+ left: -17px;
110
+ right: auto;
112
111
  }
113
112
  /* Styling;
114
113
  * Giving the connect element a border radius causes issues with using transform: scale
@@ -297,3 +296,15 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-handle {
297
296
  top: 50%;
298
297
  right: 120%;
299
298
  }
299
+ .noUi-horizontal .noUi-origin > .noUi-tooltip {
300
+ -webkit-transform: translate(50%, 0);
301
+ transform: translate(50%, 0);
302
+ left: auto;
303
+ bottom: 10px;
304
+ }
305
+ .noUi-vertical .noUi-origin > .noUi-tooltip {
306
+ -webkit-transform: translate(0, -18px);
307
+ transform: translate(0, -18px);
308
+ top: auto;
309
+ right: 28px;
310
+ }
@@ -2,7 +2,7 @@ module PgHero
2
2
  class HomeController < ActionController::Base
3
3
  layout "pg_hero/application"
4
4
 
5
- protect_from_forgery
5
+ protect_from_forgery with: :exception
6
6
 
7
7
  http_basic_authenticate_with name: PgHero.username, password: PgHero.password if PgHero.password
8
8
 
@@ -373,11 +373,7 @@ module PgHero
373
373
  protected
374
374
 
375
375
  def redirect_backward(options = {})
376
- if Rails.version >= "5.1"
377
- redirect_back options.merge(fallback_location: root_path)
378
- else
379
- redirect_to :back, options
380
- end
376
+ redirect_back fallback_location: root_path, **options
381
377
  end
382
378
 
383
379
  def set_database
data/lib/pghero.rb CHANGED
@@ -113,13 +113,13 @@ module PgHero
113
113
 
114
114
  if !ENV["PGHERO_DATABASE_URL"] && spec_supported?
115
115
  ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas: true).each do |db|
116
- databases[db.spec_name] = {"spec" => db.spec_name}
116
+ databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
117
117
  end
118
118
  end
119
119
 
120
120
  if databases.empty?
121
121
  databases["primary"] = {
122
- "url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config
122
+ "url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
123
123
  }
124
124
  end
125
125
 
@@ -196,13 +196,13 @@ module PgHero
196
196
  # stats for old databases are not cleaned up since we can't use an index
197
197
  def clean_query_stats
198
198
  each_database do |database|
199
- PgHero::QueryStats.where(database: database.id).where("captured_at < ?", 14.days.ago).delete_all
199
+ database.clean_query_stats
200
200
  end
201
201
  end
202
202
 
203
203
  def clean_space_stats
204
204
  each_database do |database|
205
- PgHero::SpaceStats.where(database: database.id).where("captured_at < ?", 90.days.ago).delete_all
205
+ database.clean_space_stats
206
206
  end
207
207
  end
208
208
 
@@ -211,6 +211,18 @@ module PgHero
211
211
  ActiveRecord::VERSION::MAJOR >= 6
212
212
  end
213
213
 
214
+ # private
215
+ def connection_config(model)
216
+ ActiveRecord::VERSION::STRING.to_f >= 6.1 ? model.connection_db_config.configuration_hash : model.connection_config
217
+ end
218
+
219
+ # private
220
+ # Rails 6.1 deprecate `spec_name` and use `name` for configurations
221
+ # https://github.com/rails/rails/pull/38536
222
+ def spec_name_key
223
+ ActiveRecord::VERSION::STRING.to_f >= 6.1 ? :name : :spec_name
224
+ end
225
+
214
226
  private
215
227
 
216
228
  def each_database
@@ -149,11 +149,14 @@ module PgHero
149
149
  # resolve spec
150
150
  if !url && config["spec"]
151
151
  raise Error, "Spec requires Rails 6+" unless PgHero.spec_supported?
152
- resolved = ActiveRecord::Base.configurations.configs_for(env_name: PgHero.env, spec_name: config["spec"], include_replicas: true)
152
+ config_options = {env_name: PgHero.env, PgHero.spec_name_key => config["spec"], include_replicas: true}
153
+ resolved = ActiveRecord::Base.configurations.configs_for(**config_options)
153
154
  raise Error, "Spec not found: #{config["spec"]}" unless resolved
154
- url = resolved.config
155
+ url = ActiveRecord::VERSION::STRING.to_f >= 6.1 ? resolved.configuration_hash : resolved.config
155
156
  end
156
157
 
158
+ url = url.dup
159
+
157
160
  Class.new(PgHero::Connection) do
158
161
  def self.name
159
162
  "PgHero::Connection::Database#{object_id}"
@@ -18,6 +18,10 @@ module PgHero
18
18
  select_one("SELECT current_database()")
19
19
  end
20
20
 
21
+ def current_user
22
+ select_one("SELECT current_user")
23
+ end
24
+
21
25
  def server_version
22
26
  @server_version ||= select_one("SHOW server_version")
23
27
  end
@@ -38,7 +42,11 @@ module PgHero
38
42
  retries = 0
39
43
  begin
40
44
  result = conn.select_all(add_source(squish(sql)))
41
- result = result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] }] }
45
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
46
+ result = result.map(&:symbolize_keys)
47
+ else
48
+ result = result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] }] }
49
+ end
42
50
  if filter_data
43
51
  query_columns.each do |column|
44
52
  result.each do |row|
@@ -56,8 +56,46 @@ module PgHero
56
56
  true
57
57
  end
58
58
 
59
- def reset_query_stats(raise_errors: false)
60
- execute("SELECT pg_stat_statements_reset()")
59
+ # TODO scope by database in PgHero 3.0
60
+ # (add database: database_name to options)
61
+ def reset_query_stats(**options)
62
+ reset_instance_query_stats(**options)
63
+ end
64
+
65
+ # resets query stats for the entire instance
66
+ # it's possible to reset stats for a specific
67
+ # database, user or query hash in Postgres 12+
68
+ def reset_instance_query_stats(database: nil, user: nil, query_hash: nil, raise_errors: false)
69
+ if database || user || query_hash
70
+ raise PgHero::Error, "Requires PostgreSQL 12+" if server_version_num < 120000
71
+
72
+ if database
73
+ database_id = execute("SELECT oid FROM pg_database WHERE datname = #{quote(database)}").first.try(:[], "oid")
74
+ raise PgHero::Error, "Database not found: #{database}" unless database_id
75
+ else
76
+ database_id = 0
77
+ end
78
+
79
+ if user
80
+ user_id = execute("SELECT usesysid FROM pg_user WHERE usename = #{quote(user)}").first.try(:[], "usesysid")
81
+ raise PgHero::Error, "User not found: #{user}" unless user_id
82
+ else
83
+ user_id = 0
84
+ end
85
+
86
+ if query_hash
87
+ query_id = query_hash.to_i
88
+ # may not be needed
89
+ # but not intuitive that all query hashes are reset with 0
90
+ raise PgHero::Error, "Invalid query hash: #{query_hash}" if query_id == 0
91
+ else
92
+ query_id = 0
93
+ end
94
+
95
+ execute("SELECT pg_stat_statements_reset(#{quote(user_id.to_i)}, #{quote(database_id.to_i)}, #{quote(query_id.to_i)})")
96
+ else
97
+ execute("SELECT pg_stat_statements_reset()")
98
+ end
61
99
  true
62
100
  rescue ActiveRecord::StatementInvalid => e
63
101
  raise e if raise_errors
@@ -104,31 +142,32 @@ module PgHero
104
142
  query_stats[database_id] = query_stats(limit: 1000000, database: database_name)
105
143
  end
106
144
 
107
- supports_query_hash = supports_query_hash?
145
+ query_stats = query_stats.select { |_, v| v.any? }
108
146
 
109
- if query_stats.any? { |_, v| v.any? } && reset_query_stats(raise_errors: raise_errors)
147
+ # nothing to do
148
+ return if query_stats.empty?
149
+
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
110
153
  query_stats.each do |db_id, db_query_stats|
111
- if db_query_stats.any?
112
- values =
113
- db_query_stats.map do |qs|
114
- [
115
- db_id,
116
- qs[:query],
117
- qs[:total_minutes] * 60 * 1000,
118
- qs[:calls],
119
- now,
120
- supports_query_hash ? qs[:query_hash] : nil,
121
- qs[:user]
122
- ]
123
- end
124
-
125
- columns = %w[database query total_time calls captured_at query_hash user]
126
- insert_stats("pghero_query_stats", columns, values)
154
+ if reset_query_stats(database: mapping[db_id], raise_errors: raise_errors)
155
+ insert_query_stats(db_id, db_query_stats, now)
156
+ end
157
+ end
158
+ else
159
+ if reset_query_stats(raise_errors: raise_errors)
160
+ query_stats.each do |db_id, db_query_stats|
161
+ insert_query_stats(db_id, db_query_stats, now)
127
162
  end
128
163
  end
129
164
  end
130
165
  end
131
166
 
167
+ def clean_query_stats
168
+ PgHero::QueryStats.where(database: id).where("captured_at < ?", 14.days.ago).delete_all
169
+ end
170
+
132
171
  def slow_queries(query_stats: nil, **options)
133
172
  query_stats ||= self.query_stats(options)
134
173
  query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
@@ -287,6 +326,24 @@ module PgHero
287
326
  def normalize_query(query)
288
327
  squish(query.to_s.gsub(/\?(, ?\?)+/, "?").gsub(/\/\*.+?\*\//, ""))
289
328
  end
329
+
330
+ def insert_query_stats(db_id, db_query_stats, now)
331
+ values =
332
+ 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
+ ]
342
+ end
343
+
344
+ columns = %w[database query total_time calls captured_at query_hash user]
345
+ insert_stats("pghero_query_stats", columns, values)
346
+ end
290
347
  end
291
348
  end
292
349
  end
@@ -47,7 +47,8 @@ module PgHero
47
47
  end
48
48
  end
49
49
 
50
- sequences.sort_by { |s| s[:sequence] }
50
+ # use to_s for unparsable sequences
51
+ sequences.sort_by { |s| s[:sequence].to_s }
51
52
  end
52
53
 
53
54
  def sequence_danger(threshold: 0.9, sequences: nil)
@@ -129,6 +129,10 @@ module PgHero
129
129
  insert_stats("pghero_space_stats", columns, values) if values.any?
130
130
  end
131
131
 
132
+ def clean_space_stats
133
+ PgHero::SpaceStats.where(database: id).where("captured_at < ?", 90.days.ago).delete_all
134
+ end
135
+
132
136
  def space_stats_enabled?
133
137
  table_exists?("pghero_space_stats")
134
138
  end
@@ -30,7 +30,23 @@ module PgHero
30
30
  if best_index[:found]
31
31
  index = best_index[:index]
32
32
  best_index[:table_indexes] = indexes_by_table[index[:table]].to_a
33
- covering_index = existing_columns[index[:using] || "btree"][index[:table]].find { |e| index_covers?(e, index[:columns]) }
33
+
34
+ # indexes of same type
35
+ indexes = existing_columns[index[:using] || "btree"][index[:table]]
36
+
37
+ if best_index[:structure][:sort].empty?
38
+ # gist indexes without an opclass
39
+ # (opclass is part of column name, so columns won't match if opclass present)
40
+ indexes += existing_columns["gist"][index[:table]]
41
+
42
+ # hash indexes work for equality
43
+ indexes += existing_columns["hash"][index[:table]] if best_index[:structure][:where].all? { |v| v[:op] == "=" }
44
+
45
+ # brin indexes work for all
46
+ indexes += existing_columns["brin"][index[:table]]
47
+ end
48
+
49
+ covering_index = indexes.find { |e| index_covers?(e.map { |v| v.sub(/ inet_ops\z/, "") }, index[:columns]) }
34
50
  if covering_index
35
51
  best_index[:covering_index] = covering_index
36
52
  best_index[:explanation] = "Covered by index on (#{covering_index.join(", ")})"
@@ -86,7 +102,7 @@ module PgHero
86
102
  # get stats about columns for relevant tables
87
103
  tables = parts.values.map { |t| t[:table] }.uniq
88
104
  # TODO get schema from query structure, then try search path
89
- schema = connection_model.connection_config[:schema] || "public"
105
+ schema = PgHero.connection_config(connection_model)[:schema] || "public"
90
106
  if tables.any?
91
107
  row_stats = Hash[table_stats(table: tables, schema: schema).map { |i| [i[:table], i[:estimated_rows]] }]
92
108
  col_stats = column_stats(table: tables, schema: schema).group_by { |i| i[:table] }
@@ -144,7 +144,7 @@ module PgHero
144
144
 
145
145
  # validate input since we need to interpolate below
146
146
  raise Error, "Invalid metric name" unless metric_name =~ /\A[a-z\/_]+\z/i
147
- raise Error, "Invalid database id" unless gcp_database_id =~ /\A[a-z\-:]+\z/i
147
+ raise Error, "Invalid database id" unless gcp_database_id =~ /\A[a-z0-9\-:]+\z/i
148
148
 
149
149
  # we handle three situations:
150
150
  # 1. google-cloud-monitoring-v3
@@ -276,7 +276,6 @@ module PgHero
276
276
 
277
277
  def add_missing_data(data, start_time, end_time, period)
278
278
  time = start_time
279
- end_time = end_time
280
279
  while time < end_time
281
280
  data[time] ||= nil
282
281
  time += period
@@ -5,7 +5,7 @@ module PgHero
5
5
  # TODO quote in 3.0, 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
- database ||= connection_model.connection_config[:database]
8
+ database ||= PgHero.connection_config(connection_model)[:database]
9
9
 
10
10
  commands =
11
11
  [
@@ -44,7 +44,7 @@ module PgHero
44
44
  # documented as unsafe to pass user input
45
45
  # TODO quote in 3.0, but still not officially supported
46
46
  def drop_user(user, schema: "public", database: nil)
47
- database ||= connection_model.connection_config[:database]
47
+ database ||= PgHero.connection_config(connection_model)[:database]
48
48
 
49
49
  # thanks shiftb
50
50
  commands =
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "2.6.0"
2
+ VERSION = "2.7.4"
3
3
  end
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Chart.js Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013-2019 Andrew Kane
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.