Class: PgQuery

Inherits:
Object
  • Object
show all
Defined in:
lib/pg_query/parse.rb,
lib/pg_query/version.rb,
lib/pg_query/param_refs.rb,
lib/pg_query/fingerprint.rb,
lib/pg_query/parse_error.rb,
lib/pg_query/filter_columns.rb

Defined Under Namespace

Classes: ParseError

Constant Summary collapse

VERSION =
'0.5.0'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(query, parsetree, warnings = []) ⇒ PgQuery

Returns a new instance of PgQuery.



25
26
27
28
29
# File 'lib/pg_query/parse.rb', line 25

def initialize(query, parsetree, warnings = [])
  @query = query
  @parsetree = parsetree
  @warnings = warnings
end

Instance Attribute Details

#parsetreeObject (readonly)

Returns the value of attribute parsetree.



23
24
25
# File 'lib/pg_query/parse.rb', line 23

def parsetree
  @parsetree
end

#queryObject (readonly)

Returns the value of attribute query.



22
23
24
# File 'lib/pg_query/parse.rb', line 22

def query
  @query
end

#warningsObject (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(input) ⇒ Object

#define DEBUG



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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'ext/pg_query/pg_query.c', line 36

static VALUE pg_query_raw_parse(VALUE self, VALUE input)
{
  Check_Type(input, T_STRING);
  
  MemoryContext ctx = NULL;
  VALUE result = Qnil;
  VALUE error = Qnil;
  char stderr_buffer[STDERR_BUFFER_LEN + 1] = {0};
#ifndef DEBUG
  int stderr_global;
  int stderr_pipe[2];
#endif

  ctx = AllocSetContextCreate(TopMemoryContext,
                "pg_query_raw_parse",
                ALLOCSET_DEFAULT_MINSIZE,
                ALLOCSET_DEFAULT_INITSIZE,
                ALLOCSET_DEFAULT_MAXSIZE);
  MemoryContextSwitchTo(ctx);
  
#ifndef DEBUG
  // Setup pipe for stderr redirection
  if (pipe(stderr_pipe) != 0)
    rb_raise(rb_eIOError, "Failed to open pipe, too many open file descriptors");

  fcntl(stderr_pipe[0], F_SETFL, fcntl(stderr_pipe[0], F_GETFL) | O_NONBLOCK);
  
  // Redirect stderr to the pipe
  stderr_global = dup(STDERR_FILENO);
  dup2(stderr_pipe[1], STDERR_FILENO);
  close(stderr_pipe[1]);
#endif
  
  // Parse it!
  PG_TRY();
  {
    List *tree;
    char *str;
    
    str = StringValueCStr(input);
    tree = raw_parser(str);
    
    str = nodeToJSONString(tree);
  
#ifndef DEBUG
    // Save stderr for result
    read(stderr_pipe[0], stderr_buffer, STDERR_BUFFER_LEN);
#endif

    result = rb_ary_new();
    rb_ary_push(result, rb_str_new2(str));
    rb_ary_push(result, rb_str_new2(stderr_buffer));
  
    pfree(str);
  }
  PG_CATCH();
  {
    ErrorData* error_data = CopyErrorData();
    error = new_parse_error(error_data);
    FlushErrorState();
  }
  PG_END_TRY();
  
#ifndef DEBUG
  // Restore stderr, close pipe
  dup2(stderr_global, STDERR_FILENO);
  close(stderr_pipe[0]);
  close(stderr_global);
#endif

  // Return to previous PostgreSQL memory context
  MemoryContextSwitchTo(TopMemoryContext);
  MemoryContextDelete(ctx);
  
  // If we got an error, throw it
  if (!NIL_P(error)) rb_exc_raise(error);
  
  return result;
}

.normalize(input) ⇒ Object



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'ext/pg_query/pg_query.c', line 408

static VALUE pg_query_normalize(VALUE self, VALUE input)
{
  Check_Type(input, T_STRING);
  
  MemoryContext ctx = NULL;
  VALUE result = Qnil;
  VALUE error = Qnil;
  
  ctx = AllocSetContextCreate(TopMemoryContext,
                "pg_query_normalize",
                ALLOCSET_DEFAULT_MINSIZE,
                ALLOCSET_DEFAULT_INITSIZE,
                ALLOCSET_DEFAULT_MAXSIZE);
  MemoryContextSwitchTo(ctx);
  
  PG_TRY();
  {
    List *tree;
    char *str;
    pgssConstLocations jstate;
    int query_len;
    
    /* Parse query */
    str = StringValueCStr(input);
    tree = raw_parser(str);
    
    /* Set up workspace for constant recording */
    jstate.clocations_buf_size = 32;
    jstate.clocations = (pgssLocationLen *)
      palloc(jstate.clocations_buf_size * sizeof(pgssLocationLen));
    jstate.clocations_count = 0;
    
    /* Walk tree and record const locations */
    const_record_walker((Node *) tree, &jstate);
    
    /* Normalize query */
    query_len = (int) strlen(str);
    str = generate_normalized_query(&jstate, str, &query_len, PG_UTF8);
  
    result = rb_str_new2(str);
  
    pfree(str);
  }
  PG_CATCH();
  {
    ErrorData* error_data = CopyErrorData();
    error = new_parse_error(error_data);
    FlushErrorState();
  }
  PG_END_TRY();
  
  MemoryContextSwitchTo(TopMemoryContext);
  MemoryContextDelete(ctx);
  
  // If we got an error, throw it
  if (!NIL_P(error)) rb_exc_raise(error);
  
  return result;
}

.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 => e
    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

#aliasesObject



36
37
38
39
# File 'lib/pg_query/parse.rb', line 36

def aliases
  load_tables_and_aliases! if @aliases.nil?
  @aliases
end

#filter_columnsObject

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
# File 'lib/pg_query/filter_columns.rb', line 6

def filter_columns
  load_tables_and_aliases! if @aliases.nil?

  # Get condition items from the parsetree
  statements = @parsetree.dup
  condition_items = []
  filter_columns = []
  loop do
    if statement = statements.shift
      if statement["SELECT"]
        if statement["SELECT"]["op"] == 0
          if statement["SELECT"]["fromClause"]
            # FROM subselects
            statement["SELECT"]["fromClause"].each do |item|
              statements << item["RANGESUBSELECT"]["subquery"] if item["RANGESUBSELECT"]
            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"]
        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
    if next_item = condition_items.shift
      if next_item.keys[0].start_with?("AEXPR") || next_item["ANY"]
        ["lexpr", "rexpr"].each do |side|
          next unless expr = next_item.values[0][side]
          next unless 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

#fingerprintObject



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
37
38
39
40
41
42
43
44
# File 'lib/pg_query/fingerprint.rb', line 4

def fingerprint
  normalized_parsetree = deep_dup(parsetree)
  exprs = normalized_parsetree.dup
  loop do
    expr = exprs.shift

    if expr.is_a?(Hash)
      expr.each do |k,v|
        if v.is_a?(Hash) && ["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)
        elsif !v.nil?
          # Remove SELECT target list names & ignore order
          if k == "targetList" && v.is_a?(Array)
            v.each {|v| v["RESTARGET"]["name"] = nil if v["RESTARGET"] } # Remove names
            v.sort_by! {|v| v.to_s }
            expr[k] = v
          end

          # Ignore INSERT cols order
          if k == "cols" && v.is_a?(Array)
            v.sort_by! {|v| v.to_s }
            expr[k] = v
          end

          # Process sub-expressions
          exprs << v
        end
      end
    elsif expr.is_a?(Array)
      exprs += expr
    end

    break if exprs.empty?
  end

  Digest::SHA1.hexdigest(normalized_parsetree.to_s)
end

#param_refsObject



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
35
36
37
38
39
# File 'lib/pg_query/param_refs.rb', line 2

def param_refs
  results = []
  exprs = parsetree.dup
  loop do
    expr = exprs.shift

    if expr.is_a?(Hash)
      expr.each do |k,v|
        if v.is_a?(Hash)
          if v["PARAMREF"]
            length = 1 # FIXME: Not true when we have actual paramrefs
            results << {"location" => v["PARAMREF"]["location"], "length" => length}
            next
          elsif (p = v["TYPECAST"]["arg"]["PARAMREF"] rescue false) && (t = v["TYPECAST"]["typeName"]["TYPENAME"] rescue false)
            location = p["location"]
            typeloc = t["location"]
            typename = t["names"].join(".")
            length = 1 # FIXME: Not true when we have actual paramrefs
            if typeloc < location
              length += location - typeloc
              location = typeloc
            end
            results << {"location" => location, "length" => length, "typename" => typename}
            next
          end
        end

        exprs << v if !v.nil?
      end
    elsif expr.is_a?(Array)
      exprs += expr
    end

    break if exprs.empty?
  end
  results.sort_by! {|r| r["location"] }
  results
end

#tablesObject



31
32
33
34
# File 'lib/pg_query/parse.rb', line 31

def tables
  load_tables_and_aliases! if @tables.nil?
  @tables
end