pg_query 0.6.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4626094955c34766049a41a21623847e80cb0d8
4
- data.tar.gz: ea9c73a5e6e410f14dd3baa3971dbb87046efab3
3
+ metadata.gz: ff45e7cd81fcb9dfcf97a5d57f9cf6c2e4131f31
4
+ data.tar.gz: 6458e3ebc2b8500062fef38392ef2c64235dafe5
5
5
  SHA512:
6
- metadata.gz: eb545f475347e8add8714fd95b9c5022d79c24296c16c73a7990e1742704de0778d713faa47ec0cb34de609ffa7e05a8a60e0fe3357910034e9d3ec633bc50b2
7
- data.tar.gz: c4981b0e1107bf5ce207376207c8d7b90244536cc480b2d0a7a3c2f311938cb7bf95bfaf7eae171e4448993da335fbd65984dadc37af0543c2970b5178d98f9c
6
+ metadata.gz: 40ef7f21ea55cc3e45d3eb88f7c095df908998773f50080524aca6b7a08db83a1191c3a874c6a04252a580ee7fc1412a2a78c778939c5b61eab5b25f9630d911
7
+ data.tar.gz: 76602b4307347a01e9262ef9065d577a16d2b4951e7125a5a2910993517b85e7202050d2e31cda685f1eae0c8b36764497253c4319497fd2010c46da462d05d2
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## 0.6.2 UNRELEASED
4
+
5
+ * ...
6
+
7
+
8
+ ## 0.6.1 2015-08-06
9
+
10
+ * Deparsing: Support WITH clauses in INSERT/UPDATE/DELETE [@JackDanger](https://github.com/JackDanger)
11
+ * Make sure gemspec includes all necessary files
12
+
13
+
14
+ ## 0.6.0 2015-08-05
15
+
16
+ * Deparsing (experimental)
17
+ * Turns parse trees into SQL again
18
+ * New truncate method to smartly truncate based on less important query parts
19
+ * Thanks to [@mme](https://github.com/mme) & [@JackDanger](https://github.com/JackDanger) for their contributions
20
+ * Restructure extension C code
21
+ * Add table/filter columns support for CTEs
22
+ * Extract views as tables from CREATE/REFRESH VIEW
23
+ * Refactor code using generic treewalker
24
+ * fingerprint: Normalize IN lists
25
+ * param_refs: Fix length attribute in result
26
+
27
+
28
+ ## 0.5.0 2015-03-26
29
+
30
+ * Query fingerprinting
31
+ * Filter columns (aka columns referenced in a query's WHERE clause)
32
+ * Parameter references: Returns all $1/$2/etc like references in the query with their location
33
+ * Remove dependency on active_support
34
+
35
+
36
+ ## 0.4.1 2014-12-18
37
+
38
+ * Fix compilation of C extension
39
+ * Fix gemspec
40
+
41
+
42
+ ## 0.4.0 2014-12-18
43
+
44
+ * Speed up build time by only building necessary objects
45
+ * PostgreSQL 9.4 parser
46
+
47
+
48
+ See git commit log for previous releases.
data/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # pg_query [ ![](https://img.shields.io/gem/v/pg_query.svg)](https://rubygems.org/gems/pg_query) [ ![](https://img.shields.io/gem/dt/pg_query.svg)](https://rubygems.org/gems/pg_query) [ ![Codeship Status for lfittl/dblint](https://img.shields.io/codeship/584524e0-ed17-0131-838b-4216c01ccc74.svg)](https://codeship.com/projects/26651)
2
+
3
+ This Ruby extension uses the actual PostgreSQL server source to parse SQL queries and return the internal PostgreSQL parsetree.
4
+
5
+ In addition the extension allows you to normalize queries (replacing constant values with ?) and parse these normalized queries into a parsetree again.
6
+
7
+ When you build this extension, it fetches a copy of the PostgreSQL server source and builds parts of it, and then statically links it into this extension.
8
+
9
+ This is slightly crazy, but is the only reliable way of parsing all valid PostgreSQL queries.
10
+
11
+ You can find further examples and a longer rationale here: https://pganalyze.com/blog/parse-postgresql-queries-in-ruby.html
12
+
13
+ ## Installation
14
+
15
+ ```
16
+ gem install pg_query
17
+ ```
18
+
19
+ Due to compiling parts of PostgreSQL, installation will take a while. Expect between 2 and 10 minutes.
20
+
21
+ Note: On some Linux systems you'll have to install the ```flex``` package beforehand.
22
+
23
+ ## Usage
24
+
25
+ ### Parsing a query
26
+
27
+ ```ruby
28
+ PgQuery.parse("SELECT 1")
29
+
30
+ => #<PgQuery:0x007fe92b27ea18
31
+ @parsetree=
32
+ [{"SELECT"=>
33
+ {"distinctClause"=>nil,
34
+ "intoClause"=>nil,
35
+ "targetList"=>
36
+ [{"RESTARGET"=>
37
+ {"name"=>nil,
38
+ "indirection"=>nil,
39
+ "val"=>{"A_CONST"=>{"val"=>1, "location"=>7}},
40
+ "location"=>7}}],
41
+ ...}}],
42
+ @query="SELECT 1",
43
+ @warnings=[]>
44
+ ```
45
+
46
+ ### Modifying a parsed query and turning it into SQL again
47
+
48
+ ```ruby
49
+ parsed_query = PgQuery.parse("SELECT * FROM users")
50
+
51
+ => #<PgQuery:0x007ff3e956c8b0
52
+ @parsetree=
53
+ [{"SELECT"=>{"distinctClause"=>nil,
54
+ "intoClause"=>nil,
55
+ "targetList"=>
56
+ [{"RESTARGET"=>
57
+ {"name"=>nil,
58
+ "indirection"=>nil,
59
+ "val"=>
60
+ {"COLUMNREF"=>
61
+ {"fields"=>[{"A_STAR"=>{}}],
62
+ "location"=>7}},
63
+ "location"=>7}}],
64
+ "fromClause"=>
65
+ [{"RANGEVAR"=>
66
+ {"schemaname"=>nil,
67
+ "relname"=>"users",
68
+ "inhOpt"=>2,
69
+ "relpersistence"=>"p",
70
+ "alias"=>nil,
71
+ "location"=>14}}],
72
+ ...}}],
73
+ @query="SELECT * FROM users",
74
+ @warnings=[]>
75
+
76
+ # Modify the parse tree in some way
77
+ parsed_query.parsetree[0]['SELECT']['fromClause'][0]['RANGEVAR']['relname'] = 'other_users'
78
+
79
+ # Turn it into SQL again
80
+ parsed_query.deparse
81
+ => "SELECT * FROM other_users"
82
+ ```
83
+
84
+ Note: The deparsing feature is experimental and does not support outputting all SQL yet.
85
+
86
+ ### Parsing a normalized query
87
+
88
+ ```ruby
89
+ # Normalizing a query (like pg_stat_statements)
90
+ PgQuery.normalize("SELECT 1 FROM x WHERE y = 'foo'")
91
+
92
+ => "SELECT ? FROM x WHERE y = ?"
93
+
94
+ # Parsing a normalized query
95
+ PgQuery.parse("SELECT ? FROM x WHERE y = ?")
96
+
97
+ => #<PgQuery:0x007fb99455a438
98
+ @parsetree=
99
+ [{"SELECT"=>
100
+ {"distinctClause"=>nil,
101
+ "intoClause"=>nil,
102
+ "targetList"=>
103
+ [{"RESTARGET"=>
104
+ {"name"=>nil,
105
+ "indirection"=>nil,
106
+ "val"=>{"PARAMREF"=>{"number"=>0, "location"=>7}},
107
+ "location"=>7}}],
108
+ "fromClause"=>
109
+ [{"RANGEVAR"=>
110
+ {"schemaname"=>nil,
111
+ "relname"=>"x",
112
+ "inhOpt"=>2,
113
+ "relpersistence"=>"p",
114
+ "alias"=>nil,
115
+ "location"=>14}}],
116
+ "whereClause"=>
117
+ {"AEXPR"=>
118
+ {"name"=>["="],
119
+ "lexpr"=>{"COLUMNREF"=>{"fields"=>["y"], "location"=>22}},
120
+ "rexpr"=>{"PARAMREF"=>{"number"=>0, "location"=>26}},
121
+ "location"=>24}},
122
+ ...}}],
123
+ @query="SELECT ? FROM x WHERE y = ?",
124
+ @warnings=[]>
125
+ ```
126
+
127
+ ### Extracting tables from a query
128
+
129
+ ```ruby
130
+ PgQuery.parse("SELECT ? FROM x JOIN y USING (id) WHERE z = ?").tables
131
+
132
+ => ["x", "y"]
133
+ ```
134
+
135
+ ### Extracting columns from a query
136
+
137
+ ```ruby
138
+ PgQuery.parse("SELECT ? FROM x WHERE x.y = ? AND z = ?").filter_columns
139
+
140
+ => [["x", "y"], [nil, "z"]]
141
+ ```
142
+
143
+ ### Fingerprinting a query
144
+
145
+ ```ruby
146
+ PgQuery.parse("SELECT 1").fingerprint
147
+
148
+ => "db76551255b7861b99bd384cf8096a3dd5162ab3"
149
+
150
+ PgQuery.parse("SELECT 2; --- comment").fingerprint
151
+
152
+ => "db76551255b7861b99bd384cf8096a3dd5162ab3"
153
+ ```
154
+
155
+ ## Differences from Upstream PostgreSQL
156
+
157
+ **This gem uses a [patched version of the latest PostgreSQL stable](https://github.com/pganalyze/postgres/compare/REL9_4_STABLE...pg_query).**
158
+
159
+ Changes:
160
+ * **scan.l/gram.y:** Modified to support parsing normalized queries
161
+ * Known regression: Removed support for custom operators containing "?" (doesn't affect hstore/JSON/geometric operators)
162
+ * **outfuncs_json.c:** Auto-generated outfuncs that outputs a parsetree as JSON (called through nodeToJSONString)
163
+
164
+ Unit tests for these patches are inside this library - the tests will break if run against upstream.
165
+
166
+
167
+ ## Authors
168
+
169
+ - [Lukas Fittl](mailto:lukas@fittl.com)
170
+
171
+
172
+ ## License
173
+
174
+ Copyright (c) 2015, pganalyze Team <team@pganalyze.com><br>
175
+ pg_query is licensed under the 3-clause BSD license, see LICENSE file for details.
176
+
177
+ Query normalization code:<br>
178
+ Copyright (c) 2008-2015, PostgreSQL Global Development Group
@@ -0,0 +1,481 @@
1
+ class PgQuery
2
+ # Reconstruct all of the parsed queries into their original form
3
+ def deparse(tree = @parsetree)
4
+ tree.map do |item|
5
+ self.class.deparse(item)
6
+ end.join('; ')
7
+ end
8
+
9
+ class << self
10
+ # Given one element of the PgQuery#parsetree reconstruct it back into the
11
+ # original query.
12
+ def deparse(item)
13
+ deparse_item(item)
14
+ end
15
+
16
+ private
17
+
18
+ def deparse_item(item, context = nil) # rubocop:disable Metrics/CyclomaticComplexity
19
+ return if item.nil?
20
+
21
+ type = item.keys[0]
22
+ node = item.values[0]
23
+
24
+ case type
25
+ when 'RANGEVAR'
26
+ deparse_rangevar(node)
27
+ when 'AEXPR'
28
+ deparse_aexpr(node)
29
+ when 'COLUMNREF'
30
+ deparse_columnref(node)
31
+ when 'A_ARRAYEXPR'
32
+ deparse_a_arrayexp(node)
33
+ when 'A_CONST'
34
+ deparse_a_const(node)
35
+ when 'A_STAR'
36
+ deparse_a_star(node)
37
+ when 'A_INDIRECTION'
38
+ deparse_a_indirection(node)
39
+ when 'A_INDICES'
40
+ deparse_a_indices(node)
41
+ when 'ALIAS'
42
+ deparse_alias(node)
43
+ when 'PARAMREF'
44
+ deparse_paramref(node)
45
+ when 'RESTARGET'
46
+ deparse_restarget(node, context)
47
+ when 'FUNCCALL'
48
+ deparse_funccall(node)
49
+ when 'RANGEFUNCTION'
50
+ deparse_range_function(node)
51
+ when 'AEXPR AND'
52
+ deparse_aexpr_and(node)
53
+ when 'JOINEXPR'
54
+ deparse_joinexpr(node)
55
+ when 'SORTBY'
56
+ deparse_sortby(node)
57
+ when 'SELECT'
58
+ deparse_select(node)
59
+ when 'WITHCLAUSE'
60
+ deparse_with_clause(node)
61
+ when 'COMMONTABLEEXPR'
62
+ deparse_cte(node)
63
+ when 'INSERT INTO'
64
+ deparse_insert_into(node)
65
+ when 'UPDATE'
66
+ deparse_update(node)
67
+ when 'TYPECAST'
68
+ deparse_typecast(node)
69
+ when 'TYPENAME'
70
+ deparse_typename(node)
71
+ when 'CASE'
72
+ deparse_case(node)
73
+ when 'WHEN'
74
+ deparse_when(node)
75
+ when 'SUBLINK'
76
+ deparse_sublink(node)
77
+ when 'RANGESUBSELECT'
78
+ deparse_rangesubselect(node)
79
+ when 'ROW'
80
+ deparse_row(node)
81
+ when 'AEXPR IN'
82
+ deparse_aexpr_in(node)
83
+ when 'AEXPR NOT'
84
+ deparse_aexpr_not(node)
85
+ when 'AEXPR OR'
86
+ deparse_aexpr_or(node)
87
+ when 'AEXPR ANY'
88
+ deparse_aexpr_any(node)
89
+ when 'NULLTEST'
90
+ deparse_nulltest(node)
91
+ when 'TRANSACTION'
92
+ deparse_transaction(node)
93
+ when 'COALESCE'
94
+ deparse_coalesce(node)
95
+ when 'DELETE FROM'
96
+ deparse_delete_from(node)
97
+ when 'A_TRUNCATED'
98
+ '...' # pg_query internal
99
+ else
100
+ fail format("Can't deparse: %s: %s", type, node.inspect)
101
+ end
102
+ end
103
+
104
+ def deparse_rangevar(node)
105
+ output = []
106
+ output << 'ONLY' if node['inhOpt'] == 0
107
+ output << node['relname']
108
+ output << deparse_item(node['alias']) if node['alias']
109
+ output.join(' ')
110
+ end
111
+
112
+ def deparse_columnref(node)
113
+ node['fields'].map do |field|
114
+ field.is_a?(String) ? field : deparse_item(field)
115
+ end.join('.')
116
+ end
117
+
118
+ def deparse_a_arrayexp(node)
119
+ 'ARRAY[' + node['elements'].map do |element|
120
+ deparse_item(element)
121
+ end.join(', ') + ']'
122
+ end
123
+
124
+ def deparse_a_const(node)
125
+ node['val'].inspect.gsub('"', '\'')
126
+ end
127
+
128
+ def deparse_a_star(_node)
129
+ '*'
130
+ end
131
+
132
+ def deparse_a_indirection(node)
133
+ output = [deparse_item(node['arg'])]
134
+ node['indirection'].each do |subnode|
135
+ output << deparse_item(subnode)
136
+ end
137
+ output.join
138
+ end
139
+
140
+ def deparse_a_indices(node)
141
+ format('[%s]', deparse_item(node['uidx']))
142
+ end
143
+
144
+ def deparse_alias(node)
145
+ node['aliasname']
146
+ end
147
+
148
+ def deparse_paramref(node)
149
+ if node['number'] == 0
150
+ '?'
151
+ else
152
+ format('$%d', node['number'])
153
+ end
154
+ end
155
+
156
+ def deparse_restarget(node, context)
157
+ if context == :select
158
+ [deparse_item(node['val']), node['name']].compact.join(' AS ')
159
+ elsif context == :update
160
+ [node['name'], deparse_item(node['val'])].compact.join(' = ')
161
+ elsif node['val'].nil?
162
+ node['name']
163
+ else
164
+ fail format("Can't deparse %s in context %s", node.inspect, context)
165
+ end
166
+ end
167
+
168
+ def deparse_funccall(node)
169
+ args = Array(node['args']).map { |arg| deparse_item(arg) }
170
+ format('%s(%s)', node['funcname'].join('.'), args.join(', '))
171
+ end
172
+
173
+ def deparse_aexpr_in(node)
174
+ rexpr = Array(node['rexpr']).map { |arg| deparse_item(arg) }
175
+ format('%s IN (%s)', deparse_item(node['lexpr']), rexpr.join(', '))
176
+ end
177
+
178
+ def deparse_aexpr_not(node)
179
+ format('NOT %s', deparse_item(node['rexpr']))
180
+ end
181
+
182
+ def deparse_range_function(node)
183
+ output = []
184
+ output << 'LATERAL' if node['lateral']
185
+ output << deparse_item(node['functions'][0][0]) # FIXME: Needs more test cases
186
+ output << deparse_item(node['alias']) if node['alias']
187
+ output.join(' ')
188
+ end
189
+
190
+ def deparse_aexpr(node)
191
+ output = []
192
+ output << deparse_item(node['lexpr'])
193
+ output << deparse_item(node['rexpr'])
194
+ output.join(' ' + node['name'][0] + ' ')
195
+ end
196
+
197
+ def deparse_aexpr_and(node)
198
+ # Only put parantheses around OR nodes that are inside this one
199
+ lexpr = format(['AEXPR OR'].include?(node['lexpr'].keys[0]) ? '(%s)' : '%s', deparse_item(node['lexpr']))
200
+ rexpr = format(['AEXPR OR'].include?(node['rexpr'].keys[0]) ? '(%s)' : '%s', deparse_item(node['rexpr']))
201
+ format('%s AND %s', lexpr, rexpr)
202
+ end
203
+
204
+ def deparse_aexpr_or(node)
205
+ # Put parantheses around AND + OR nodes that are inside
206
+ lexpr = format(['AEXPR AND', 'AEXPR OR'].include?(node['lexpr'].keys[0]) ? '(%s)' : '%s', deparse_item(node['lexpr']))
207
+ rexpr = format(['AEXPR AND', 'AEXPR OR'].include?(node['rexpr'].keys[0]) ? '(%s)' : '%s', deparse_item(node['rexpr']))
208
+ format('%s OR %s', lexpr, rexpr)
209
+ end
210
+
211
+ def deparse_aexpr_any(node)
212
+ output = []
213
+ output << deparse_item(node['lexpr'])
214
+ output << format('ANY(%s)', deparse_item(node['rexpr']))
215
+ output.join(' ' + node['name'][0] + ' ')
216
+ end
217
+
218
+ def deparse_joinexpr(node)
219
+ output = []
220
+ output << deparse_item(node['larg'])
221
+ output << 'LEFT' if node['jointype'] == 1
222
+ output << 'JOIN'
223
+ output << deparse_item(node['rarg'])
224
+
225
+ if node['quals']
226
+ output << 'ON'
227
+ output << deparse_item(node['quals'])
228
+ end
229
+
230
+ output.join(' ')
231
+ end
232
+
233
+ def deparse_sortby(node)
234
+ output = []
235
+ output << deparse_item(node['node'])
236
+ output << 'ASC' if node['sortby_dir'] == 1
237
+ output.join(' ')
238
+ end
239
+
240
+ def deparse_with_clause(node)
241
+ output = ['WITH']
242
+ output << 'RECURSIVE' if node['recursive']
243
+ output << node['ctes'].map do |cte|
244
+ deparse_item(cte)
245
+ end.join(', ')
246
+ output.join(' ')
247
+ end
248
+
249
+ def deparse_cte(node)
250
+ output = ''
251
+ output += node['ctename']
252
+ output += format('(%s)', node['aliascolnames'].join(', ')) if node['aliascolnames']
253
+ output += format(' AS (%s)', deparse_item(node['ctequery']))
254
+ output
255
+ end
256
+
257
+ def deparse_case(node)
258
+ output = ['CASE']
259
+ output += node['args'].map { |arg| deparse_item(arg) }
260
+ if node['defresult']
261
+ output << 'ELSE'
262
+ output << deparse_item(node['defresult'])
263
+ end
264
+ output << 'END'
265
+ output.join(' ')
266
+ end
267
+
268
+ def deparse_when(node)
269
+ output = ['WHEN']
270
+ output << deparse_item(node['expr'])
271
+ output << 'THEN'
272
+ output << deparse_item(node['result'])
273
+ output.join(' ')
274
+ end
275
+
276
+ def deparse_sublink(node)
277
+ if node['subLinkType'] == 2 && node['operName'] == ['=']
278
+ return format('%s IN (%s)', deparse_item(node['testexpr']), deparse_item(node['subselect']))
279
+ elsif node['subLinkType'] == 0
280
+ return format('EXISTS(%s)', deparse_item(node['subselect']))
281
+ else
282
+ return format('(%s)', deparse_item(node['subselect']))
283
+ end
284
+ end
285
+
286
+ def deparse_rangesubselect(node)
287
+ output = '('
288
+ output += deparse_item(node['subquery'])
289
+ output += ')'
290
+ output += ' ' + node['alias']['ALIAS']['aliasname'] if node['alias']
291
+ output
292
+ end
293
+
294
+ def deparse_row(node)
295
+ 'ROW(' + node['args'].map { |arg| deparse_item(arg) }.join(', ') + ')'
296
+ end
297
+
298
+ def deparse_select(node) # rubocop:disable Metrics/CyclomaticComplexity
299
+ output = []
300
+
301
+ if node['op'] == 1
302
+ output << deparse_item(node['larg'])
303
+ output << 'UNION'
304
+ output << 'ALL' if node['all']
305
+ output << deparse_item(node['rarg'])
306
+ return output.join(' ')
307
+ end
308
+
309
+ output << deparse_item(node['withClause']) if node['withClause']
310
+
311
+ if node['targetList']
312
+ output << 'SELECT'
313
+ output << node['targetList'].map do |item|
314
+ deparse_item(item, :select)
315
+ end.join(', ')
316
+ end
317
+
318
+ if node['fromClause']
319
+ output << 'FROM'
320
+ output << node['fromClause'].map do |item|
321
+ deparse_item(item)
322
+ end.join(', ')
323
+ end
324
+
325
+ if node['whereClause']
326
+ output << 'WHERE'
327
+ output << deparse_item(node['whereClause'])
328
+ end
329
+
330
+ if node['valuesLists']
331
+ output << 'VALUES'
332
+ output << node['valuesLists'].map do |value_list|
333
+ '(' + value_list.map { |v| deparse_item(v) }.join(', ') + ')'
334
+ end.join(', ')
335
+ end
336
+
337
+ if node['groupClause']
338
+ output << 'GROUP BY'
339
+ output << node['groupClause'].map do |item|
340
+ deparse_item(item)
341
+ end.join(', ')
342
+ end
343
+
344
+ if node['sortClause']
345
+ output << 'ORDER BY'
346
+ output << node['sortClause'].map do |item|
347
+ deparse_item(item)
348
+ end.join(', ')
349
+ end
350
+
351
+ output.join(' ')
352
+ end
353
+
354
+ def deparse_insert_into(node)
355
+ output = []
356
+ output << deparse_item(node['withClause']) if node['withClause']
357
+
358
+ output << 'INSERT INTO'
359
+ output << deparse_item(node['relation'])
360
+
361
+ if node['cols']
362
+ output << '(' + node['cols'].map do |column|
363
+ deparse_item(column)
364
+ end.join(', ') + ')'
365
+ end
366
+
367
+ output << deparse_item(node['selectStmt'])
368
+
369
+ output.join(' ')
370
+ end
371
+
372
+ def deparse_update(node)
373
+ output = []
374
+ output << deparse_item(node['withClause']) if node['withClause']
375
+
376
+ output << 'UPDATE'
377
+ output << deparse_item(node['relation'])
378
+
379
+ if node['targetList']
380
+ output << 'SET'
381
+ node['targetList'].each do |item|
382
+ output << deparse_item(item, :update)
383
+ end
384
+ end
385
+
386
+ if node['whereClause']
387
+ output << 'WHERE'
388
+ output << deparse_item(node['whereClause'])
389
+ end
390
+
391
+ if node['returningList']
392
+ output << 'RETURNING'
393
+ output << node['returningList'].map do |item|
394
+ # RETURNING is formatted like a SELECT
395
+ deparse_item(item, :select)
396
+ end.join(', ')
397
+ end
398
+
399
+ output.join(' ')
400
+ end
401
+
402
+ def deparse_typecast(node)
403
+ if deparse_item(node['typeName']) == :boolean
404
+ deparse_item(node['arg']) == "'t'" ? 'true' : 'false'
405
+ else
406
+ deparse_item(node['arg']) + '::' + deparse_typename(node['typeName']['TYPENAME'])
407
+ end
408
+ end
409
+
410
+ def deparse_typename(node)
411
+ if node['names'] == %w(pg_catalog bool)
412
+ :boolean
413
+ else
414
+ node['names'].join('.')
415
+ end
416
+ end
417
+
418
+ def deparse_nulltest(node)
419
+ output = [deparse_item(node['arg'])]
420
+ if node['nulltesttype'] == 0
421
+ output << 'IS NULL'
422
+ elsif node['nulltesttype'] == 1
423
+ output << 'IS NOT NULL'
424
+ end
425
+ output.join(' ')
426
+ end
427
+
428
+ TRANSACTION_CMDS = {
429
+ 0 => 'BEGIN',
430
+ 2 => 'COMMIT',
431
+ 3 => 'ROLLBACK',
432
+ 4 => 'SAVEPOINT',
433
+ 5 => 'RELEASE',
434
+ 6 => 'ROLLBACK TO SAVEPOINT'
435
+ }
436
+ def deparse_transaction(node)
437
+ output = []
438
+ output << TRANSACTION_CMDS[node['kind']] || fail(format("Can't deparse TRANSACTION %s", node.inspect))
439
+
440
+ if node['options'] && node['options'][0]['DEFELEM']
441
+ output << node['options'][0]['DEFELEM']['arg']
442
+ end
443
+
444
+ output.join(' ')
445
+ end
446
+
447
+ def deparse_coalesce(node)
448
+ format('COALESCE(%s)', node['args'].map { |a| deparse_item(a) }.join(', '))
449
+ end
450
+
451
+ def deparse_delete_from(node)
452
+ output = []
453
+ output << deparse_item(node['withClause']) if node['withClause']
454
+
455
+ output << 'DELETE FROM'
456
+ output << deparse_item(node['relation'])
457
+
458
+ if node['usingClause']
459
+ output << 'USING'
460
+ output << node['usingClause'].map do |item|
461
+ deparse_item(item)
462
+ end.join(', ')
463
+ end
464
+
465
+ if node['whereClause']
466
+ output << 'WHERE'
467
+ output << deparse_item(node['whereClause'])
468
+ end
469
+
470
+ if node['returningList']
471
+ output << 'RETURNING'
472
+ output << node['returningList'].map do |item|
473
+ # RETURNING is formatted like a SELECT
474
+ deparse_item(item, :select)
475
+ end.join(', ')
476
+ end
477
+
478
+ output.join(' ')
479
+ end
480
+ end
481
+ end
@@ -0,0 +1,47 @@
1
+ class PgQuery
2
+ private
3
+
4
+ def treewalker!(normalized_parsetree, &block)
5
+ exprs = normalized_parsetree.dup.map { |e| [e, []] }
6
+
7
+ loop do
8
+ expr, parent_location = exprs.shift
9
+
10
+ if expr.is_a?(Hash)
11
+ expr.each do |k, v|
12
+ location = parent_location + [k]
13
+
14
+ block.call(expr, k, v, location)
15
+
16
+ exprs << [v, location] unless v.nil?
17
+ end
18
+ elsif expr.is_a?(Array)
19
+ exprs += expr.map.with_index { |e, idx| [e, parent_location + [idx]] }
20
+ end
21
+
22
+ break if exprs.empty?
23
+ end
24
+ end
25
+
26
+ def find_tree_location(normalized_parsetree, searched_location, &block)
27
+ treewalker! normalized_parsetree do |expr, k, v, location|
28
+ next unless location == searched_location
29
+ block.call(expr, k, v)
30
+ end
31
+ end
32
+
33
+ def deep_dup(obj)
34
+ case obj
35
+ when Hash
36
+ obj.each_with_object(obj.dup) do |(key, value), hash|
37
+ hash[deep_dup(key)] = deep_dup(value)
38
+ end
39
+ when Array
40
+ obj.map { |it| deep_dup(it) }
41
+ when NilClass, FalseClass, TrueClass, Symbol, Numeric
42
+ obj # Can't be duplicated
43
+ else
44
+ obj.dup
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ class PgQuery
2
+ PossibleTruncation = Struct.new(:location, :node_type, :length, :is_array)
3
+
4
+ # Truncates the query string to be below the specified length, first trying to
5
+ # omit less important parts of the query, and only then cutting off the end.
6
+ def truncate(max_length)
7
+ output = deparse(parsetree)
8
+
9
+ # Early exit if we're already below the max length
10
+ return output if output.size <= max_length
11
+
12
+ truncations = find_possible_truncations
13
+
14
+ # Truncate the deepest possible truncation that is the longest first
15
+ truncations.sort_by! { |t| [-t.location.size, -t.length] }
16
+
17
+ tree = deep_dup(parsetree)
18
+ truncations.each do |truncation|
19
+ next if truncation.length < 3
20
+
21
+ find_tree_location(tree, truncation.location) do |expr, k|
22
+ expr[k] = { 'A_TRUNCATED' => nil }
23
+ expr[k] = [expr[k]] if truncation.is_array
24
+ end
25
+
26
+ output = deparse(tree)
27
+ return output if output.size <= max_length
28
+ end
29
+
30
+ # We couldn't do a proper smart truncation, so we need a hard cut-off
31
+ output[0..max_length - 4] + '...'
32
+ end
33
+
34
+ private
35
+
36
+ def find_possible_truncations
37
+ truncations = []
38
+
39
+ treewalker! parsetree do |_expr, k, v, location|
40
+ case k
41
+ when 'targetList'
42
+ length = deparse([{ 'SELECT' => { k => v } }]).size - 'SELECT '.size
43
+
44
+ truncations << PossibleTruncation.new(location, 'targetList', length, true)
45
+ when 'whereClause'
46
+ length = deparse([{ 'SELECT' => { k => v } }]).size
47
+
48
+ truncations << PossibleTruncation.new(location, 'whereClause', length, false)
49
+ when 'ctequery'
50
+ truncations << PossibleTruncation.new(location, 'ctequery', deparse([v]).size, false)
51
+ when 'cols'
52
+ truncations << PossibleTruncation.new(location, 'cols', deparse(v).size, true)
53
+ end
54
+ end
55
+
56
+ truncations
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  class PgQuery
2
- VERSION = '0.6.0'
2
+ VERSION = '0.6.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukas Fittl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-05 00:00:00.000000000 Z
11
+ date: 2015-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -87,7 +87,9 @@ extensions:
87
87
  - ext/pg_query/extconf.rb
88
88
  extra_rdoc_files: []
89
89
  files:
90
+ - CHANGELOG.md
90
91
  - LICENSE
92
+ - README.md
91
93
  - Rakefile
92
94
  - ext/pg_query/extconf.rb
93
95
  - ext/pg_query/pg_polyfills.c
@@ -97,11 +99,14 @@ files:
97
99
  - ext/pg_query/pg_query_normalize.c
98
100
  - ext/pg_query/pg_query_parse.c
99
101
  - lib/pg_query.rb
102
+ - lib/pg_query/deparse.rb
100
103
  - lib/pg_query/filter_columns.rb
101
104
  - lib/pg_query/fingerprint.rb
102
105
  - lib/pg_query/param_refs.rb
103
106
  - lib/pg_query/parse.rb
104
107
  - lib/pg_query/parse_error.rb
108
+ - lib/pg_query/treewalker.rb
109
+ - lib/pg_query/truncate.rb
105
110
  - lib/pg_query/version.rb
106
111
  homepage: http://github.com/pganalyze/pg_query
107
112
  licenses: