pg_query 6.2.3 → 6.2.5

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.
@@ -93,6 +93,12 @@ module PgQuery
93
93
  case postgres_field_name
94
94
  when 'location'
95
95
  next
96
+ when 'arg_location'
97
+ next if node.is_a?(DefElem)
98
+ when 'payload_location'
99
+ next if node.is_a?(NotifyStmt)
100
+ when 'conninfo_location'
101
+ next if [CreateSubscriptionStmt, AlterSubscriptionStmt].include?(node.class)
96
102
  when 'name'
97
103
  next if [PrepareStmt, ExecuteStmt, DeallocateStmt, FunctionParameter].include?(node.class)
98
104
  next if node.is_a?(ResTarget) && parent_node_name == 'SelectStmt' && parent_field_name == 'targetList'
@@ -102,6 +102,7 @@ module PgQuery
102
102
  @cte_names = []
103
103
  @aliases = {}
104
104
  @functions = [] # types: call, ddl
105
+ @cte_self_reference_locations = []
105
106
 
106
107
  statements = @tree.stmts.dup.to_a.map(&:stmt)
107
108
  from_clause_items = [] # types: select, dml, ddl
@@ -135,6 +136,7 @@ module PgQuery
135
136
  end
136
137
 
137
138
  if statement.select_stmt.with_clause
139
+ record_cte_self_references!(statement.select_stmt.with_clause)
138
140
  cte_statements, cte_names = statements_and_cte_names_for_with_clause(statement.select_stmt.with_clause)
139
141
  @cte_names.concat(cte_names)
140
142
  statements.concat(cte_statements)
@@ -173,6 +175,7 @@ module PgQuery
173
175
  end
174
176
 
175
177
  if value.with_clause
178
+ record_cte_self_references!(value.with_clause)
176
179
  cte_statements, cte_names = statements_and_cte_names_for_with_clause(value.with_clause)
177
180
  @cte_names.concat(cte_names)
178
181
  statements.concat(cte_statements)
@@ -347,7 +350,7 @@ module PgQuery
347
350
  from_clause_items += next_item[:item].row_expr.args.map { |a| { item: a, type: next_item[:type] } }
348
351
  when :range_var
349
352
  rangevar = next_item[:item].range_var
350
- next if rangevar.schemaname.empty? && @cte_names.include?(rangevar.relname)
353
+ next if cte_reference?(rangevar, next_item[:type])
351
354
 
352
355
  table = [rangevar.schemaname, rangevar.relname].reject { |s| s.nil? || s.empty? }.join('.')
353
356
  @tables << {
@@ -386,5 +389,78 @@ module PgQuery
386
389
 
387
390
  [statements, cte_names]
388
391
  end
392
+
393
+ # Determines whether a RangeVar refers to a CTE rather than to a relation.
394
+ #
395
+ # Only plain (SELECT-style) references can resolve to a CTE: a CTE is not a
396
+ # valid target for DML (INSERT/UPDATE/DELETE/COPY) or DDL, so those always
397
+ # name a real relation even when a CTE in the same statement shares the name.
398
+ def cte_reference?(rangevar, type)
399
+ return false unless type == :select
400
+ return false unless rangevar.schemaname.empty?
401
+ return false unless @cte_names.include?(rangevar.relname)
402
+
403
+ !@cte_self_reference_locations.include?(rangevar.location)
404
+ end
405
+
406
+ # A non-recursive CTE is not visible inside its own definition, so a
407
+ # reference to its own name there resolves to a real relation:
408
+ #
409
+ # WITH users AS (SELECT * FROM users) SELECT * FROM users
410
+ # ^^^^^ the table ^^^^^ the CTE
411
+ #
412
+ # Records the locations of those self-references so they are not mistaken
413
+ # for CTE references. Locations uniquely identify a RangeVar occurrence,
414
+ # which keeps the outer (genuine) CTE reference above excluded.
415
+ def record_cte_self_references!(with_clause)
416
+ return if with_clause.recursive
417
+
418
+ with_clause.ctes.each do |item|
419
+ next unless item.node == :common_table_expr
420
+
421
+ record_self_references_for_cte!(item.common_table_expr)
422
+ end
423
+ end
424
+
425
+ def record_self_references_for_cte!(cte)
426
+ return if cte.ctequery.nil?
427
+ return if @cte_names.include?(cte.ctename)
428
+
429
+ record_self_references_in(cte.ctequery, cte.ctename)
430
+ end
431
+
432
+ # Depth-first scan of a CTE definition for unqualified RangeVars matching
433
+ # the CTE's own name.
434
+ #
435
+ # This deliberately avoids #walk!, which yields every node and iterates all
436
+ # of PgQuery::Node's oneof fields. Following the oneof directly via #inner
437
+ # keeps this proportional to the nodes actually present.
438
+ def record_self_references_in(node, cte_name)
439
+ case node
440
+ when PgQuery::Node
441
+ record_self_references_in(node.inner, cte_name)
442
+ when PgQuery::RangeVar
443
+ @cte_self_reference_locations << node.location if cte_self_reference?(node, cte_name)
444
+ when Google::Protobuf::RepeatedField
445
+ node.each { |child| record_self_references_in(child, cte_name) }
446
+ when Google::Protobuf::MessageExts
447
+ record_self_references_in_message(node, cte_name)
448
+ end
449
+ end
450
+
451
+ def record_self_references_in_message(message, cte_name)
452
+ message.class.descriptor.each do |field_descriptor|
453
+ child = field_descriptor.get(message)
454
+ next unless child.is_a?(Google::Protobuf::MessageExts) || child.is_a?(Google::Protobuf::RepeatedField)
455
+
456
+ record_self_references_in(child, cte_name)
457
+ end
458
+ end
459
+
460
+ def cte_self_reference?(node, cte_name)
461
+ node.schemaname.empty? &&
462
+ node.relname == cte_name &&
463
+ !node.location.negative?
464
+ end
389
465
  end
390
466
  end