pg_query 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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: