File Coverage

blib/lib/Pg/SQL/PrettyPrinter/Node/FuncCall.pm
Criterion Covered Total %
statement 178 192 92.7
branch 80 100 80.0
condition 7 7 100.0
subroutine 22 22 100.0
pod 8 8 100.0
total 295 329 89.6


line stmt bran cond sub pod time code
1             package Pg::SQL::PrettyPrinter::Node::FuncCall;
2              
3             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
4 4     4   6072 use v5.26;
  4         16  
5 4     4   23 use strict;
  4         8  
  4         95  
6 4     4   17 use warnings;
  4         6  
  4         258  
7 4     4   22 use warnings qw( FATAL utf8 );
  4         7  
  4         238  
8 4     4   22 use utf8;
  4         22  
  4         28  
9 4     4   132 use open qw( :std :utf8 );
  4         84  
  4         25  
10 4     4   603 use Unicode::Normalize qw( NFC );
  4         8  
  4         247  
11 4     4   22 use Unicode::Collate;
  4         67  
  4         174  
12 4     4   19 use Encode qw( decode );
  4         7  
  4         553  
13              
14             if ( grep /\P{ASCII}/ => @ARGV ) {
15             @ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
16             }
17              
18             # If there is __DATA__,then uncomment next line:
19             # binmode( DATA, ':encoding(UTF-8)' );
20             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
21              
22             # Useful common code
23 4     4   25 use autodie;
  4         8  
  4         31  
24 4     4   19850 use Carp qw( carp croak confess cluck );
  4         8  
  4         453  
25 4     4   28 use English qw( -no_match_vars );
  4         8  
  4         31  
26 4     4   1658 use Data::Dumper qw( Dumper );
  4         10  
  4         955  
27              
28             # give a full stack dump on any untrapped exceptions
29             local $SIG{ __DIE__ } = sub {
30             confess "Uncaught exception: @_" unless $^S;
31             };
32              
33             # now promote run-time warnings into stackdumped exceptions
34             # *unless* we're in an try block, in which
35             # case just generate a clucking stackdump instead
36             local $SIG{ __WARN__ } = sub {
37             if ( $^S ) { cluck "Trapped warning: @_" }
38             else { confess "Deadly warning: @_" }
39             };
40              
41             # Useful common code
42              
43 4     4   68 use parent qw( Pg::SQL::PrettyPrinter::Node );
  4         10  
  4         29  
44              
45             # Taken from PostgreSQL sources, from src/include/nodes/parsenodes.h
46             our $FRAMEOPTION_NONDEFAULT = 0x00001; # any specified?
47             our $FRAMEOPTION_RANGE = 0x00002; # RANGE behavior
48             our $FRAMEOPTION_ROWS = 0x00004; # ROWS behavior
49             our $FRAMEOPTION_GROUPS = 0x00008; # GROUPS behavior
50             our $FRAMEOPTION_BETWEEN = 0x00010; # BETWEEN given?
51             our $FRAMEOPTION_START_UNBOUNDED_PRECEDING = 0x00020; # start is U. P.
52             our $FRAMEOPTION_END_UNBOUNDED_PRECEDING = 0x00040; # (disallowed)
53             our $FRAMEOPTION_START_UNBOUNDED_FOLLOWING = 0x00080; # (disallowed)
54             our $FRAMEOPTION_END_UNBOUNDED_FOLLOWING = 0x00100; # end is U. F.
55             our $FRAMEOPTION_START_CURRENT_ROW = 0x00200; # start is C. R.
56             our $FRAMEOPTION_END_CURRENT_ROW = 0x00400; # end is C. R.
57             our $FRAMEOPTION_START_OFFSET_PRECEDING = 0x00800; # start is O. P.
58             our $FRAMEOPTION_END_OFFSET_PRECEDING = 0x01000; # end is O. P.
59             our $FRAMEOPTION_START_OFFSET_FOLLOWING = 0x02000; # start is O. F.
60             our $FRAMEOPTION_END_OFFSET_FOLLOWING = 0x04000; # end is O. F.
61             our $FRAMEOPTION_EXCLUDE_CURRENT_ROW = 0x08000; # omit C.R.
62             our $FRAMEOPTION_EXCLUDE_GROUP = 0x10000; # omit C.R. & peers
63             our $FRAMEOPTION_EXCLUDE_TIES = 0x20000; # omit C.R.'s peers
64              
65             sub new {
66 47     47 1 983 my $class = shift;
67 47         138 my $self = $class->SUPER::new( @_ );
68 47         71 bless $self, $class;
69              
70             $self->objectify(
71             'funcname',
72             'args',
73             'agg_filter',
74             'agg_order',
75 47         94 map { [ 'over', $_ ] } qw( orderClause partitionClause startOffset endOffset )
  188         419  
76             );
77              
78 47 100       207 if ( $self->{ 'func_variadic' } ) {
79 4         31 my $last_type = ref $self->{ 'args' }->[ -1 ];
80 4         9 $last_type =~ s/^Pg::SQL::PrettyPrinter::Node:://;
81 4 50       20 croak( "Function is variadic, but last arg is not an array/subquery: ${last_type}" ) unless $last_type =~ m{\A(?:A_ArrayExpr|SubLink)\z};
82             }
83              
84 47         125 return $self;
85             }
86              
87             sub func_name {
88 102     102 1 193 my $self = shift;
89 102 100       174 unless ( exists $self->{ '_funcname' } ) {
90 47         60 $self->{ '_funcname' } = join '.', map { $_->as_ident } @{ $self->{ 'funcname' } };
  48         99  
  47         80  
91             }
92 102         326 return $self->{ '_funcname' };
93             }
94              
95             sub over_clause_as_text {
96 58     58 1 62 my $self = shift;
97 58 100       189 return unless exists $self->{ 'over' };
98              
99             # shortcut
100 11         14 my $O = $self->{ 'over' };
101              
102             # Build the clause from parts, as it's simpler that way.
103 11         15 my @parts = ();
104              
105 11 100       16 if ( exists $O->{ 'partitionClause' } ) {
106 9         13 push @parts, 'PARTITION BY ' . join( ', ', map { $_->as_text } @{ $O->{ 'partitionClause' } } );
  11         20  
  9         43  
107             }
108 11 100       20 if ( exists $O->{ 'orderClause' } ) {
109 9         11 push @parts, 'ORDER BY ' . join( ', ', map { $_->as_text } @{ $O->{ 'orderClause' } } );
  10         21  
  9         17  
110             }
111              
112             # If there is no frame clause it will be empty array, so nothing will get pushed.
113 11         23 push @parts, $self->frame_clause();
114              
115             # Shortcut for over without clauses
116 11 100       22 return ' OVER ()' if 0 == scalar @parts;
117              
118 10         28 return sprintf( ' OVER ( %s )', join( ' ', @parts ) );
119             }
120              
121             sub over_clause_pretty {
122 42     42 1 50 my $self = shift;
123 42 100       132 return unless exists $self->{ 'over' };
124              
125             # shortcut
126 11         12 my $O = $self->{ 'over' };
127              
128             # Build the clause from parts, as it's simpler that way.
129 11         16 my @parts = ();
130              
131 11 100       18 if ( exists $O->{ 'partitionClause' } ) {
132 9         15 push @parts, 'PARTITION BY ' . join( ', ', map { $_->pretty_print } @{ $O->{ 'partitionClause' } } );
  11         23  
  9         12  
133             }
134 11 100       21 if ( exists $O->{ 'orderClause' } ) {
135 9         11 push @parts, 'ORDER BY ' . join( ', ', map { $_->pretty_print } @{ $O->{ 'orderClause' } } );
  10         18  
  9         16  
136             }
137              
138             # If there is no frame clause it will be empty array, so nothing will get pushed.
139 11         22 push @parts, $self->frame_clause();
140              
141             # Shortcut for over without clauses
142 11 100       20 return ' OVER ()' if 0 == scalar @parts;
143              
144             # Shortcut for over with just 1 clause
145 10 100       21 return sprintf( ' OVER ( %s )', $parts[ 0 ] ) if 1 == scalar @parts;
146              
147 8         9 my @lines = ();
148 8         10 push @lines, ' OVER (';
149 8         9 push @lines, map { $self->increase_indent( $_ ) } @parts;
  23         75  
150 8         16 push @lines, ')';
151 8         27 return join( "\n", @lines );
152             }
153              
154             sub frame_clause {
155 22     22 1 25 my $self = shift;
156              
157             # shortcuts
158 22         26 my $O = $self->{ 'over' };
159 22         27 my $FO = $O->{ 'frameOptions' };
160              
161             # Make sure it's called for FuncCalls with some frameOptions.
162 22 50       33 return unless defined $FO;
163              
164             # Make sure the frameOptions are not default.
165 22 100       36 return unless $FO & $FRAMEOPTION_NONDEFAULT;
166              
167 14         17 my @elements = ();
168              
169             # Frame based off what? range? rows? groups?
170 14 100       30 if ( $FO & $FRAMEOPTION_RANGE ) {
    100          
    50          
171 4         6 push @elements, 'RANGE';
172             }
173             elsif ( $FO & $FRAMEOPTION_ROWS ) {
174 8         11 push @elements, 'ROWS';
175             }
176             elsif ( $FO & $FRAMEOPTION_GROUPS ) {
177 2         5 push @elements, 'GROUPS';
178             }
179             else {
180 0         0 croak( "Bad (#1) frameOptions: $FO" );
181             }
182              
183             # Calculate start clause, as it's used in both between and just-start frames
184 14         14 my $start_clause;
185 14 100       29 if ( $FO & $FRAMEOPTION_START_UNBOUNDED_PRECEDING ) {
    100          
    100          
    50          
186 4         6 $start_clause = 'UNBOUNDED PRECEDING';
187             }
188             elsif ( $FO & $FRAMEOPTION_START_CURRENT_ROW ) {
189 2         4 $start_clause = 'CURRENT ROW';
190             }
191             elsif ( $FO & $FRAMEOPTION_START_OFFSET_PRECEDING ) {
192 6         16 $start_clause = $self->{ 'over' }->{ 'startOffset' }->as_text . ' PRECEDING';
193             }
194             elsif ( $FO & $FRAMEOPTION_START_OFFSET_FOLLOWING ) {
195 2         6 $start_clause = $self->{ 'over' }->{ 'startOffset' }->as_text . ' FOLLOWING';
196             }
197             else {
198 0         0 croak( "Bad (#2) frameOptions: $FO" );
199             }
200              
201 14 100       21 if ( $FO & $FRAMEOPTION_BETWEEN ) {
202              
203             # It's frame with BETWEEN operation. It needs end_clause and proper format ...
204 4         6 my $end_clause = '';
205 4 100       10 if ( $FO & $FRAMEOPTION_END_UNBOUNDED_FOLLOWING ) {
    50          
    50          
    50          
206 2         2 $end_clause = 'UNBOUNDED FOLLOWING';
207             }
208             elsif ( $FO & $FRAMEOPTION_END_CURRENT_ROW ) {
209 0         0 $end_clause = 'CURRENT ROW';
210             }
211             elsif ( $FO & $FRAMEOPTION_END_OFFSET_PRECEDING ) {
212 0         0 $end_clause = $self->{ 'over' }->{ 'endOffset' }->as_text . ' PRECEDING';
213             }
214             elsif ( $FO & $FRAMEOPTION_END_OFFSET_FOLLOWING ) {
215 2         6 $end_clause = $self->{ 'over' }->{ 'endOffset' }->as_text . ' FOLLOWING';
216             }
217             else {
218 0         0 croak( "Bad (#3) frameOptions: $FO" );
219             }
220              
221             # Put the elements of between clause together.
222 4         7 push @elements, 'BETWEEN';
223 4         4 push @elements, $start_clause;
224 4         5 push @elements, 'AND';
225 4         6 push @elements, $end_clause;
226             }
227             else {
228             # If it's not BETWEEN frame, just put start clause to output
229 10         13 push @elements, $start_clause;
230             }
231              
232             # Handle excludes in the frame.
233 14 100       30 if ( $FO & $FRAMEOPTION_EXCLUDE_CURRENT_ROW ) {
    100          
    100          
234 2         3 push @elements, 'EXCLUDE CURRENT ROW';
235             }
236             elsif ( $FO & $FRAMEOPTION_EXCLUDE_GROUP ) {
237 2         3 push @elements, 'EXCLUDE GROUP';
238             }
239             elsif ( $FO & $FRAMEOPTION_EXCLUDE_TIES ) {
240 2         3 push @elements, 'EXCLUDE TIES';
241             }
242              
243 14         31 return join( ' ', @elements );
244             }
245              
246             sub as_text {
247 58     58 1 79 my $self = shift;
248              
249 58   100     144 my $suffix = $self->over_clause_as_text // '';
250 58 50       117 if ( exists $self->{ 'agg_filter' } ) {
251 0         0 $suffix .= ' FILTER ( WHERE ' . $self->{ 'agg_filter' }->as_text . ' )';
252             }
253 58         101 my $agg_order = $self->get_agg_order();
254              
255 58 100       269 if ( $self->{ 'agg_star' } ) {
256 8 50       65 my $internal = $agg_order ? " * ${agg_order} " : '*';
257 8         25 return sprintf( '%s(%s)%s', $self->func_name, $internal, $suffix );
258             }
259 50 100       84 unless ( exists $self->{ 'args' } ) {
260 3 50       11 return $self->func_name . '()' . $suffix unless $agg_order;
261 0         0 return $self->func_name . "( ${agg_order} )" . $suffix;
262             }
263              
264 47         57 my @args_as_text = map { $_->as_text } @{ $self->{ 'args' } };
  74         143  
  47         75  
265 47 100       96 if ( $self->{ 'func_variadic' } ) {
266 4         24 $args_as_text[ -1 ] = 'VARIADIC ' . $args_as_text[ -1 ];
267             }
268 47         91 my $args_str = join( ', ', @args_as_text );
269 47 50       79 $args_str .= ' ' . $agg_order if $agg_order;
270 47 50       77 $args_str = 'DISTINCT ' . $args_str if $self->{ 'agg_distinct' };
271 47         78 return $self->func_name . '( ' . $args_str . ' )' . $suffix;
272             }
273              
274             sub get_agg_order {
275 100     100 1 167 my $self = shift;
276 100 50       196 return '' unless exists $self->{ 'agg_order' };
277 0         0 return sprintf( 'ORDER BY %s', join( ', ', map { $_->as_text } @{ $self->{ 'agg_order' } } ) );
  0         0  
  0         0  
278             }
279              
280             sub pretty_print {
281 42     42 1 60 my $self = shift;
282              
283 42   100     90 my $suffix = $self->over_clause_pretty // '';
284 42 50       87 if ( exists $self->{ 'agg_filter' } ) {
285 0         0 $suffix .= ' FILTER ( WHERE ' . $self->{ 'agg_filter' }->as_text . ' )';
286             }
287 42         67 my $agg_order = $self->get_agg_order();
288              
289 42 100       94 if ( $self->{ 'agg_star' } ) {
290 7 50       56 my $internal = $agg_order ? " * ${agg_order} " : '*';
291 7         16 return sprintf( '%s(%s)%s', $self->func_name, $internal, $suffix );
292             }
293 35 100       73 unless ( exists $self->{ 'args' } ) {
294 3 50       8 return $self->func_name . '()' . $suffix unless $agg_order;
295 0         0 return $self->func_name . "( ${agg_order} )" . $suffix;
296             }
297              
298 32         35 my @args_as_text = map { $_->as_text } @{ $self->{ 'args' } };
  50         104  
  32         88  
299 32 100       71 if ( $self->{ 'func_variadic' } ) {
300 4         27 $args_as_text[ -1 ] = 'VARIADIC ' . $args_as_text[ -1 ];
301             }
302 32         63 my $args_str = join( ', ', @args_as_text );
303 32 50       53 $args_str .= ' ' . $agg_order if $agg_order;
304 32 100 100     36 if ( ( 1 == scalar @{ $self->{ 'args' } } )
  32         104  
305             && ( 40 > length( $args_str ) ) )
306             {
307 19 50       37 if ( $self->{ 'agg_distinct' } ) {
308 0         0 $args_str = 'DISTINCT ' . $args_str;
309             }
310 19         35 return $self->func_name . '( ' . $args_str . ' )' . $suffix;
311             }
312 13         22 my @lines = ();
313 13         29 push @lines, $self->func_name . '(';
314 13         22 my @args_pp = map { $_->pretty_print } @{ $self->{ 'args' } };
  31         69  
  13         24  
315 13 100       30 if ( $self->{ 'func_variadic' } ) {
316 3         20 $args_pp[ -1 ] = 'VARIADIC ' . $args_pp[ -1 ];
317             }
318 13 50       25 if ( $self->{ 'agg_distinct' } ) {
319 0         0 $args_pp[ 0 ] = 'DISTINCT ' . $args_pp[ 0 ];
320             }
321 13         22 push @lines, map { $self->increase_indent( $_ ) . ',' } @args_pp;
  31         106  
322 13         47 $lines[ -1 ] =~ s/,\z//;
323 13 50       29 push @lines, $self->increase_indent( $agg_order ) if $agg_order;
324 13         23 push @lines, ')' . $suffix;
325 13         51 return join( "\n", @lines );
326             }
327              
328             1;