Skip to content

Commit 661c815

Browse files
committed
Add .parse_plpgsql method to parse PL/pgSQL function definitions
This uses Postgres' PL/pgSQL parser (as extracted in libpg_query) to parse a PL/pgSQL CREATE FUNCTION statement into the AST.
1 parent f23f1df commit 661c815

File tree

3 files changed

+82
-0
lines changed

3 files changed

+82
-0
lines changed

Diff for: ext/pg_query/pg_query_ruby.c

+37
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ void raise_ruby_parse_error(PgQueryProtobufParseResult result);
66
void raise_ruby_normalize_error(PgQueryNormalizeResult result);
77
void raise_ruby_fingerprint_error(PgQueryFingerprintResult result);
88
void raise_ruby_scan_error(PgQueryScanResult result);
9+
void raise_ruby_plpgsql_parse_error(PgQueryPlpgsqlParseResult result);
910

1011
VALUE pg_query_ruby_parse_protobuf(VALUE self, VALUE input);
1112
VALUE pg_query_ruby_deparse_protobuf(VALUE self, VALUE input);
1213
VALUE pg_query_ruby_normalize(VALUE self, VALUE input);
1314
VALUE pg_query_ruby_fingerprint(VALUE self, VALUE input);
15+
VALUE pg_query_ruby_parse_plpgsql(VALUE self, VALUE input);
1416
VALUE pg_query_ruby_scan(VALUE self, VALUE input);
1517
VALUE pg_query_ruby_hash_xxh3_64(VALUE self, VALUE input, VALUE seed);
1618

@@ -24,6 +26,7 @@ __attribute__((visibility ("default"))) void Init_pg_query(void)
2426
rb_define_singleton_method(cPgQuery, "deparse_protobuf", pg_query_ruby_deparse_protobuf, 1);
2527
rb_define_singleton_method(cPgQuery, "normalize", pg_query_ruby_normalize, 1);
2628
rb_define_singleton_method(cPgQuery, "fingerprint", pg_query_ruby_fingerprint, 1);
29+
rb_define_singleton_method(cPgQuery, "_raw_parse_plpgsql", pg_query_ruby_parse_plpgsql, 1);
2730
rb_define_singleton_method(cPgQuery, "_raw_scan", pg_query_ruby_scan, 1);
2831
rb_define_singleton_method(cPgQuery, "hash_xxh3_64", pg_query_ruby_hash_xxh3_64, 2);
2932
rb_define_const(cPgQuery, "PG_VERSION", rb_str_new2(PG_VERSION));
@@ -121,6 +124,24 @@ void raise_ruby_scan_error(PgQueryScanResult result)
121124
rb_exc_raise(rb_class_new_instance(4, args, cScanError));
122125
}
123126

127+
void raise_ruby_plpgsql_parse_error(PgQueryPlpgsqlParseResult result)
128+
{
129+
VALUE cPgQuery, cPlpgsqlParseError;
130+
VALUE args[4];
131+
132+
cPgQuery = rb_const_get(rb_cObject, rb_intern("PgQuery"));
133+
cPlpgsqlParseError = rb_const_get_at(cPgQuery, rb_intern("PlpgsqlParseError"));
134+
135+
args[0] = rb_str_new2(result.error->message);
136+
args[1] = rb_str_new2(result.error->filename);
137+
args[2] = INT2NUM(result.error->lineno);
138+
args[3] = INT2NUM(result.error->cursorpos);
139+
140+
pg_query_free_plpgsql_parse_result(result);
141+
142+
rb_exc_raise(rb_class_new_instance(4, args, cPlpgsqlParseError));
143+
}
144+
124145
VALUE pg_query_ruby_parse_protobuf(VALUE self, VALUE input)
125146
{
126147
Check_Type(input, T_STRING);
@@ -197,6 +218,22 @@ VALUE pg_query_ruby_fingerprint(VALUE self, VALUE input)
197218
return output;
198219
}
199220

221+
VALUE pg_query_ruby_parse_plpgsql(VALUE self, VALUE input)
222+
{
223+
Check_Type(input, T_STRING);
224+
225+
VALUE output;
226+
PgQueryPlpgsqlParseResult result = pg_query_parse_plpgsql(StringValueCStr(input));
227+
228+
if (result.error) raise_ruby_plpgsql_parse_error(result);
229+
230+
output = rb_str_new2(result.plpgsql_funcs);
231+
232+
pg_query_free_plpgsql_parse_result(result);
233+
234+
return output;
235+
}
236+
200237
VALUE pg_query_ruby_scan(VALUE self, VALUE input)
201238
{
202239
Check_Type(input, T_STRING);

Diff for: lib/pg_query.rb

+2
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515
require 'pg_query/deparse'
1616
require 'pg_query/truncate'
1717

18+
require 'pg_query/parse_plpgsql'
19+
1820
require 'pg_query/scan'

Diff for: lib/pg_query/parse_plpgsql.rb

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
require 'json'
2+
module PgQuery
3+
class PlpgsqlParseError < ArgumentError
4+
attr_reader :location
5+
def initialize(message, source_file, source_line, location)
6+
super("#{message} (#{source_file}:#{source_line})")
7+
@location = location
8+
end
9+
end
10+
11+
def self.parse_plpgsql(input)
12+
PlpgsqlParserResult.new(input, JSON.parse(_raw_parse_plpgsql(input)))
13+
end
14+
15+
class PlpgsqlParserResult
16+
attr_reader :input
17+
attr_reader :tree
18+
19+
def initialize(input, tree)
20+
@input = input
21+
@tree = tree
22+
end
23+
24+
def walk!
25+
nodes = [tree.dup]
26+
loop do
27+
parent_node = nodes.shift
28+
if parent_node.is_a?(Array)
29+
parent_node.each do |node|
30+
yield(node)
31+
nodes << node
32+
end
33+
elsif parent_node.is_a?(Hash)
34+
parent_node.each do |k, node|
35+
yield(node)
36+
nodes << node
37+
end
38+
end
39+
break if nodes.empty?
40+
end
41+
end
42+
end
43+
end

0 commit comments

Comments
 (0)