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   13052 use v5.26;
  4         17  
5 4     4   21 use strict;
  4         17  
  4         104  
6 4     4   26 use warnings;
  4         9  
  4         283  
7 4     4   19 use warnings qw( FATAL utf8 );
  4         14  
  4         231  
8 4     4   21 use utf8;
  4         23  
  4         40  
9 4     4   127 use open qw( :std :utf8 );
  4         79  
  4         57  
10 4     4   745 use Unicode::Normalize qw( NFC );
  4         22  
  4         335  
11 4     4   89 use Unicode::Collate;
  4         9  
  4         109  
12 4     4   19 use Encode qw( decode );
  4         8  
  4         656  
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   30 use autodie;
  4         8  
  4         33  
24 4     4   30854 use Carp qw( carp croak confess cluck );
  4         11  
  4         502  
25 4     4   36 use English qw( -no_match_vars );
  4         8  
  4         37  
26 4     4   2145 use Data::Dumper qw( Dumper );
  4         11  
  4         1268  
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   30 use parent qw( Pg::SQL::PrettyPrinter::Node );
  4         16  
  4         35  
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 1360 my $class = shift;
67 47         201 my $self = $class->SUPER::new( @_ );
68 47         81 bless $self, $class;
69              
70             $self->objectify(
71             'funcname',
72             'args',
73             'agg_filter',
74             'agg_order',
75 47         125 map { [ 'over', $_ ] } qw( orderClause partitionClause startOffset endOffset )
  188         520  
76             );
77              
78 47 100       322 if ( $self->{ 'func_variadic' } ) {
79 4         45 my $last_type = ref $self->{ 'args' }->[ -1 ];
80 4         19 $last_type =~ s/^Pg::SQL::PrettyPrinter::Node:://;
81 4 50       28 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         128 return $self;
85             }
86              
87             sub func_name {
88 102     102 1 175 my $self = shift;
89 102 100       235 unless ( exists $self->{ '_funcname' } ) {
90 47         77 $self->{ '_funcname' } = join '.', map { $_->as_ident } @{ $self->{ 'funcname' } };
  48         170  
  47         91  
91             }
92 102         456 return $self->{ '_funcname' };
93             }
94              
95             sub over_clause_as_text {
96 58     58 1 89 my $self = shift;
97 58 100       260 return unless exists $self->{ 'over' };
98              
99             # shortcut
100 11         16 my $O = $self->{ 'over' };
101              
102             # Build the clause from parts, as it's simpler that way.
103 11         14 my @parts = ();
104              
105 11 100       24 if ( exists $O->{ 'partitionClause' } ) {
106 9         12 push @parts, 'PARTITION BY ' . join( ', ', map { $_->as_text } @{ $O->{ 'partitionClause' } } );
  11         17  
  9         15  
107             }
108 11 100       24 if ( exists $O->{ 'orderClause' } ) {
109 9         12 push @parts, 'ORDER BY ' . join( ', ', map { $_->as_text } @{ $O->{ 'orderClause' } } );
  10         20  
  9         14  
110             }
111              
112             # If there is no frame clause it will be empty array, so nothing will get pushed.
113 11         21 push @parts, $self->frame_clause();
114              
115             # Shortcut for over without clauses
116 11 100       26 return ' OVER ()' if 0 == scalar @parts;
117              
118 10         30 return sprintf( ' OVER ( %s )', join( ' ', @parts ) );
119             }
120              
121             sub over_clause_pretty {
122 42     42 1 66 my $self = shift;
123 42 100       212 return unless exists $self->{ 'over' };
124              
125             # shortcut
126 11         15 my $O = $self->{ 'over' };
127              
128             # Build the clause from parts, as it's simpler that way.
129 11         15 my @parts = ();
130              
131 11 100       20 if ( exists $O->{ 'partitionClause' } ) {
132 9         12 push @parts, 'PARTITION BY ' . join( ', ', map { $_->pretty_print } @{ $O->{ 'partitionClause' } } );
  11         37  
  9         16  
133             }
134 11 100       23 if ( exists $O->{ 'orderClause' } ) {
135 9         13 push @parts, 'ORDER BY ' . join( ', ', map { $_->pretty_print } @{ $O->{ 'orderClause' } } );
  10         19  
  9         12  
136             }
137              
138             # If there is no frame clause it will be empty array, so nothing will get pushed.
139 11         20 push @parts, $self->frame_clause();
140              
141             # Shortcut for over without clauses
142 11 100       21 return ' OVER ()' if 0 == scalar @parts;
143              
144             # Shortcut for over with just 1 clause
145 10 100       19 return sprintf( ' OVER ( %s )', $parts[ 0 ] ) if 1 == scalar @parts;
146              
147 8         12 my @lines = ();
148 8         10 push @lines, ' OVER (';
149 8         10 push @lines, map { $self->increase_indent( $_ ) } @parts;
  23         54  
150 8         14 push @lines, ')';
151 8         23 return join( "\n", @lines );
152             }
153              
154             sub frame_clause {
155 22     22 1 30 my $self = shift;
156              
157             # shortcuts
158 22         28 my $O = $self->{ 'over' };
159 22         28 my $FO = $O->{ 'frameOptions' };
160              
161             # Make sure it's called for FuncCalls with some frameOptions.
162 22 50       31 return unless defined $FO;
163              
164             # Make sure the frameOptions are not default.
165 22 100       40 return unless $FO & $FRAMEOPTION_NONDEFAULT;
166              
167 14         16 my @elements = ();
168              
169             # Frame based off what? range? rows? groups?
170 14 100       27 if ( $FO & $FRAMEOPTION_RANGE ) {
    100          
    50          
171 4         8 push @elements, 'RANGE';
172             }
173             elsif ( $FO & $FRAMEOPTION_ROWS ) {
174 8         10 push @elements, 'ROWS';
175             }
176             elsif ( $FO & $FRAMEOPTION_GROUPS ) {
177 2         4 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         16 my $start_clause;
185 14 100       33 if ( $FO & $FRAMEOPTION_START_UNBOUNDED_PRECEDING ) {
    100          
    100          
    50          
186 4         8 $start_clause = 'UNBOUNDED PRECEDING';
187             }
188             elsif ( $FO & $FRAMEOPTION_START_CURRENT_ROW ) {
189 2         3 $start_clause = 'CURRENT ROW';
190             }
191             elsif ( $FO & $FRAMEOPTION_START_OFFSET_PRECEDING ) {
192 6         18 $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       23 if ( $FO & $FRAMEOPTION_BETWEEN ) {
202              
203             # It's frame with BETWEEN operation. It needs end_clause and proper format ...
204 4         5 my $end_clause = '';
205 4 100       12 if ( $FO & $FRAMEOPTION_END_UNBOUNDED_FOLLOWING ) {
    50          
    50          
    50          
206 2         3 $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         5 push @elements, 'BETWEEN';
223 4         5 push @elements, $start_clause;
224 4         7 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         15 push @elements, $start_clause;
230             }
231              
232             # Handle excludes in the frame.
233 14 100       43 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         5 push @elements, 'EXCLUDE GROUP';
238             }
239             elsif ( $FO & $FRAMEOPTION_EXCLUDE_TIES ) {
240 2         5 push @elements, 'EXCLUDE TIES';
241             }
242              
243 14         36 return join( ' ', @elements );
244             }
245              
246             sub as_text {
247 58     58 1 107 my $self = shift;
248              
249 58   100     225 my $suffix = $self->over_clause_as_text // '';
250 58 50       179 if ( exists $self->{ 'agg_filter' } ) {
251 0         0 $suffix .= ' FILTER ( WHERE ' . $self->{ 'agg_filter' }->as_text . ' )';
252             }
253 58         197 my $agg_order = $self->get_agg_order();
254              
255 58 100       765 if ( $self->{ 'agg_star' } ) {
256 8 50       89 my $internal = $agg_order ? " * ${agg_order} " : '*';
257 8         29 return sprintf( '%s(%s)%s', $self->func_name, $internal, $suffix );
258             }
259 50 100       114 unless ( exists $self->{ 'args' } ) {
260 3 50       14 return $self->func_name . '()' . $suffix unless $agg_order;
261 0         0 return $self->func_name . "( ${agg_order} )" . $suffix;
262             }
263              
264 47         76 my @args_as_text = map { $_->as_text } @{ $self->{ 'args' } };
  74         203  
  47         91  
265 47 100       141 if ( $self->{ 'func_variadic' } ) {
266 4         39 $args_as_text[ -1 ] = 'VARIADIC ' . $args_as_text[ -1 ];
267             }
268 47         123 my $args_str = join( ', ', @args_as_text );
269 47 50       121 $args_str .= ' ' . $agg_order if $agg_order;
270 47 50       122 $args_str = 'DISTINCT ' . $args_str if $self->{ 'agg_distinct' };
271 47         160 return $self->func_name . '( ' . $args_str . ' )' . $suffix;
272             }
273              
274             sub get_agg_order {
275 100     100 1 163 my $self = shift;
276 100 50       282 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 70 my $self = shift;
282              
283 42   100     121 my $suffix = $self->over_clause_pretty // '';
284 42 50       114 if ( exists $self->{ 'agg_filter' } ) {
285 0         0 $suffix .= ' FILTER ( WHERE ' . $self->{ 'agg_filter' }->as_text . ' )';
286             }
287 42         115 my $agg_order = $self->get_agg_order();
288              
289 42 100       123 if ( $self->{ 'agg_star' } ) {
290 7 50       67 my $internal = $agg_order ? " * ${agg_order} " : '*';
291 7         21 return sprintf( '%s(%s)%s', $self->func_name, $internal, $suffix );
292             }
293 35 100       86 unless ( exists $self->{ 'args' } ) {
294 3 50       13 return $self->func_name . '()' . $suffix unless $agg_order;
295 0         0 return $self->func_name . "( ${agg_order} )" . $suffix;
296             }
297              
298 32         76 my @args_as_text = map { $_->as_text } @{ $self->{ 'args' } };
  50         159  
  32         92  
299 32 100       100 if ( $self->{ 'func_variadic' } ) {
300 4         44 $args_as_text[ -1 ] = 'VARIADIC ' . $args_as_text[ -1 ];
301             }
302 32         86 my $args_str = join( ', ', @args_as_text );
303 32 50       74 $args_str .= ' ' . $agg_order if $agg_order;
304 32 100 100     44 if ( ( 1 == scalar @{ $self->{ 'args' } } )
  32         163  
305             && ( 40 > length( $args_str ) ) )
306             {
307 19 50       48 if ( $self->{ 'agg_distinct' } ) {
308 0         0 $args_str = 'DISTINCT ' . $args_str;
309             }
310 19         42 return $self->func_name . '( ' . $args_str . ' )' . $suffix;
311             }
312 13         22 my @lines = ();
313 13         40 push @lines, $self->func_name . '(';
314 13         33 my @args_pp = map { $_->pretty_print } @{ $self->{ 'args' } };
  31         96  
  13         37  
315 13 100       41 if ( $self->{ 'func_variadic' } ) {
316 3         50 $args_pp[ -1 ] = 'VARIADIC ' . $args_pp[ -1 ];
317             }
318 13 50       37 if ( $self->{ 'agg_distinct' } ) {
319 0         0 $args_pp[ 0 ] = 'DISTINCT ' . $args_pp[ 0 ];
320             }
321 13         27 push @lines, map { $self->increase_indent( $_ ) . ',' } @args_pp;
  31         88  
322 13         59 $lines[ -1 ] =~ s/,\z//;
323 13 50       36 push @lines, $self->increase_indent( $agg_order ) if $agg_order;
324 13         32 push @lines, ')' . $suffix;
325 13         82 return join( "\n", @lines );
326             }
327              
328             1;