gitlab-pg_query 1.3.0

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.
@@ -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: []