pghero 1.2.4 → 1.3.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
  SHA1:
3
- metadata.gz: 1ff54cb7718c9fa71235f8cd8902d892b22fa846
4
- data.tar.gz: 8063771e39db3713fcdc24b2e7ec9ad6d79b99fa
3
+ metadata.gz: 5de5f457151a5d2e77e515b117225033f2f5153b
4
+ data.tar.gz: fa0a4eea1887464cc49e376aa5675610819f92fb
5
5
  SHA512:
6
- metadata.gz: 5abef652a9cf1bf074cb3ebf13ca92a15985ceb9d398bc648865345ae24297f7a6034b59f32265390eb88f314ca8174e04669b2552120eb5f396ebf5db41d281
7
- data.tar.gz: af7042728d2272c44a2a9986b5c61892aa20955eb6513655cf3d8c5b05237dfec7d2f6b2a2f61a8af5215b23d87a03c179351230796f8bbb6540a6f4c10d72c1
6
+ metadata.gz: af9c72a1eaf428e6c2b7b43a5df732f848633e5f1fa4835c697ff801384fd3b8920d43516a7a5fef9f212b46ee70b9f33ef81e7557fa78b1d086d7e180321aab
7
+ data.tar.gz: 2a687404ad19e82ee2bd7517cd7a81837fe9ea469110028f2fe656b82849977ad139c5407b52168678dfd006e547a316ebc886f953e7db034ea8daae99eb79b4
@@ -1,3 +1,9 @@
1
+ ## 1.3.0
2
+
3
+ - Added query hash for better query stats grouping
4
+ - Added sequence danger check
5
+ - Added `capture_query_stats` option to config
6
+
1
7
  ## 1.2.4
2
8
 
3
9
  - Fixed user methods
@@ -31,6 +31,7 @@ module PgHero
31
31
  end
32
32
  @unused_indexes = PgHero.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
33
33
  @invalid_indexes = PgHero.invalid_indexes
34
+ @duplicate_indexes = PgHero.duplicate_indexes if params[:duplicate_indexes]
34
35
  @good_cache_rate = @table_hit_rate >= PgHero.cache_hit_rate_threshold.to_f / 100 && @index_hit_rate >= PgHero.cache_hit_rate_threshold.to_f / 100
35
36
  @query_stats_available = PgHero.query_stats_available?
36
37
  @total_connections = PgHero.total_connections
@@ -42,6 +43,7 @@ module PgHero
42
43
  @transaction_id_danger = PgHero.transaction_id_danger(threshold: 1500000000)
43
44
  set_suggested_indexes((params[:min_average_time] || 20).to_f, (params[:min_calls] || 50).to_i)
44
45
  @show_migrations = PgHero.show_migrations
46
+ @sequence_danger = PgHero.sequence_danger(threshold: params[:sequence_threshold])
45
47
  end
46
48
 
47
49
  def index_usage
@@ -422,6 +422,7 @@
422
422
  <li class="<%= controller.action_name == "space" ? "active" : "" %>"><%= link_to "Space", space_path %></li>
423
423
  <li class="<%= controller.action_name == "connections" ? "active" : "" %>"><%= link_to "Connections", connections_path %></li>
424
424
  <li class="<%= controller.action_name == "live_queries" ? "active" : "" %>"><%= link_to "Live Queries", live_queries_path %></li>
425
+ <li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
425
426
  <li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
426
427
  <li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
427
428
  </ul>
@@ -15,7 +15,7 @@
15
15
  <td><%= query["pid"] %></td>
16
16
  <td><%= query["state"] %></td>
17
17
  <td><%= query["started_at"] ? "#{number_with_delimiter(((now - Time.parse(query["started_at"])) * 1000).round)} ms" : nil %></td>
18
- <td><%= query["source"] %></td>
18
+ <td><%= query["source"] %> <span class="text-muted"><%= query["user"] %></span></td>
19
19
  <td class="text-right">
20
20
  <%= button_to "Explain", explain_path(query: query["query"]), form: {target: "_blank"}, class: "btn btn-info" %>
21
21
  <%= button_to "Kill", kill_path(pid: query["pid"]), class: "btn btn-danger" %>
@@ -38,6 +38,13 @@
38
38
  Vacuuming healthy
39
39
  <% end %>
40
40
  </div>
41
+ <div class="alert alert-<%= @sequence_danger.empty? ? "success" : "warning" %>">
42
+ <% if @sequence_danger.any? %>
43
+ <%= pluralize(@sequence_danger.size, "columns") %> approaching overflow
44
+ <% else %>
45
+ No columns near overflow
46
+ <% end %>
47
+ </div>
41
48
  <div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
42
49
  <% if @invalid_indexes.any? %>
43
50
  <%= pluralize(@invalid_indexes.size, "invalid index", "invalid indexes") %>
@@ -45,6 +52,15 @@
45
52
  No invalid indexes
46
53
  <% end %>
47
54
  </div>
55
+ <% if @duplicate_indexes %>
56
+ <div class="alert alert-<%= @duplicate_indexes.empty? ? "success" : "warning" %>">
57
+ <% if @duplicate_indexes.any? %>
58
+ <%= pluralize(@duplicate_indexes.size, "duplicate index", "duplicate indexes") %>
59
+ <% else %>
60
+ No duplicate indexes
61
+ <% end %>
62
+ </div>
63
+ <% end %>
48
64
  <% if PgHero.suggested_indexes_enabled? %>
49
65
  <div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
50
66
  <% if @suggested_indexes.any? %>
@@ -139,6 +155,37 @@
139
155
  </div>
140
156
  <% end %>
141
157
 
158
+ <% if @sequence_danger.any? %>
159
+ <div class="content">
160
+ <h2>Columns Near Overflow</h2>
161
+ <p>Consider changing the column type to support a larger range of values.</p>
162
+ <table class="table">
163
+ <thead>
164
+ <tr>
165
+ <th>Column</th>
166
+ <th style="width: 20%;">Type</th>
167
+ <th style="width: 20%;">Values Left</th>
168
+ </tr>
169
+ </thead>
170
+ <tbody>
171
+ <% @sequence_danger.sort_by { |s| [s["table"], s["column"]] }.each do |query| %>
172
+ <tr>
173
+ <td>
174
+ <%= query["table"] %>.<%= query["column"] %>
175
+ </td>
176
+ <td>
177
+ <span class="text-muted"><%= query["column_type"] %></span>
178
+ </td>
179
+ <td>
180
+ <%= number_with_delimiter(query["max_value"].to_i - query["last_value"].to_i) %>
181
+ </td>
182
+ </tr>
183
+ <% end %>
184
+ </tbody>
185
+ </table>
186
+ </div>
187
+ <% end %>
188
+
142
189
  <% if @invalid_indexes.any? %>
143
190
  <div class="content">
144
191
  <h1>Invalid Indexes</h1>
@@ -162,6 +209,49 @@
162
209
  </div>
163
210
  <% end %>
164
211
 
212
+ <% if @duplicate_indexes && @duplicate_indexes.any? %>
213
+ <div class="content">
214
+ <h1>Duplicate Indexes</h1>
215
+
216
+ <p>
217
+ These indexes exist, but aren’t needed. Remove them
218
+ <% if @show_migrations %>
219
+ <a href="javascript: void(0);" onclick="document.getElementById('migration2').style.display = 'block';">with a migration</a>
220
+ <% end %>
221
+ for faster writes.
222
+ </p>
223
+
224
+ <div id="migration2" style="display: none;">
225
+ <pre>rails g migration remove_unneeded_indexes</pre>
226
+ <p>And paste</p>
227
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
228
+ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= query["unneeded_index"]["name"].to_s.inspect %><% end %></pre>
229
+ </div>
230
+
231
+ <table class="table">
232
+ <thead>
233
+ <tr>
234
+ <th>Details</th>
235
+ </tr>
236
+ </thead>
237
+ <tbody>
238
+ <% @duplicate_indexes.each do |index| %>
239
+ <% unneeded_index = index["unneeded_index"] %>
240
+ <% covering_index = index["covering_index"] %>
241
+ <tr>
242
+ <td style="padding-top: 15px; padding-bottom: 5px;">
243
+ On <%= unneeded_index["table"] %>
244
+ <pre><%= unneeded_index["name"] %> (<%= unneeded_index["columns"].join(", ") %>)</pre>
245
+ is covered by
246
+ <pre><%= covering_index["name"] %> (<%= covering_index["columns"].join(", ") %>)</pre>
247
+ </td>
248
+ </tr>
249
+ <% end %>
250
+ </tbody>
251
+ </table>
252
+ </div>
253
+ <% end %>
254
+
165
255
  <% if @suggested_indexes.any? %>
166
256
  <div class="content">
167
257
  <h1>Suggested Indexes</h1>
@@ -242,6 +242,23 @@ Minimum connections for high connections warning
242
242
  PgHero.total_connections_threshold = 100 # default
243
243
  ```
244
244
 
245
+ ## Upgrading
246
+
247
+ ### 1.3.0
248
+
249
+ For better query stats grouping with Postgres 9.4+, create a migration with:
250
+
251
+ ```ruby
252
+ add_column :pghero_query_stats, :query_hash, :integer, limit: 8
253
+ ```
254
+
255
+ If you get an error with `queryid`, recreate the `pg_stat_statements` extension.
256
+
257
+ ```sql
258
+ DROP EXTENSION pg_stat_statements;
259
+ CREATE EXTENSION pg_stat_statements;
260
+ ```
261
+
245
262
  ## Bonus
246
263
 
247
264
  - See where queries come from with [Marginalia](https://github.com/basecamp/marginalia) - comments appear on the Live Queries tab.
@@ -3,6 +3,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
3
3
  create_table :pghero_query_stats do |t|
4
4
  t.text :database
5
5
  t.text :query
6
+ t.integer :query_hash, limit: 8
6
7
  t.float :total_time
7
8
  t.integer :calls, limit: 8
8
9
  t.timestamp :captured_at
@@ -18,6 +18,7 @@ require "pghero/methods/maintenance"
18
18
  require "pghero/methods/queries"
19
19
  require "pghero/methods/query_stats"
20
20
  require "pghero/methods/replica"
21
+ require "pghero/methods/sequences"
21
22
  require "pghero/methods/space"
22
23
  require "pghero/methods/suggested_indexes"
23
24
  require "pghero/methods/system"
@@ -47,6 +48,7 @@ module PgHero
47
48
  extend Methods::Queries
48
49
  extend Methods::QueryStats
49
50
  extend Methods::Replica
51
+ extend Methods::Sequences
50
52
  extend Methods::Space
51
53
  extend Methods::SuggestedIndexes
52
54
  extend Methods::System
@@ -99,6 +99,14 @@ module PgHero
99
99
  def quote_table_name(value)
100
100
  connection.quote_table_name(value)
101
101
  end
102
+
103
+ def unquote(part)
104
+ if part && part.start_with?('"')
105
+ part[1..-2]
106
+ else
107
+ part
108
+ end
109
+ end
102
110
  end
103
111
  end
104
112
  end
@@ -133,7 +133,7 @@ module PgHero
133
133
  ORDER BY
134
134
  1, 2
135
135
  SQL
136
- ).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", "); v }
136
+ ).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
137
137
  end
138
138
 
139
139
  def duplicate_indexes
@@ -19,6 +19,7 @@ module PgHero
19
19
  WHERE
20
20
  pid <> pg_backend_pid()
21
21
  AND query <> '<insufficient privilege>'
22
+ AND datname = current_database()
22
23
  SQL
23
24
  true
24
25
  end
@@ -10,13 +10,15 @@ module PgHero
10
10
  age(now(), xact_start) AS duration,
11
11
  waiting,
12
12
  query,
13
- xact_start AS started_at
13
+ xact_start AS started_at,
14
+ usename AS user
14
15
  FROM
15
16
  pg_stat_activity
16
17
  WHERE
17
18
  query <> '<insufficient privilege>'
18
19
  AND state <> 'idle'
19
20
  AND pid <> pg_backend_pid()
21
+ AND datname = current_database()
20
22
  ORDER BY
21
23
  query_start DESC
22
24
  SQL
@@ -31,7 +33,8 @@ module PgHero
31
33
  age(now(), xact_start) AS duration,
32
34
  waiting,
33
35
  query,
34
- xact_start AS started_at
36
+ xact_start AS started_at,
37
+ usename AS user
35
38
  FROM
36
39
  pg_stat_activity
37
40
  WHERE
@@ -39,6 +42,7 @@ module PgHero
39
42
  AND state <> 'idle'
40
43
  AND pid <> pg_backend_pid()
41
44
  AND now() - query_start > interval '#{long_running_query_sec.to_i} seconds'
45
+ AND datname = current_database()
42
46
  ORDER BY
43
47
  query_start DESC
44
48
  SQL
@@ -63,6 +67,7 @@ module PgHero
63
67
  pg_stat_activity.query <> '<insufficient privilege>'
64
68
  AND pg_locks.mode = 'ExclusiveLock'
65
69
  AND pg_stat_activity.pid <> pg_backend_pid()
70
+ AND pg_stat_activity.datname = current_database()
66
71
  ORDER BY
67
72
  pid,
68
73
  query_start
@@ -2,20 +2,21 @@ module PgHero
2
2
  module Methods
3
3
  module QueryStats
4
4
  def query_stats(options = {})
5
- current_query_stats = (options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)).index_by { |q| q["query"] }
6
- historical_query_stats = (options[:historical] ? historical_query_stats(options) : []).index_by { |q| q["query"] }
5
+ current_query_stats = (options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)).index_by { |q| q["query_hash"] }
6
+ historical_query_stats = (options[:historical] ? historical_query_stats(options) : []).index_by { |q| q["query_hash"] }
7
7
  current_query_stats.default = {}
8
8
  historical_query_stats.default = {}
9
9
 
10
10
  query_stats = []
11
- (current_query_stats.keys + historical_query_stats.keys).uniq.each do |query|
11
+ (current_query_stats.keys + historical_query_stats.keys).uniq.each do |query_hash|
12
12
  value = {
13
- "query" => query,
14
- "total_minutes" => current_query_stats[query]["total_minutes"].to_f + historical_query_stats[query]["total_minutes"].to_f,
15
- "calls" => current_query_stats[query]["calls"].to_i + historical_query_stats[query]["calls"].to_i
13
+ "query" => current_query_stats[query_hash]["query"] || historical_query_stats[query_hash]["query"],
14
+ "query_hash" => query_hash,
15
+ "total_minutes" => current_query_stats[query_hash]["total_minutes"].to_f + historical_query_stats[query_hash]["total_minutes"].to_f,
16
+ "calls" => current_query_stats[query_hash]["calls"].to_i + historical_query_stats[query_hash]["calls"].to_i
16
17
  }
17
18
  value["average_time"] = value["total_minutes"] * 1000 * 60 / value["calls"]
18
- value["total_percent"] = value["total_minutes"] * 100.0 / (current_query_stats[query]["all_queries_total_minutes"].to_f + historical_query_stats[query]["all_queries_total_minutes"].to_f)
19
+ value["total_percent"] = value["total_minutes"] * 100.0 / (current_query_stats[query_hash]["all_queries_total_minutes"].to_f + historical_query_stats[query_hash]["all_queries_total_minutes"].to_f)
19
20
  query_stats << value
20
21
  end
21
22
  sort = options[:sort] || "total_minutes"
@@ -61,24 +62,56 @@ module PgHero
61
62
  end
62
63
  end
63
64
 
65
+ # resetting query stats will reset across the entire Postgres instance
66
+ # this is problematic if multiple PgHero databases use the same Postgres instance
67
+ #
68
+ # to get around this, we capture queries for every Postgres database before we
69
+ # reset query stats for the Postgres instance with the `capture_query_stats` option
64
70
  def capture_query_stats
65
- config["databases"].keys.each do |database|
71
+ # get database names
72
+ pg_databases = {}
73
+ supports_query_hash = {}
74
+ config["databases"].each do |k, _|
75
+ with(k) do
76
+ pg_databases[k] = execute("SELECT current_database()").first["current_database"]
77
+ supports_query_hash[k] = supports_query_hash?
78
+ end
79
+ end
80
+
81
+ config["databases"].reject { |_, v| v["capture_query_stats"] && v["capture_query_stats"] != true }.each do |database, _|
66
82
  with(database) do
83
+ mapping = {database => pg_databases[database]}
84
+ config["databases"].select { |_, v| v["capture_query_stats"] == database }.each do |k, _|
85
+ mapping[k] = pg_databases[k]
86
+ end
87
+
67
88
  now = Time.now
68
- query_stats = self.query_stats(limit: 1000000)
69
- if query_stats.any? && reset_query_stats
70
- values =
71
- query_stats.map do |qs|
72
- [
73
- database,
74
- qs["query"],
75
- qs["total_minutes"].to_f * 60 * 1000,
76
- qs["calls"],
77
- now
78
- ].map { |v| quote(v) }.join(",")
79
- end.map { |v| "(#{v})" }.join(",")
80
-
81
- stats_connection.execute("INSERT INTO pghero_query_stats (database, query, total_time, calls, captured_at) VALUES #{values}")
89
+ query_stats = {}
90
+ mapping.each do |db, pg_database|
91
+ query_stats[db] = self.query_stats(limit: 1000000, database: pg_database)
92
+ end
93
+
94
+ if query_stats.any? { |_, v| v.any? } && reset_query_stats
95
+ query_stats.each do |db, db_query_stats|
96
+ if db_query_stats.any?
97
+ values =
98
+ db_query_stats.map do |qs|
99
+ values = [
100
+ db,
101
+ qs["query"],
102
+ qs["total_minutes"].to_f * 60 * 1000,
103
+ qs["calls"],
104
+ now
105
+ ]
106
+ values << qs["query_hash"] if supports_query_hash[db]
107
+ values.map { |v| quote(v) }.join(",")
108
+ end.map { |v| "(#{v})" }.join(",")
109
+
110
+ columns = %w[database query total_time calls captured_at]
111
+ columns << "query_hash" if supports_query_hash[db]
112
+ stats_connection.execute("INSERT INTO pghero_query_stats (#{columns.join(", ")}) VALUES #{values}")
113
+ end
114
+ end
82
115
  end
83
116
  end
84
117
  end
@@ -115,22 +148,25 @@ module PgHero
115
148
  if query_stats_enabled?
116
149
  limit = options[:limit] || 100
117
150
  sort = options[:sort] || "total_minutes"
151
+ database = options[:database] ? quote(options[:database]) : "current_database()"
118
152
  select_all <<-SQL
119
153
  WITH query_stats AS (
120
154
  SELECT
121
155
  query,
122
- (total_time / 1000 / 60) as total_minutes,
123
- (total_time / calls) as average_time,
156
+ #{supports_query_hash? ? "queryid" : "md5(query)"} AS query_hash,
157
+ (total_time / 1000 / 60) AS total_minutes,
158
+ (total_time / calls) AS average_time,
124
159
  calls
125
160
  FROM
126
161
  pg_stat_statements
127
162
  INNER JOIN
128
163
  pg_database ON pg_database.oid = pg_stat_statements.dbid
129
164
  WHERE
130
- pg_database.datname = current_database()
165
+ pg_database.datname = #{database}
131
166
  )
132
167
  SELECT
133
168
  query,
169
+ query_hash,
134
170
  total_minutes,
135
171
  average_time,
136
172
  calls,
@@ -153,20 +189,23 @@ module PgHero
153
189
  stats_connection.select_all squish <<-SQL
154
190
  WITH query_stats AS (
155
191
  SELECT
156
- query,
157
- (SUM(total_time) / 1000 / 60) as total_minutes,
158
- (SUM(total_time) / SUM(calls)) as average_time,
159
- SUM(calls) as calls
192
+ #{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
193
+ MAX(query) AS query,
194
+ (SUM(total_time) / 1000 / 60) AS total_minutes,
195
+ (SUM(total_time) / SUM(calls)) AS average_time,
196
+ SUM(calls) AS calls
160
197
  FROM
161
198
  pghero_query_stats
162
199
  WHERE
163
200
  database = #{quote(current_database)}
201
+ #{supports_query_hash? ? "AND query_hash IS NOT NULL" : ""}
164
202
  #{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
165
203
  #{options[:end_at] ? "AND captured_at <= #{quote(options[:end_at])}" : ""}
166
204
  GROUP BY
167
- query
205
+ 1
168
206
  )
169
207
  SELECT
208
+ query_hash,
170
209
  query,
171
210
  total_minutes,
172
211
  average_time,
@@ -183,6 +222,19 @@ module PgHero
183
222
  []
184
223
  end
185
224
  end
225
+
226
+ def supports_query_hash?
227
+ @supports_query_hash ||= {}
228
+ if @supports_query_hash[current_database].nil?
229
+ @supports_query_hash[current_database] = server_version >= 90400 && historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("query_hash")
230
+ end
231
+ @supports_query_hash[current_database]
232
+ end
233
+
234
+ def server_version
235
+ @server_version ||= {}
236
+ @server_version[current_database] ||= select_all("SHOW server_version_num").first["server_version_num"].to_i
237
+ end
186
238
  end
187
239
  end
188
240
  end
@@ -0,0 +1,37 @@
1
+ module PgHero
2
+ module Methods
3
+ module Sequences
4
+ def sequences
5
+ sequences = select_all <<-SQL
6
+ SELECT
7
+ sequence_schema AS schema,
8
+ table_name AS table,
9
+ column_name AS column,
10
+ c.data_type AS column_type,
11
+ CASE WHEN c.data_type = 'integer' THEN 2147483647::bigint ELSE maximum_value::bigint END AS max_value,
12
+ sequence_name AS sequence
13
+ FROM
14
+ information_schema.columns c
15
+ INNER JOIN
16
+ information_schema.sequences iss ON iss.sequence_name = regexp_replace(c.column_default, '^nextval\\(''(.*)''\\:\\:regclass\\)$', '\\1')
17
+ WHERE
18
+ column_default LIKE 'nextval%'
19
+ AND table_catalog = current_database()
20
+ ORDER BY
21
+ sequence_name ASC
22
+ SQL
23
+
24
+ select_all(sequences.map { |s| "SELECT last_value FROM #{s["sequence"]}" }.join(" UNION ALL ")).each_with_index do |row, i|
25
+ sequences[i]["last_value"] = row["last_value"]
26
+ end
27
+
28
+ sequences
29
+ end
30
+
31
+ def sequence_danger(options = {})
32
+ threshold = (options[:threshold] || 0.9).to_f
33
+ sequences.select { |s| s["last_value"].to_i / s["max_value"].to_f > threshold }.sort_by { |s| s["max_value"].to_i - s["last_value"].to_i }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -274,11 +274,17 @@ module PgHero
274
274
  if tree["BOOLEXPR"]
275
275
  if tree["BOOLEXPR"]["boolop"] == 0
276
276
  tree["BOOLEXPR"]["args"].flat_map { |v| parse_where(v) }
277
+ else
278
+ raise "Not Implemented"
277
279
  end
278
280
  elsif tree["AEXPR AND"]
279
281
  left = parse_where(tree["AEXPR AND"]["lexpr"])
280
282
  right = parse_where(tree["AEXPR AND"]["rexpr"])
281
- left + right if left && right
283
+ if left && right
284
+ left + right
285
+ else
286
+ raise "Not Implemented"
287
+ end
282
288
  elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first)
283
289
  [{column: aexpr["lexpr"]["COLUMNREF"]["fields"].last, op: aexpr["name"].first}]
284
290
  elsif tree["AEXPR IN"] && ["=", "<>"].include?(tree["AEXPR IN"]["name"].first)
@@ -286,6 +292,8 @@ module PgHero
286
292
  elsif tree["NULLTEST"]
287
293
  op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
288
294
  [{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
295
+ else
296
+ raise "Not Implemented"
289
297
  end
290
298
  end
291
299
 
@@ -25,9 +25,9 @@ module PgHero
25
25
  if system_stats_enabled?
26
26
  client =
27
27
  if defined?(Aws)
28
- Aws::CloudWatch::Client.new(access_key_id: access_key_id, secret_access_key: secret_access_key)
28
+ Aws::CloudWatch::Client.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region)
29
29
  else
30
- AWS::CloudWatch.new(access_key_id: access_key_id, secret_access_key: secret_access_key).client
30
+ AWS::CloudWatch.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region).client
31
31
  end
32
32
 
33
33
  now = Time.now
@@ -62,6 +62,10 @@ module PgHero
62
62
  ENV["PGHERO_SECRET_ACCESS_KEY"] || ENV["AWS_SECRET_ACCESS_KEY"]
63
63
  end
64
64
 
65
+ def region
66
+ ENV["PGHERO_REGION"] || ENV["AWS_REGION"] || (defined?(Aws) && Aws.config[:region]) || "us-east-1"
67
+ end
68
+
65
69
  def db_instance_identifier
66
70
  databases[current_database].db_instance_identifier
67
71
  end
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.2.4"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -130,6 +130,15 @@ class BestIndexTest < Minitest::Test
130
130
  assert_no_index "Unknown structure", "SELECT NOW()"
131
131
  end
132
132
 
133
+ def test_where_or
134
+ assert_no_index "Unknown structure", "SELECT FROM users WHERE login_attempts = 0 OR login_attempts = 1"
135
+ end
136
+
137
+ def test_where_nested_or
138
+ assert_no_index "Unknown structure", "SELECT FROM users WHERE city_id = 1 AND (login_attempts = 0 OR login_attempts = 1)"
139
+ end
140
+
141
+
133
142
  def test_multiple_tables
134
143
  assert_no_index "JOIN not supported yet", "SELECT * FROM users INNER JOIN cities ON cities.id = users.city_id"
135
144
  end
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in pghero.gemspec
4
+ gemspec path: "../.."
5
+
6
+ gem "activerecord", "~> 4.1.0"
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: 1.2.4
4
+ version: 1.3.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: 2016-05-06 00:00:00.000000000 Z
11
+ date: 2016-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -162,6 +162,7 @@ files:
162
162
  - lib/pghero/methods/queries.rb
163
163
  - lib/pghero/methods/query_stats.rb
164
164
  - lib/pghero/methods/replica.rb
165
+ - lib/pghero/methods/sequences.rb
165
166
  - lib/pghero/methods/space.rb
166
167
  - lib/pghero/methods/suggested_indexes.rb
167
168
  - lib/pghero/methods/system.rb
@@ -173,6 +174,7 @@ files:
173
174
  - pghero.gemspec
174
175
  - test/best_index_test.rb
175
176
  - test/explain_test.rb
177
+ - test/gemfiles/activerecord41.gemfile
176
178
  - test/suggested_indexes_test.rb
177
179
  - test/test_helper.rb
178
180
  homepage: https://github.com/ankane/pghero
@@ -208,6 +210,7 @@ test_files:
208
210
  - guides/Suggested-Indexes.md
209
211
  - test/best_index_test.rb
210
212
  - test/explain_test.rb
213
+ - test/gemfiles/activerecord41.gemfile
211
214
  - test/suggested_indexes_test.rb
212
215
  - test/test_helper.rb
213
216
  has_rdoc: