pg_query 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,10 @@ require 'pg_query/parse_error'
3
3
 
4
4
  require 'pg_query/pg_query'
5
5
  require 'pg_query/parse'
6
+ require 'pg_query/treewalker'
7
+
6
8
  require 'pg_query/filter_columns'
7
9
  require 'pg_query/fingerprint'
8
10
  require 'pg_query/param_refs'
11
+ require 'pg_query/deparse'
12
+ require 'pg_query/truncate'
@@ -3,7 +3,7 @@ class PgQuery
3
3
  # target list, but includes things like JOIN condition and WHERE clause.
4
4
  #
5
5
  # Note: This also traverses into sub-selects.
6
- def filter_columns
6
+ def filter_columns # rubocop:disable Metrics/CyclomaticComplexity
7
7
  load_tables_and_aliases! if @aliases.nil?
8
8
 
9
9
  # Get condition items from the parsetree
@@ -11,53 +11,63 @@ class PgQuery
11
11
  condition_items = []
12
12
  filter_columns = []
13
13
  loop do
14
- if statement = statements.shift
15
- if statement["SELECT"]
16
- if statement["SELECT"]["op"] == 0
17
- if statement["SELECT"]["fromClause"]
14
+ statement = statements.shift
15
+ if statement
16
+ if statement['SELECT']
17
+ if statement['SELECT']['op'] == 0
18
+ if statement['SELECT']['fromClause']
18
19
  # FROM subselects
19
- statement["SELECT"]["fromClause"].each do |item|
20
- statements << item["RANGESUBSELECT"]["subquery"] if item["RANGESUBSELECT"]
20
+ statement['SELECT']['fromClause'].each do |item|
21
+ next unless item['RANGESUBSELECT']
22
+ statements << item['RANGESUBSELECT']['subquery']
21
23
  end
22
24
 
23
25
  # JOIN ON conditions
24
- condition_items += conditions_from_join_clauses(statement["SELECT"]["fromClause"])
26
+ condition_items += conditions_from_join_clauses(statement['SELECT']['fromClause'])
25
27
  end
26
28
 
27
29
  # WHERE clause
28
- condition_items << statement["SELECT"]["whereClause"] if statement["SELECT"]["whereClause"]
29
- elsif statement["SELECT"]["op"] == 1
30
- statements << statement["SELECT"]["larg"] if statement["SELECT"]["larg"]
31
- statements << statement["SELECT"]["rarg"] if statement["SELECT"]["rarg"]
30
+ condition_items << statement['SELECT']['whereClause'] if statement['SELECT']['whereClause']
31
+
32
+ # CTEs
33
+ if statement['SELECT']['withClause']
34
+ statement['SELECT']['withClause']['WITHCLAUSE']['ctes'].each do |item|
35
+ statements << item['COMMONTABLEEXPR']['ctequery'] if item['COMMONTABLEEXPR']
36
+ end
37
+ end
38
+ elsif statement['SELECT']['op'] == 1
39
+ statements << statement['SELECT']['larg'] if statement['SELECT']['larg']
40
+ statements << statement['SELECT']['rarg'] if statement['SELECT']['rarg']
32
41
  end
33
- elsif statement["UPDATE"]
34
- condition_items << statement["UPDATE"]["whereClause"] if statement["UPDATE"]["whereClause"]
35
- elsif statement["DELETE FROM"]
36
- condition_items << statement["DELETE FROM"]["whereClause"] if statement["DELETE FROM"]["whereClause"]
42
+ elsif statement['UPDATE']
43
+ condition_items << statement['UPDATE']['whereClause'] if statement['UPDATE']['whereClause']
44
+ elsif statement['DELETE FROM']
45
+ condition_items << statement['DELETE FROM']['whereClause'] if statement['DELETE FROM']['whereClause']
37
46
  end
38
47
  end
39
48
 
40
49
  # Process both JOIN and WHERE conditions here
41
- if next_item = condition_items.shift
42
- if next_item.keys[0].start_with?("AEXPR") || next_item["ANY"]
43
- ["lexpr", "rexpr"].each do |side|
44
- next unless expr = next_item.values[0][side]
45
- next unless expr.is_a?(Hash)
50
+ next_item = condition_items.shift
51
+ if next_item
52
+ if next_item.keys[0].start_with?('AEXPR') || next_item['ANY']
53
+ %w(lexpr rexpr).each do |side|
54
+ expr = next_item.values[0][side]
55
+ next unless expr && expr.is_a?(Hash)
46
56
  condition_items << expr
47
57
  end
48
- elsif next_item["ROW"]
49
- condition_items += next_item["ROW"]["args"]
50
- elsif next_item["COLUMNREF"]
51
- column, table = next_item["COLUMNREF"]["fields"].reverse
58
+ elsif next_item['ROW']
59
+ condition_items += next_item['ROW']['args']
60
+ elsif next_item['COLUMNREF']
61
+ column, table = next_item['COLUMNREF']['fields'].reverse
52
62
  filter_columns << [@aliases[table] || table, column]
53
- elsif next_item["NULLTEST"]
54
- condition_items << next_item["NULLTEST"]["arg"]
55
- elsif next_item["FUNCCALL"]
63
+ elsif next_item['NULLTEST']
64
+ condition_items << next_item['NULLTEST']['arg']
65
+ elsif next_item['FUNCCALL']
56
66
  # FIXME: This should actually be extracted as a funccall and be compared with those indices
57
- condition_items += next_item["FUNCCALL"]["args"] if next_item["FUNCCALL"]["args"]
58
- elsif next_item["SUBLINK"]
59
- condition_items << next_item["SUBLINK"]["testexpr"]
60
- statements << next_item["SUBLINK"]["subselect"]
67
+ condition_items += next_item['FUNCCALL']['args'] if next_item['FUNCCALL']['args']
68
+ elsif next_item['SUBLINK']
69
+ condition_items << next_item['SUBLINK']['testexpr']
70
+ statements << next_item['SUBLINK']['subselect']
61
71
  end
62
72
  end
63
73
 
@@ -67,18 +77,21 @@ class PgQuery
67
77
  filter_columns.uniq
68
78
  end
69
79
 
70
- protected
80
+ protected
81
+
71
82
  def conditions_from_join_clauses(from_clause)
72
83
  condition_items = []
73
84
  from_clause.each do |item|
74
- next unless item["JOINEXPR"]
85
+ next unless item['JOINEXPR']
75
86
 
76
- joinexpr_items = [item["JOINEXPR"]]
87
+ joinexpr_items = [item['JOINEXPR']]
77
88
  loop do
78
- break unless next_item = joinexpr_items.shift
79
- condition_items << next_item["quals"] if next_item["quals"]
80
- ["larg", "rarg"].each do |side|
81
- joinexpr_items << next_item[side]["JOINEXPR"] if next_item[side]["JOINEXPR"]
89
+ next_item = joinexpr_items.shift
90
+ break unless next_item
91
+ condition_items << next_item['quals'] if next_item['quals']
92
+ %w(larg rarg).each do |side|
93
+ next unless next_item[side]['JOINEXPR']
94
+ joinexpr_items << next_item[side]['JOINEXPR']
82
95
  end
83
96
  end
84
97
  end
@@ -1,62 +1,37 @@
1
1
  require 'digest'
2
2
 
3
3
  class PgQuery
4
- def fingerprint
4
+ def fingerprint # rubocop:disable Metrics/CyclomaticComplexity
5
5
  normalized_parsetree = deep_dup(parsetree)
6
- exprs = normalized_parsetree.dup
7
- loop do
8
- expr = exprs.shift
9
6
 
10
- if expr.is_a?(Hash)
11
- expr.each do |k,v|
12
- if v.is_a?(Hash) && ["A_CONST", "ALIAS", "PARAMREF"].include?(v.keys[0])
13
- # Remove constants, aliases and param references from tree
14
- expr[k] = nil
15
- elsif k == "location"
16
- # Remove location info in order to ignore whitespace and target list ordering
17
- expr.delete(k)
18
- elsif !v.nil?
19
- # Remove SELECT target list names & ignore order
20
- if k == "targetList" && v.is_a?(Array)
21
- v.each {|v| v["RESTARGET"]["name"] = nil if v["RESTARGET"] } # Remove names
22
- v.sort_by! {|v| v.to_s }
23
- expr[k] = v
24
- end
25
-
26
- # Ignore INSERT cols order
27
- if k == "cols" && v.is_a?(Array)
28
- v.sort_by! {|v| v.to_s }
29
- expr[k] = v
30
- end
31
-
32
- # Process sub-expressions
33
- exprs << v
34
- end
35
- end
36
- elsif expr.is_a?(Array)
37
- exprs += expr
7
+ # First delete all simple elements and attributes that can be removed
8
+ treewalker! normalized_parsetree do |expr, k, v|
9
+ if v.is_a?(Hash) && %w(A_CONST ALIAS PARAMREF).include?(v.keys[0])
10
+ # Remove constants, aliases and param references from tree
11
+ expr[k] = nil
12
+ elsif k == 'location'
13
+ # Remove location info in order to ignore whitespace and target list ordering
14
+ expr.delete(k)
38
15
  end
39
-
40
- break if exprs.empty?
41
16
  end
42
17
 
43
- Digest::SHA1.hexdigest(normalized_parsetree.to_s)
44
- end
45
-
46
- private
47
-
48
- def deep_dup(obj)
49
- case obj
50
- when Hash
51
- obj.each_with_object(obj.dup) do |(key, value), hash|
52
- hash[deep_dup(key)] = deep_dup(value)
18
+ # Now remove all unnecessary info
19
+ treewalker! normalized_parsetree do |expr, k, v|
20
+ if k == 'AEXPR IN' && v.is_a?(Hash) && v['rexpr'].is_a?(Array)
21
+ # Compact identical IN list elements to one
22
+ v['rexpr'].uniq!
23
+ elsif k == 'targetList' && v.is_a?(Array)
24
+ # Remove SELECT target list names & ignore order
25
+ v.each { |v2| v2['RESTARGET']['name'] = nil if v2['RESTARGET'] } # Remove names
26
+ v.sort_by!(&:to_s)
27
+ expr[k] = v
28
+ elsif k == 'cols' && v.is_a?(Array)
29
+ # Ignore INSERT cols order
30
+ v.sort_by!(&:to_s)
31
+ expr[k] = v
53
32
  end
54
- when Array
55
- obj.map { |it| deep_dup(it) }
56
- when NilClass, FalseClass, TrueClass, Symbol, Numeric
57
- obj # Can't be duplicated
58
- else
59
- obj.dup
60
33
  end
34
+
35
+ Digest::SHA1.hexdigest(normalized_parsetree.to_s)
61
36
  end
62
37
  end
@@ -1,40 +1,45 @@
1
1
  class PgQuery
2
- def param_refs
2
+ def param_refs # rubocop:disable Metrics/CyclomaticComplexity
3
3
  results = []
4
- exprs = parsetree.dup
5
- loop do
6
- expr = exprs.shift
7
-
8
- if expr.is_a?(Hash)
9
- expr.each do |k,v|
10
- if v.is_a?(Hash)
11
- if v["PARAMREF"]
12
- length = 1 # FIXME: Not true when we have actual paramrefs
13
- results << {"location" => v["PARAMREF"]["location"], "length" => length}
14
- next
15
- elsif (p = v["TYPECAST"]["arg"]["PARAMREF"] rescue false) && (t = v["TYPECAST"]["typeName"]["TYPENAME"] rescue false)
16
- location = p["location"]
17
- typeloc = t["location"]
18
- typename = t["names"].join(".")
19
- length = 1 # FIXME: Not true when we have actual paramrefs
20
- if typeloc < location
21
- length += location - typeloc
22
- location = typeloc
23
- end
24
- results << {"location" => location, "length" => length, "typename" => typename}
25
- next
26
- end
27
- end
28
-
29
- exprs << v if !v.nil?
4
+
5
+ treewalker! parsetree do |_, _, v|
6
+ next unless v.is_a?(Hash)
7
+
8
+ if v['PARAMREF']
9
+ results << { 'location' => v['PARAMREF']['location'],
10
+ 'length' => param_ref_length(v['PARAMREF']) }
11
+ elsif v['TYPECAST']
12
+ next unless v['TYPECAST']['arg'] && v['TYPECAST']['typeName']
13
+
14
+ p = v['TYPECAST']['arg'].delete('PARAMREF')
15
+ t = v['TYPECAST']['typeName'].delete('TYPENAME')
16
+ next unless p && t
17
+
18
+ location = p['location']
19
+ typeloc = t['location']
20
+ typename = t['names'].join('.')
21
+ length = param_ref_length(p)
22
+
23
+ if typeloc < location
24
+ length += location - typeloc
25
+ location = typeloc
30
26
  end
31
- elsif expr.is_a?(Array)
32
- exprs += expr
33
- end
34
27
 
35
- break if exprs.empty?
28
+ results << { 'location' => location, 'length' => length, 'typename' => typename }
29
+ end
36
30
  end
37
- results.sort_by! {|r| r["location"] }
31
+
32
+ results.sort_by! { |r| r['location'] }
38
33
  results
39
34
  end
35
+
36
+ private
37
+
38
+ def param_ref_length(paramref_node)
39
+ if paramref_node['number'] == 0
40
+ 1 # Actually a ? replacement character
41
+ else
42
+ ('$' + paramref_node['number'].to_s).size
43
+ end
44
+ end
40
45
  end
@@ -6,8 +6,8 @@ class PgQuery
6
6
 
7
7
  begin
8
8
  parsetree = JSON.parse(parsetree, max_nesting: 1000)
9
- rescue JSON::ParserError => e
10
- raise ParseError.new("Failed to parse JSON", -1)
9
+ rescue JSON::ParserError
10
+ raise ParseError.new('Failed to parse JSON', -1)
11
11
  end
12
12
 
13
13
  warnings = []
@@ -22,6 +22,7 @@ class PgQuery
22
22
  attr_reader :query
23
23
  attr_reader :parsetree
24
24
  attr_reader :warnings
25
+
25
26
  def initialize(query, parsetree, warnings = [])
26
27
  @query = query
27
28
  @parsetree = parsetree
@@ -38,8 +39,9 @@ class PgQuery
38
39
  @aliases
39
40
  end
40
41
 
41
- protected
42
- def load_tables_and_aliases!
42
+ protected
43
+
44
+ def load_tables_and_aliases! # rubocop:disable Metrics/CyclomaticComplexity
43
45
  @tables = []
44
46
  @aliases = {}
45
47
 
@@ -48,32 +50,47 @@ protected
48
50
  where_clause_items = []
49
51
 
50
52
  loop do
51
- if statement = statements.shift
53
+ statement = statements.shift
54
+ if statement
52
55
  case statement.keys[0]
53
- when "SELECT"
54
- if statement["SELECT"]["op"] == 0
55
- (statement["SELECT"]["fromClause"] || []).each do |item|
56
- if item["RANGESUBSELECT"]
57
- statements << item["RANGESUBSELECT"]["subquery"]
56
+ when 'SELECT'
57
+ if statement['SELECT']['op'] == 0
58
+ (statement['SELECT']['fromClause'] || []).each do |item|
59
+ if item['RANGESUBSELECT']
60
+ statements << item['RANGESUBSELECT']['subquery']
58
61
  else
59
62
  from_clause_items << item
60
63
  end
61
64
  end
62
- elsif statement["SELECT"]["op"] == 1
63
- statements << statement["SELECT"]["larg"] if statement["SELECT"]["larg"]
64
- statements << statement["SELECT"]["rarg"] if statement["SELECT"]["rarg"]
65
+
66
+ # CTEs
67
+ if statement['SELECT']['withClause']
68
+ statement['SELECT']['withClause']['WITHCLAUSE']['ctes'].each do |item|
69
+ statements << item['COMMONTABLEEXPR']['ctequery'] if item['COMMONTABLEEXPR']
70
+ end
71
+ end
72
+ elsif statement['SELECT']['op'] == 1
73
+ statements << statement['SELECT']['larg'] if statement['SELECT']['larg']
74
+ statements << statement['SELECT']['rarg'] if statement['SELECT']['rarg']
75
+ end
76
+ when 'INSERT INTO', 'UPDATE', 'DELETE FROM', 'VACUUM', 'COPY', 'ALTER TABLE', 'CREATESTMT', 'INDEXSTMT', 'RULESTMT', 'CREATETRIGSTMT'
77
+ from_clause_items << statement.values[0]['relation']
78
+ when 'VIEWSTMT'
79
+ from_clause_items << statement['VIEWSTMT']['view']
80
+ statements << statement['VIEWSTMT']['query']
81
+ when 'REFRESHMATVIEWSTMT'
82
+ from_clause_items << statement['REFRESHMATVIEWSTMT']['relation']
83
+ when 'EXPLAIN'
84
+ statements << statement['EXPLAIN']['query']
85
+ when 'CREATE TABLE AS'
86
+ if statement['CREATE TABLE AS']['into'] && statement['CREATE TABLE AS']['into']['INTOCLAUSE']['rel']
87
+ from_clause_items << statement['CREATE TABLE AS']['into']['INTOCLAUSE']['rel']
65
88
  end
66
- when "INSERT INTO", "UPDATE", "DELETE FROM", "VACUUM", "COPY", "ALTER TABLE", "CREATESTMT", "INDEXSTMT", "RULESTMT", "CREATETRIGSTMT"
67
- from_clause_items << statement.values[0]["relation"]
68
- when "EXPLAIN", "VIEWSTMT"
69
- statements << statement.values[0]["query"]
70
- when "CREATE TABLE AS"
71
- from_clause_items << statement["CREATE TABLE AS"]["into"]["INTOCLAUSE"]["rel"] rescue nil
72
- when "LOCK", "TRUNCATE"
73
- from_clause_items += statement.values[0]["relations"]
74
- when "GRANTSTMT"
75
- objects = statement["GRANTSTMT"]["objects"]
76
- case statement["GRANTSTMT"]["objtype"]
89
+ when 'LOCK', 'TRUNCATE'
90
+ from_clause_items += statement.values[0]['relations']
91
+ when 'GRANTSTMT'
92
+ objects = statement['GRANTSTMT']['objects']
93
+ case statement['GRANTSTMT']['objtype']
77
94
  when 0 # Column
78
95
  # FIXME
79
96
  when 1 # Table
@@ -81,27 +98,29 @@ protected
81
98
  when 2 # Sequence
82
99
  # FIXME
83
100
  end
84
- when "DROP"
85
- objects = statement["DROP"]["objects"]
86
- case statement["DROP"]["removeType"]
101
+ when 'DROP'
102
+ objects = statement['DROP']['objects']
103
+ case statement['DROP']['removeType']
87
104
  when 26 # Table
88
- @tables += objects.map {|r| r.join('.') }
105
+ @tables += objects.map { |r| r.join('.') }
89
106
  when 23 # Rule
90
- @tables += objects.map {|r| r[0..-2].join('.') }
107
+ @tables += objects.map { |r| r[0..-2].join('.') }
91
108
  when 28 # Trigger
92
- @tables += objects.map {|r| r[0..-2].join('.') }
109
+ @tables += objects.map { |r| r[0..-2].join('.') }
93
110
  end
94
111
  end
95
112
 
96
- where_clause_items << statement.values[0]["whereClause"] if !statement.empty? && statement.values[0]["whereClause"]
113
+ where_clause_items << statement.values[0]['whereClause'] if !statement.empty? && statement.values[0]['whereClause']
97
114
  end
98
115
 
99
116
  # Find subselects in WHERE clause
100
- if next_item = where_clause_items.shift
117
+ next_item = where_clause_items.shift
118
+ if next_item
101
119
  case next_item.keys[0]
102
120
  when /^AEXPR/, 'ANY'
103
- ["lexpr", "rexpr"].each do |side|
104
- next unless elem = next_item.values[0][side]
121
+ %w(lexpr rexpr).each do |side|
122
+ elem = next_item.values[0][side]
123
+ next unless elem
105
124
  if elem.is_a?(Array)
106
125
  where_clause_items += elem
107
126
  else
@@ -109,7 +128,7 @@ protected
109
128
  end
110
129
  end
111
130
  when 'SUBLINK'
112
- statements << next_item["SUBLINK"]["subselect"]
131
+ statements << next_item['SUBLINK']['subselect']
113
132
  end
114
133
  end
115
134
 
@@ -117,20 +136,21 @@ protected
117
136
  end
118
137
 
119
138
  loop do
120
- break unless next_item = from_clause_items.shift
139
+ next_item = from_clause_items.shift
140
+ break unless next_item
121
141
 
122
142
  case next_item.keys[0]
123
- when "JOINEXPR"
124
- ["larg", "rarg"].each do |side|
125
- from_clause_items << next_item["JOINEXPR"][side]
143
+ when 'JOINEXPR'
144
+ %w(larg rarg).each do |side|
145
+ from_clause_items << next_item['JOINEXPR'][side]
126
146
  end
127
- when "ROW"
128
- from_clause_items += next_item["ROW"]["args"]
129
- when "RANGEVAR"
130
- rangevar = next_item["RANGEVAR"]
131
- table = [rangevar["schemaname"], rangevar["relname"]].compact.join('.')
147
+ when 'ROW'
148
+ from_clause_items += next_item['ROW']['args']
149
+ when 'RANGEVAR'
150
+ rangevar = next_item['RANGEVAR']
151
+ table = [rangevar['schemaname'], rangevar['relname']].compact.join('.')
132
152
  @tables << table
133
- @aliases[rangevar["alias"]["ALIAS"]["aliasname"]] = table if rangevar["alias"]
153
+ @aliases[rangevar['alias']['ALIAS']['aliasname']] = table if rangevar['alias']
134
154
  end
135
155
  end
136
156