File Coverage

blib/lib/Pg/Explain/FromText.pm
Criterion Covered Total %
statement 211 218 96.7
branch 98 112 87.5
condition 3 3 100.0
subroutine 18 18 100.0
pod 4 4 100.0
total 334 355 94.0


line stmt bran cond sub pod time code
1             package Pg::Explain::FromText;
2              
3             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
4 79     79   1087 use v5.18;
  79         291  
5 79     79   434 use strict;
  79         155  
  79         2088  
6 79     79   1003 use warnings;
  79         368  
  79         8335  
7 79     79   777 use warnings qw( FATAL utf8 );
  79         236  
  79         4943  
8 79     79   481 use utf8;
  79         170  
  79         585  
9 79     79   3538 use open qw( :std :utf8 );
  79         210  
  79         567  
10 79     79   13266 use Unicode::Normalize qw( NFC );
  79         196  
  79         5240  
11 79     79   614 use Unicode::Collate;
  79         249  
  79         2795  
12 79     79   411 use Encode qw( decode );
  79         155  
  79         4720  
13 79     79   46452 use English qw( -no_match_vars );
  79         271712  
  79         530  
14              
15             if ( grep /\P{ASCII}/ => @ARGV ) {
16             @ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
17             }
18              
19             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
20              
21 79     79   38543 use Carp;
  79         176  
  79         6572  
22 79     79   54853 use Pg::Explain::Node;
  79         354  
  79         5078  
23 79     79   67923 use Pg::Explain::Buffers;
  79         295  
  79         4119  
24 79     79   51330 use Pg::Explain::JIT;
  79         264  
  79         341586  
25              
26             =head1 NAME
27              
28             Pg::Explain::FromText - Parser for text based explains
29              
30             =head1 VERSION
31              
32             Version 2.9
33              
34             =cut
35              
36             our $VERSION = '2.9';
37              
38             =head1 SYNOPSIS
39              
40             It's internal class to wrap some work. It should be used by Pg::Explain, and not directly.
41              
42             =head1 FUNCTIONS
43              
44             =head2 new
45              
46             Object constructor.
47              
48             This is not really useful in this particular class, but it's to have the same API for all Pg::Explain::From* classes.
49              
50             =cut
51              
52             sub new {
53 319     319 1 874 my $class = shift;
54 319         965 my $self = bless {}, $class;
55 319         1022 return $self;
56             }
57              
58             =head2 explain
59              
60             Get/Set master explain object.
61              
62             =cut
63              
64 1670 100   1670 1 2955 sub explain { my $self = shift; $self->{ 'explain' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'explain' }; }
  1670         5250  
  1670         7082  
65              
66             =head2 split_into_lines
67              
68             Splits source into lines, while fixing (well, trying to fix) cases where input has been force-wrapped to some length.
69              
70             =cut
71              
72             sub split_into_lines {
73 319     319 1 637 my $self = shift;
74 319         702 my $source = shift;
75              
76 319         5980 my @lines = split /\r?\n/, $source;
77              
78 319         837 my @out = ();
79 319         887 for my $l ( @lines ) {
80              
81             # Ignore certain lines
82 2568 100       7201 next if $l =~ m{\A \s* \( \d+ \s+ rows? \) \s* \z}xms;
83 2567 50       11856 next if $l =~ m{\A \s* query \s plan \s* \z}xmsi;
84 2567 100       20165 next if $l =~ m{\A \s* (?: -+ | ─+ ) \s* \z}xms;
85              
86 2565 100       10691 if ( $l =~ m{ \A Trigger \s+ }xms ) {
    100          
    100          
87 18         36 push @out, $l;
88             }
89             elsif ( $l =~ m{ \A (?: Total \s+ runtime | Planning \s+ time | Execution \s+ time | Time | Filter | Output | JIT | Planning | Settings | Query \s+ Identifier ): }xmsi ) {
90 173         430 push @out, $l;
91             }
92             elsif ( $l =~ m{\A\S} ) {
93 233 100       748 if ( 0 < scalar @out ) {
94 26         1390 $out[ -1 ] .= $l;
95             }
96             else {
97 207         703 push @out, $l;
98             }
99             }
100             else {
101 2141         4522 push @out, $l;
102             }
103             }
104              
105 319         1985 return @out;
106             }
107              
108             =head2 parse_source
109              
110             Function which parses actual plan, and constructs Pg::Explain::Node objects
111             which represent it.
112              
113             Returns Top node of query plan.
114              
115             =cut
116              
117             sub parse_source {
118 319     319 1 809 my $self = shift;
119 319         761 my $source = shift;
120              
121             # Store jit text info, and flag whether we're in JIT parsing phase
122 319         743 my $jit = undef;
123 319         586 my $in_jit = undef;
124              
125             # Store information about planning buffers
126 319         703 my $planning_buffers = undef;
127 319         551 my $in_planning = undef;
128              
129 319         630 my $top_node = undef;
130 319         2682 my %element_at_depth = (); # element is hashref, contains 2 keys: node (Pg::Explain::Node) and subelement-type, which can be: subnode, initplan or subplan.
131              
132 319         1369 my @lines = $self->split_into_lines( $source );
133              
134 319         2048 my $costs_re = qr{ \( cost=(?\d+\.\d+)\.\.(?\d+\.\d+) \s+ rows=(?\d+) \s+ width=(?\d+) \) }xms;
135 319         1147 my $analyze_re = qr{ \(
136             (?:
137             actual \s time=(?\d+\.\d+)\.\.(?\d+\.\d+) \s rows=(?\d+(?:\.\d+)?) \s loops=(?\d+)
138             |
139             actual \s rows=(?\d+(?:\.\d+)?) \s loops=(?\d+)
140             |
141             (? never \s+ executed )
142             )
143             \) }xms;
144              
145 319         1124 my $guc_name = qr{ [a-zA-Z_.]+ }xms;
146 319         959 my $guc_value = qr{ ' (?:[^']+|'' )* ' }xms;
147 319         8602 my $single_guc = qr{ ( $guc_name ) \s* = \s* ( $guc_value ) }xms;
148 319         7682 my $multiple_gucs = qr{ $single_guc (?: , \s* $single_guc )* }xms;
149              
150 319         828 my $query = '';
151 319         707 my $plan_started = 0;
152             LINE:
153 319         1985 for my $line ( @lines ) {
154              
155             # Remove trailing whitespace - it makes next line matches MUCH faster.
156 2539         15623 $line =~ s/\s+\z//;
157              
158             # There could be stray " at the end. No idea why, but some people paste such explains on explain.depesz.com
159 2539         5351 $line =~ s/\s*"\z//;
160              
161             # Replace tabs with 4 spaces
162 2539         4884 $line =~ s/\t/ /g;
163              
164 2539 100 100     114684 if (
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
165             ( $line =~ m{\(} )
166             && (
167             $line =~ m{
168             \A
169             (?\s* -> \s* | \s* )
170             (?\S.*?)
171             \s+
172             (?:
173             $costs_re \s+ $analyze_re
174             |
175             $costs_re
176             |
177             $analyze_re
178             )
179             \s*
180             \z
181             }xms
182             )
183             )
184             {
185 1002         2465 $plan_started = 1;
186              
187 1002         23516 my $new_node = Pg::Explain::Node->new( %+ );
188 1002         7338 $new_node->explain( $self->explain );
189 1002 100       6971 if ( defined $+{ 'never_executed' } ) {
190 22         83 $new_node->actual_loops( 0 );
191 22         68 $new_node->never_executed( 1 );
192             }
193 1002         4167 my $element = { 'node' => $new_node, 'subelement-type' => 'subnode', };
194              
195 1002         1912 $in_jit = undef;
196              
197 1002         4169 my $prefix = $+{ 'prefix' };
198 1002         6101 $prefix =~ s/->.*//;
199 1002         2812 my $prefix_length = length $prefix;
200              
201 1002 100       4255 if ( 0 == scalar keys %element_at_depth ) {
202 319         950 $element_at_depth{ '0' } = $element;
203 319         647 $top_node = $new_node;
204 319         1721 next LINE;
205             }
206 683         3371 my @existing_depths = sort { $a <=> $b } keys %element_at_depth;
  2891         5671  
207 683         1890 for my $key ( grep { $_ >= $prefix_length } @existing_depths ) {
  2234         4753  
208 323         957 delete $element_at_depth{ $key };
209             }
210              
211 683         1986 my $maximal_depth = ( sort { $b <=> $a } keys %element_at_depth )[ 0 ];
  2159         3558  
212 683 50       1977 if ( !defined $maximal_depth ) {
213 0         0 croak( "Didn't find current_element by depth - this shouldn't happen - please contact author.\n" );
214             }
215 683         2527 my $previous_element = $element_at_depth{ $maximal_depth };
216              
217 683         1955 $element_at_depth{ $prefix_length } = $element;
218              
219 683 100       2352 if ( $previous_element->{ 'subelement-type' } eq 'subnode' ) {
    100          
    100          
    50          
220 597         2517 $previous_element->{ 'node' }->add_sub_node( $new_node );
221             }
222             elsif ( $previous_element->{ 'subelement-type' } eq 'initplan' ) {
223 31         181 $previous_element->{ 'node' }->add_initplan( $new_node, $previous_element->{ 'metainfo' } );
224             }
225             elsif ( $previous_element->{ 'subelement-type' } eq 'subplan' ) {
226 32         174 $previous_element->{ 'node' }->add_subplan( $new_node );
227             }
228             elsif ( $previous_element->{ 'subelement-type' } =~ /^cte:(.+)$/ ) {
229 23         124 $previous_element->{ 'node' }->add_cte( $1, $new_node );
230 23         124 delete $element_at_depth{ $maximal_depth };
231             }
232             else {
233 0         0 croak( "Bad subelement-type in previous_element - this shouldn't happen - please contact author.\n" );
234             }
235             }
236             elsif ( $line =~ m{ \A (\s*) InitPlan \s* ( \d+ )? \s* (?: \( returns \s+ (.*) \) \s* )? \z }xms ) {
237 29         171 my ( $prefix, $name, $returns ) = ( $1, $2, $3 );
238 29         63 $in_jit = undef;
239              
240 29         112 my @remove_elements = grep { $_ >= length $prefix } keys %element_at_depth;
  63         214  
241 29 100       142 delete @element_at_depth{ @remove_elements } unless 0 == scalar @remove_elements;
242              
243 29         109 my $maximal_depth = ( sort { $b <=> $a } keys %element_at_depth )[ 0 ];
  62         120  
244 29 50       101 if ( !defined $maximal_depth ) {
245 0         0 croak( "Didn't find current_element by depth - this shouldn't happen - please contact author (subplan).\n" );
246             }
247 29         66 my $previous_element = $element_at_depth{ $maximal_depth };
248              
249 29         56 my $metainfo = {};
250 29 100       126 $metainfo->{ 'name' } = $name if defined $name;
251 29 100       124 $metainfo->{ 'returns' } = $returns if defined $returns;
252 29 100       53 $metainfo = undef if 0 == scalar keys %{ $metainfo };
  29         108  
253              
254             $element_at_depth{ 1 + length $prefix } = {
255 29         207 'node' => $previous_element->{ 'node' },
256             'subelement-type' => 'initplan',
257             'metainfo' => $metainfo,
258             };
259 29         118 next LINE;
260             }
261             elsif ( $line =~ m{ \A (\s*) SubPlan \s* (?: \d+ \s* )? \s* (?: \( returns .* \) \s* )? \z }xms ) {
262 28         100 my $prefix = $1;
263              
264 28         63 $in_jit = undef;
265              
266 28         115 my @remove_elements = grep { $_ >= length $prefix } keys %element_at_depth;
  82         246  
267 28 100       167 delete @element_at_depth{ @remove_elements } unless 0 == scalar @remove_elements;
268              
269 28         111 my $maximal_depth = ( sort { $b <=> $a } keys %element_at_depth )[ 0 ];
  33         74  
270 28 50       104 if ( !defined $maximal_depth ) {
271 0         0 croak( "Didn't find current_element by depth - this shouldn't happen - please contact author (subplan).\n" );
272             }
273 28         72 my $previous_element = $element_at_depth{ $maximal_depth };
274              
275             $element_at_depth{ 1 + length $prefix } = {
276 28         189 'node' => $previous_element->{ 'node' },
277             'subelement-type' => 'subplan',
278             };
279 28         135 next LINE;
280             }
281             elsif ( $line =~ m{ \A (\s*) CTE \s+ (\S+) \s* \z }xms ) {
282 23         128 my ( $prefix, $cte_name ) = ( $1, $2 );
283              
284 23         42 $in_jit = undef;
285              
286 23         71 my @remove_elements = grep { $_ >= length $prefix } keys %element_at_depth;
  32         124  
287 23 100       81 delete @element_at_depth{ @remove_elements } unless 0 == scalar @remove_elements;
288              
289 23         87 my $maximal_depth = ( sort { $b <=> $a } keys %element_at_depth )[ 0 ];
  4         7  
290 23 50       108 if ( !defined $maximal_depth ) {
291 0         0 croak( "Didn't find current_element by depth - this shouldn't happen - please contact author (CTE).\n" );
292             }
293 23         67 my $previous_element = $element_at_depth{ $maximal_depth };
294              
295             $element_at_depth{ length $prefix } = {
296 23         139 'node' => $previous_element->{ 'node' },
297             'subelement-type' => 'cte:' . $cte_name,
298             };
299              
300 23         115 next LINE;
301             }
302             elsif ( $line =~ m{ \A \s* (Planning|Execution) \s+ time: \s+ (\d+\.\d+) \s+ ms \s* \z }xmsi ) {
303 214         1057 my ( $type, $time ) = ( $1, $2 );
304              
305 214         381 $in_jit = undef;
306              
307 214 100       954 $self->explain->planning_time( $time ) if 'planning' eq lc( $type );
308 214 100       839 $self->explain->execution_time( $time ) if 'execution' eq lc( $type );
309             }
310             elsif ( $line =~ m{ \A \s* Total \s+ runtime: \s+ (\d+\.\d+) \s+ ms \s* \z }xmsi ) {
311 52         437 my ( $time ) = ( $1 );
312              
313 52         107 $in_jit = undef;
314              
315 52         212 $self->explain->total_runtime( $time );
316             }
317             elsif ( $line =~ m{ \A \s* Settings: \s* ( $multiple_gucs ) \s* \z }xmsi ) {
318 3         11 my $gucs = $1;
319 3         6 my $settings = {};
320 3         126 my @elements = $gucs =~ m{ $single_guc }xmsg;
321 3         17 for ( my $i = 0 ; $i < @elements ; $i += 2 ) {
322 5         10 my $val = $elements[ $i + 1 ];
323 5         24 $val =~ s/\A'|'\z//g;
324 5         13 $val =~ s/''/'/g;
325 5         19 $settings->{ $elements[ $i ] } = $val;
326             }
327 3 50       20 $self->explain->settings( $settings ) if 0 < scalar keys %{ $settings };
  3         22  
328             }
329             elsif ( $line =~ m{ \A \s* Trigger \s+ (.*) : \s+ time=(\d+\.\d+) \s+ calls=(\d+) \s* \z }xmsi ) {
330 20         64 my ( $name, $time, $calls ) = ( $1, $2, $3 );
331              
332 20         26 $in_jit = undef;
333              
334 20         56 $self->explain->add_trigger_time(
335             {
336             'name' => $name,
337             'time' => $time,
338             'calls' => $calls,
339             }
340             );
341             }
342             elsif ( $line =~ m{ \A (\s*) JIT: \s* \z }xmsi ) {
343 9         24 $in_jit = 1;
344 9         29 $jit = [ $line ];
345             }
346             elsif ( $line =~ m{ \A (\s*) Planning: \s* \z }xmsi ) {
347 16         42 $in_planning = 1;
348             }
349             elsif ( $line =~ m{ \A \s* Query \s+ Text: \s+ ( .* ) \z }xms ) {
350 4         18 $query = $1;
351 4         21 $plan_started = 0;
352             }
353             elsif ( $plan_started == 0 ) {
354 35         183 $query = "$query\n$line";
355             }
356             elsif ( $line =~ m{ \A (\s*) ( \S .* \S ) \s* \z }xms ) {
357 1099         5046 my ( $infoprefix, $info ) = ( $1, $2 );
358 1099 100       2675 if ( $in_jit ) {
359 27         43 push @{ $jit }, $line;
  27         652  
360 27         74 next LINE;
361             }
362 1072         3282 my $maximal_depth = ( sort { $b <=> $a } grep { $_ < length $infoprefix } keys %element_at_depth )[ 0 ];
  3266         5373  
  3013         9529  
363 1072 100       2943 next LINE unless defined $maximal_depth;
364 1067         2078 my $previous_element = $element_at_depth{ $maximal_depth };
365 1067 50       2450 next LINE unless $previous_element;
366 1067         1935 my $node = $previous_element->{ 'node' };
367 1067 100       5593 if ( $info =~ m{ \A Workers \s+ Launched: \s+ ( \d+ ) \z }xmsi ) {
    100          
    100          
368 17         167 $node->workers_launched( $1 );
369 17         54 $node->add_extra_info( $info );
370             }
371             elsif ( $info =~ m{ \A Buffers: \s }xms ) {
372 127         249 eval {
373 127         781 my $buffers = Pg::Explain::Buffers->new( $info );
374 127 100       367 if ( $in_planning ) {
375 16         144 $planning_buffers = $buffers;
376             }
377             else {
378 111         441 $node->buffers( $buffers );
379             }
380             };
381 127 50       507 $node->add_extra_info( $info ) if $EVAL_ERROR;
382             }
383             elsif ( $info =~ m{ \A I/O \s Timings: \s }xms ) {
384 45         83 eval {
385 45 100       104 if ( $in_planning ) {
386 11 50       43 $planning_buffers->add_timing( $info ) if $planning_buffers;
387             }
388             else {
389 34 50       110 $node->buffers->add_timing( $info ) if $node->buffers;
390             }
391             };
392 45 50       193 if ( $EVAL_ERROR ) {
393 0         0 print "[[ $EVAL_ERROR ]]\n";
394 0 0       0 $node->add_extra_info( $info ) if $EVAL_ERROR;
395             }
396             }
397             else {
398 878         3386 $node->add_extra_info( $info );
399             }
400             }
401             }
402 319 100       1471 $self->explain->jit( Pg::Explain::JIT->new( 'lines' => $jit ) ) if defined $jit;
403 319 100       951 $self->explain->query( $query ) if $query;
404 319 100       1096 $self->explain->planning_buffers( $planning_buffers ) if $planning_buffers;
405 319         4070 return $top_node;
406             }
407              
408             =head1 AUTHOR
409              
410             hubert depesz lubaczewski, C<< >>
411              
412             =head1 BUGS
413              
414             Please report any bugs or feature requests to C.
415              
416             =head1 SUPPORT
417              
418             You can find documentation for this module with the perldoc command.
419              
420             perldoc Pg::Explain
421              
422             =head1 COPYRIGHT & LICENSE
423              
424             Copyright 2008-2023 hubert depesz lubaczewski, all rights reserved.
425              
426             This program is free software; you can redistribute it and/or modify it
427             under the same terms as Perl itself.
428              
429             =cut
430              
431             1; # End of Pg::Explain::FromText