File Coverage

blib/lib/Pg/SQL/PrettyPrinter/Node/FuncCall.pm
Criterion Covered Total %
statement 175 187 93.5
branch 77 94 81.9
condition 7 7 100.0
subroutine 22 22 100.0
pod 8 8 100.0
total 289 318 90.8


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