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 74     74   944 use v5.18;
  74         262  
5 74     74   411 use strict;
  74         166  
  74         1518  
6 74     74   373 use warnings;
  74         183  
  74         2153  
7 74     74   372 use warnings qw( FATAL utf8 );
  74         152  
  74         2191  
8 74     74   420 use utf8;
  74         155  
  74         427  
9 74     74   1850 use open qw( :std :utf8 );
  74         147  
  74         366  
10 74     74   9281 use Unicode::Normalize qw( NFC );
  74         180  
  74         3602  
11 74     74   449 use Unicode::Collate;
  74         176  
  74         2185  
12 74     74   397 use Encode qw( decode );
  74         173  
  74         4092  
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 74     74   15085 use Clone qw( clone );
  74         153  
  74         4127  
21 74     74   36478 use HOP::Lexer qw( string_lexer );
  74         216894  
  74         4529  
22 74     74   547 use Carp;
  74         174  
  74         3989  
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 74     74   423 no warnings 'recursion';
  74         156  
  74         578491  
27              
28             =head1 NAME
29              
30             Pg::Explain::Node - Class representing single node from query plan
31              
32             =head1 VERSION
33              
34             Version 2.4
35              
36             =cut
37              
38             our $VERSION = '2.4';
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 1700     1700 1 7883 sub id { my $self = shift; return $self->{ 'id' }; }
  1700         5128  
216 5526 100   5526 1 7343 sub actual_loops { my $self = shift; $self->{ 'actual_loops' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_loops' }; }
  5526         10166  
  5526         17996  
217 1387 50   1387 1 3030 sub actual_rows { my $self = shift; $self->{ 'actual_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_rows' }; }
  1387         2548  
  1387         3302  
218 1209 50   1209 1 1580 sub actual_time_first { my $self = shift; $self->{ 'actual_time_first' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_first' }; }
  1209         2131  
  1209         2941  
219 2846 50   2846 1 3756 sub actual_time_last { my $self = shift; $self->{ 'actual_time_last' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_last' }; }
  2846         4842  
  2846         7464  
220 999 100   999 1 1461 sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; }
  999         1919  
  999         2205  
221 6598 100   6598 1 8806 sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; }
  6598         10990  
  6598         14276  
222 1370 50   1370 1 1852 sub estimated_rows { my $self = shift; $self->{ 'estimated_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_rows' }; }
  1370         2486  
  1370         3221  
223 1536 50   1536 1 2090 sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_row_width' }; }
  1536         2807  
  1536         6854  
224 1370 50   1370 1 1876 sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; }
  1370         2383  
  1370         3834  
225 1370 50   1370 1 1966 sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; }
  1370         2492  
  1370         3520  
226 5054 100   5054 1 6820 sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; }
  5054         9849  
  5054         12581  
227 7031 100   7031 1 9326 sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; }
  7031         12100  
  7031         14702  
228 751 100   751 1 1065 sub initplans_metainfo { my $self = shift; $self->{ 'initplans_metainfo' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans_metainfo' }; }
  751         1383  
  751         1873  
229 697 100   697 1 1044 sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; }
  697         1372  
  697         2390  
230 1103 100   1103 1 1597 sub parent { my $self = shift; $self->{ 'parent' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'parent' }; }
  1103         2811  
  1103         1849  
231 2917 100   2917 1 4335 sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; }
  2917         6179  
  2917         10224  
232 9974 100   9974 1 13017 sub sub_nodes { my $self = shift; $self->{ 'sub_nodes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'sub_nodes' }; }
  9974         17732  
  9974         21446  
233 5752 100   5752 1 7435 sub subplans { my $self = shift; $self->{ 'subplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'subplans' }; }
  5752         10100  
  5752         11054  
234 10955 100   10955 1 115439 sub type { my $self = shift; $self->{ 'type' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'type' }; }
  10955         20355  
  10955         33871  
235 1335 100   1335 1 1984 sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; }
  1335         2875  
  1335         3027  
236 2341 100 50 2341 1 4210 sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; }
  2341         5594  
  2341         6881  
237 1176 100   1176 1 1610 sub buffers { my $self = shift; $self->{ 'buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'buffers' }; }
  1176         2299  
  1176         2766  
238 402 100 100 402 1 561 sub exclusive_fix { my $self = shift; $self->{ 'exclusive_fix' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'exclusive_fix' } // 0; }
  402         869  
  402         1480  
239              
240             =head2 new
241              
242             Object constructor.
243              
244             =cut
245              
246             sub new {
247 1516     1516 1 3360 my $class = shift;
248 1516         4853 my $self = bless { 'id' => $base_id++ }, $class;
249              
250 1516         2667 my %args;
251 1516 50       3635 if ( 0 == scalar @_ ) {
252 0         0 croak( 'Args should be passed as either hash or hashref' );
253             }
254 1516 50       4473 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 1516         16685 %args = @_;
267             }
268 1516 50       4660 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 1516         3034 for my $key ( qw( estimated_rows estimated_row_width estimated_startup_cost estimated_total_cost ) ) {
272 6064 100       12325 $args{ $key } = 0 unless defined $args{ $key };
273             }
274              
275 1516         4982 @{ $self }{ keys %args } = values %args;
  1516         9306  
276              
277 1516 100       4592 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 316         939 $self->type( $1 );
285 316         1502 $self->scan_on( { 'table_name' => $2, } );
286 316 100       1097 $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         83 $self->type( $1 );
290 21         124 $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 120         391 $self->type( $1 );
294 120         804 $self->scan_on(
295             {
296             'index_name' => $2,
297             'table_name' => $3,
298             }
299             );
300 120 100       466 $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         109 $self->type( $1 );
304 29         149 $self->scan_on( { 'cte_name' => $2, } );
305 29 100       110 $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         12 $self->type( $1 );
309 3         16 $self->scan_on( { 'worktable_name' => $2, } );
310 3 50       13 $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         52 $self->type( $1 );
314 11         66 $self->scan_on( { 'function_name' => $2, } );
315 11 100       63 $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         22 $self->type( $1 );
319 3         7 my $name = $2;
320 3         19 $name =~ s{\A"(.*)"\z}{$1};
321 3         14 $self->scan_on( { 'subquery_name' => $name, } );
322             }
323 1516         6109 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 1516     1516 1 2238 my $self = shift;
336 1516         2122 my $explain = shift;
337 1516 50       3340 if ( defined $explain ) {
338 1516         2839 $self->{ 'explain' } = $explain;
339 1516         3389 $explain->node( $self->id, $self );
340             }
341 1516         3069 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 1369     1369 1 2119 my $self = shift;
358 1369 100       2760 if ( $self->extra_info ) {
359 570         909 push @{ $self->extra_info }, @_;
  570         1050  
360             }
361             else {
362 799         2145 $self->extra_info( [ @_ ] );
363             }
364 1369         4052 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 97 my $self = shift;
408 35         85 my @nodes = map { $_->parent( $self ); $_ } @_;
  35         163  
  35         112  
409 35 100       112 if ( $self->subplans ) {
410 5         11 push @{ $self->subplans }, @nodes;
  5         18  
411             }
412             else {
413 30         96 $self->subplans( [ @nodes ] );
414             }
415 35         146 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 75 my $self = shift;
441 37         92 my ( $node, $node_info ) = @_;
442              
443 37 100       143 $self->initplans( [] ) unless $self->initplans;
444 37 100       122 $self->initplans_metainfo( [] ) unless $self->initplans_metainfo;
445              
446 37         175 $node->parent( $self );
447 37         72 push @{ $self->initplans }, $node;
  37         88  
448 37         80 push @{ $self->initplans_metainfo }, $node_info;
  37         91  
449 37         161 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 65 my $self = shift;
464 32         100 my ( $name, $cte ) = @_;
465 32         109 $cte->parent( $self );
466              
467 32 100       94 if ( $self->ctes ) {
468 9         29 $self->ctes->{ $name } = $cte;
469 9         16 push @{ $self->cte_order }, $name;
  9         32  
470             }
471             else {
472 23         121 $self->ctes( { $name => $cte } );
473 23         117 $self->cte_order( [ $name ] );
474             }
475 32         90 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 22 my $self = shift;
486 11         21 my $name = shift;
487 11         28 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 900     900 1 1512 my $self = shift;
515 900         1838 my @nodes = map { $_->parent( $self ); $_ } @_;
  900         2460  
  900         2303  
516 900 100       2113 if ( $self->sub_nodes ) {
517 248         460 push @{ $self->sub_nodes }, @nodes;
  248         617  
518             }
519             else {
520 652         1710 $self->sub_nodes( [ @nodes ] );
521             }
522 900         3246 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 939 my $self = shift;
546 539         861 my $reply = {};
547              
548 539 50       1109 $reply->{ 'estimated_row_width' } = $self->estimated_row_width if defined $self->estimated_row_width;
549 539 50       1174 $reply->{ 'estimated_rows' } = $self->estimated_rows if defined $self->estimated_rows;
550 539 50       1147 $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       1214 $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       1122 $reply->{ 'actual_loops' } = $self->actual_loops if defined $self->actual_loops;
553 539 100       1099 $reply->{ 'actual_rows' } = $self->actual_rows if defined $self->actual_rows;
554 539 100       1091 $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       1181 $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       1137 $reply->{ 'type' } = $self->type if defined $self->type;
557 539 100       1137 $reply->{ 'scan_on' } = clone( $self->scan_on ) if defined $self->scan_on;
558 539 100       1236 $reply->{ 'extra_info' } = clone( $self->extra_info ) if defined $self->extra_info;
559 539 100       1114 $reply->{ 'initplans_metainfo' } = clone( $self->initplans_metainfo ) if defined $self->initplans_metainfo;
560              
561 539         1055 $reply->{ 'is_analyzed' } = $self->is_analyzed;
562              
563 539 100       1062 $reply->{ 'sub_nodes' } = [ map { $_->get_struct } @{ $self->sub_nodes } ] if defined $self->sub_nodes;
  292         792  
  234         456  
564 539 100       1055 $reply->{ 'initplans' } = [ map { $_->get_struct } @{ $self->initplans } ] if defined $self->initplans;
  33         107  
  30         64  
565 539 100       999 $reply->{ 'subplans' } = [ map { $_->get_struct } @{ $self->subplans } ] if defined $self->subplans;
  24         61  
  21         56  
566              
567 539 100       1003 $reply->{ 'buffers' } = $self->buffers->get_struct() if $self->buffers;
568              
569 539 100       1031 $reply->{ 'cte_order' } = clone( $self->cte_order ) if defined $self->cte_order;
570 539 100       923 if ( defined $self->ctes ) {
571 9         26 $reply->{ 'ctes' } = {};
572 9         16 while ( my ( $key, $cte_node ) = each %{ $self->ctes } ) {
  27         61  
573 18         39 my $struct = $cte_node->get_struct;
574 18         47 $reply->{ 'ctes' }->{ $key } = $struct;
575             }
576             }
577 539         1547 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 885 my $self = shift;
588 614 100       1022 return unless defined $self->actual_time_last;
589 610 50       1176 return unless defined $self->actual_loops;
590 610         1071 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 276 my $self = shift;
601 167 50       286 return unless defined $self->actual_time_last;
602 167 50       299 return unless defined $self->actual_loops;
603 167 100       300 return $self->actual_loops * $self->actual_rows if 1 == $self->workers;
604 1         5 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 365 my $self = shift;
629 251 100       396 return 0 unless $self->extra_info;
630 249         342 my $removed = 0;
631 249         294 for my $line ( @{ $self->extra_info } ) {
  249         373  
632 502 100       1343 next unless $line =~ m{^Rows Removed by (?:Conflict Filter|Filter|Index Recheck|Join Filter): (\d+)$};
633 250         639 $removed += $1;
634             }
635 249 100       459 return $self->actual_loops * $removed if 1 == $self->workers;
636 1         7 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 509 my $self = shift;
647              
648 251         463 my $time = $self->total_inclusive_time;
649 251 100       556 return unless defined $time;
650              
651 250         471 for my $node ( map { @{ $_ } } grep { defined $_ } ( $self->sub_nodes ) ) {
  63         80  
  63         154  
  250         623  
652 84   50     154 $time -= ( $node->total_inclusive_time || 0 );
653             }
654              
655 250         536 for my $plan ( map { @{ $_ } } grep { defined $_ } ( $self->subplans ) ) {
  33         56  
  33         89  
  250         609  
656 34   100     91 $time -= ( $plan->total_inclusive_time || 0 );
657             }
658              
659             # Apply fix from ->exclusive_fix
660 250         502 $time += $self->exclusive_fix;
661              
662             # ignore negative times - these come from rounding errors on nodes with loops > 1.
663 250 100       537 return 0 if $time < 0;
664              
665 249         886 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 7 my $self = shift;
676              
677 3 50       8 return unless $self->buffers;
678              
679 3         10 my @nodes = grep { $_->buffers } $self->all_subnodes;
  4         9  
680 3 50       9 return $self->buffers if 0 == scalar @nodes;
681              
682 3         43 my $sub_node_buffers = $nodes[ 0 ]->buffers;
683 3         7 shift @nodes;
684 3         7 for my $n ( @nodes ) {
685 1         13 $sub_node_buffers = $sub_node_buffers + $n->buffers;
686             }
687              
688 3         8 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 80 my $self = shift;
699 57         77 my @nodes = ();
700 57 100       91 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  29         47  
701 57 100       108 push @nodes, @{ $self->initplans } if $self->initplans;
  2         4  
702 57 100       99 push @nodes, @{ $self->subplans } if $self->subplans;
  4         7  
703 57 50       92 push @nodes, values %{ $self->ctes } if $self->ctes;
  0         0  
704 57         157 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 3138     3138 1 4276 my $self = shift;
715 3138         4288 my @nodes = ();
716 3138 100       4867 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  1316         2160  
717 3138 100       5147 push @nodes, @{ $self->initplans } if $self->initplans;
  113         195  
718 3138 100       5132 push @nodes, @{ $self->subplans } if $self->subplans;
  60         128  
719 3138 100       5020 push @nodes, values %{ $self->ctes } if $self->ctes;
  75         158  
720              
721 3138         7231 return map { $_, $_->all_recursive_subnodes } @nodes;
  2166         4033  
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 11475 my $self = shift;
734 20         34 my @nodes = ();
735 20         27 my $current = $self;
736 20         33 while ( my $next = $current->parent ) {
737 79         107 unshift @nodes, $next;
738 79         113 $current = $next;
739             }
740 20         48 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 2418     2418 1 3583 my $self = shift;
751              
752 2418 100 66     4448 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 292     292 1 491 my $self = shift;
765 292         484 my $prefix = shift;
766 292 100       725 $prefix = '' unless defined $prefix;
767              
768 292 100       755 $prefix .= '-> ' if '' ne $prefix;
769 292         582 my $prefix_on_spaces = $prefix . " ";
770 292         1356 $prefix_on_spaces =~ s/[^ ]/ /g;
771              
772 292         740 my $heading_line = $self->type;
773              
774 292 100       631 if ( $self->scan_on ) {
775 144         342 my $S = $self->scan_on;
776 144 100       853 if ( $S->{ 'cte_name' } ) {
    100          
    100          
    100          
    100          
777 11         39 $heading_line .= " on " . $S->{ 'cte_name' };
778 11 100       56 $heading_line .= " " . $S->{ 'cte_alias' } if $S->{ 'cte_alias' };
779             }
780             elsif ( $S->{ 'function_name' } ) {
781 3         15 $heading_line .= " on " . $S->{ 'function_name' };
782 3 50       15 $heading_line .= " " . $S->{ 'function_alias' } if $S->{ 'function_alias' };
783             }
784             elsif ( $S->{ 'index_name' } ) {
785 42 100       106 if ( $S->{ 'table_name' } ) {
786 34         151 $heading_line .= " using " . $S->{ 'index_name' } . " on " . $S->{ 'table_name' };
787 34 100       119 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
788             }
789             else {
790 8         37 $heading_line .= " on " . $S->{ 'index_name' };
791             }
792             }
793             elsif ( $S->{ 'subquery_name' } ) {
794 1         7 $heading_line .= " on " . $S->{ 'subquery_name' },;
795             }
796             elsif ( $S->{ 'worktable_name' } ) {
797 4         14 $heading_line .= " on " . $S->{ 'worktable_name' },;
798 4 50       13 $heading_line .= " " . $S->{ 'worktable_alias' } if $S->{ 'worktable_alias' };
799             }
800             else {
801 83         250 $heading_line .= " on " . $S->{ 'table_name' };
802 83 100       349 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
803             }
804             }
805 292         740 $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 292 100       1070 if ( $self->is_analyzed ) {
807 241         404 my $inner;
808 241 100       766 if ( $self->never_executed ) {
    100          
809 6         15 $inner = 'never executed';
810             }
811             elsif ( defined $self->actual_time_last ) {
812 227         521 $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         15 $inner = sprintf 'actual rows=%s loops=%d', $self->actual_rows, $self->actual_loops;
816             }
817 241         898 $heading_line .= " ($inner)";
818             }
819              
820 292         585 my @lines = ();
821              
822 292         711 push @lines, $prefix . $heading_line;
823 292 100       612 if ( $self->extra_info ) {
824 161         246 push @lines, $prefix_on_spaces . " " . $_ for @{ $self->extra_info };
  161         358  
825             }
826 292         1106 my $textual = join( "\n", @lines ) . "\n";
827 292 100       800 if ( $self->buffers ) {
828 62         175 my $buf_info = $self->buffers->as_text;
829 62         357 $buf_info =~ s/^/${prefix_on_spaces}/gm;
830 62         245 $textual .= $buf_info . "\n";
831             }
832              
833 292 100       659 if ( $self->cte_order ) {
834 9         14 for my $cte_name ( @{ $self->cte_order } ) {
  9         22  
835 11         75 $textual .= $prefix_on_spaces . "CTE " . $cte_name . "\n";
836 11         58 $textual .= $self->cte( $cte_name )->as_text( $prefix_on_spaces . " " );
837             }
838             }
839              
840 292 100       616 if ( $self->initplans ) {
841 11         37 for my $i ( 0 .. $#{ $self->initplans } ) {
  11         29  
842 12         29 my $ip = $self->initplans->[ $i ];
843 12         32 my $meta = $self->initplans_metainfo->[ $i ];
844 12         24 my $init_name;
845 12 100       35 if ( $meta ) {
846 8         45 $init_name = sprintf "InitPlan %d (returns %s)\n", $meta->{ 'name' }, $meta->{ 'returns' };
847             }
848             else {
849 4         7 $init_name = "InitPlan\n";
850             }
851 12         39 $textual .= $prefix_on_spaces . $init_name;
852 12         88 $textual .= $ip->as_text( $prefix_on_spaces . " " );
853             }
854             }
855 292 100       640 if ( $self->sub_nodes ) {
856 139         226 $textual .= $_->as_text( $prefix_on_spaces ) for @{ $self->sub_nodes };
  139         291  
857             }
858 292 100       677 if ( $self->subplans ) {
859 6         15 for my $ip ( @{ $self->subplans } ) {
  6         121  
860 7         29 $textual .= $prefix_on_spaces . "SubPlan\n";
861 7         118 $textual .= $ip->as_text( $prefix_on_spaces . " " );
862             }
863             }
864 292         1425 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 57     57 1 118 my $self = shift;
875 57         111 my $anonymizer = shift;
876              
877 57 100       174 if ( $self->scan_on ) {
878 28         68 $anonymizer->add( values %{ $self->scan_on } );
  28         92  
879             }
880              
881 57 100       252 if ( $self->cte_order ) {
882 2         9 $anonymizer->add( $self->{ 'cte_order' } );
883             }
884              
885 57 100       186 if ( $self->extra_info ) {
886 45         73 for my $line ( @{ $self->extra_info } ) {
  45         99  
887 91         12793 my $copy = $line;
888 91 100       315 if ( $copy =~ m{^Foreign File:\s+(\S.*?)\s*$} ) {
889 3         10 $anonymizer->add( $1 );
890 3         6 next;
891             }
892 88 100       840 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 48         155 my $prefix = $1;
894 48         179 my $lexer = $self->_make_lexer( $copy );
895 48         44532 while ( my $x = $lexer->() ) {
896 1942 50       731557 next unless ref $x;
897 1942 100       7794 $anonymizer->add( $x->[ 1 ] ) if $x->[ 0 ] =~ m{\A (?: STRING_LITERAL | QUOTED_IDENTIFIER | IDENTIFIER ) \z}x;
898             }
899             }
900             }
901              
902 57         5356 for my $key ( qw( sub_nodes initplans subplans ) ) {
903 171 100       493 next unless $self->{ $key };
904 33         73 $_->anonymize_gathering( $anonymizer ) for @{ $self->{ $key } };
  33         255  
905             }
906              
907 57 100       178 if ( $self->{ 'ctes' } ) {
908 2         9 $_->anonymize_gathering( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         20  
909             }
910 57         173 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 96     96   158 my $self = shift;
921 96         173 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 96         349 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 96         12685 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 96         768 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 57     57 1 117 my $self = shift;
1057 57         159 my $anonymizer = shift;
1058              
1059 57 100       175 if ( $self->scan_on ) {
1060 28         216 while ( my ( $key, $value ) = each %{ $self->scan_on } ) {
  73         186  
1061 45         145 $self->scan_on->{ $key } = $anonymizer->anonymized( $value );
1062             }
1063             }
1064              
1065 57 100       191 if ( $self->cte_order ) {
1066 2         9 my @new_order = ();
1067 2         5 for my $cte_name ( @{ $self->cte_order } ) {
  2         6  
1068 2         10 my $new_name = $anonymizer->anonymized( $cte_name );
1069 2         7 push @new_order, $new_name;
1070 2         14 $self->ctes->{ $new_name } = delete $self->{ 'ctes' }->{ $cte_name };
1071             }
1072 2         9 $self->cte_order( \@new_order );
1073             }
1074              
1075 57 100       164 if ( $self->extra_info ) {
1076 45         109 my @new_extra_info = ();
1077 45         72 for my $line ( @{ $self->extra_info } ) {
  45         123  
1078 91 100       365 if ( $line =~ m{^(Foreign File:\s+)(\S.*?)(\s*)$} ) {
1079 3         8 push @new_extra_info, $1 . $anonymizer->anonymized( $2 ) . $3;
1080 3         8 next;
1081             }
1082 88 100       811 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 40         113 push @new_extra_info, $line;
1084 40         124 next;
1085             }
1086 48         149 my $output = $1;
1087 48         140 my $lexer = $self->_make_lexer( $line );
1088 48         41248 while ( my $x = $lexer->() ) {
1089 1942 50       735123 if ( ref $x ) {
1090 1942 100       5469 if ( $x->[ 0 ] eq 'STRING_LITERAL' ) {
    50          
    100          
1091 21         243 $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 618         1808 $output .= $anonymizer->anonymized( $x->[ 1 ] );
1098             }
1099             else {
1100 1303         3367 $output .= $x->[ 1 ];
1101             }
1102             }
1103             else {
1104 0         0 $output .= $x;
1105             }
1106             }
1107 48         18274 push @new_extra_info, $output;
1108             }
1109 45         190 $self->{ 'extra_info' } = \@new_extra_info;
1110             }
1111              
1112 57         185 for my $key ( qw( sub_nodes initplans subplans ) ) {
1113 171 100       438 next unless $self->{ $key };
1114 33         66 $_->anonymize_substitute( $anonymizer ) for @{ $self->{ $key } };
  33         307  
1115             }
1116              
1117 57 100       204 if ( $self->{ 'ctes' } ) {
1118 2         5 $_->anonymize_substitute( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         30  
1119             }
1120 57         225 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