pghero 3.0.1 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +1 -1
- data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
- data/app/assets/javascripts/pghero/chartkick.js +834 -764
- data/app/assets/stylesheets/pghero/application.css +2 -2
- data/app/controllers/pg_hero/home_controller.rb +40 -11
- data/app/views/layouts/pg_hero/application.html.erb +3 -1
- data/app/views/pg_hero/home/explain.html.erb +3 -1
- data/app/views/pg_hero/home/queries.html.erb +4 -2
- data/app/views/pg_hero/home/show_query.html.erb +1 -1
- data/lib/generators/pghero/templates/config.yml.tt +3 -0
- data/lib/pghero/methods/explain.rb +34 -0
- data/lib/pghero/methods/query_stats.rb +5 -3
- data/lib/pghero/methods/space.rb +3 -2
- data/lib/pghero/methods/suggested_indexes.rb +8 -4
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +36 -26
- data/lib/tasks/pghero.rake +11 -1
- data/licenses/LICENSE-chart.js.txt +1 -1
- data/licenses/LICENSE-date-fns.txt +21 -20
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- metadata +4 -3
@@ -187,7 +187,7 @@ hr {
|
|
187
187
|
}
|
188
188
|
|
189
189
|
#slider-container {
|
190
|
-
padding: 6px
|
190
|
+
padding: 6px 8px 14px 8px;
|
191
191
|
}
|
192
192
|
|
193
193
|
.queries-table th a, .space-table th a {
|
@@ -195,7 +195,7 @@ hr {
|
|
195
195
|
}
|
196
196
|
|
197
197
|
#slider {
|
198
|
-
margin
|
198
|
+
margin: 0px 14px 18px 14px;
|
199
199
|
}
|
200
200
|
|
201
201
|
#range-start {
|
@@ -263,30 +263,57 @@ module PgHero
|
|
263
263
|
end
|
264
264
|
|
265
265
|
def explain
|
266
|
+
unless @explain_enabled
|
267
|
+
render_text "Explain not enabled", status: :bad_request
|
268
|
+
return
|
269
|
+
end
|
270
|
+
|
266
271
|
@title = "Explain"
|
267
272
|
@query = params[:query]
|
273
|
+
@explain_analyze_enabled = PgHero.explain_mode == "analyze"
|
274
|
+
|
268
275
|
# TODO use get + token instead of post so users can share links
|
269
276
|
# need to prevent CSRF and DoS
|
270
|
-
if request.post? && @query
|
277
|
+
if request.post? && @query.present?
|
271
278
|
begin
|
272
|
-
|
279
|
+
explain_options =
|
273
280
|
case params[:commit]
|
274
281
|
when "Analyze"
|
275
|
-
|
282
|
+
{analyze: true}
|
276
283
|
when "Visualize"
|
277
|
-
|
284
|
+
if @explain_analyze_enabled
|
285
|
+
{analyze: true, costs: true, verbose: true, buffers: true, format: "json"}
|
286
|
+
else
|
287
|
+
{costs: true, verbose: true, format: "json"}
|
288
|
+
end
|
278
289
|
else
|
279
|
-
|
290
|
+
{}
|
280
291
|
end
|
281
|
-
|
292
|
+
|
293
|
+
if explain_options[:analyze] && !@explain_analyze_enabled
|
294
|
+
render_text "Explain analyze not enabled", status: :bad_request
|
295
|
+
return
|
296
|
+
end
|
297
|
+
|
298
|
+
@explanation = @database.explain_v2(@query, **explain_options)
|
282
299
|
@suggested_index = @database.suggested_indexes(queries: [@query]).first if @database.suggested_indexes_enabled?
|
283
300
|
@visualize = params[:commit] == "Visualize"
|
284
301
|
rescue ActiveRecord::StatementInvalid => e
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
302
|
+
message = e.message
|
303
|
+
@error =
|
304
|
+
if message == "Unsafe statement"
|
305
|
+
"Unsafe statement"
|
306
|
+
elsif message.start_with?("PG::ProtocolViolation: ERROR: bind message supplies 0 parameters")
|
307
|
+
"Can't explain queries with bind parameters"
|
308
|
+
elsif message.start_with?("PG::SyntaxError")
|
309
|
+
"Syntax error with query"
|
310
|
+
elsif message.start_with?("PG::QueryCanceled")
|
311
|
+
"Query timed out"
|
312
|
+
else
|
313
|
+
# default to a generic message
|
314
|
+
# since data can be extracted through the Postgres error message
|
315
|
+
"Error explaining query"
|
316
|
+
end
|
290
317
|
end
|
291
318
|
end
|
292
319
|
end
|
@@ -368,6 +395,7 @@ module PgHero
|
|
368
395
|
redirect_backward alert: "The database user does not have permission to enable query stats"
|
369
396
|
end
|
370
397
|
|
398
|
+
# TODO disable if historical query stats enabled?
|
371
399
|
def reset_query_stats
|
372
400
|
success =
|
373
401
|
if @database.server_version_num >= 120000
|
@@ -409,6 +437,7 @@ module PgHero
|
|
409
437
|
@query_stats_enabled = @database.query_stats_enabled?
|
410
438
|
@system_stats_enabled = @database.system_stats_enabled?
|
411
439
|
@replica = @database.replica?
|
440
|
+
@explain_enabled = PgHero.explain_enabled?
|
412
441
|
end
|
413
442
|
|
414
443
|
def set_suggested_indexes(min_average_time = 0, min_calls = 0)
|
@@ -48,7 +48,9 @@
|
|
48
48
|
<% unless @database.replica? %>
|
49
49
|
<li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
|
50
50
|
<% end %>
|
51
|
-
|
51
|
+
<% if @explain_enabled %>
|
52
|
+
<li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
|
53
|
+
<% end %>
|
52
54
|
<li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
|
53
55
|
</ul>
|
54
56
|
|
@@ -5,7 +5,9 @@
|
|
5
5
|
<div class="field"><%= text_area_tag :query, @query, placeholder: "Enter a SQL query" %></div>
|
6
6
|
<p>
|
7
7
|
<%= submit_tag "Explain", class: "btn btn-info", style: "margin-right: 10px;" %>
|
8
|
-
|
8
|
+
<% if @explain_analyze_enabled %>
|
9
|
+
<%= submit_tag "Analyze", class: "btn btn-danger", style: "margin-right: 10px;" %>
|
10
|
+
<% end %>
|
9
11
|
<%= submit_tag "Visualize", class: "btn btn-danger" %>
|
10
12
|
</p>
|
11
13
|
<% end %>
|
@@ -1,9 +1,11 @@
|
|
1
1
|
<div class="content">
|
2
|
-
<% if @query_stats_enabled %>
|
2
|
+
<% if @query_stats_enabled && !@historical_query_stats_enabled %>
|
3
3
|
<%= button_to "Reset", reset_query_stats_path, class: "btn btn-danger", style: "float: right;" %>
|
4
4
|
<% end %>
|
5
5
|
|
6
|
-
|
6
|
+
<% if !@historical_query_stats_enabled %>
|
7
|
+
<h1 style="float: left;">Queries</h1>
|
8
|
+
<% end %>
|
7
9
|
|
8
10
|
<% if @historical_query_stats_enabled %>
|
9
11
|
<%= render partial: "query_stats_slider" %>
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Explain
|
4
|
+
# TODO remove in 4.0
|
5
|
+
# note: this method is not affected by the explain option
|
4
6
|
def explain(sql)
|
5
7
|
sql = squish(sql)
|
6
8
|
explanation = nil
|
@@ -16,6 +18,23 @@ module PgHero
|
|
16
18
|
explanation
|
17
19
|
end
|
18
20
|
|
21
|
+
# TODO rename to explain in 4.0
|
22
|
+
# note: this method is not affected by the explain option
|
23
|
+
def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
|
24
|
+
options = []
|
25
|
+
add_explain_option(options, "ANALYZE", analyze)
|
26
|
+
add_explain_option(options, "VERBOSE", verbose)
|
27
|
+
add_explain_option(options, "SETTINGS", settings)
|
28
|
+
add_explain_option(options, "COSTS", costs)
|
29
|
+
add_explain_option(options, "BUFFERS", buffers)
|
30
|
+
add_explain_option(options, "WAL", wal)
|
31
|
+
add_explain_option(options, "TIMING", timing)
|
32
|
+
add_explain_option(options, "SUMMARY", summary)
|
33
|
+
options << "FORMAT #{explain_format(format)}"
|
34
|
+
|
35
|
+
explain("(#{options.join(", ")}) #{sql}")
|
36
|
+
end
|
37
|
+
|
19
38
|
private
|
20
39
|
|
21
40
|
def explain_safe?
|
@@ -24,6 +43,21 @@ module PgHero
|
|
24
43
|
rescue ActiveRecord::StatementInvalid
|
25
44
|
true
|
26
45
|
end
|
46
|
+
|
47
|
+
def add_explain_option(options, name, value)
|
48
|
+
unless value.nil?
|
49
|
+
options << "#{name}#{value ? "" : " FALSE"}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# important! validate format to prevent injection
|
54
|
+
def explain_format(format)
|
55
|
+
if ["text", "xml", "json", "yaml"].include?(format)
|
56
|
+
format.upcase
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Unknown format"
|
59
|
+
end
|
60
|
+
end
|
27
61
|
end
|
28
62
|
end
|
29
63
|
end
|
@@ -162,8 +162,9 @@ module PgHero
|
|
162
162
|
end
|
163
163
|
end
|
164
164
|
|
165
|
-
def clean_query_stats
|
166
|
-
|
165
|
+
def clean_query_stats(before: nil)
|
166
|
+
before ||= 14.days.ago
|
167
|
+
PgHero::QueryStats.where(database: id).where("captured_at < ?", before).delete_all
|
167
168
|
end
|
168
169
|
|
169
170
|
def slow_queries(query_stats: nil, **options)
|
@@ -226,6 +227,7 @@ module PgHero
|
|
226
227
|
)
|
227
228
|
SELECT
|
228
229
|
query,
|
230
|
+
query AS explainable_query,
|
229
231
|
query_hash,
|
230
232
|
query_stats.user,
|
231
233
|
total_minutes,
|
@@ -243,7 +245,7 @@ module PgHero
|
|
243
245
|
# we may be able to skip query_columns
|
244
246
|
# in more recent versions of Postgres
|
245
247
|
# as pg_stat_statements should be already normalized
|
246
|
-
select_all(query, query_columns: [:query])
|
248
|
+
select_all(query, query_columns: [:query, :explainable_query])
|
247
249
|
else
|
248
250
|
raise NotEnabled, "Query stats not enabled"
|
249
251
|
end
|
data/lib/pghero/methods/space.rb
CHANGED
@@ -129,8 +129,9 @@ 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
|
-
|
132
|
+
def clean_space_stats(before: nil)
|
133
|
+
before ||= 90.days.ago
|
134
|
+
PgHero::SpaceStats.where(database: id).where("captured_at < ?", before).delete_all
|
134
135
|
end
|
135
136
|
|
136
137
|
def space_stats_enabled?
|
@@ -286,20 +286,24 @@ module PgHero
|
|
286
286
|
else
|
287
287
|
raise "Not Implemented"
|
288
288
|
end
|
289
|
-
elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr.name.first.string.
|
290
|
-
[{column: aexpr.lexpr.column_ref.fields.last.string.
|
289
|
+
elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr.name.first.string.send(str_method))
|
290
|
+
[{column: aexpr.lexpr.column_ref.fields.last.string.send(str_method), op: aexpr.name.first.string.send(str_method)}]
|
291
291
|
elsif tree.null_test
|
292
292
|
op = tree.null_test.nulltesttype == :IS_NOT_NULL ? "not_null" : "null"
|
293
|
-
[{column: tree.null_test.arg.column_ref.fields.last.string.
|
293
|
+
[{column: tree.null_test.arg.column_ref.fields.last.string.send(str_method), op: op}]
|
294
294
|
else
|
295
295
|
raise "Not Implemented"
|
296
296
|
end
|
297
297
|
end
|
298
298
|
|
299
|
+
def str_method
|
300
|
+
@str_method ||= Gem::Version.new(PgQuery::VERSION) >= Gem::Version.new("4") ? :sval : :str
|
301
|
+
end
|
302
|
+
|
299
303
|
def parse_sort(sort_clause)
|
300
304
|
sort_clause.map do |v|
|
301
305
|
{
|
302
|
-
column: v.sort_by.node.column_ref.fields.last.string.
|
306
|
+
column: v.sort_by.node.column_ref.fields.last.string.send(str_method),
|
303
307
|
direction: v.sort_by.sortby_dir == :SORTBY_DESC ? "desc" : "asc"
|
304
308
|
}
|
305
309
|
end
|
data/lib/pghero/version.rb
CHANGED
data/lib/pghero.rb
CHANGED
@@ -3,27 +3,27 @@ require "active_support"
|
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
# methods
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
6
|
+
require_relative "pghero/methods/basic"
|
7
|
+
require_relative "pghero/methods/connections"
|
8
|
+
require_relative "pghero/methods/constraints"
|
9
|
+
require_relative "pghero/methods/explain"
|
10
|
+
require_relative "pghero/methods/indexes"
|
11
|
+
require_relative "pghero/methods/kill"
|
12
|
+
require_relative "pghero/methods/maintenance"
|
13
|
+
require_relative "pghero/methods/queries"
|
14
|
+
require_relative "pghero/methods/query_stats"
|
15
|
+
require_relative "pghero/methods/replication"
|
16
|
+
require_relative "pghero/methods/sequences"
|
17
|
+
require_relative "pghero/methods/settings"
|
18
|
+
require_relative "pghero/methods/space"
|
19
|
+
require_relative "pghero/methods/suggested_indexes"
|
20
|
+
require_relative "pghero/methods/system"
|
21
|
+
require_relative "pghero/methods/tables"
|
22
|
+
require_relative "pghero/methods/users"
|
23
|
+
|
24
|
+
require_relative "pghero/database"
|
25
|
+
require_relative "pghero/engine" if defined?(Rails)
|
26
|
+
require_relative "pghero/version"
|
27
27
|
|
28
28
|
module PgHero
|
29
29
|
autoload :Connection, "pghero/connection"
|
@@ -91,6 +91,16 @@ module PgHero
|
|
91
91
|
@stats_database_url ||= (file_config || {})["stats_database_url"] || ENV["PGHERO_STATS_DATABASE_URL"]
|
92
92
|
end
|
93
93
|
|
94
|
+
# private
|
95
|
+
def explain_enabled?
|
96
|
+
explain_mode.nil? || explain_mode == true || explain_mode == "analyze"
|
97
|
+
end
|
98
|
+
|
99
|
+
# private
|
100
|
+
def explain_mode
|
101
|
+
@config["explain"]
|
102
|
+
end
|
103
|
+
|
94
104
|
def visualize_url
|
95
105
|
@visualize_url ||= config["visualize_url"] || ENV["PGHERO_VISUALIZE_URL"] || "https://tatiyants.com/pev/#/plans/new"
|
96
106
|
end
|
@@ -109,7 +119,7 @@ module PgHero
|
|
109
119
|
|
110
120
|
config_file_exists = File.exist?(path)
|
111
121
|
|
112
|
-
config = YAML.
|
122
|
+
config = YAML.safe_load(ERB.new(File.read(path)).result) if config_file_exists
|
113
123
|
config ||= {}
|
114
124
|
|
115
125
|
@file_config =
|
@@ -212,15 +222,15 @@ module PgHero
|
|
212
222
|
# delete previous stats
|
213
223
|
# go database by database to use an index
|
214
224
|
# stats for old databases are not cleaned up since we can't use an index
|
215
|
-
def clean_query_stats
|
225
|
+
def clean_query_stats(before: nil)
|
216
226
|
each_database do |database|
|
217
|
-
database.clean_query_stats
|
227
|
+
database.clean_query_stats(before: before)
|
218
228
|
end
|
219
229
|
end
|
220
230
|
|
221
|
-
def clean_space_stats
|
231
|
+
def clean_space_stats(before: nil)
|
222
232
|
each_database do |database|
|
223
|
-
database.clean_space_stats
|
233
|
+
database.clean_space_stats(before: before)
|
224
234
|
end
|
225
235
|
end
|
226
236
|
|
data/lib/tasks/pghero.rake
CHANGED
@@ -22,6 +22,16 @@ namespace :pghero do
|
|
22
22
|
desc "Remove old query stats"
|
23
23
|
task clean_query_stats: :environment do
|
24
24
|
puts "Deleting old query stats..."
|
25
|
-
|
25
|
+
options = {}
|
26
|
+
options[:before] = Float(ENV["KEEP_DAYS"]).days.ago if ENV["KEEP_DAYS"].present?
|
27
|
+
PgHero.clean_query_stats(**options)
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Remove old space stats"
|
31
|
+
task clean_space_stats: :environment do
|
32
|
+
puts "Deleting old space stats..."
|
33
|
+
options = {}
|
34
|
+
options[:before] = Float(ENV["KEEP_DAYS"]).days.ago if ENV["KEEP_DAYS"].present?
|
35
|
+
PgHero.clean_space_stats(**options)
|
26
36
|
end
|
27
37
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2014-
|
3
|
+
Copyright (c) 2014-2022 Chart.js Contributors
|
4
4
|
|
5
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
6
|
|
@@ -1,20 +1,21 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
The
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Sasha Koss and Lesha Koss https://kossnocorp.mit-license.org
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018-2021 Jukka Kurkela
|
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.
|
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: 3.0
|
4
|
+
version: 3.2.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:
|
11
|
+
date: 2023-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -103,6 +103,7 @@ files:
|
|
103
103
|
- licenses/LICENSE-date-fns.txt
|
104
104
|
- licenses/LICENSE-highlight.js.txt
|
105
105
|
- licenses/LICENSE-jquery.txt
|
106
|
+
- licenses/LICENSE-kurkle-color.txt
|
106
107
|
- licenses/LICENSE-nouislider.txt
|
107
108
|
homepage: https://github.com/ankane/pghero
|
108
109
|
licenses:
|
@@ -123,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
124
|
- !ruby/object:Gem::Version
|
124
125
|
version: '0'
|
125
126
|
requirements: []
|
126
|
-
rubygems_version: 3.
|
127
|
+
rubygems_version: 3.4.6
|
127
128
|
signing_key:
|
128
129
|
specification_version: 4
|
129
130
|
summary: A performance dashboard for Postgres
|