Skip to content

Commit

Permalink
PgQuery::Node: Add inner and inner= helpers to modify inner object
Browse files Browse the repository at this point in the history
Because of how oneof messages work in protobuf, its a bit tedious to
make modifications to a Node object, in particular when changing the type
of the inner object held within the Node in a generic code path (i.e.
with a dynamic inner type), which required first knowing the name of the
new inner object type, and then using public_send to set the new value.

To help, introduce the new "inner" and "inner=" methods for PgQuery::Node,
that get/set the inner object directly, avoiding the use of public_send.

This will be of particular help for anyone utilizing the walk! API to
make modifications to the query tree. To illustrate, an example is added
to the treewalker spec showing how to utilize this.
  • Loading branch information
lfittl committed Jan 3, 2024
1 parent b0bb868 commit 7fa8700
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 12 deletions.
27 changes: 16 additions & 11 deletions lib/pg_query/node.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
module PgQuery
# Patch the auto-generated generic node type with additional convenience functions
class Node
def self.inner_class_to_name(klass)
@class_to_name ||= descriptor.lookup_oneof('node').map { |f| [f.subtype.msgclass, f.name.to_sym] }.to_h
@class_to_name[klass]
end

def inner
self[node.to_s]
end

def inner=(submsg)
name = self.class.inner_class_to_name(submsg.class)
public_send("#{name}=", submsg)
end

def inspect
node ? format('<PgQuery::Node: %s: %s>', node, public_send(node).inspect) : '<PgQuery::Node>'
node ? format('<PgQuery::Node: %s: %s>', node, inner.inspect) : '<PgQuery::Node>'
end

# Make it easier to initialize nodes from a given node child object
def self.from(node_field_val)
# This needs to match libpg_query naming for the Node message field names
# (see "underscore" method in libpg_query's scripts/generate_protobuf_and_funcs.rb)
node_field_name = node_field_val.class.name.split('::').last
node_field_name.gsub!(/^([A-Z\d])([A-Z][a-z])/, '\1__\2')
node_field_name.gsub!(/([A-Z\d]+[a-z]+)([A-Z][a-z])/, '\1_\2')
node_field_name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
node_field_name.tr!('-', '_')
node_field_name.downcase!

PgQuery::Node.new(node_field_name => node_field_val)
PgQuery::Node.new(inner_class_to_name(node_field_val.class) => node_field_val)
end

# Make it easier to initialize value nodes
Expand Down
2 changes: 1 addition & 1 deletion lib/pg_query/parse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def load_objects! # rubocop:disable Metrics/CyclomaticComplexity
end
# The following statements modify the contents of a table
when :insert_stmt, :update_stmt, :delete_stmt
value = statement.public_send(statement.node)
value = statement.inner
from_clause_items << { item: PgQuery::Node.new(range_var: value.relation), type: :dml }
statements << value.select_stmt if statement.node == :insert_stmt && value.select_stmt

Expand Down
10 changes: 10 additions & 0 deletions spec/lib/treewalker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,14 @@
[:stmts, 0, :stmt, :select_stmt, :target_list, 0, :res_target, :val, :func_call, :args, 0, :param_ref]
]
end

it 'allows recursively replacing nodes' do
query = PgQuery.parse("SELECT * FROM tbl WHERE col::text = ANY(((ARRAY[$39, $40])::varchar[])::text[])")
query.walk! do |_parent_node, _parent_field, node, _location|
next unless node.is_a?(PgQuery::Node)
# Keep removing type casts until we hit a different class
node.inner = node.type_cast.arg.inner while node.node == :type_cast
end
expect(query.deparse).to eq 'SELECT * FROM tbl WHERE col = ANY(ARRAY[$39, $40])'
end
end

0 comments on commit 7fa8700

Please sign in to comment.