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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +178 -0
- data/lib/pg_query/deparse.rb +481 -0
- data/lib/pg_query/treewalker.rb +47 -0
- data/lib/pg_query/truncate.rb +58 -0
- data/lib/pg_query/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff45e7cd81fcb9dfcf97a5d57f9cf6c2e4131f31
|
4
|
+
data.tar.gz: 6458e3ebc2b8500062fef38392ef2c64235dafe5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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://rubygems.org/gems/pg_query) [ ](https://rubygems.org/gems/pg_query) [ ](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
|
data/lib/pg_query/version.rb
CHANGED
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.
|
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-
|
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:
|