gitlab-pg_query 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ class PgQuery
2
+ def param_refs # rubocop:disable Metrics/CyclomaticComplexity
3
+ results = []
4
+
5
+ treewalker! @tree do |_, _, v|
6
+ next unless v.is_a?(Hash)
7
+
8
+ if v[PARAM_REF]
9
+ results << { 'location' => v[PARAM_REF]['location'],
10
+ 'length' => param_ref_length(v[PARAM_REF]) }
11
+ elsif v[TYPE_CAST]
12
+ next unless v[TYPE_CAST]['arg'] && v[TYPE_CAST]['typeName']
13
+
14
+ p = v[TYPE_CAST]['arg'].delete(PARAM_REF)
15
+ t = v[TYPE_CAST]['typeName'].delete(TYPE_NAME)
16
+ next unless p && t
17
+
18
+ location = p['location']
19
+ typeloc = t['location']
20
+ typename = t['names']
21
+ length = param_ref_length(p)
22
+
23
+ if typeloc < location
24
+ length += location - typeloc
25
+ location = typeloc
26
+ end
27
+
28
+ results << { 'location' => location, 'length' => length, 'typename' => typename }
29
+ end
30
+ end
31
+
32
+ results.sort_by! { |r| r['location'] }
33
+ results
34
+ end
35
+
36
+ private
37
+
38
+ def param_ref_length(paramref_node)
39
+ if paramref_node['number'] == 0 # rubocop:disable Style/NumericPredicate
40
+ 1 # Actually a ? replacement character
41
+ else
42
+ ('$' + paramref_node['number'].to_s).size
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,247 @@
1
+ require 'json'
2
+
3
+ class PgQuery
4
+ def self.parse(query)
5
+ tree, stderr = _raw_parse(query)
6
+
7
+ begin
8
+ tree = JSON.parse(tree, max_nesting: 1000)
9
+ rescue JSON::ParserError
10
+ raise ParseError.new('Failed to parse JSON', __FILE__, __LINE__, -1)
11
+ end
12
+
13
+ warnings = []
14
+ stderr.each_line do |line|
15
+ next unless line[/^WARNING/]
16
+ warnings << line.strip
17
+ end
18
+
19
+ PgQuery.new(query, tree, warnings)
20
+ end
21
+
22
+ attr_reader :query
23
+ attr_reader :tree
24
+ attr_reader :warnings
25
+
26
+ def initialize(query, tree, warnings = [])
27
+ @query = query
28
+ @tree = tree
29
+ @warnings = warnings
30
+ @tables = nil
31
+ @aliases = nil
32
+ @cte_names = nil
33
+ end
34
+
35
+ def tables
36
+ tables_with_types.map { |t| t[:table] }
37
+ end
38
+
39
+ def select_tables
40
+ tables_with_types.select { |t| t[:type] == :select }.map { |t| t[:table] }
41
+ end
42
+
43
+ def dml_tables
44
+ tables_with_types.select { |t| t[:type] == :dml }.map { |t| t[:table] }
45
+ end
46
+
47
+ def ddl_tables
48
+ tables_with_types.select { |t| t[:type] == :ddl }.map { |t| t[:table] }
49
+ end
50
+
51
+ def cte_names
52
+ load_tables_and_aliases! if @cte_names.nil?
53
+ @cte_names
54
+ end
55
+
56
+ def aliases
57
+ load_tables_and_aliases! if @aliases.nil?
58
+ @aliases
59
+ end
60
+
61
+ def tables_with_types
62
+ load_tables_and_aliases! if @tables.nil?
63
+ @tables
64
+ end
65
+
66
+ protected
67
+
68
+ def load_tables_and_aliases! # rubocop:disable Metrics/CyclomaticComplexity
69
+ @tables = [] # types: select, dml, ddl
70
+ @cte_names = []
71
+ @aliases = {}
72
+
73
+ statements = @tree.dup
74
+ from_clause_items = [] # types: select, dml, ddl
75
+ subselect_items = []
76
+
77
+ loop do
78
+ statement = statements.shift
79
+ if statement
80
+ case statement.keys[0]
81
+ when RAW_STMT
82
+ statements << statement[RAW_STMT][STMT_FIELD]
83
+ # The following statement types do not modify tables and are added to from_clause_items
84
+ # (and subsequently @tables)
85
+ when SELECT_STMT
86
+ case statement[SELECT_STMT]['op']
87
+ when 0
88
+ (statement[SELECT_STMT][FROM_CLAUSE_FIELD] || []).each do |item|
89
+ if item[RANGE_SUBSELECT]
90
+ statements << item[RANGE_SUBSELECT]['subquery']
91
+ else
92
+ from_clause_items << { item: item, type: :select }
93
+ end
94
+ end
95
+ when 1
96
+ statements << statement[SELECT_STMT]['larg'] if statement[SELECT_STMT]['larg']
97
+ statements << statement[SELECT_STMT]['rarg'] if statement[SELECT_STMT]['rarg']
98
+ end
99
+
100
+ if (with_clause = statement[SELECT_STMT]['withClause'])
101
+ cte_statements, cte_names = statements_and_cte_names_for_with_clause(with_clause)
102
+ @cte_names.concat(cte_names)
103
+ statements.concat(cte_statements)
104
+ end
105
+ # The following statements modify the contents of a table
106
+ when INSERT_STMT, UPDATE_STMT, DELETE_STMT
107
+ value = statement.values[0]
108
+ from_clause_items << { item: value['relation'], type: :dml }
109
+ statements << value['selectStmt'] if value.key?('selectStmt')
110
+ statements << value['withClause'] if value.key?('withClause')
111
+
112
+ if (with_clause = value['withClause'])
113
+ cte_statements, cte_names = statements_and_cte_names_for_with_clause(with_clause)
114
+ @cte_names.concat(cte_names)
115
+ statements.concat(cte_statements)
116
+ end
117
+ when COPY_STMT
118
+ from_clause_items << { item: statement.values[0]['relation'], type: :dml } if statement.values[0]['relation']
119
+ statements << statement.values[0]['query']
120
+ # The following statement types are DDL (changing table structure)
121
+ when ALTER_TABLE_STMT, CREATE_STMT
122
+ from_clause_items << { item: statement.values[0]['relation'], type: :ddl }
123
+ when CREATE_TABLE_AS_STMT
124
+ if statement[CREATE_TABLE_AS_STMT]['into'] && statement[CREATE_TABLE_AS_STMT]['into'][INTO_CLAUSE]['rel']
125
+ from_clause_items << { item: statement[CREATE_TABLE_AS_STMT]['into'][INTO_CLAUSE]['rel'], type: :ddl }
126
+ end
127
+ if statement[CREATE_TABLE_AS_STMT]['query']
128
+ statements << statement[CREATE_TABLE_AS_STMT]['query']
129
+ end
130
+ when TRUNCATE_STMT
131
+ from_clause_items += statement.values[0]['relations'].map { |r| { item: r, type: :ddl } }
132
+ when VIEW_STMT
133
+ from_clause_items << { item: statement[VIEW_STMT]['view'], type: :ddl }
134
+ statements << statement[VIEW_STMT]['query']
135
+ when VACUUM_STMT, INDEX_STMT, CREATE_TRIG_STMT, RULE_STMT
136
+ from_clause_items << { item: statement.values[0]['relation'], type: :ddl }
137
+ when REFRESH_MAT_VIEW_STMT
138
+ from_clause_items << { item: statement[REFRESH_MAT_VIEW_STMT]['relation'], type: :ddl }
139
+ when DROP_STMT
140
+ objects = statement[DROP_STMT]['objects'].map do |obj|
141
+ if obj.is_a?(Array)
142
+ obj.map { |obj2| obj2['String'] && obj2['String']['str'] }
143
+ else
144
+ obj['String'] && obj['String']['str']
145
+ end
146
+ end
147
+ case statement[DROP_STMT]['removeType']
148
+ when OBJECT_TYPE_TABLE
149
+ @tables += objects.map { |r| { table: r.join('.'), type: :ddl } }
150
+ when OBJECT_TYPE_RULE, OBJECT_TYPE_TRIGGER
151
+ @tables += objects.map { |r| { table: r[0..-2].join('.'), type: :ddl } }
152
+ end
153
+ when GRANT_STMT
154
+ objects = statement[GRANT_STMT]['objects']
155
+ case statement[GRANT_STMT]['objtype']
156
+ when 0 # Column # rubocop:disable Lint/EmptyWhen
157
+ # FIXME
158
+ when 1 # Table
159
+ from_clause_items += objects.map { |o| { item: o, type: :ddl } }
160
+ when 2 # Sequence # rubocop:disable Lint/EmptyWhen
161
+ # FIXME
162
+ end
163
+ when LOCK_STMT
164
+ from_clause_items += statement.values[0]['relations'].map { |r| { item: r, type: :ddl } }
165
+ # The following are other statements that don't fit into query/DML/DDL
166
+ when EXPLAIN_STMT
167
+ statements << statement[EXPLAIN_STMT]['query']
168
+ end
169
+
170
+ statement_value = statement.values[0]
171
+ unless statement.empty?
172
+ subselect_items.concat(statement_value['targetList']) if statement_value['targetList']
173
+ subselect_items << statement_value['whereClause'] if statement_value['whereClause']
174
+ subselect_items.concat(statement_value['sortClause'].collect { |h| h[SORT_BY]['node'] }) if statement_value['sortClause']
175
+ subselect_items.concat(statement_value['groupClause']) if statement_value['groupClause']
176
+ subselect_items << statement_value['havingClause'] if statement_value['havingClause']
177
+ end
178
+ end
179
+
180
+ next_item = subselect_items.shift
181
+ if next_item
182
+ case next_item.keys[0]
183
+ when A_EXPR
184
+ %w[lexpr rexpr].each do |side|
185
+ elem = next_item.values[0][side]
186
+ next unless elem
187
+ if elem.is_a?(Array)
188
+ subselect_items += elem
189
+ else
190
+ subselect_items << elem
191
+ end
192
+ end
193
+ when BOOL_EXPR
194
+ subselect_items.concat(next_item.values[0]['args'])
195
+ when RES_TARGET
196
+ subselect_items << next_item[RES_TARGET]['val']
197
+ when SUB_LINK
198
+ statements << next_item[SUB_LINK]['subselect']
199
+ end
200
+ end
201
+
202
+ break if subselect_items.empty? && statements.empty?
203
+ end
204
+
205
+ loop do
206
+ next_item = from_clause_items.shift
207
+ break unless next_item && next_item[:item]
208
+
209
+ case next_item[:item].keys[0]
210
+ when JOIN_EXPR
211
+ %w[larg rarg].each do |side|
212
+ from_clause_items << { item: next_item[:item][JOIN_EXPR][side], type: next_item[:type] }
213
+ end
214
+ when ROW_EXPR
215
+ from_clause_items += next_item[:item][ROW_EXPR]['args'].map { |a| { item: a, type: next_item[:type] } }
216
+ when RANGE_VAR
217
+ rangevar = next_item[:item][RANGE_VAR]
218
+ next if !rangevar['schemaname'] && @cte_names.include?(rangevar['relname'])
219
+
220
+ table = [rangevar['schemaname'], rangevar['relname']].compact.join('.')
221
+ @tables << { table: table, type: next_item[:type] }
222
+ @aliases[rangevar['alias'][ALIAS]['aliasname']] = table if rangevar['alias']
223
+ when RANGE_SUBSELECT
224
+ from_clause_items << { item: next_item[:item][RANGE_SUBSELECT]['subquery'], type: next_item[:type] }
225
+ when SELECT_STMT
226
+ from_clause = next_item[:item][SELECT_STMT][FROM_CLAUSE_FIELD]
227
+ from_clause_items += from_clause.map { |r| { item: r, type: next_item[:type] } } if from_clause
228
+ end
229
+ end
230
+
231
+ @tables.uniq!
232
+ @cte_names.uniq!
233
+ end
234
+
235
+ def statements_and_cte_names_for_with_clause(with_clause)
236
+ statements = []
237
+ cte_names = []
238
+
239
+ with_clause[WITH_CLAUSE]['ctes'].each do |item|
240
+ next unless item[COMMON_TABLE_EXPR]
241
+ cte_names << item[COMMON_TABLE_EXPR]['ctename']
242
+ statements << item[COMMON_TABLE_EXPR]['ctequery']
243
+ end
244
+
245
+ [statements, cte_names]
246
+ end
247
+ end
@@ -0,0 +1,9 @@
1
+ class PgQuery
2
+ class ParseError < ArgumentError
3
+ attr_reader :location
4
+ def initialize(message, source_file, source_line, location)
5
+ super("#{message} (#{source_file}:#{source_line})")
6
+ @location = location
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ class PgQuery
2
+ private
3
+
4
+ def treewalker!(normalized_parsetree)
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
+ yield(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)
27
+ treewalker! normalized_parsetree do |expr, k, v, location|
28
+ next unless location == searched_location
29
+ yield(expr, k, v)
30
+ end
31
+ end
32
+
33
+ def transform_nodes!(parsetree)
34
+ result = deep_dup(parsetree)
35
+ exprs = result.dup
36
+
37
+ loop do
38
+ expr = exprs.shift
39
+
40
+ if expr.is_a?(Hash)
41
+ yield(expr) if expr.size == 1 && expr.keys[0][/^[A-Z]+/]
42
+
43
+ exprs += expr.values.compact
44
+ elsif expr.is_a?(Array)
45
+ exprs += expr
46
+ end
47
+
48
+ break if exprs.empty?
49
+ end
50
+
51
+ result
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ class PgQuery
2
+ PossibleTruncation = Struct.new(:location, :node_type, :length, :is_array)
3
+
4
+ A_TRUNCATED = 'A_Truncated'.freeze
5
+
6
+ # Truncates the query string to be below the specified length, first trying to
7
+ # omit less important parts of the query, and only then cutting off the end.
8
+ def truncate(max_length)
9
+ output = deparse(@tree)
10
+
11
+ # Early exit if we're already below the max length
12
+ return output if output.size <= max_length
13
+
14
+ truncations = find_possible_truncations
15
+
16
+ # Truncate the deepest possible truncation that is the longest first
17
+ truncations.sort_by! { |t| [-t.location.size, -t.length] }
18
+
19
+ tree = deep_dup(@tree)
20
+ truncations.each do |truncation|
21
+ next if truncation.length < 3
22
+
23
+ find_tree_location(tree, truncation.location) do |expr, k|
24
+ expr[k] = { A_TRUNCATED => nil }
25
+ expr[k] = [expr[k]] if truncation.is_array
26
+ end
27
+
28
+ output = deparse(tree)
29
+ return output if output.size <= max_length
30
+ end
31
+
32
+ # We couldn't do a proper smart truncation, so we need a hard cut-off
33
+ output[0..max_length - 4] + '...'
34
+ end
35
+
36
+ private
37
+
38
+ def find_possible_truncations
39
+ truncations = []
40
+
41
+ treewalker! @tree do |_expr, k, v, location|
42
+ case k
43
+ when TARGET_LIST_FIELD
44
+ length = deparse([{ SELECT_STMT => { k => v } }]).size - 7 # 'SELECT '.size
45
+
46
+ truncations << PossibleTruncation.new(location, TARGET_LIST_FIELD, length, true)
47
+ when 'whereClause'
48
+ length = deparse([{ SELECT_STMT => { k => v } }]).size
49
+
50
+ truncations << PossibleTruncation.new(location, 'whereClause', length, false)
51
+ when 'ctequery'
52
+ truncations << PossibleTruncation.new(location, 'ctequery', deparse([v]).size, false)
53
+ when 'cols'
54
+ truncations << PossibleTruncation.new(location, 'cols', deparse(v).size, true)
55
+ end
56
+ end
57
+
58
+ truncations
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ class PgQuery
2
+ VERSION = '1.3.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitlab-pg_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Lukas Fittl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.49.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.49.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.15.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.15.1
69
+ description: Parses SQL queries using a copy of the PostgreSQL server query parser
70
+ email: lukas@fittl.com
71
+ executables: []
72
+ extensions:
73
+ - ext/pg_query/extconf.rb
74
+ extra_rdoc_files:
75
+ - CHANGELOG.md
76
+ - README.md
77
+ files:
78
+ - CHANGELOG.md
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - ext/pg_query/extconf.rb
83
+ - ext/pg_query/pg_query_ruby.c
84
+ - ext/pg_query/pg_query_ruby.h
85
+ - ext/pg_query/pg_query_ruby.sym
86
+ - lib/pg_query.rb
87
+ - lib/pg_query/deep_dup.rb
88
+ - lib/pg_query/deparse.rb
89
+ - lib/pg_query/deparse/alter_table.rb
90
+ - lib/pg_query/deparse/interval.rb
91
+ - lib/pg_query/deparse/keywords.rb
92
+ - lib/pg_query/deparse/rename.rb
93
+ - lib/pg_query/filter_columns.rb
94
+ - lib/pg_query/fingerprint.rb
95
+ - lib/pg_query/legacy_parsetree.rb
96
+ - lib/pg_query/node_types.rb
97
+ - lib/pg_query/param_refs.rb
98
+ - lib/pg_query/parse.rb
99
+ - lib/pg_query/parse_error.rb
100
+ - lib/pg_query/treewalker.rb
101
+ - lib/pg_query/truncate.rb
102
+ - lib/pg_query/version.rb
103
+ homepage: http://github.com/lfittl/pg_query
104
+ licenses:
105
+ - BSD-3-Clause
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options:
109
+ - "--main"
110
+ - README.md
111
+ - "--exclude"
112
+ - ext/
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.0.3
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: PostgreSQL query parsing and normalization library
130
+ test_files: []