Class: PgQuery
- Inherits:
-
Object
- Object
- PgQuery
- Defined in:
- lib/pg_query/parse.rb,
lib/pg_query/deparse.rb,
lib/pg_query/version.rb,
lib/pg_query/truncate.rb,
lib/pg_query/param_refs.rb,
lib/pg_query/treewalker.rb,
lib/pg_query/fingerprint.rb,
lib/pg_query/parse_error.rb,
lib/pg_query/filter_columns.rb
Defined Under Namespace
Classes: ParseError, PossibleTruncation
Constant Summary collapse
- VERSION =
'0.6.1'
Instance Attribute Summary collapse
-
#parsetree ⇒ Object
readonly
Returns the value of attribute parsetree.
-
#query ⇒ Object
readonly
Returns the value of attribute query.
-
#warnings ⇒ Object
readonly
Returns the value of attribute warnings.
Class Method Summary collapse
- ._raw_parse ⇒ Object
-
.deparse(item) ⇒ Object
Given one element of the PgQuery#parsetree reconstruct it back into the original query.
- .normalize ⇒ Object
- .parse(query) ⇒ Object
Instance Method Summary collapse
- #aliases ⇒ Object
-
#deparse(tree = @parsetree) ⇒ Object
Reconstruct all of the parsed queries into their original form.
-
#filter_columns ⇒ Object
Returns a list of columns that the query filters by - this excludes the target list, but includes things like JOIN condition and WHERE clause.
-
#fingerprint ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity.
-
#initialize(query, parsetree, warnings = []) ⇒ PgQuery
constructor
A new instance of PgQuery.
-
#param_refs ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity.
- #tables ⇒ Object
-
#truncate(max_length) ⇒ Object
Truncates the query string to be below the specified length, first trying to omit less important parts of the query, and only then cutting off the end.
Constructor Details
#initialize(query, parsetree, warnings = []) ⇒ PgQuery
Returns a new instance of PgQuery.
26 27 28 29 30 |
# File 'lib/pg_query/parse.rb', line 26 def initialize(query, parsetree, warnings = []) @query = query @parsetree = parsetree @warnings = warnings end |
Instance Attribute Details
#parsetree ⇒ Object (readonly)
Returns the value of attribute parsetree.
23 24 25 |
# File 'lib/pg_query/parse.rb', line 23 def parsetree @parsetree end |
#query ⇒ Object (readonly)
Returns the value of attribute query.
22 23 24 |
# File 'lib/pg_query/parse.rb', line 22 def query @query end |
#warnings ⇒ Object (readonly)
Returns the value of attribute warnings.
24 25 26 |
# File 'lib/pg_query/parse.rb', line 24 def warnings @warnings end |
Class Method Details
._raw_parse ⇒ Object
.deparse(item) ⇒ Object
Given one element of the PgQuery#parsetree reconstruct it back into the original query.
12 13 14 |
# File 'lib/pg_query/deparse.rb', line 12 def deparse(item) deparse_item(item) end |
.normalize ⇒ Object
.parse(query) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/pg_query/parse.rb', line 4 def self.parse(query) parsetree, stderr = _raw_parse(query) begin parsetree = JSON.parse(parsetree, max_nesting: 1000) rescue JSON::ParserError raise ParseError.new('Failed to parse JSON', -1) end warnings = [] stderr.each_line do |line| next unless line[/^WARNING/] warnings << line.strip end PgQuery.new(query, parsetree, warnings) end |
Instance Method Details
#aliases ⇒ Object
37 38 39 40 |
# File 'lib/pg_query/parse.rb', line 37 def aliases load_tables_and_aliases! if @aliases.nil? @aliases end |
#deparse(tree = @parsetree) ⇒ Object
Reconstruct all of the parsed queries into their original form
3 4 5 6 7 |
# File 'lib/pg_query/deparse.rb', line 3 def deparse(tree = @parsetree) tree.map do |item| self.class.deparse(item) end.join('; ') end |
#filter_columns ⇒ Object
Returns a list of columns that the query filters by - this excludes the target list, but includes things like JOIN condition and WHERE clause.
Note: This also traverses into sub-selects.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/pg_query/filter_columns.rb', line 6 def filter_columns # rubocop:disable Metrics/CyclomaticComplexity load_tables_and_aliases! if @aliases.nil? # Get condition items from the parsetree statements = @parsetree.dup condition_items = [] filter_columns = [] loop do statement = statements.shift if statement if statement['SELECT'] if statement['SELECT']['op'] == 0 if statement['SELECT']['fromClause'] # FROM subselects statement['SELECT']['fromClause'].each do |item| next unless item['RANGESUBSELECT'] statements << item['RANGESUBSELECT']['subquery'] end # JOIN ON conditions condition_items += conditions_from_join_clauses(statement['SELECT']['fromClause']) end # WHERE clause condition_items << statement['SELECT']['whereClause'] if statement['SELECT']['whereClause'] # CTEs if statement['SELECT']['withClause'] statement['SELECT']['withClause']['WITHCLAUSE']['ctes'].each do |item| statements << item['COMMONTABLEEXPR']['ctequery'] if item['COMMONTABLEEXPR'] end end elsif statement['SELECT']['op'] == 1 statements << statement['SELECT']['larg'] if statement['SELECT']['larg'] statements << statement['SELECT']['rarg'] if statement['SELECT']['rarg'] end elsif statement['UPDATE'] condition_items << statement['UPDATE']['whereClause'] if statement['UPDATE']['whereClause'] elsif statement['DELETE FROM'] condition_items << statement['DELETE FROM']['whereClause'] if statement['DELETE FROM']['whereClause'] end end # Process both JOIN and WHERE conditions here next_item = condition_items.shift if next_item if next_item.keys[0].start_with?('AEXPR') || next_item['ANY'] %w(lexpr rexpr).each do |side| expr = next_item.values[0][side] next unless expr && expr.is_a?(Hash) condition_items << expr end elsif next_item['ROW'] condition_items += next_item['ROW']['args'] elsif next_item['COLUMNREF'] column, table = next_item['COLUMNREF']['fields'].reverse filter_columns << [@aliases[table] || table, column] elsif next_item['NULLTEST'] condition_items << next_item['NULLTEST']['arg'] elsif next_item['FUNCCALL'] # FIXME: This should actually be extracted as a funccall and be compared with those indices condition_items += next_item['FUNCCALL']['args'] if next_item['FUNCCALL']['args'] elsif next_item['SUBLINK'] condition_items << next_item['SUBLINK']['testexpr'] statements << next_item['SUBLINK']['subselect'] end end break if statements.empty? && condition_items.empty? end filter_columns.uniq end |
#fingerprint ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/pg_query/fingerprint.rb', line 4 def fingerprint # rubocop:disable Metrics/CyclomaticComplexity normalized_parsetree = deep_dup(parsetree) # First delete all simple elements and attributes that can be removed treewalker! normalized_parsetree do |expr, k, v| if v.is_a?(Hash) && %w(A_CONST ALIAS PARAMREF).include?(v.keys[0]) # Remove constants, aliases and param references from tree expr[k] = nil elsif k == 'location' # Remove location info in order to ignore whitespace and target list ordering expr.delete(k) end end # Now remove all unnecessary info treewalker! normalized_parsetree do |expr, k, v| if k == 'AEXPR IN' && v.is_a?(Hash) && v['rexpr'].is_a?(Array) # Compact identical IN list elements to one v['rexpr'].uniq! elsif k == 'targetList' && v.is_a?(Array) # Remove SELECT target list names & ignore order v.each { |v2| v2['RESTARGET']['name'] = nil if v2['RESTARGET'] } # Remove names v.sort_by!(&:to_s) expr[k] = v elsif k == 'cols' && v.is_a?(Array) # Ignore INSERT cols order v.sort_by!(&:to_s) expr[k] = v end end Digest::SHA1.hexdigest(normalized_parsetree.to_s) end |
#param_refs ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/pg_query/param_refs.rb', line 2 def param_refs # rubocop:disable Metrics/CyclomaticComplexity results = [] treewalker! parsetree do |_, _, v| next unless v.is_a?(Hash) if v['PARAMREF'] results << { 'location' => v['PARAMREF']['location'], 'length' => param_ref_length(v['PARAMREF']) } elsif v['TYPECAST'] next unless v['TYPECAST']['arg'] && v['TYPECAST']['typeName'] p = v['TYPECAST']['arg'].delete('PARAMREF') t = v['TYPECAST']['typeName'].delete('TYPENAME') next unless p && t location = p['location'] typeloc = t['location'] typename = t['names'].join('.') length = param_ref_length(p) if typeloc < location length += location - typeloc location = typeloc end results << { 'location' => location, 'length' => length, 'typename' => typename } end end results.sort_by! { |r| r['location'] } results end |
#tables ⇒ Object
32 33 34 35 |
# File 'lib/pg_query/parse.rb', line 32 def tables load_tables_and_aliases! if @tables.nil? @tables end |
#truncate(max_length) ⇒ Object
Truncates the query string to be below the specified length, first trying to omit less important parts of the query, and only then cutting off the end.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/pg_query/truncate.rb', line 6 def truncate(max_length) output = deparse(parsetree) # Early exit if we're already below the max length return output if output.size <= max_length truncations = find_possible_truncations # Truncate the deepest possible truncation that is the longest first truncations.sort_by! { |t| [-t.location.size, -t.length] } tree = deep_dup(parsetree) truncations.each do |truncation| next if truncation.length < 3 find_tree_location(tree, truncation.location) do |expr, k| expr[k] = { 'A_TRUNCATED' => nil } expr[k] = [expr[k]] if truncation.is_array end output = deparse(tree) return output if output.size <= max_length end # We couldn't do a proper smart truncation, so we need a hard cut-off output[0..max_length - 4] + '...' end |