File Coverage

blib/lib/Pg/Explain/Node.pm
Criterion Covered Total %
statement 445 460 96.7
branch 231 264 87.5
condition 8 11 72.7
subroutine 59 60 98.3
pod 46 46 100.0
total 789 841 93.8


line stmt bran cond sub pod time code
1             package Pg::Explain::Node;
2              
3             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
4 72     72   830 use v5.18;
  72         230  
5 72     72   337 use strict;
  72         111  
  72         1211  
6 72     72   277 use warnings;
  72         118  
  72         1769  
7 72     72   283 use warnings qw( FATAL utf8 );
  72         119  
  72         1877  
8 72     72   296 use utf8;
  72         118  
  72         394  
9 72     72   1433 use open qw( :std :utf8 );
  72         126  
  72         353  
10 72     72   8043 use Unicode::Normalize qw( NFC );
  72         127  
  72         3104  
11 72     72   438 use Unicode::Collate;
  72         125  
  72         1685  
12 72     72   346 use Encode qw( decode );
  72         142  
  72         3492  
13              
14             if ( grep /\P{ASCII}/ => @ARGV ) {
15             @ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
16             }
17              
18             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
19              
20 72     72   13043 use Clone qw( clone );
  72         123  
  72         3549  
21 72     72   30856 use HOP::Lexer qw( string_lexer );
  72         176190  
  72         3747  
22 72     72   481 use Carp;
  72         119  
  72         3483  
23              
24             # I'm reasonably sure that there are no infinite recusion paths, but in some cases the plan is just deep enough to cause Perl to
25             # issue warning about it. Since the warnings don't bring anything good to the table, let's disable them.
26 72     72   342 no warnings 'recursion';
  72         121  
  72         464578  
27              
28             =head1 NAME
29              
30             Pg::Explain::Node - Class representing single node from query plan
31              
32             =head1 VERSION
33              
34             Version 2.2
35              
36             =cut
37              
38             our $VERSION = '2.2';
39              
40             # Start counter for all node ids.
41             our $base_id = 1;
42              
43             =head1 SYNOPSIS
44              
45             Quick summary of what the module does.
46              
47             Perhaps a little code snippet.
48              
49             use Pg::Explain::Node;
50              
51             my $foo = Pg::Explain::Node->new();
52             ...
53              
54             =head1 FUNCTIONS
55              
56             =head2 id
57              
58             Unique identifier of this node in this explain. It's read-only, autoincrementing integer.
59              
60             =head2 actual_loops
61              
62             Returns number how many times current node has been executed.
63              
64             This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output.
65              
66             =head2 actual_rows
67              
68             Returns amount of rows current node returnes in single execution (i.e. if given node was executed 10 times, you have to multiply actual_rows by 10, to get full number of returned rows.
69              
70             This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output.
71              
72             =head2 actual_time_first
73              
74             Returns time (in miliseconds) how long it took PostgreSQL to return 1st row from given node.
75              
76             This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output.
77              
78             =head2 actual_time_last
79              
80             Returns time (in miliseconds) how long it took PostgreSQL to return all rows from given node. This number represents single execution of the node, so if given node was executed 10 times, you have to multiply actual_time_last by 10 to get total time of running of this node.
81              
82             This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output.
83              
84             =head2 estimated_rows
85              
86             Returns estimated number of rows to be returned from this node.
87              
88             =head2 estimated_row_width
89              
90             Returns estimated width (in bytes) of single row returned from this node.
91              
92             =head2 estimated_startup_cost
93              
94             Returns estimated cost of starting execution of given node. Some node types do not have startup cost (i.e., it is 0), but some do. For example - Seq Scan has startup cost = 0, but Sort node has
95             startup cost depending on number of rows.
96              
97             This cost is measured in units of "single-page seq scan".
98              
99             =head2 estimated_total_cost
100              
101             Returns estimated full cost of given node.
102              
103             This cost is measured in units of "single-page seq scan".
104              
105             =head2 workers_launched
106              
107             How many worker processes this node launched.
108              
109             =head2 workers
110              
111             How many workers was this node processed on. Always set to at least 1.
112              
113             =head2 type
114              
115             Textual representation of type of current node. Some types for example:
116              
117             =over
118              
119             =item * Index Scan
120              
121             =item * Index Scan Backward
122              
123             =item * Limit
124              
125             =item * Nested Loop
126              
127             =item * Nested Loop Left Join
128              
129             =item * Result
130              
131             =item * Seq Scan
132              
133             =item * Sort
134              
135             =back
136              
137             =head2 buffers
138              
139             Information about inclusive buffers usage in given node. It's either undef, or object of Pg::Explain::Buffers class.
140              
141             =cut
142              
143             =head2 scan_on
144              
145             Hashref with extra information in case of table scans.
146              
147             For Seq Scan it contains always 'table_name' key, and optionally 'table_alias' key.
148              
149             For Index Scan and Backward Index Scan, it also contains (always) 'index_name' key.
150              
151             =head2 extra_info
152              
153             ArrayRef of strings, each contains textual information (leading and tailing spaces removed) for given node.
154              
155             This is not always filled, as it depends heavily on node type and PostgreSQL version.
156              
157             =head2 sub_nodes
158              
159             ArrayRef of Pg::Explain::Node objects, which represent sub nodes.
160              
161             For more details, check ->add_sub_node method description.
162              
163             =head2 initplans
164              
165             ArrayRef of Pg::Explain::Node objects, which represent init plan.
166              
167             For more details, check ->add_initplan method description.
168              
169             =head2 initplans_metainfo
170              
171             ArrayRef of Hashrefs, where each hashref can contains:
172              
173             =over
174              
175             =item * 'name' - name of the InitPlan, generally number
176              
177             =item * 'returns' - string listing what the initplan returns. Generally a list of $X values (where X is 0 or positive integer) separated by comma.
178              
179             =back
180              
181             For more details, check ->add_initplan method description.
182              
183             =head2 subplans
184              
185             ArrayRef of Pg::Explain::Node objects, which represent sub plan.
186              
187             For more details, check ->add_subplan method description.
188              
189             =head2 ctes
190              
191             HashRef of Pg::Explain::Node objects, which represent CTE plans.
192              
193             For more details, check ->add_cte method description.
194              
195             =head2 cte_order
196              
197             ArrayRef of names of CTE nodes in given node.
198              
199             For more details, check ->add_cte method description.
200              
201             =head2 never_executed
202              
203             Returns true if given node was not executed, according to plan.
204              
205             =head2 parent
206              
207             Parent node of current node, or undef if it's top node.
208              
209             =head2 exclusive_fix
210              
211             Numeric value that will be added to total_exclusive_time. It is set by Pg::Explain::check_for_exclusive_time_fixes method once after parsing the explain.
212              
213             =cut
214              
215 1693     1693 1 6623 sub id { my $self = shift; return $self->{ 'id' }; }
  1693         4768  
216 5500 100   5500 1 6347 sub actual_loops { my $self = shift; $self->{ 'actual_loops' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_loops' }; }
  5500         8206  
  5500         14810  
217 1377 50   1377 1 2509 sub actual_rows { my $self = shift; $self->{ 'actual_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_rows' }; }
  1377         2087  
  1377         2712  
218 1199 50   1199 1 1339 sub actual_time_first { my $self = shift; $self->{ 'actual_time_first' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_first' }; }
  1199         1837  
  1199         2428  
219 2826 50   2826 1 3112 sub actual_time_last { my $self = shift; $self->{ 'actual_time_last' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_last' }; }
  2826         4278  
  2826         6078  
220 979 100   979 1 1153 sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; }
  979         1662  
  979         1857  
221 6570 100   6570 1 7049 sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; }
  6570         9225  
  6570         11873  
222 1360 50   1360 1 1547 sub estimated_rows { my $self = shift; $self->{ 'estimated_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_rows' }; }
  1360         2108  
  1360         2615  
223 1526 50   1526 1 1712 sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_row_width' }; }
  1526         2284  
  1526         5623  
224 1360 50   1360 1 1498 sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; }
  1360         2060  
  1360         3154  
225 1360 50   1360 1 1503 sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; }
  1360         2090  
  1360         2782  
226 4992 100   4992 1 5526 sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; }
  4992         8083  
  4992         10444  
227 6993 100   6993 1 7558 sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; }
  6993         9785  
  6993         12194  
228 751 100   751 1 922 sub initplans_metainfo { my $self = shift; $self->{ 'initplans_metainfo' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans_metainfo' }; }
  751         1142  
  751         1525  
229 687 100   687 1 852 sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; }
  687         2466  
  687         2398  
230 1098 100   1098 1 1373 sub parent { my $self = shift; $self->{ 'parent' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'parent' }; }
  1098         2463  
  1098         1596  
231 2876 100   2876 1 3630 sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; }
  2876         5298  
  2876         8434  
232 9915 100   9915 1 11044 sub sub_nodes { my $self = shift; $self->{ 'sub_nodes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'sub_nodes' }; }
  9915         14507  
  9915         17809  
233 5721 100   5721 1 6075 sub subplans { my $self = shift; $self->{ 'subplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'subplans' }; }
  5721         8100  
  5721         9407  
234 10909 100   10909 1 80623 sub type { my $self = shift; $self->{ 'type' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'type' }; }
  10909         16744  
  10909         28369  
235 1328 100   1328 1 1534 sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; }
  1328         2128  
  1328         2528  
236 2334 100 50 2334 1 3676 sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; }
  2334         4572  
  2334         5730  
237 1166 100   1166 1 1371 sub buffers { my $self = shift; $self->{ 'buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'buffers' }; }
  1166         1920  
  1166         2233  
238 402 100 100 402 1 501 sub exclusive_fix { my $self = shift; $self->{ 'exclusive_fix' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'exclusive_fix' } // 0; }
  402         651  
  402         1263  
239              
240             =head2 new
241              
242             Object constructor.
243              
244             =cut
245              
246             sub new {
247 1509     1509 1 2641 my $class = shift;
248 1509         4264 my $self = bless { 'id' => $base_id++ }, $class;
249              
250 1509         2180 my %args;
251 1509 50       3186 if ( 0 == scalar @_ ) {
252 0         0 croak( 'Args should be passed as either hash or hashref' );
253             }
254 1509 50       3923 if ( 1 == scalar @_ ) {
    50          
255 0 0       0 if ( 'HASH' eq ref $_[ 0 ] ) {
256 0         0 %args = @{ $_[ 0 ] };
  0         0  
257             }
258             else {
259 0         0 croak( 'Args should be passed as either hash or hashref' );
260             }
261             }
262             elsif ( 1 == ( scalar( @_ ) % 2 ) ) {
263 0         0 croak( 'Args should be passed as either hash or hashref' );
264             }
265             else {
266 1509         14259 %args = @_;
267             }
268 1509 50       3863 croak( 'type has to be passed to constructor of explain node' ) unless defined $args{ 'type' };
269              
270             # Backfill costs if they are not given from plan
271 1509         2637 for my $key ( qw( estimated_rows estimated_row_width estimated_startup_cost estimated_total_cost ) ) {
272 6036 100       10148 $args{ $key } = 0 unless defined $args{ $key };
273             }
274              
275 1509         4687 @{ $self }{ keys %args } = values %args;
  1509         7967  
276              
277 1509 100       4005 if (
    100          
    100          
    100          
    100          
    100          
    100          
278             $self->type =~ m{ \A (
279             (?: Parallel \s+ )?
280             (?: Seq \s Scan | Tid \s+ Scan | Bitmap \s+ Heap \s+ Scan | Foreign \s+ Scan | Update | Insert | Delete )
281             ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms
282             )
283             {
284 314         821 $self->type( $1 );
285 314         1357 $self->scan_on( { 'table_name' => $2, } );
286 314 100       1002 $self->scan_on->{ 'table_alias' } = $3 if defined $3;
287             }
288             elsif ( $self->type =~ m{ \A ( Bitmap \s+ Index \s+ Scan) \s on \s (\S+) \z }xms ) {
289 21         66 $self->type( $1 );
290 21         99 $self->scan_on( { 'index_name' => $2, } );
291             }
292             elsif ( $self->type =~ m{ \A ( (?: Parallel \s+ )? Index (?: \s Only )? \s Scan (?: \s Backward )? ) \s using \s (\S+) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) {
293 119         314 $self->type( $1 );
294 119         580 $self->scan_on(
295             {
296             'index_name' => $2,
297             'table_name' => $3,
298             }
299             );
300 119 100       392 $self->scan_on->{ 'table_alias' } = $4 if defined $4;
301             }
302             elsif ( $self->type =~ m{ \A ( CTE \s Scan ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) {
303 29         88 $self->type( $1 );
304 29         113 $self->scan_on( { 'cte_name' => $2, } );
305 29 100       95 $self->scan_on->{ 'cte_alias' } = $3 if defined $3;
306             }
307             elsif ( $self->type =~ m{ \A ( WorkTable \s Scan ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) {
308 3         11 $self->type( $1 );
309 3         18 $self->scan_on( { 'worktable_name' => $2, } );
310 3 50       15 $self->scan_on->{ 'worktable_alias' } = $3 if defined $3;
311             }
312             elsif ( $self->type =~ m{ \A ( Function \s Scan ) \s on \s (\S+) (?: \s+ (\S+) )? \z }xms ) {
313 11         38 $self->type( $1 );
314 11         64 $self->scan_on( { 'function_name' => $2, } );
315 11 100       66 $self->scan_on->{ 'function_alias' } = $3 if defined $3;
316             }
317             elsif ( $self->type =~ m{ \A ( Subquery \s Scan ) \s on \s (.+) \z }xms ) {
318 3         8 $self->type( $1 );
319 3         5 my $name = $2;
320 3         15 $name =~ s{\A"(.*)"\z}{$1};
321 3         11 $self->scan_on( { 'subquery_name' => $name, } );
322             }
323 1509         5339 return $self;
324             }
325              
326             =head2 explain
327              
328             Returns/sets Pg::Explain for this node.
329              
330             Also, calls $explain->node( $id, $self );
331              
332             =cut
333              
334             sub explain {
335 1509     1509 1 1977 my $self = shift;
336 1509         1765 my $explain = shift;
337 1509 50       2792 if ( defined $explain ) {
338 1509         2414 $self->{ 'explain' } = $explain;
339 1509         3249 $explain->node( $self->id, $self );
340             }
341 1509         2500 return $self->{ 'explain' };
342             }
343              
344             =head2 add_extra_info
345              
346             Adds new line of extra information to explain node.
347              
348             It will be available at $node->extra_info (returns arrayref)
349              
350             Extra_info is used by some nodes to provide additional information. For example
351             - for Sort nodes, they usually contain informtion about used memory, used sort
352             method and keys.
353              
354             =cut
355              
356             sub add_extra_info {
357 1358     1358 1 1790 my $self = shift;
358 1358 100       2422 if ( $self->extra_info ) {
359 566         724 push @{ $self->extra_info }, @_;
  566         897  
360             }
361             else {
362 792         1875 $self->extra_info( [ @_ ] );
363             }
364 1358         3388 return;
365             }
366              
367             =head2 add_trigger_time
368              
369             Adds new information about trigger time.
370              
371             It will be available at $node->trigger_times (returns arrayref)
372              
373             =cut
374              
375             sub add_trigger_time {
376 0     0 1 0 my $self = shift;
377 0 0       0 if ( $self->trigger_times ) {
378 0         0 push @{ $self->trigger_times }, @_;
  0         0  
379             }
380             else {
381 0         0 $self->trigger_times( [ @_ ] );
382             }
383 0         0 return;
384             }
385              
386             =head2 add_subplan
387              
388             Adds new subplan node.
389              
390             It will be available at $node->subplans (returns arrayref)
391              
392             Example of plan with subplan:
393              
394             # explain select *, (select oid::int4 from pg_class c2 where c2.relname = c.relname) - oid::int4 from pg_class c;
395             QUERY PLAN
396             ------------------------------------------------------------------------------------------------------
397             Seq Scan on pg_class c (cost=0.00..1885.60 rows=227 width=200)
398             SubPlan
399             -> Index Scan using pg_class_relname_nsp_index on pg_class c2 (cost=0.00..8.27 rows=1 width=4)
400             Index Cond: (relname = $0)
401             (4 rows)
402              
403              
404             =cut
405              
406             sub add_subplan {
407 35     35 1 56 my $self = shift;
408 35         76 my @nodes = map { $_->parent( $self ); $_ } @_;
  35         108  
  35         91  
409 35 100       99 if ( $self->subplans ) {
410 5         11 push @{ $self->subplans }, @nodes;
  5         13  
411             }
412             else {
413 30         93 $self->subplans( [ @nodes ] );
414             }
415 35         129 return;
416             }
417              
418             =head2 add_initplan
419              
420             Adds new initplan node.
421              
422             Expects to get node object and hashred with metainformation.
423              
424             It will be available at $node->initplans (returns arrayref) and $node->initplans_metainfo (also arrayref);
425              
426             Example of plan with initplan:
427              
428             # explain analyze select 1 = (select 1);
429             QUERY PLAN
430             --------------------------------------------------------------------------------------------
431             Result (cost=0.01..0.02 rows=1 width=0) (actual time=0.033..0.035 rows=1 loops=1)
432             InitPlan
433             -> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.003..0.005 rows=1 loops=1)
434             Total runtime: 0.234 ms
435             (4 rows)
436              
437             =cut
438              
439             sub add_initplan {
440 37     37 1 73 my $self = shift;
441 37         94 my ( $node, $node_info ) = @_;
442              
443 37 100       121 $self->initplans( [] ) unless $self->initplans;
444 37 100       100 $self->initplans_metainfo( [] ) unless $self->initplans_metainfo;
445              
446 37         105 $node->parent( $self );
447 37         52 push @{ $self->initplans }, $node;
  37         75  
448 37         118 push @{ $self->initplans_metainfo }, $node_info;
  37         74  
449 37         130 return;
450             }
451              
452             =head2 add_cte
453              
454             Adds new cte node. CTE has to be named, so this function requires 2 arguments: name, and cte object itself.
455              
456             It will be available at $node->cte( name ), or $node->ctes (returns hashref).
457              
458             Since we need order (ctes are stored unordered, in hash), there is also $node->cte_order() which returns arrayref of names.
459              
460             =cut
461              
462             sub add_cte {
463 32     32 1 55 my $self = shift;
464 32         89 my ( $name, $cte ) = @_;
465 32         104 $cte->parent( $self );
466              
467 32 100       90 if ( $self->ctes ) {
468 9         22 $self->ctes->{ $name } = $cte;
469 9         13 push @{ $self->cte_order }, $name;
  9         15  
470             }
471             else {
472 23         115 $self->ctes( { $name => $cte } );
473 23         82 $self->cte_order( [ $name ] );
474             }
475 32         75 return;
476             }
477              
478             =head2 cte
479              
480             Returns CTE object that has given name.
481              
482             =cut
483              
484             sub cte {
485 11     11 1 19 my $self = shift;
486 11         18 my $name = shift;
487 11         25 return $self->ctes->{ $name };
488             }
489              
490             =head2 add_sub_node
491              
492             Adds new sub node.
493              
494             It will be available at $node->sub_nodes (returns arrayref)
495              
496             Sub nodes are nodes that are used by given node as data sources.
497              
498             For example - "Join" node, has 2 sources (sub_nodes), which are table scans (Seq Scan, Index Scan or Backward Index Scan) over some tables.
499              
500             Example plan which contains subnode:
501              
502             # explain select * from test limit 1;
503             QUERY PLAN
504             --------------------------------------------------------------
505             Limit (cost=0.00..0.01 rows=1 width=4)
506             -> Seq Scan on test (cost=0.00..14.00 rows=1000 width=4)
507             (2 rows)
508              
509             Node 'Limit' has 1 sub_plan, which is "Seq Scan"
510              
511             =cut
512              
513             sub add_sub_node {
514 895     895 1 1349 my $self = shift;
515 895         1626 my @nodes = map { $_->parent( $self ); $_ } @_;
  895         2240  
  895         2133  
516 895 100       1884 if ( $self->sub_nodes ) {
517 247         378 push @{ $self->sub_nodes }, @nodes;
  247         441  
518             }
519             else {
520 648         1442 $self->sub_nodes( [ @nodes ] );
521             }
522 895         2740 return;
523             }
524              
525             =head2 get_struct
526              
527             Function which returns simple, not blessed, hashref with all information about given explain node and it's children.
528              
529             This can be used for debug purposes, or as a base to print information to user.
530              
531             Output looks like this:
532              
533             {
534             'estimated_rows' => '10000',
535             'estimated_row_width' => '148',
536             'estimated_startup_cost' => '0',
537             'estimated_total_cost' => '333',
538             'scan_on' => { 'table_name' => 'tenk1', },
539             'type' => 'Seq Scan',
540             }
541              
542             =cut
543              
544             sub get_struct {
545 539     539 1 960 my $self = shift;
546 539         725 my $reply = {};
547              
548 539 50       940 $reply->{ 'estimated_row_width' } = $self->estimated_row_width if defined $self->estimated_row_width;
549 539 50       922 $reply->{ 'estimated_rows' } = $self->estimated_rows if defined $self->estimated_rows;
550 539 50       910 $reply->{ 'estimated_startup_cost' } = 0 + $self->estimated_startup_cost if defined $self->estimated_startup_cost; # "0+" to remove .00 in case of integers
551 539 50       1010 $reply->{ 'estimated_total_cost' } = 0 + $self->estimated_total_cost if defined $self->estimated_total_cost; # "0+" to remove .00 in case of integers
552 539 100       870 $reply->{ 'actual_loops' } = $self->actual_loops if defined $self->actual_loops;
553 539 100       923 $reply->{ 'actual_rows' } = $self->actual_rows if defined $self->actual_rows;
554 539 100       849 $reply->{ 'actual_time_first' } = 0 + $self->actual_time_first if defined $self->actual_time_first; # "0+" to remove .00 in case of integers
555 539 100       892 $reply->{ 'actual_time_last' } = 0 + $self->actual_time_last if defined $self->actual_time_last; # "0+" to remove .00 in case of integers
556 539 50       963 $reply->{ 'type' } = $self->type if defined $self->type;
557 539 100       887 $reply->{ 'scan_on' } = clone( $self->scan_on ) if defined $self->scan_on;
558 539 100       1082 $reply->{ 'extra_info' } = clone( $self->extra_info ) if defined $self->extra_info;
559 539 100       938 $reply->{ 'initplans_metainfo' } = clone( $self->initplans_metainfo ) if defined $self->initplans_metainfo;
560              
561 539         807 $reply->{ 'is_analyzed' } = $self->is_analyzed;
562              
563 539 100       904 $reply->{ 'sub_nodes' } = [ map { $_->get_struct } @{ $self->sub_nodes } ] if defined $self->sub_nodes;
  292         655  
  234         325  
564 539 100       898 $reply->{ 'initplans' } = [ map { $_->get_struct } @{ $self->initplans } ] if defined $self->initplans;
  33         86  
  30         51  
565 539 100       791 $reply->{ 'subplans' } = [ map { $_->get_struct } @{ $self->subplans } ] if defined $self->subplans;
  24         48  
  21         39  
566              
567 539 100       820 $reply->{ 'buffers' } = $self->buffers->get_struct() if $self->buffers;
568              
569 539 100       865 $reply->{ 'cte_order' } = clone( $self->cte_order ) if defined $self->cte_order;
570 539 100       831 if ( defined $self->ctes ) {
571 9         18 $reply->{ 'ctes' } = {};
572 9         17 while ( my ( $key, $cte_node ) = each %{ $self->ctes } ) {
  27         39  
573 18         31 my $struct = $cte_node->get_struct;
574 18         46 $reply->{ 'ctes' }->{ $key } = $struct;
575             }
576             }
577 539         1352 return $reply;
578             }
579              
580             =head2 total_inclusive_time
581              
582             Method for getting total node time, summarized with times of all subnodes, subplans and initplans - which is basically ->actual_loops * ->actual_time_last.
583              
584             =cut
585              
586             sub total_inclusive_time {
587 614     614 1 699 my $self = shift;
588 614 100       869 return unless defined $self->actual_time_last;
589 610 50       936 return unless defined $self->actual_loops;
590 610         860 return $self->actual_loops * $self->actual_time_last / $self->workers;
591             }
592              
593             =head2 total_rows
594              
595             Method for getting total number of rows returned by current node. This takes into account parallelization and multiple loops.
596              
597             =cut
598              
599             sub total_rows {
600 167     167 1 227 my $self = shift;
601 167 50       254 return unless defined $self->actual_time_last;
602 167 50       242 return unless defined $self->actual_loops;
603 167 100       252 return $self->actual_loops * $self->actual_rows if 1 == $self->workers;
604 1         3 return $self->workers * $self->actual_rows;
605             }
606              
607             =head2 total_rows_removed
608              
609             Sum of rows removed by:
610              
611             =over
612              
613             =item * Conflict Filter
614              
615             =item * Filter
616              
617             =item * Index Recheck
618              
619             =item * Join Filter
620              
621             =back
622              
623             in given node.
624              
625             =cut
626              
627             sub total_rows_removed {
628 251     251 1 281 my $self = shift;
629 251 100       349 return 0 unless $self->extra_info;
630 249         285 my $removed = 0;
631 249         247 for my $line ( @{ $self->extra_info } ) {
  249         322  
632 502 100       1132 next unless $line =~ m{^Rows Removed by (?:Conflict Filter|Filter|Index Recheck|Join Filter): (\d+)$};
633 250         521 $removed += $1;
634             }
635 249 100       368 return $self->actual_loops * $removed if 1 == $self->workers;
636 1         5 return $self->workers * $removed;
637             }
638              
639             =head2 total_exclusive_time
640              
641             Method for getting total node time, without times of subnodes - which amounts to time PostgreSQL spent running this paricular node.
642              
643             =cut
644              
645             sub total_exclusive_time {
646 251     251 1 452 my $self = shift;
647              
648 251         409 my $time = $self->total_inclusive_time;
649 251 100       453 return unless defined $time;
650              
651 250         394 for my $node ( map { @{ $_ } } grep { defined $_ } ( $self->sub_nodes ) ) {
  63         79  
  63         184  
  250         521  
652 84   50     126 $time -= ( $node->total_inclusive_time || 0 );
653             }
654              
655 250         439 for my $plan ( map { @{ $_ } } grep { defined $_ } ( $self->subplans ) ) {
  33         46  
  33         85  
  250         448  
656 34   100     70 $time -= ( $plan->total_inclusive_time || 0 );
657             }
658              
659             # Apply fix from ->exclusive_fix
660 250         435 $time += $self->exclusive_fix;
661              
662             # ignore negative times - these come from rounding errors on nodes with loops > 1.
663 250 100       428 return 0 if $time < 0;
664              
665 249         767 return $time;
666             }
667              
668             =head2 total_exclusive_buffers
669              
670             Method for getting total buffers used by node, without buffers used by subnodes.
671              
672             =cut
673              
674             sub total_exclusive_buffers {
675 3     3 1 5 my $self = shift;
676              
677 3 50       8 return unless $self->buffers;
678              
679 3         9 my @nodes = grep { $_->buffers } $self->all_subnodes;
  4         9  
680 3 50       9 return $self->buffers if 0 == scalar @nodes;
681              
682 3         40 my $sub_node_buffers = $nodes[ 0 ]->buffers;
683 3         4 shift @nodes;
684 3         8 for my $n ( @nodes ) {
685 1         10 $sub_node_buffers = $sub_node_buffers + $n->buffers;
686             }
687              
688 3         7 return $self->buffers - $sub_node_buffers;
689             }
690              
691             =head2 all_subnodes
692              
693             Returns list of all subnodes of current node.
694              
695             =cut
696              
697             sub all_subnodes {
698 57     57 1 71 my $self = shift;
699 57         66 my @nodes = ();
700 57 100       88 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  29         45  
701 57 100       94 push @nodes, @{ $self->initplans } if $self->initplans;
  2         5  
702 57 100       87 push @nodes, @{ $self->subplans } if $self->subplans;
  4         9  
703 57 50       127 push @nodes, values %{ $self->ctes } if $self->ctes;
  0         0  
704 57         140 return @nodes;
705             }
706              
707             =head2 all_recursive_subnodes
708              
709             Returns list of all subnodes of current node and its subnodes, and their subnodes, and ...
710              
711             =cut
712              
713             sub all_recursive_subnodes {
714 3124     3124 1 3557 my $self = shift;
715 3124         3560 my @nodes = ();
716 3124 100       3941 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  1308         1739  
717 3124 100       4297 push @nodes, @{ $self->initplans } if $self->initplans;
  113         160  
718 3124 100       4129 push @nodes, @{ $self->subplans } if $self->subplans;
  60         100  
719 3124 100       4108 push @nodes, values %{ $self->ctes } if $self->ctes;
  75         128  
720              
721 3124         5508 return map { $_, $_->all_recursive_subnodes } @nodes;
  2156         3416  
722             }
723              
724             =head2 all_parents
725              
726             Returns list of all nodes that are "above" given node in explain.
727              
728             List can be empty if it's top level node.
729              
730             =cut
731              
732             sub all_parents {
733 20     20 1 10700 my $self = shift;
734 20         26 my @nodes = ();
735 20         26 my $current = $self;
736 20         35 while ( my $next = $current->parent ) {
737 79         85 unshift @nodes, $next;
738 79         104 $current = $next;
739             }
740 20         38 return @nodes;
741             }
742              
743             =head2 is_analyzed
744              
745             Returns 1 if the explain node it represents was generated by EXPLAIN ANALYZE. 0 otherwise.
746              
747             =cut
748              
749             sub is_analyzed {
750 2402     2402 1 3006 my $self = shift;
751              
752 2402 100 66     3967 return defined $self->actual_loops || $self->never_executed ? 1 : 0;
753             }
754              
755             =head2 as_text
756              
757             Returns textual representation of explain nodes from given node down.
758              
759             This is used to build textual explains out of in-memory data structures.
760              
761             =cut
762              
763             sub as_text {
764 282     282 1 447 my $self = shift;
765 282         374 my $prefix = shift;
766 282 100       625 $prefix = '' unless defined $prefix;
767              
768 282 100       620 $prefix .= '-> ' if '' ne $prefix;
769 282         465 my $prefix_on_spaces = $prefix . " ";
770 282         1150 $prefix_on_spaces =~ s/[^ ]/ /g;
771              
772 282         673 my $heading_line = $self->type;
773              
774 282 100       528 if ( $self->scan_on ) {
775 140         238 my $S = $self->scan_on;
776 140 100       602 if ( $S->{ 'cte_name' } ) {
    100          
    100          
    100          
    100          
777 11         33 $heading_line .= " on " . $S->{ 'cte_name' };
778 11 100       55 $heading_line .= " " . $S->{ 'cte_alias' } if $S->{ 'cte_alias' };
779             }
780             elsif ( $S->{ 'function_name' } ) {
781 3         13 $heading_line .= " on " . $S->{ 'function_name' };
782 3 50       13 $heading_line .= " " . $S->{ 'function_alias' } if $S->{ 'function_alias' };
783             }
784             elsif ( $S->{ 'index_name' } ) {
785 42 100       85 if ( $S->{ 'table_name' } ) {
786 34         91 $heading_line .= " using " . $S->{ 'index_name' } . " on " . $S->{ 'table_name' };
787 34 100       99 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
788             }
789             else {
790 8         36 $heading_line .= " on " . $S->{ 'index_name' };
791             }
792             }
793             elsif ( $S->{ 'subquery_name' } ) {
794 1         4 $heading_line .= " on " . $S->{ 'subquery_name' },;
795             }
796             elsif ( $S->{ 'worktable_name' } ) {
797 4         12 $heading_line .= " on " . $S->{ 'worktable_name' },;
798 4 50       22 $heading_line .= " " . $S->{ 'worktable_alias' } if $S->{ 'worktable_alias' };
799             }
800             else {
801 79         191 $heading_line .= " on " . $S->{ 'table_name' };
802 79 100       264 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
803             }
804             }
805 282         556 $heading_line .= sprintf ' (cost=%.2f..%.2f rows=%s width=%d)', $self->estimated_startup_cost, $self->estimated_total_cost, $self->estimated_rows, $self->estimated_row_width;
806 282 100       829 if ( $self->is_analyzed ) {
807 231         302 my $inner;
808 231 100       579 if ( $self->never_executed ) {
    100          
809 6         10 $inner = 'never executed';
810             }
811             elsif ( defined $self->actual_time_last ) {
812 217         374 $inner = sprintf 'actual time=%.3f..%.3f rows=%s loops=%d', $self->actual_time_first, $self->actual_time_last, $self->actual_rows, $self->actual_loops;
813             }
814             else {
815 8         13 $inner = sprintf 'actual rows=%s loops=%d', $self->actual_rows, $self->actual_loops;
816             }
817 231         699 $heading_line .= " ($inner)";
818             }
819              
820 282         496 my @lines = ();
821              
822 282         574 push @lines, $prefix . $heading_line;
823 282 100       476 if ( $self->extra_info ) {
824 151         189 push @lines, $prefix_on_spaces . " " . $_ for @{ $self->extra_info };
  151         232  
825             }
826 282         856 my $textual = join( "\n", @lines ) . "\n";
827 282 100       578 if ( $self->buffers ) {
828 62         112 my $buf_info = $self->buffers->as_text;
829 62         286 $buf_info =~ s/^/${prefix_on_spaces}/gm;
830 62         226 $textual .= $buf_info . "\n";
831             }
832              
833 282 100       535 if ( $self->cte_order ) {
834 9         16 for my $cte_name ( @{ $self->cte_order } ) {
  9         19  
835 11         62 $textual .= $prefix_on_spaces . "CTE " . $cte_name . "\n";
836 11         38 $textual .= $self->cte( $cte_name )->as_text( $prefix_on_spaces . " " );
837             }
838             }
839              
840 282 100       481 if ( $self->initplans ) {
841 11         17 for my $i ( 0 .. $#{ $self->initplans } ) {
  11         25  
842 12         20 my $ip = $self->initplans->[ $i ];
843 12         21 my $meta = $self->initplans_metainfo->[ $i ];
844 12         19 my $init_name;
845 12 100       23 if ( $meta ) {
846 8         34 $init_name = sprintf "InitPlan %d (returns %s)\n", $meta->{ 'name' }, $meta->{ 'returns' };
847             }
848             else {
849 4         5 $init_name = "InitPlan\n";
850             }
851 12         43 $textual .= $prefix_on_spaces . $init_name;
852 12         57 $textual .= $ip->as_text( $prefix_on_spaces . " " );
853             }
854             }
855 282 100       505 if ( $self->sub_nodes ) {
856 133         169 $textual .= $_->as_text( $prefix_on_spaces ) for @{ $self->sub_nodes };
  133         203  
857             }
858 282 100       512 if ( $self->subplans ) {
859 6         7 for my $ip ( @{ $self->subplans } ) {
  6         86  
860 7         22 $textual .= $prefix_on_spaces . "SubPlan\n";
861 7         104 $textual .= $ip->as_text( $prefix_on_spaces . " " );
862             }
863             }
864 282         1036 return $textual;
865             }
866              
867             =head2 anonymize_gathering
868              
869             First stage of anonymization - gathering of all possible strings that could and should be anonymized.
870              
871             =cut
872              
873             sub anonymize_gathering {
874 52     52 1 89 my $self = shift;
875 52         73 my $anonymizer = shift;
876              
877 52 100       102 if ( $self->scan_on ) {
878 26         49 $anonymizer->add( values %{ $self->scan_on } );
  26         48  
879             }
880              
881 52 100       127 if ( $self->cte_order ) {
882 2         7 $anonymizer->add( $self->{ 'cte_order' } );
883             }
884              
885 52 100       108 if ( $self->extra_info ) {
886 40         68 for my $line ( @{ $self->extra_info } ) {
  40         79  
887 83         9220 my $copy = $line;
888 83 100       238 if ( $copy =~ m{^Foreign File:\s+(\S.*?)\s*$} ) {
889 3         8 $anonymizer->add( $1 );
890 3         5 next;
891             }
892 80 100       614 next unless $copy =~ s{^((?:Join Filter|Index Cond|Recheck Cond|Hash Cond|Merge Cond|Filter|Group Key|Sort Key|Output|One-Time Filter):\s+)(.*)$}{$2};
893 44         109 my $prefix = $1;
894 44         107 my $lexer = $self->_make_lexer( $copy );
895 44         33494 while ( my $x = $lexer->() ) {
896 1867 50       551567 next unless ref $x;
897 1867 100       6590 $anonymizer->add( $x->[ 1 ] ) if $x->[ 0 ] =~ m{\A (?: STRING_LITERAL | QUOTED_IDENTIFIER | IDENTIFIER ) \z}x;
898             }
899             }
900             }
901              
902 52         4187 for my $key ( qw( sub_nodes initplans subplans ) ) {
903 156 100       326 next unless $self->{ $key };
904 30         69 $_->anonymize_gathering( $anonymizer ) for @{ $self->{ $key } };
  30         169  
905             }
906              
907 52 100       138 if ( $self->{ 'ctes' } ) {
908 2         5 $_->anonymize_gathering( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         13  
909             }
910 52         130 return;
911             }
912              
913             =head2 _make_lexer
914              
915             Helper function which creates HOP::Lexer based lexer for given line of input
916              
917             =cut
918              
919             sub _make_lexer {
920 88     88   111 my $self = shift;
921 88         114 my $data = shift;
922              
923             ## Got from PostgreSQL 9.2devel with:
924             # SQL # with z as (
925             # SQL # select
926             # SQL # typname::text as a,
927             # SQL # oid::regtype::text as b
928             # SQL # from
929             # SQL # pg_type
930             # SQL # where
931             # SQL # typrelid = 0
932             # SQL # and typnamespace = 11
933             # SQL # ),
934             # SQL # d as (
935             # SQL # select a from z
936             # SQL # union
937             # SQL # select b from z
938             # SQL # ),
939             # SQL # f as (
940             # SQL # select distinct
941             # SQL # regexp_replace(
942             # SQL # regexp_replace(
943             # SQL # regexp_replace( a, '^_', '' ),
944             # SQL # E'\\[\\]$',
945             # SQL # ''
946             # SQL # ),
947             # SQL # '^"(.*)"$',
948             # SQL # E'\\1'
949             # SQL # ) as t
950             # SQL # from
951             # SQL # d
952             # SQL # )
953             # SQL # select
954             # SQL # t
955             # SQL # from
956             # SQL # f
957             # SQL # order by
958             # SQL # length(t) desc,
959             # SQL # t asc;
960              
961             # Following regexp was generated by feeding list from above query to:
962             # use Regexp::List;
963             # my $q = Regexp::List->new();
964             # print = $q->list2re( @_ );
965             # It is faster than normal alternative regexp like:
966             # (?:timestamp without time zone|timestamp with time zone|time without time zone|....|xid|xml)
967 88         242 my $any_pgtype =
968             qr{(?-xism:(?=[abcdfgilmnoprstuvx])(?:t(?:i(?:me(?:stamp(?:\ with(?:out)?\ time\ zone|tz)?|\ with(?:out)?\ time\ zone|tz)?|nterval|d)|s(?:(?:tz)?range|vector|query)|(?:xid_snapsho|ex)t|rigger)|c(?:har(?:acter(?:\ varying)?)?|i(?:dr?|rcle)|string)|d(?:ate(?:range)?|ouble\ precision)|l(?:anguage_handler|ine|seg)|re(?:g(?:proc(?:edure)?|oper(?:ator)?|c(?:onfig|lass)|dictionary|type)|fcursor|ltime|cord|al)|p(?:o(?:lygon|int)|g_node_tree|ath)|a(?:ny(?:e(?:lement|num)|(?:non)?array|range)?|bstime|clitem)|b(?:i(?:t(?:\ varying)?|gint)|o(?:ol(?:ean)?|x)|pchar|ytea)|f(?:loat[48]|dw_handler)|in(?:t(?:2(?:vector)?|4(?:range)?|8(?:range)?|e(?:r[nv]al|ger))|et)|o(?:id(?:vector)?|paque)|n(?:um(?:range|eric)|ame)|sm(?:allint|gr)|m(?:acaddr|oney)|u(?:nknown|uid)|v(?:ar(?:char|bit)|oid)|x(?:id|ml)|gtsvector))};
969              
970 88         8883 my @input_tokens = (
971             [ 'STRING_LITERAL', qr{'(?:''|[^']+)+'} ],
972             [ 'PGTYPECAST', qr{::"?_?$any_pgtype"?(?:\[\])?} ],
973             [ 'QUOTED_IDENTIFIER', qr{"(?:""|[^"]+)+"} ],
974             [ 'AND', qr{\bAND\b}i ],
975             [ 'ANY', qr{\bANY\b}i ],
976             [ 'ARRAY', qr{\bARRAY\b}i ],
977             [ 'AS', qr{\bAS\b}i ],
978             [ 'ASC', qr{\bASC\b}i ],
979             [ 'CASE', qr{\bCASE\b}i ],
980             [ 'CAST', qr{\bCAST\b}i ],
981             [ 'CHECK', qr{\bCHECK\b}i ],
982             [ 'COLLATE', qr{\bCOLLATE\b}i ],
983             [ 'COLUMN', qr{\bCOLUMN\b}i ],
984             [ 'CURRENT_CATALOG', qr{\bCURRENT_CATALOG\b}i ],
985             [ 'CURRENT_DATE', qr{\bCURRENT_DATE\b}i ],
986             [ 'CURRENT_ROLE', qr{\bCURRENT_ROLE\b}i ],
987             [ 'CURRENT_TIME', qr{\bCURRENT_TIME\b}i ],
988             [ 'CURRENT_TIMESTAMP', qr{\bCURRENT_TIMESTAMP\b}i ],
989             [ 'CURRENT_USER', qr{\bCURRENT_USER\b}i ],
990             [ 'DEFAULT', qr{\bDEFAULT\b}i ],
991             [ 'DESC', qr{\bDESC\b}i ],
992             [ 'DISTINCT', qr{\bDISTINCT\b}i ],
993             [ 'DO', qr{\bDO\b}i ],
994             [ 'ELSE', qr{\bELSE\b}i ],
995             [ 'END', qr{\bEND\b}i ],
996             [ 'EXCEPT', qr{\bEXCEPT\b}i ],
997             [ 'FALSE', qr{\bFALSE\b}i ],
998             [ 'FETCH', qr{\bFETCH\b}i ],
999             [ 'FOR', qr{\bFOR\b}i ],
1000             [ 'FOREIGN', qr{\bFOREIGN\b}i ],
1001             [ 'FROM', qr{\bFROM\b}i ],
1002             [ 'IN', qr{\bIN\b}i ],
1003             [ 'INITIALLY', qr{\bINITIALLY\b}i ],
1004             [ 'INTERSECT', qr{\bINTERSECT\b}i ],
1005             [ 'INTO', qr{\bINTO\b}i ],
1006             [ 'LEADING', qr{\bLEADING\b}i ],
1007             [ 'LIMIT', qr{\bLIMIT\b}i ],
1008             [ 'LOCALTIME', qr{\bLOCALTIME\b}i ],
1009             [ 'LOCALTIMESTAMP', qr{\bLOCALTIMESTAMP\b}i ],
1010             [ 'NOT', qr{\bNOT\b}i ],
1011             [ 'NULL', qr{\bNULL\b}i ],
1012             [ 'OFFSET', qr{\bOFFSET\b}i ],
1013             [ 'ON', qr{\bON\b}i ],
1014             [ 'ONLY', qr{\bONLY\b}i ],
1015             [ 'OR', qr{\bOR\b}i ],
1016             [ 'ORDER', qr{\bORDER\b}i ],
1017             [ 'PLACING', qr{\bPLACING\b}i ],
1018             [ 'PRIMARY', qr{\bPRIMARY\b}i ],
1019             [ 'REFERENCES', qr{\bREFERENCES\b}i ],
1020             [ 'RETURNING', qr{\bRETURNING\b}i ],
1021             [ 'SESSION_USER', qr{\bSESSION_USER\b}i ],
1022             [ 'SOME', qr{\bSOME\b}i ],
1023             [ 'SYMMETRIC', qr{\bSYMMETRIC\b}i ],
1024             [ 'THEN', qr{\bTHEN\b}i ],
1025             [ 'TO', qr{\bTO\b}i ],
1026             [ 'TRAILING', qr{\bTRAILING\b}i ],
1027             [ 'TRUE', qr{\bTRUE\b}i ],
1028             [ 'UNION', qr{\bUNION\b}i ],
1029             [ 'UNIQUE', qr{\bUNIQUE\b}i ],
1030             [ 'USER', qr{\bUSER\b}i ],
1031             [ 'USING', qr{\bUSING\b}i ],
1032             [ 'WHEN', qr{\bWHEN\b}i ],
1033             [ 'WHERE', qr{\bWHERE\b}i ],
1034             [ 'CAST:', qr{::}i ],
1035             [ 'COMMA', qr{,}i ],
1036             [ 'DOT', qr{\.}i ],
1037             [ 'LEFT_PARENTHESIS', qr{\(}i ],
1038             [ 'RIGHT_PARENTHESIS', qr{\)}i ],
1039             [ 'DOT', qr{\.}i ],
1040             [ 'STAR', qr{[*]} ],
1041             [ 'OP', qr{[+=/<>!~@-]} ],
1042             [ 'NUM', qr{-?(?:\d*\.\d+|\d+)} ],
1043             [ 'IDENTIFIER', qr{[a-z_][a-z0-9_]*}i ],
1044             [ 'SPACE', qr{\s+} ],
1045             );
1046 88         492 return string_lexer( $data, @input_tokens );
1047             }
1048              
1049             =head2 anonymize_substitute
1050              
1051             Second stage of anonymization - actual changing strings into anonymized versions.
1052              
1053             =cut
1054              
1055             sub anonymize_substitute {
1056 52     52 1 87 my $self = shift;
1057 52         70 my $anonymizer = shift;
1058              
1059 52 100       132 if ( $self->scan_on ) {
1060 26         49 while ( my ( $key, $value ) = each %{ $self->scan_on } ) {
  67         102  
1061 41         234 $self->scan_on->{ $key } = $anonymizer->anonymized( $value );
1062             }
1063             }
1064              
1065 52 100       135 if ( $self->cte_order ) {
1066 2         5 my @new_order = ();
1067 2         3 for my $cte_name ( @{ $self->cte_order } ) {
  2         5  
1068 2         8 my $new_name = $anonymizer->anonymized( $cte_name );
1069 2         6 push @new_order, $new_name;
1070 2         7 $self->ctes->{ $new_name } = delete $self->{ 'ctes' }->{ $cte_name };
1071             }
1072 2         7 $self->cte_order( \@new_order );
1073             }
1074              
1075 52 100       110 if ( $self->extra_info ) {
1076 40         63 my @new_extra_info = ();
1077 40         49 for my $line ( @{ $self->extra_info } ) {
  40         62  
1078 83 100       243 if ( $line =~ m{^(Foreign File:\s+)(\S.*?)(\s*)$} ) {
1079 3         8 push @new_extra_info, $1 . $anonymizer->anonymized( $2 ) . $3;
1080 3         7 next;
1081             }
1082 80 100       581 unless ( $line =~ s{^((?:Join Filter|Index Cond|Recheck Cond|Hash Cond|Merge Cond|Filter|Group Key|Sort Key|Output|One-Time Filter):\s+)(.*)$}{$2} ) {
1083 36         65 push @new_extra_info, $line;
1084 36         168 next;
1085             }
1086 44         116 my $output = $1;
1087 44         106 my $lexer = $self->_make_lexer( $line );
1088 44         30816 while ( my $x = $lexer->() ) {
1089 1867 50       553321 if ( ref $x ) {
1090 1867 100       4111 if ( $x->[ 0 ] eq 'STRING_LITERAL' ) {
    50          
    100          
1091 17         71 $output .= "'" . $anonymizer->anonymized( $x->[ 1 ] ) . "'";
1092             }
1093             elsif ( $x->[ 0 ] eq 'QUOTED_IDENTIFIER' ) {
1094 0         0 $output .= '"' . $anonymizer->anonymized( $x->[ 1 ] ) . '"';
1095             }
1096             elsif ( $x->[ 0 ] eq 'IDENTIFIER' ) {
1097 604         1547 $output .= $anonymizer->anonymized( $x->[ 1 ] );
1098             }
1099             else {
1100 1246         2606 $output .= $x->[ 1 ];
1101             }
1102             }
1103             else {
1104 0         0 $output .= $x;
1105             }
1106             }
1107 44         13069 push @new_extra_info, $output;
1108             }
1109 40         131 $self->{ 'extra_info' } = \@new_extra_info;
1110             }
1111              
1112 52         126 for my $key ( qw( sub_nodes initplans subplans ) ) {
1113 156 100       301 next unless $self->{ $key };
1114 30         43 $_->anonymize_substitute( $anonymizer ) for @{ $self->{ $key } };
  30         204  
1115             }
1116              
1117 52 100       115 if ( $self->{ 'ctes' } ) {
1118 2         6 $_->anonymize_substitute( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         21  
1119             }
1120 52         145 return;
1121             }
1122              
1123             =head1 AUTHOR
1124              
1125             hubert depesz lubaczewski, C<< >>
1126              
1127             =head1 BUGS
1128              
1129             Please report any bugs or feature requests to C.
1130              
1131             =head1 SUPPORT
1132              
1133             You can find documentation for this module with the perldoc command.
1134              
1135             perldoc Pg::Explain::Node
1136              
1137             =head1 COPYRIGHT & LICENSE
1138              
1139             Copyright 2008-2021 hubert depesz lubaczewski, all rights reserved.
1140              
1141             This program is free software; you can redistribute it and/or modify it
1142             under the same terms as Perl itself.
1143              
1144             =cut
1145              
1146             1; # End of Pg::Explain::Node