pghero 1.4.2 → 1.5.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.

Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +8 -8
  4. data/app/assets/javascripts/pghero/Chart.bundle.js +183 -55
  5. data/app/assets/javascripts/pghero/chartkick.js +53 -20
  6. data/app/assets/stylesheets/pghero/application.css +7 -0
  7. data/app/controllers/pg_hero/home_controller.rb +61 -57
  8. data/app/views/layouts/pg_hero/application.html.erb +3 -3
  9. data/app/views/pg_hero/home/_connections_table.html.erb +1 -1
  10. data/app/views/pg_hero/home/_queries_table.html.erb +6 -1
  11. data/app/views/pg_hero/home/connections.html.erb +1 -1
  12. data/app/views/pg_hero/home/explain.html.erb +11 -2
  13. data/app/views/pg_hero/home/index.html.erb +4 -4
  14. data/app/views/pg_hero/home/maintenance.html.erb +2 -2
  15. data/app/views/pg_hero/home/system.html.erb +2 -2
  16. data/guides/Rails.md +8 -0
  17. data/lib/generators/pghero/space_stats_generator.rb +29 -0
  18. data/lib/generators/pghero/templates/space_stats.rb +13 -0
  19. data/lib/pghero.rb +109 -23
  20. data/lib/pghero/database.rb +46 -8
  21. data/lib/pghero/engine.rb +3 -1
  22. data/lib/pghero/methods/basic.rb +3 -42
  23. data/lib/pghero/methods/connections.rb +18 -1
  24. data/lib/pghero/methods/explain.rb +2 -0
  25. data/lib/pghero/methods/indexes.rb +2 -2
  26. data/lib/pghero/methods/kill.rb +1 -1
  27. data/lib/pghero/methods/queries.rb +2 -2
  28. data/lib/pghero/methods/query_stats.rb +81 -75
  29. data/lib/pghero/methods/space.rb +12 -1
  30. data/lib/pghero/methods/suggested_indexes.rb +71 -31
  31. data/lib/pghero/version.rb +1 -1
  32. data/lib/tasks/pghero.rake +5 -0
  33. metadata +4 -3
  34. data/lib/pghero/methods/databases.rb +0 -39
@@ -6,7 +6,24 @@ module PgHero
6
6
  end
7
7
 
8
8
  def connection_sources(options = {})
9
- if options[:by_database]
9
+ if options[:by_database_and_user]
10
+ select_all <<-SQL
11
+ SELECT
12
+ datname AS database,
13
+ usename AS user,
14
+ application_name AS source,
15
+ client_addr AS ip,
16
+ COUNT(*) AS total_connections
17
+ FROM
18
+ pg_stat_activity
19
+ WHERE
20
+ pid <> pg_backend_pid()
21
+ GROUP BY
22
+ 1, 2, 3, 4
23
+ ORDER BY
24
+ 5 DESC, 1, 2, 3, 4
25
+ SQL
26
+ elsif options[:by_database]
10
27
  select_all <<-SQL
11
28
  SELECT
12
29
  application_name AS source,
@@ -18,6 +18,8 @@ module PgHero
18
18
  explanation
19
19
  end
20
20
 
21
+ private
22
+
21
23
  def explain_safe?
22
24
  select_all("SELECT 1; SELECT 1")
23
25
  false
@@ -140,8 +140,8 @@ module PgHero
140
140
  indexes = []
141
141
 
142
142
  indexes_by_table = self.indexes.group_by { |i| i["table"] }
143
- indexes_by_table.values.flatten.select { |i| falsey?(i["primary"]) && falsey?(i["unique"]) && !i["indexprs"] && !i["indpred"] && truthy?(i["valid"]) }.each do |index|
144
- covering_index = indexes_by_table[index["table"]].find { |i| index_covers?(i["columns"], index["columns"]) && i["using"] == index["using"] && i["name"] != index["name"] && truthy?(i["valid"]) }
143
+ indexes_by_table.values.flatten.select { |i| PgHero.falsey?(i["primary"]) && PgHero.falsey?(i["unique"]) && !i["indexprs"] && !i["indpred"] && PgHero.truthy?(i["valid"]) }.each do |index|
144
+ covering_index = indexes_by_table[index["table"]].find { |i| index_covers?(i["columns"], index["columns"]) && i["using"] == index["using"] && i["name"] != index["name"] && PgHero.truthy?(i["valid"]) }
145
145
  if covering_index
146
146
  indexes << {"unneeded_index" => index, "covering_index" => covering_index}
147
147
  end
@@ -2,7 +2,7 @@ module PgHero
2
2
  module Methods
3
3
  module Kill
4
4
  def kill(pid)
5
- truthy? execute("SELECT pg_terminate_backend(#{pid.to_i})").first["pg_terminate_backend"]
5
+ PgHero.truthy? execute("SELECT pg_terminate_backend(#{pid.to_i})").first["pg_terminate_backend"]
6
6
  end
7
7
 
8
8
  def kill_long_running_queries
@@ -41,7 +41,7 @@ module PgHero
41
41
  query <> '<insufficient privilege>'
42
42
  AND state <> 'idle'
43
43
  AND pid <> pg_backend_pid()
44
- AND now() - query_start > interval '#{long_running_query_sec.to_i} seconds'
44
+ AND now() - query_start > interval '#{PgHero.long_running_query_sec.to_i} seconds'
45
45
  AND datname = current_database()
46
46
  ORDER BY
47
47
  query_start DESC
@@ -50,7 +50,7 @@ module PgHero
50
50
 
51
51
  def slow_queries(options = {})
52
52
  query_stats = options[:query_stats] || self.query_stats(options.except(:query_stats))
53
- query_stats.select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
53
+ query_stats.select { |q| q["calls"].to_i >= PgHero.slow_query_calls.to_i && q["average_time"].to_i >= PgHero.slow_query_ms.to_i }
54
54
  end
55
55
 
56
56
  def locks
@@ -5,8 +5,8 @@ module PgHero
5
5
  current_query_stats = options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)
6
6
  historical_query_stats = options[:historical] ? historical_query_stats(options) : []
7
7
 
8
- query_stats = combine_query_stats((current_query_stats + historical_query_stats).group_by { |q| q["query_hash"] })
9
- query_stats = combine_query_stats(query_stats.group_by { |q| normalize_query(q["query"]) })
8
+ query_stats = combine_query_stats((current_query_stats + historical_query_stats).group_by { |q| [q["query_hash"], q["user"]] })
9
+ query_stats = combine_query_stats(query_stats.group_by { |q| [normalize_query(q["query"]), q["user"]] })
10
10
 
11
11
  # add percentages
12
12
  all_queries_total_minutes = [current_query_stats, historical_query_stats].sum { |s| (s.first || {})["all_queries_total_minutes"].to_f }
@@ -63,65 +63,10 @@ module PgHero
63
63
  end
64
64
  end
65
65
 
66
- # resetting query stats will reset across the entire Postgres instance
67
- # this is problematic if multiple PgHero databases use the same Postgres instance
68
- #
69
- # to get around this, we capture queries for every Postgres database before we
70
- # reset query stats for the Postgres instance with the `capture_query_stats` option
71
- def capture_query_stats
72
- # get database names
73
- pg_databases = {}
74
- supports_query_hash = {}
75
- config["databases"].each do |k, _|
76
- with(k) do
77
- pg_databases[k] = execute("SELECT current_database()").first["current_database"]
78
- supports_query_hash[k] = supports_query_hash?
79
- end
80
- end
81
-
82
- config["databases"].reject { |_, v| v["capture_query_stats"] && v["capture_query_stats"] != true }.each do |database, _|
83
- with(database) do
84
- mapping = {database => pg_databases[database]}
85
- config["databases"].select { |_, v| v["capture_query_stats"] == database }.each do |k, _|
86
- mapping[k] = pg_databases[k]
87
- end
88
-
89
- now = Time.now
90
- query_stats = {}
91
- mapping.each do |db, pg_database|
92
- query_stats[db] = self.query_stats(limit: 1000000, database: pg_database)
93
- end
94
-
95
- if query_stats.any? { |_, v| v.any? } && reset_query_stats
96
- query_stats.each do |db, db_query_stats|
97
- if db_query_stats.any?
98
- values =
99
- db_query_stats.map do |qs|
100
- values = [
101
- db,
102
- qs["query"],
103
- qs["total_minutes"].to_f * 60 * 1000,
104
- qs["calls"],
105
- now
106
- ]
107
- values << qs["query_hash"] if supports_query_hash[db]
108
- values.map { |v| quote(v) }.join(",")
109
- end.map { |v| "(#{v})" }.join(",")
110
-
111
- columns = %w[database query total_time calls captured_at]
112
- columns << "query_hash" if supports_query_hash[db]
113
- stats_connection.execute("INSERT INTO pghero_query_stats (#{columns.join(", ")}) VALUES #{values}")
114
- end
115
- end
116
- end
117
- end
118
- end
119
- end
120
-
121
66
  # http://stackoverflow.com/questions/20582500/how-to-check-if-a-table-exists-in-a-given-schema
122
67
  def historical_query_stats_enabled?
123
68
  # TODO use schema from config
124
- truthy? stats_connection.select_all(squish <<-SQL
69
+ PgHero.truthy?(stats_connection.select_all(squish <<-SQL
125
70
  SELECT EXISTS (
126
71
  SELECT
127
72
  1
@@ -135,15 +80,80 @@ module PgHero
135
80
  AND c.relkind = 'r'
136
81
  )
137
82
  SQL
138
- ).to_a.first["exists"]
83
+ ).to_a.first["exists"]) && capture_query_stats?
139
84
  end
140
85
 
141
- def stats_connection
142
- ::PgHero::QueryStats.connection
86
+ def supports_query_hash?
87
+ @supports_query_hash ||= server_version >= 90400 && historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("query_hash")
88
+ end
89
+
90
+ def supports_query_stats_user?
91
+ @supports_query_stats_user ||= historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("user")
92
+ end
93
+
94
+ def insert_stats(table, columns, values)
95
+ values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
96
+ columns = columns.map { |v| quote_table_name(v) }.join(",")
97
+ stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
98
+ end
99
+
100
+ # resetting query stats will reset across the entire Postgres instance
101
+ # this is problematic if multiple PgHero databases use the same Postgres instance
102
+ #
103
+ # to get around this, we capture queries for every Postgres database before we
104
+ # reset query stats for the Postgres instance with the `capture_query_stats` option
105
+ def capture_query_stats
106
+ return if config["capture_query_stats"] && config["capture_query_stats"] != true
107
+
108
+ # get all databases that use same query stats and build mapping
109
+ mapping = {id => database_name}
110
+ PgHero.databases.select { |_, d| d.config["capture_query_stats"] == id }.each do |_, d|
111
+ mapping[d.id] = d.database_name
112
+ end
113
+
114
+ now = Time.now
115
+
116
+ query_stats = {}
117
+ mapping.each do |database_id, database_name|
118
+ query_stats[database_id] = query_stats(limit: 1000000, database: database_name)
119
+ end
120
+
121
+ if query_stats.any? { |_, v| v.any? } && reset_query_stats
122
+ query_stats.each do |db_id, db_query_stats|
123
+ if db_query_stats.any?
124
+ supports_query_hash = PgHero.databases[db_id].supports_query_hash?
125
+ supports_query_stats_user = PgHero.databases[db_id].supports_query_stats_user?
126
+
127
+ values =
128
+ db_query_stats.map do |qs|
129
+ values = [
130
+ db_id,
131
+ qs["query"],
132
+ qs["total_minutes"].to_f * 60 * 1000,
133
+ qs["calls"],
134
+ now
135
+ ]
136
+ values << qs["query_hash"] if supports_query_hash
137
+ values << qs["user"] if supports_query_stats_user
138
+ values
139
+ end
140
+
141
+ columns = %w[database query total_time calls captured_at]
142
+ columns << "query_hash" if supports_query_hash
143
+ columns << "user" if supports_query_stats_user
144
+
145
+ insert_stats("pghero_query_stats", columns, values)
146
+ end
147
+ end
148
+ end
143
149
  end
144
150
 
145
151
  private
146
152
 
153
+ def stats_connection
154
+ ::PgHero::QueryStats.connection
155
+ end
156
+
147
157
  # http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
148
158
  def current_query_stats(options = {})
149
159
  if query_stats_enabled?
@@ -155,6 +165,7 @@ module PgHero
155
165
  SELECT
156
166
  LEFT(query, 10000) AS query,
157
167
  #{supports_query_hash? ? "queryid" : "md5(query)"} AS query_hash,
168
+ #{supports_query_stats_user? ? "rolname" : "NULL"} AS user,
158
169
  (total_time / 1000 / 60) AS total_minutes,
159
170
  (total_time / calls) AS average_time,
160
171
  calls
@@ -162,12 +173,15 @@ module PgHero
162
173
  pg_stat_statements
163
174
  INNER JOIN
164
175
  pg_database ON pg_database.oid = pg_stat_statements.dbid
176
+ INNER JOIN
177
+ pg_roles ON pg_roles.oid = pg_stat_statements.userid
165
178
  WHERE
166
179
  pg_database.datname = #{database}
167
180
  )
168
181
  SELECT
169
182
  query,
170
183
  query_hash,
184
+ query_stats.user,
171
185
  total_minutes,
172
186
  average_time,
173
187
  calls,
@@ -191,6 +205,7 @@ module PgHero
191
205
  WITH query_stats AS (
192
206
  SELECT
193
207
  #{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
208
+ #{supports_query_stats_user? ? "pghero_query_stats.user" : "NULL"} AS user,
194
209
  array_agg(LEFT(query, 10000)) AS query,
195
210
  (SUM(total_time) / 1000 / 60) AS total_minutes,
196
211
  (SUM(total_time) / SUM(calls)) AS average_time,
@@ -198,15 +213,16 @@ module PgHero
198
213
  FROM
199
214
  pghero_query_stats
200
215
  WHERE
201
- database = #{quote(current_database)}
216
+ database = #{quote(id)}
202
217
  #{supports_query_hash? ? "AND query_hash IS NOT NULL" : ""}
203
218
  #{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
204
219
  #{options[:end_at] ? "AND captured_at <= #{quote(options[:end_at])}" : ""}
205
220
  GROUP BY
206
- 1
221
+ 1, 2
207
222
  )
208
223
  SELECT
209
224
  query_hash,
225
+ query_stats.user,
210
226
  query[1],
211
227
  total_minutes,
212
228
  average_time,
@@ -224,26 +240,16 @@ module PgHero
224
240
  end
225
241
  end
226
242
 
227
- def supports_query_hash?
228
- @supports_query_hash ||= {}
229
- if @supports_query_hash[current_database].nil?
230
- @supports_query_hash[current_database] = server_version >= 90400 && historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("query_hash")
231
- end
232
- @supports_query_hash[current_database]
233
- end
234
-
235
243
  def server_version
236
- @server_version ||= {}
237
- @server_version[current_database] ||= select_all("SHOW server_version_num").first["server_version_num"].to_i
244
+ @server_version ||= select_all("SHOW server_version_num").first["server_version_num"].to_i
238
245
  end
239
246
 
240
- private
241
-
242
247
  def combine_query_stats(grouped_stats)
243
248
  query_stats = []
244
249
  grouped_stats.each do |_, stats2|
245
250
  value = {
246
251
  "query" => (stats2.find { |s| s["query"] } || {})["query"],
252
+ "user" => (stats2.find { |s| s["user"] } || {})["user"],
247
253
  "query_hash" => (stats2.find { |s| s["query"] } || {})["query_hash"],
248
254
  "total_minutes" => stats2.sum { |s| s["total_minutes"].to_f },
249
255
  "calls" => stats2.sum { |s| s["calls"].to_i },
@@ -11,7 +11,8 @@ module PgHero
11
11
  n.nspname AS schema,
12
12
  c.relname AS name,
13
13
  CASE WHEN c.relkind = 'r' THEN 'table' ELSE 'index' END AS type,
14
- pg_size_pretty(pg_table_size(c.oid)) AS size
14
+ pg_size_pretty(pg_table_size(c.oid)) AS size,
15
+ pg_table_size(c.oid) AS size_bytes
15
16
  FROM
16
17
  pg_class c
17
18
  LEFT JOIN
@@ -25,6 +26,16 @@ module PgHero
25
26
  name ASC
26
27
  SQL
27
28
  end
29
+
30
+ def capture_space_stats
31
+ now = Time.now
32
+ columns = %w[database schema relation size captured_at]
33
+ values = []
34
+ relation_sizes.each do |rs|
35
+ values << [id, rs["schema"], rs["name"], rs["size_bytes"].to_i, now]
36
+ end
37
+ insert_stats("pghero_space_stats", columns, values)
38
+ end
28
39
  end
29
40
  end
30
41
  end
@@ -188,7 +188,9 @@ module PgHero
188
188
  return {error: "Too large"} if statement.to_s.length > 10000
189
189
 
190
190
  begin
191
- tree = PgQuery.parse(statement).parsetree
191
+ parsed_statement = PgQuery.parse(statement)
192
+ v2 = parsed_statement.respond_to?(:tree)
193
+ tree = v2 ? parsed_statement.tree : parsed_statement.parsetree
192
194
  rescue PgQuery::ParseError
193
195
  return {error: "Parse error"}
194
196
  end
@@ -199,10 +201,14 @@ module PgHero
199
201
  unless table
200
202
  error =
201
203
  case tree.keys.first
202
- when "INSERT INTO"
204
+ when "InsertStmt", "INSERT INTO"
203
205
  "INSERT statement"
204
- when "SET"
206
+ when "VariableSetStmt", "SET"
205
207
  "SET statement"
208
+ when "SelectStmt"
209
+ if (tree["SelectStmt"]["fromClause"].first["JoinExpr"] rescue false)
210
+ "JOIN not supported yet"
211
+ end
206
212
  when "SELECT"
207
213
  if (tree["SELECT"]["fromClause"].first["JOINEXPR"] rescue false)
208
214
  "JOIN not supported yet"
@@ -211,11 +217,11 @@ module PgHero
211
217
  return {error: error || "Unknown structure"}
212
218
  end
213
219
 
214
- select = tree["SELECT"] || tree["DELETE FROM"] || tree["UPDATE"]
215
- where = (select["whereClause"] ? parse_where(select["whereClause"]) : []) rescue nil
220
+ select = tree.values.first
221
+ where = (select["whereClause"] ? parse_where(select["whereClause"], v2) : []) rescue nil
216
222
  return {error: "Unknown structure"} unless where
217
223
 
218
- sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue []
224
+ sort = (select["sortClause"] ? parse_sort(select["sortClause"], v2) : []) rescue []
219
225
 
220
226
  {table: table, where: where, sort: sort}
221
227
  end
@@ -260,6 +266,12 @@ module PgHero
260
266
 
261
267
  def parse_table(tree)
262
268
  case tree.keys.first
269
+ when "SelectStmt"
270
+ tree["SelectStmt"]["fromClause"].first["RangeVar"]["relname"]
271
+ when "DeleteStmt"
272
+ tree["DeleteStmt"]["relation"]["RangeVar"]["relname"]
273
+ when "UpdateStmt"
274
+ tree["UpdateStmt"]["relation"]["RangeVar"]["relname"]
263
275
  when "SELECT"
264
276
  tree["SELECT"]["fromClause"].first["RANGEVAR"]["relname"]
265
277
  when "DELETE FROM"
@@ -270,41 +282,69 @@ module PgHero
270
282
  end
271
283
 
272
284
  # TODO capture values
273
- def parse_where(tree)
274
- aexpr = tree["AEXPR"] || tree[nil]
285
+ def parse_where(tree, v2 = false)
286
+ if v2
287
+ aexpr = tree["A_Expr"]
275
288
 
276
- if tree["BOOLEXPR"]
277
- if tree["BOOLEXPR"]["boolop"] == 0
278
- tree["BOOLEXPR"]["args"].flat_map { |v| parse_where(v) }
289
+ if tree["BoolExpr"]
290
+ if tree["BoolExpr"]["boolop"] == 0
291
+ tree["BoolExpr"]["args"].flat_map { |v| parse_where(v, v2) }
292
+ else
293
+ raise "Not Implemented"
294
+ end
295
+ elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first["String"]["str"])
296
+ [{column: aexpr["lexpr"]["ColumnRef"]["fields"].last["String"]["str"], op: aexpr["name"].first["String"]["str"]}]
297
+ elsif tree["NullTest"]
298
+ op = tree["NullTest"]["nulltesttype"] == 1 ? "not_null" : "null"
299
+ [{column: tree["NullTest"]["arg"]["ColumnRef"]["fields"].last["String"]["str"], op: op}]
279
300
  else
280
301
  raise "Not Implemented"
281
302
  end
282
- elsif tree["AEXPR AND"]
283
- left = parse_where(tree["AEXPR AND"]["lexpr"])
284
- right = parse_where(tree["AEXPR AND"]["rexpr"])
285
- if left && right
286
- left + right
303
+ else
304
+ aexpr = tree["AEXPR"] || tree[nil]
305
+
306
+ if tree["BOOLEXPR"]
307
+ if tree["BOOLEXPR"]["boolop"] == 0
308
+ tree["BOOLEXPR"]["args"].flat_map { |v| parse_where(v) }
309
+ else
310
+ raise "Not Implemented"
311
+ end
312
+ elsif tree["AEXPR AND"]
313
+ left = parse_where(tree["AEXPR AND"]["lexpr"])
314
+ right = parse_where(tree["AEXPR AND"]["rexpr"])
315
+ if left && right
316
+ left + right
317
+ else
318
+ raise "Not Implemented"
319
+ end
320
+ elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first)
321
+ [{column: aexpr["lexpr"]["COLUMNREF"]["fields"].last, op: aexpr["name"].first}]
322
+ elsif tree["AEXPR IN"] && ["=", "<>"].include?(tree["AEXPR IN"]["name"].first)
323
+ [{column: tree["AEXPR IN"]["lexpr"]["COLUMNREF"]["fields"].last, op: tree["AEXPR IN"]["name"].first}]
324
+ elsif tree["NULLTEST"]
325
+ op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
326
+ [{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
287
327
  else
288
328
  raise "Not Implemented"
289
329
  end
290
- elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first)
291
- [{column: aexpr["lexpr"]["COLUMNREF"]["fields"].last, op: aexpr["name"].first}]
292
- elsif tree["AEXPR IN"] && ["=", "<>"].include?(tree["AEXPR IN"]["name"].first)
293
- [{column: tree["AEXPR IN"]["lexpr"]["COLUMNREF"]["fields"].last, op: tree["AEXPR IN"]["name"].first}]
294
- elsif tree["NULLTEST"]
295
- op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
296
- [{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
297
- else
298
- raise "Not Implemented"
299
330
  end
300
331
  end
301
332
 
302
- def parse_sort(sort_clause)
303
- sort_clause.map do |v|
304
- {
305
- column: v["SORTBY"]["node"]["COLUMNREF"]["fields"].last,
306
- direction: v["SORTBY"]["sortby_dir"] == 2 ? "desc" : "asc"
307
- }
333
+ def parse_sort(sort_clause, v2)
334
+ if v2
335
+ sort_clause.map do |v|
336
+ {
337
+ column: v["SortBy"]["node"]["ColumnRef"]["fields"].last["String"]["str"],
338
+ direction: v["SortBy"]["sortby_dir"] == 2 ? "desc" : "asc"
339
+ }
340
+ end
341
+ else
342
+ sort_clause.map do |v|
343
+ {
344
+ column: v["SORTBY"]["node"]["COLUMNREF"]["fields"].last,
345
+ direction: v["SORTBY"]["sortby_dir"] == 2 ? "desc" : "asc"
346
+ }
347
+ end
308
348
  end
309
349
  end
310
350