pghero 4.0.0 → 4.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/app/controllers/pg_hero/home_controller.rb +9 -4
- data/app/views/pg_hero/home/_live_queries_table.html.erb +4 -4
- data/app/views/pg_hero/home/_queries_table.html.erb +2 -2
- data/app/views/pg_hero/home/index.html.erb +1 -4
- data/config/routes.rb +1 -1
- data/lib/pghero/database.rb +1 -1
- data/lib/pghero/engine.rb +7 -0
- data/lib/pghero/methods/basic.rb +14 -2
- data/lib/pghero/methods/indexes.rb +2 -2
- data/lib/pghero/methods/kill.rb +28 -6
- data/lib/pghero/methods/queries.rb +11 -2
- data/lib/pghero/methods/sequences.rb +39 -17
- data/lib/pghero/methods/space.rb +1 -1
- data/lib/pghero/methods/suggested_indexes.rb +1 -0
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3acbc4629f0e8c8b468669dbcbeeb01ef5e8b3827f891772412a19d92d1c3b76
|
|
4
|
+
data.tar.gz: d9f2f710437de7b76e0d6757be81c36ca68f2469ee9c44ec751972a55d00e20f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eecc0a4dce22f7203e7f18928ea9537cb35e4f9b7814ed68cf216b669795bfaa85ec33bd31b94e86c523564d1965058d19fed919471e3882cccd6762be6c7a15
|
|
7
|
+
data.tar.gz: 64d275ba685349df7be06a829927f5a711a36c33afffc4af43bc3bb916b819b3978e0f51fa01c7085d57406dc9fff7aeadd8796c2d772c2c38790259879a87b1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
## 4.1.0 (2026-10-20)
|
|
2
|
+
|
|
3
|
+
- Restricted killing queries to current database and certain backend types - [more info](https://github.com/ankane/pghero/security/advisories/GHSA-5287-hmjh-qmqq)
|
|
4
|
+
- Added grouping for parallel queries
|
|
5
|
+
- Fixed error when index is dropped during indexes query
|
|
6
|
+
- Fixed `unable to filter data` messages
|
|
7
|
+
- Removed replication backend types from live queries page
|
|
8
|
+
|
|
9
|
+
## 4.0.1 (2026-08-15)
|
|
10
|
+
|
|
11
|
+
- Added sequences from identity columns
|
|
12
|
+
|
|
1
13
|
## 4.0.0 (2026-08-05)
|
|
2
14
|
|
|
15
|
+
See the upgrade notes for [Docker](guides/Docker.md#upgrading) or [Rails](guides/Rails.md#upgrading)
|
|
16
|
+
|
|
3
17
|
- Reduced space for historical query stats
|
|
4
18
|
- Added `disable_kill` option to disable killing queries and connections
|
|
5
19
|
- Added `unused_index_megabytes` option to set minimum size for unused indexes
|
|
@@ -36,8 +36,7 @@ module PgHero
|
|
|
36
36
|
@inactive_replication_slots = @database.replication_slots.select { |r| !r[:active] }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
@
|
|
40
|
-
@autovacuum_queries, @long_running_queries = long_running_queries.partition { |q| q[:query].starts_with?("autovacuum:") }
|
|
39
|
+
@autovacuum_queries, @long_running_queries = @database.long_running_queries.partition { |q| q[:backend_type] == "autovacuum worker" }
|
|
41
40
|
|
|
42
41
|
connection_states = @database.connection_states
|
|
43
42
|
@total_connections = connection_states.values.sum
|
|
@@ -416,21 +415,27 @@ module PgHero
|
|
|
416
415
|
end
|
|
417
416
|
|
|
418
417
|
def kill
|
|
419
|
-
if @database.kill(params[:pid])
|
|
418
|
+
if @database.kill(params[:pid], key: params[:key].to_i)
|
|
420
419
|
redirect_backward notice: "Query killed"
|
|
421
420
|
else
|
|
422
421
|
redirect_backward notice: "Query no longer running"
|
|
423
422
|
end
|
|
423
|
+
rescue ActiveRecord::StatementInvalid
|
|
424
|
+
redirect_backward alert: "The database user does not have permission to kill this query"
|
|
424
425
|
end
|
|
425
426
|
|
|
426
427
|
def kill_long_running_queries
|
|
427
428
|
@database.kill_long_running_queries
|
|
428
429
|
redirect_backward notice: "Queries killed"
|
|
430
|
+
rescue ActiveRecord::StatementInvalid
|
|
431
|
+
redirect_backward alert: "The database user does not have permission to kill some queries"
|
|
429
432
|
end
|
|
430
433
|
|
|
431
434
|
def kill_all
|
|
432
435
|
@database.kill_all
|
|
433
436
|
redirect_backward notice: "Connections killed"
|
|
437
|
+
rescue ActiveRecord::StatementInvalid
|
|
438
|
+
redirect_backward alert: "The database user does not have permission to kill some connections"
|
|
434
439
|
end
|
|
435
440
|
|
|
436
441
|
def enable_query_stats
|
|
@@ -528,7 +533,7 @@ module PgHero
|
|
|
528
533
|
end
|
|
529
534
|
|
|
530
535
|
def group_connections_by_key(connections, key)
|
|
531
|
-
group_connections(connections, [key]).
|
|
536
|
+
group_connections(connections, [key]).to_h { |v| [v[key], v[:total_connections]] }
|
|
532
537
|
end
|
|
533
538
|
|
|
534
539
|
def check_api
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<tbody>
|
|
11
11
|
<% queries.reverse.each do |query| %>
|
|
12
12
|
<tr>
|
|
13
|
-
<td><%= query[:pid] %></td>
|
|
13
|
+
<td><%= ([query[:pid]] + query[:worker_pids]).join(", ") %></td>
|
|
14
14
|
<td>
|
|
15
15
|
<% if query[:duration_ms] %>
|
|
16
16
|
<% sec = query[:duration_ms] / 1000.0 %>
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
<% end %>
|
|
33
33
|
</td>
|
|
34
34
|
<td class="text-right">
|
|
35
|
-
<%
|
|
35
|
+
<% if @explain_enabled && (!@database.filter_data || @database.server_version_num >= 160000) %>
|
|
36
36
|
<%= button_to "Explain", explain_path, params: {query: query[:query]}, form: {target: "_blank"}, class: "btn btn-info" %>
|
|
37
37
|
<% end %>
|
|
38
|
-
<% if @kill_enabled %>
|
|
39
|
-
<%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
|
|
38
|
+
<% if @kill_enabled && ["client backend", "autovacuum worker"].include?(query[:backend_type]) %>
|
|
39
|
+
<%= button_to "Kill", kill_path(pid: query[:pid], key: query[:started_at].usec), class: "btn btn-danger" %>
|
|
40
40
|
<% end %>
|
|
41
41
|
</td>
|
|
42
42
|
</tr>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<span class="user">
|
|
47
47
|
<% if query[:user] %>
|
|
48
48
|
<%= @link_user ? link_to(query[:user], {user: query[:user]}.merge(@link_options)) : query[:user] %>
|
|
49
|
-
<% if @show_details %>
|
|
49
|
+
<% if @show_details && query[:query_hash] %>
|
|
50
50
|
·
|
|
51
51
|
<% end %>
|
|
52
52
|
<% end %>
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
<td colspan="3" class="query-row">
|
|
62
62
|
<pre><code class="query-code language-pgsql"><%= query[:query] %></code></pre>
|
|
63
63
|
<% if query[:query] == "<insufficient privilege>" %>
|
|
64
|
-
<p class="text-muted">
|
|
64
|
+
<p class="text-muted">Add the <code>pg_read_all_stats</code> role to see queries executed by other users.</p>
|
|
65
65
|
<% end %>
|
|
66
66
|
<% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query[:query]]) && (details[:index] || @debug) %>
|
|
67
67
|
<%= render partial: "suggested_index", locals: {index: details[:index], details: details} %>
|
|
@@ -26,9 +26,6 @@
|
|
|
26
26
|
<% if @autovacuum_queries.any? %>
|
|
27
27
|
<span class="tiny"><%= @autovacuum_queries.size %> autovacuum</span>
|
|
28
28
|
<% end %>
|
|
29
|
-
<% if @walsender_queries.any? %>
|
|
30
|
-
<span class="tiny"><%= @walsender_queries.size %> walsender</span>
|
|
31
|
-
<% end %>
|
|
32
29
|
</div>
|
|
33
30
|
<% if @extended %>
|
|
34
31
|
<div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
|
|
@@ -68,7 +65,7 @@
|
|
|
68
65
|
<% end %>
|
|
69
66
|
</div>
|
|
70
67
|
<div class="alert alert-<%= @invalid_indexes.empty? && @invalid_constraints.empty? ? "success" : "warning" %>">
|
|
71
|
-
<% if @invalid_indexes.empty? && @invalid_constraints.empty?
|
|
68
|
+
<% if @invalid_indexes.empty? && @invalid_constraints.empty? %>
|
|
72
69
|
No invalid indexes or constraints
|
|
73
70
|
<% else %>
|
|
74
71
|
<% if @invalid_indexes.any? %>
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
PgHero::Engine.routes.draw do
|
|
2
|
-
scope "(:database)", constraints:
|
|
2
|
+
scope "(:database)", constraints: PgHero::DatabaseConstraint.new do
|
|
3
3
|
get "space", to: "home#space"
|
|
4
4
|
get "space/:relation", to: "home#relation_space", as: :relation_space
|
|
5
5
|
get "index_bloat", to: "home#index_bloat"
|
data/lib/pghero/database.rb
CHANGED
data/lib/pghero/engine.rb
CHANGED
|
@@ -14,4 +14,11 @@ module PgHero
|
|
|
14
14
|
PgHero.time_zone = file_config["time_zone"] if file_config["time_zone"]
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
class DatabaseConstraint
|
|
19
|
+
def matches?(request)
|
|
20
|
+
database = request.params[:database]
|
|
21
|
+
database.nil? || PgHero.config["databases"].key?(database)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
17
24
|
end
|
data/lib/pghero/methods/basic.rb
CHANGED
|
@@ -66,8 +66,20 @@ module PgHero
|
|
|
66
66
|
if filter_data
|
|
67
67
|
query_columns.each do |column|
|
|
68
68
|
result.each do |row|
|
|
69
|
+
value = row[column]
|
|
70
|
+
if value.nil? || value == "<insufficient privilege>"
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if value.start_with?("autovacuum:")
|
|
75
|
+
prefix = "autovacuum:"
|
|
76
|
+
value = value.delete_prefix("autovacuum:")
|
|
77
|
+
else
|
|
78
|
+
prefix = ""
|
|
79
|
+
end
|
|
80
|
+
|
|
69
81
|
begin
|
|
70
|
-
row[column] = PgQuery.normalize(
|
|
82
|
+
row[column] = prefix + PgQuery.normalize(value)
|
|
71
83
|
rescue PgQuery::ParseError
|
|
72
84
|
# try replacing "interval $1" with "$1::interval"
|
|
73
85
|
# see https://github.com/lfittl/pg_query/issues/169 for more info
|
|
@@ -75,7 +87,7 @@ module PgHero
|
|
|
75
87
|
# we could skip normalization
|
|
76
88
|
# but this has a very small chance of data leakage
|
|
77
89
|
begin
|
|
78
|
-
row[column] = PgQuery.normalize(
|
|
90
|
+
row[column] = prefix + PgQuery.normalize(value.gsub(/\binterval\s+(\$\d+)\b/i, "\\1::interval"))
|
|
79
91
|
rescue PgQuery::ParseError
|
|
80
92
|
row[column] = "<unable to filter data>"
|
|
81
93
|
end
|
|
@@ -154,7 +154,7 @@ module PgHero
|
|
|
154
154
|
ORDER BY
|
|
155
155
|
1, 2
|
|
156
156
|
SQL
|
|
157
|
-
).map { |v| v[:columns] = v[:columns].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
|
|
157
|
+
).select { |v| v[:definition] }.map { |v| v[:columns] = v[:columns].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
|
|
158
158
|
|
|
159
159
|
# determine if any invalid indexes being created
|
|
160
160
|
# hacky, but works for simple cases
|
|
@@ -322,7 +322,7 @@ module PgHero
|
|
|
322
322
|
wastedbytes DESC,
|
|
323
323
|
index_name
|
|
324
324
|
SQL
|
|
325
|
-
select_all(sql, {min_size: min_size.to_i})
|
|
325
|
+
select_all(sql, {min_size: min_size.to_i}).select { |v| v[:definition] }
|
|
326
326
|
end
|
|
327
327
|
|
|
328
328
|
protected
|
data/lib/pghero/methods/kill.rb
CHANGED
|
@@ -2,29 +2,51 @@ module PgHero
|
|
|
2
2
|
module Methods
|
|
3
3
|
module Kill
|
|
4
4
|
# note: this method is not affected by disable_kill option
|
|
5
|
-
def kill(pid)
|
|
6
|
-
|
|
5
|
+
def kill(pid, key: nil)
|
|
6
|
+
kill_backends(pid: pid, key: key, backend_type: ["client backend", "autovacuum worker"]).any? { |v| v[:pg_terminate_backend] }
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
# note: this method is not affected by disable_kill option
|
|
10
10
|
def kill_long_running_queries(min_duration: nil)
|
|
11
|
-
|
|
11
|
+
kill_backends(min_duration: min_duration || long_running_query_sec)
|
|
12
12
|
true
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# note: this method is not affected by disable_kill option
|
|
16
16
|
def kill_all
|
|
17
|
-
|
|
17
|
+
kill_backends
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def kill_backends(pid: nil, key: nil, min_duration: nil, backend_type: ["client backend"])
|
|
24
|
+
select_expr = "pg_terminate_backend(pid)"
|
|
25
|
+
# key is used to reduce chance of killing different query
|
|
26
|
+
# check *after* row matches other conditions
|
|
27
|
+
if key
|
|
28
|
+
select_expr = "CASE WHEN EXTRACT(MICROSECONDS FROM COALESCE(query_start, xact_start)) % 1000000 = :key THEN #{select_expr} ELSE NULL END AS pg_terminate_backend"
|
|
29
|
+
end
|
|
30
|
+
query = <<~SQL
|
|
18
31
|
SELECT
|
|
19
|
-
|
|
32
|
+
#{select_expr}
|
|
20
33
|
FROM
|
|
21
34
|
pg_stat_activity
|
|
22
35
|
WHERE
|
|
23
36
|
pid <> pg_backend_pid()
|
|
24
37
|
AND query <> '<insufficient privilege>'
|
|
25
38
|
AND datname = current_database()
|
|
39
|
+
AND backend_type IN (:backend_type)
|
|
40
|
+
#{"AND pid = :pid" if pid}
|
|
41
|
+
#{"AND state <> 'idle' AND NOW() - COALESCE(query_start, xact_start) > interval :min_duration" if min_duration}
|
|
26
42
|
SQL
|
|
27
|
-
|
|
43
|
+
|
|
44
|
+
binds = {}
|
|
45
|
+
binds[:backend_type] = Array(backend_type)
|
|
46
|
+
binds[:pid] = pid.to_i if pid
|
|
47
|
+
binds[:key] = key.to_i if key
|
|
48
|
+
binds[:min_duration] = "#{min_duration.to_i} seconds" if min_duration
|
|
49
|
+
select_all(query, binds)
|
|
28
50
|
end
|
|
29
51
|
end
|
|
30
52
|
end
|
|
@@ -5,6 +5,7 @@ module PgHero
|
|
|
5
5
|
query = <<~SQL
|
|
6
6
|
SELECT
|
|
7
7
|
pid,
|
|
8
|
+
leader_pid,
|
|
8
9
|
state,
|
|
9
10
|
application_name AS source,
|
|
10
11
|
age(NOW(), COALESCE(query_start, xact_start)) AS duration,
|
|
@@ -20,6 +21,7 @@ module PgHero
|
|
|
20
21
|
state <> 'idle'
|
|
21
22
|
AND pid <> pg_backend_pid()
|
|
22
23
|
AND datname = current_database()
|
|
24
|
+
AND backend_type IN ('client backend', 'parallel worker', 'autovacuum worker')
|
|
23
25
|
#{"AND NOW() - COALESCE(query_start, xact_start) > interval :min_duration" if min_duration}
|
|
24
26
|
#{"AND query <> '<insufficient privilege>'" if !all}
|
|
25
27
|
ORDER BY
|
|
@@ -28,7 +30,14 @@ module PgHero
|
|
|
28
30
|
|
|
29
31
|
binds = {}
|
|
30
32
|
binds[:min_duration] = "#{min_duration.to_i} seconds" if min_duration
|
|
31
|
-
select_all(query, binds, query_columns: [:query])
|
|
33
|
+
queries = select_all(query, binds, query_columns: [:query])
|
|
34
|
+
groups = queries.group_by { |v| v[:leader_pid] || v[:pid] }
|
|
35
|
+
groups.map do |pid, queries|
|
|
36
|
+
leader = queries.find { |v| v[:leader_pid].nil? } || queries.first
|
|
37
|
+
leader.delete(:leader_pid)
|
|
38
|
+
leader[:worker_pids] = queries.filter_map { |v| v[:pid] if v.key?(:leader_pid) }
|
|
39
|
+
leader
|
|
40
|
+
end
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
def long_running_queries
|
|
@@ -67,10 +76,10 @@ module PgHero
|
|
|
67
76
|
NOT blockedl.granted
|
|
68
77
|
AND blockeda.query <> '<insufficient privilege>'
|
|
69
78
|
AND blockeda.datname = current_database()
|
|
79
|
+
AND blockeda.backend_type IN ('client backend', 'parallel worker', 'autovacuum worker')
|
|
70
80
|
ORDER BY
|
|
71
81
|
blocked_duration DESC
|
|
72
82
|
SQL
|
|
73
|
-
|
|
74
83
|
select_all(query, query_columns: [:blocked_query, :current_or_recent_query_in_blocking_process])
|
|
75
84
|
end
|
|
76
85
|
end
|
|
@@ -2,13 +2,16 @@ module PgHero
|
|
|
2
2
|
module Methods
|
|
3
3
|
module Sequences
|
|
4
4
|
def sequences
|
|
5
|
-
# get columns with default values
|
|
6
|
-
# use
|
|
5
|
+
# get columns with default values and identity columns
|
|
6
|
+
# use pg_depend to get owned sequences (like pg_get_serial_sequence)
|
|
7
|
+
# use pg_get_expr to get correct default value for rest
|
|
7
8
|
# it's what information_schema.columns uses
|
|
8
9
|
# also, exclude temporary tables to prevent error
|
|
9
10
|
# when accessing across sessions
|
|
10
11
|
sequences = select_all <<~SQL
|
|
11
12
|
SELECT
|
|
13
|
+
sn.nspname AS schema,
|
|
14
|
+
s.relname AS sequence,
|
|
12
15
|
n.nspname AS table_schema,
|
|
13
16
|
c.relname AS table,
|
|
14
17
|
attname AS column,
|
|
@@ -20,32 +23,51 @@ module PgHero
|
|
|
20
23
|
pg_catalog.pg_class c ON c.oid = a.attrelid
|
|
21
24
|
INNER JOIN
|
|
22
25
|
pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
23
|
-
|
|
24
|
-
pg_catalog.
|
|
26
|
+
LEFT JOIN
|
|
27
|
+
pg_catalog.pg_depend dep ON dep.refclassid = 'pg_catalog.pg_class'::regclass
|
|
28
|
+
AND dep.refobjid = a.attrelid
|
|
29
|
+
AND dep.refobjsubid = a.attnum
|
|
30
|
+
AND dep.classid = 'pg_catalog.pg_class'::regclass
|
|
31
|
+
AND dep.objsubid = 0
|
|
32
|
+
AND dep.deptype IN ('i', 'a')
|
|
33
|
+
AND dep.objid IN (SELECT oid FROM pg_class WHERE relkind = 'S')
|
|
34
|
+
LEFT JOIN
|
|
35
|
+
pg_catalog.pg_class s ON s.oid = dep.objid
|
|
36
|
+
LEFT JOIN
|
|
37
|
+
pg_catalog.pg_namespace sn ON sn.oid = s.relnamespace
|
|
38
|
+
LEFT JOIN
|
|
39
|
+
pg_catalog.pg_attrdef d ON a.attrelid = d.adrelid
|
|
40
|
+
AND a.attnum = d.adnum
|
|
41
|
+
AND s.relkind IS NULL
|
|
25
42
|
WHERE
|
|
26
43
|
NOT a.attisdropped
|
|
27
44
|
AND a.attnum > 0
|
|
28
|
-
AND pg_get_expr(d.adbin, d.adrelid) LIKE 'nextval%'
|
|
29
|
-
AND
|
|
45
|
+
AND (pg_get_expr(d.adbin, d.adrelid) LIKE 'nextval%' OR s.relname IS NOT NULL)
|
|
46
|
+
AND c.relpersistence <> 't'
|
|
30
47
|
SQL
|
|
31
48
|
|
|
32
|
-
# parse out sequence
|
|
33
49
|
sequences.each do |column|
|
|
34
|
-
column[:max_value] =
|
|
50
|
+
column[:max_value] =
|
|
51
|
+
case column[:column_type]
|
|
52
|
+
when "smallint"
|
|
53
|
+
32767
|
|
54
|
+
when "integer"
|
|
55
|
+
2147483647
|
|
56
|
+
else
|
|
57
|
+
9223372036854775807
|
|
58
|
+
end
|
|
35
59
|
|
|
36
|
-
|
|
60
|
+
unless column[:sequence]
|
|
61
|
+
column[:schema], column[:sequence] = parse_default_value(column[:default_value])
|
|
62
|
+
end
|
|
37
63
|
column.delete(:default_value) if column[:sequence]
|
|
38
64
|
end
|
|
39
65
|
|
|
40
66
|
add_sequence_attributes(sequences)
|
|
41
67
|
|
|
42
68
|
last_value = {}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
select_all(sql).zip(slice) do |row, seq|
|
|
47
|
-
last_value[seq] = row[:last_value]
|
|
48
|
-
end
|
|
69
|
+
select_all("SELECT schemaname AS schema, sequencename AS sequence, COALESCE(last_value, start_value) AS last_value FROM pg_sequences").each do |row|
|
|
70
|
+
last_value[[row[:schema], row[:sequence]]] = row[:last_value]
|
|
49
71
|
end
|
|
50
72
|
|
|
51
73
|
sequences.select { |s| s[:readable] }.each do |seq|
|
|
@@ -67,8 +89,8 @@ module PgHero
|
|
|
67
89
|
# nextval('id_seq'::regclass)
|
|
68
90
|
# nextval(('id_seq'::text)::regclass)
|
|
69
91
|
def parse_default_value(default_value)
|
|
70
|
-
m =
|
|
71
|
-
m =
|
|
92
|
+
m = /\Anextval\('(.+)'\:\:regclass\)\z/.match(default_value)
|
|
93
|
+
m = /\Anextval\(\('(.+)'\:\:text\)\:\:regclass\)\z/.match(default_value) unless m
|
|
72
94
|
if m
|
|
73
95
|
unquote_ident(m[1])
|
|
74
96
|
else
|
data/lib/pghero/methods/space.rb
CHANGED
|
@@ -93,7 +93,7 @@ module PgHero
|
|
|
93
93
|
def relation_space_stats(relation, schema: "public")
|
|
94
94
|
if space_stats_enabled?
|
|
95
95
|
relation_sizes ||= self.relation_sizes
|
|
96
|
-
sizes = relation_sizes.
|
|
96
|
+
sizes = relation_sizes.to_h { |r| [[r[:schema], r[:relation]], r[:size_bytes]] }
|
|
97
97
|
start_at = 30.days.ago
|
|
98
98
|
|
|
99
99
|
sql = <<~SQL
|
|
@@ -198,6 +198,7 @@ module PgHero
|
|
|
198
198
|
def best_index_structure(statement)
|
|
199
199
|
return {error: "Empty statement"} if statement.to_s.empty?
|
|
200
200
|
return {error: "Too large"} if statement.to_s.length > 10000
|
|
201
|
+
return {error: "Insufficient privilege"} if statement == "<insufficient privilege>"
|
|
201
202
|
|
|
202
203
|
begin
|
|
203
204
|
tree = PgQuery.parse(statement).tree
|
data/lib/pghero/version.rb
CHANGED
data/lib/pghero.rb
CHANGED
|
@@ -83,7 +83,7 @@ module PgHero
|
|
|
83
83
|
|
|
84
84
|
# private
|
|
85
85
|
def explain_mode
|
|
86
|
-
|
|
86
|
+
config["explain"]
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def visualize_url
|
|
@@ -163,7 +163,7 @@ module PgHero
|
|
|
163
163
|
# return if another process initialized while we were waiting
|
|
164
164
|
return @databases if defined?(@databases)
|
|
165
165
|
|
|
166
|
-
@databases = config["databases"].
|
|
166
|
+
@databases = config["databases"].to_h { |id, c| [id.to_sym, Database.new(id, c)] }
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pghero
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
@@ -121,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
121
121
|
- !ruby/object:Gem::Version
|
|
122
122
|
version: '0'
|
|
123
123
|
requirements: []
|
|
124
|
-
rubygems_version: 4.0.
|
|
124
|
+
rubygems_version: 4.0.20
|
|
125
125
|
specification_version: 4
|
|
126
126
|
summary: A performance dashboard for Postgres
|
|
127
127
|
test_files: []
|