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 73     73   938 use v5.18;
  73         262  
5 73     73   413 use strict;
  73         179  
  73         1494  
6 73     73   354 use warnings;
  73         160  
  73         2088  
7 73     73   376 use warnings qw( FATAL utf8 );
  73         145  
  73         2217  
8 73     73   370 use utf8;
  73         151  
  73         381  
9 73     73   1883 use open qw( :std :utf8 );
  73         182  
  73         382  
10 73     73   9363 use Unicode::Normalize qw( NFC );
  73         173  
  73         3878  
11 73     73   483 use Unicode::Collate;
  73         156  
  73         2370  
12 73     73   410 use Encode qw( decode );
  73         177  
  73         4347  
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 73     73   14956 use Clone qw( clone );
  73         167  
  73         4133  
21 73     73   36266 use HOP::Lexer qw( string_lexer );
  73         216967  
  73         4576  
22 73     73   526 use Carp;
  73         175  
  73         4112  
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 73     73   474 no warnings 'recursion';
  73         140  
  73         573479  
27              
28             =head1 NAME
29              
30             Pg::Explain::Node - Class representing single node from query plan
31              
32             =head1 VERSION
33              
34             Version 2.3
35              
36             =cut
37              
38             our $VERSION = '2.3';
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 1695     1695 1 6738 sub id { my $self = shift; return $self->{ 'id' }; }
  1695         5590  
216 5503 100   5503 1 7322 sub actual_loops { my $self = shift; $self->{ 'actual_loops' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_loops' }; }
  5503         9816  
  5503         17895  
217 1377 50   1377 1 3144 sub actual_rows { my $self = shift; $self->{ 'actual_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_rows' }; }
  1377         2457  
  1377         3279  
218 1199 50   1199 1 1657 sub actual_time_first { my $self = shift; $self->{ 'actual_time_first' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_first' }; }
  1199         2242  
  1199         2969  
219 2826 50   2826 1 3685 sub actual_time_last { my $self = shift; $self->{ 'actual_time_last' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_last' }; }
  2826         4929  
  2826         7510  
220 979 100   979 1 1424 sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; }
  979         1940  
  979         2252  
221 6578 100   6578 1 8469 sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; }
  6578         12099  
  6578         13874  
222 1360 50   1360 1 1866 sub estimated_rows { my $self = shift; $self->{ 'estimated_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_rows' }; }
  1360         2619  
  1360         3170  
223 1526 50   1526 1 2098 sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_row_width' }; }
  1526         2829  
  1526         6661  
224 1360 50   1360 1 1830 sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; }
  1360         2482  
  1360         3724  
225 1360 50   1360 1 1850 sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; }
  1360         2480  
  1360         3410  
226 4998 100   4998 1 6665 sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; }
  4998         9673  
  4998         12416  
227 7001 100   7001 1 9216 sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; }
  7001         12117  
  7001         14746  
228 751 100   751 1 1172 sub initplans_metainfo { my $self = shift; $self->{ 'initplans_metainfo' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans_metainfo' }; }
  751         1506  
  751         1895  
229 687 100   687 1 1060 sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; }
  687         1381  
  687         2386  
230 1099 100   1099 1 1685 sub parent { my $self = shift; $self->{ 'parent' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'parent' }; }
  1099         2928  
  1099         1909  
231 2877 100   2877 1 4301 sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; }
  2877         6291  
  2877         10462  
232 9926 100   9926 1 12989 sub sub_nodes { my $self = shift; $self->{ 'sub_nodes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'sub_nodes' }; }
  9926         17615  
  9926         22070  
233 5727 100   5727 1 7484 sub subplans { my $self = shift; $self->{ 'subplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'subplans' }; }
  5727         9971  
  5727         11151  
234 10920 100   10920 1 118304 sub type { my $self = shift; $self->{ 'type' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'type' }; }
  10920         19897  
  10920         35007  
235 1330 100   1330 1 1907 sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; }
  1330         2740  
  1330         3096  
236 2336 100 50 2336 1 4269 sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; }
  2336         5364  
  2336         7034  
237 1166 100   1166 1 1692 sub buffers { my $self = shift; $self->{ 'buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'buffers' }; }
  1166         2339  
  1166         2802  
238 402 100 100 402 1 584 sub exclusive_fix { my $self = shift; $self->{ 'exclusive_fix' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'exclusive_fix' } // 0; }
  402         848  
  402         1468  
239              
240             =head2 new
241              
242             Object constructor.
243              
244             =cut
245              
246             sub new {
247 1511     1511 1 3468 my $class = shift;
248 1511         5013 my $self = bless { 'id' => $base_id++ }, $class;
249              
250 1511         2690 my %args;
251 1511 50       3735 if ( 0 == scalar @_ ) {
252 0         0 croak( 'Args should be passed as either hash or hashref' );
253             }
254 1511 50       5165 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 1511         16993 %args = @_;
267             }
268 1511 50       4693 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 1511         3151 for my $key ( qw( estimated_rows estimated_row_width estimated_startup_cost estimated_total_cost ) ) {
272 6044 100       12845 $args{ $key } = 0 unless defined $args{ $key };
273             }
274              
275 1511         5019 @{ $self }{ keys %args } = values %args;
  1511         9455  
276              
277 1511 100       4769 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         941 $self->type( $1 );
285 314         1496 $self->scan_on( { 'table_name' => $2, } );
286 314 100       1082 $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         95 $self->type( $1 );
290 21         122 $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         384 $self->type( $1 );
294 120         738 $self->scan_on(
295             {
296             'index_name' => $2,
297             'table_name' => $3,
298             }
299             );
300 120 100       486 $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         103 $self->type( $1 );
304 29         169 $self->scan_on( { 'cte_name' => $2, } );
305 29 100       144 $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         14 $self->type( $1 );
309 3         19 $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         49 $self->type( $1 );
314 11         68 $self->scan_on( { 'function_name' => $2, } );
315 11 100       97 $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         9 $self->type( $1 );
319 3         8 my $name = $2;
320 3         23 $name =~ s{\A"(.*)"\z}{$1};
321 3         16 $self->scan_on( { 'subquery_name' => $name, } );
322             }
323 1511         6153 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 1511     1511 1 2265 my $self = shift;
336 1511         2208 my $explain = shift;
337 1511 50       3251 if ( defined $explain ) {
338 1511         2749 $self->{ 'explain' } = $explain;
339 1511         3526 $explain->node( $self->id, $self );
340             }
341 1511         3035 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 1361     1361 1 2144 my $self = shift;
358 1361 100       2960 if ( $self->extra_info ) {
359 567         911 push @{ $self->extra_info }, @_;
  567         1124  
360             }
361             else {
362 794         2307 $self->extra_info( [ @_ ] );
363             }
364 1361         4038 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 84 my $self = shift;
408 35         84 my @nodes = map { $_->parent( $self ); $_ } @_;
  35         112  
  35         112  
409 35 100       110 if ( $self->subplans ) {
410 5         18 push @{ $self->subplans }, @nodes;
  5         22  
411             }
412             else {
413 30         110 $self->subplans( [ @nodes ] );
414             }
415 35         142 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 84 my $self = shift;
441 37         91 my ( $node, $node_info ) = @_;
442              
443 37 100       182 $self->initplans( [] ) unless $self->initplans;
444 37 100       138 $self->initplans_metainfo( [] ) unless $self->initplans_metainfo;
445              
446 37         118 $node->parent( $self );
447 37         70 push @{ $self->initplans }, $node;
  37         87  
448 37         65 push @{ $self->initplans_metainfo }, $node_info;
  37         110  
449 37         153 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 82 my $self = shift;
464 32         108 my ( $name, $cte ) = @_;
465 32         111 $cte->parent( $self );
466              
467 32 100       107 if ( $self->ctes ) {
468 9         32 $self->ctes->{ $name } = $cte;
469 9         16 push @{ $self->cte_order }, $name;
  9         23  
470             }
471             else {
472 23         123 $self->ctes( { $name => $cte } );
473 23         147 $self->cte_order( [ $name ] );
474             }
475 32         84 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 17 my $self = shift;
486 11         21 my $name = shift;
487 11         31 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 896     896 1 1445 my $self = shift;
515 896         1846 my @nodes = map { $_->parent( $self ); $_ } @_;
  896         2463  
  896         2350  
516 896 100       2172 if ( $self->sub_nodes ) {
517 247         460 push @{ $self->sub_nodes }, @nodes;
  247         546  
518             }
519             else {
520 649         1758 $self->sub_nodes( [ @nodes ] );
521             }
522 896         3250 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 812 my $self = shift;
546 539         965 my $reply = {};
547              
548 539 50       1142 $reply->{ 'estimated_row_width' } = $self->estimated_row_width if defined $self->estimated_row_width;
549 539 50       1170 $reply->{ 'estimated_rows' } = $self->estimated_rows if defined $self->estimated_rows;
550 539 50       1120 $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       1187 $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       1089 $reply->{ 'actual_loops' } = $self->actual_loops if defined $self->actual_loops;
553 539 100       1119 $reply->{ 'actual_rows' } = $self->actual_rows if defined $self->actual_rows;
554 539 100       1124 $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       1141 $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       1164 $reply->{ 'type' } = $self->type if defined $self->type;
557 539 100       1065 $reply->{ 'scan_on' } = clone( $self->scan_on ) if defined $self->scan_on;
558 539 100       1298 $reply->{ 'extra_info' } = clone( $self->extra_info ) if defined $self->extra_info;
559 539 100       1164 $reply->{ 'initplans_metainfo' } = clone( $self->initplans_metainfo ) if defined $self->initplans_metainfo;
560              
561 539         1080 $reply->{ 'is_analyzed' } = $self->is_analyzed;
562              
563 539 100       1092 $reply->{ 'sub_nodes' } = [ map { $_->get_struct } @{ $self->sub_nodes } ] if defined $self->sub_nodes;
  292         761  
  234         447  
564 539 100       1176 $reply->{ 'initplans' } = [ map { $_->get_struct } @{ $self->initplans } ] if defined $self->initplans;
  33         105  
  30         77  
565 539 100       1031 $reply->{ 'subplans' } = [ map { $_->get_struct } @{ $self->subplans } ] if defined $self->subplans;
  24         58  
  21         72  
566              
567 539 100       1054 $reply->{ 'buffers' } = $self->buffers->get_struct() if $self->buffers;
568              
569 539 100       1092 $reply->{ 'cte_order' } = clone( $self->cte_order ) if defined $self->cte_order;
570 539 100       963 if ( defined $self->ctes ) {
571 9         18 $reply->{ 'ctes' } = {};
572 9         18 while ( my ( $key, $cte_node ) = each %{ $self->ctes } ) {
  27         49  
573 18         43 my $struct = $cte_node->get_struct;
574 18         85 $reply->{ 'ctes' }->{ $key } = $struct;
575             }
576             }
577 539         1518 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 860 my $self = shift;
588 614 100       1048 return unless defined $self->actual_time_last;
589 610 50       1152 return unless defined $self->actual_loops;
590 610         1051 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 218 my $self = shift;
601 167 50       323 return unless defined $self->actual_time_last;
602 167 50       304 return unless defined $self->actual_loops;
603 167 100       341 return $self->actual_loops * $self->actual_rows if 1 == $self->workers;
604 1         2 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 345 my $self = shift;
629 251 100       371 return 0 unless $self->extra_info;
630 249         346 my $removed = 0;
631 249         303 for my $line ( @{ $self->extra_info } ) {
  249         369  
632 502 100       1378 next unless $line =~ m{^Rows Removed by (?:Conflict Filter|Filter|Index Recheck|Join Filter): (\d+)$};
633 250         635 $removed += $1;
634             }
635 249 100       488 return $self->actual_loops * $removed if 1 == $self->workers;
636 1         4 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 574 my $self = shift;
647              
648 251         467 my $time = $self->total_inclusive_time;
649 251 100       532 return unless defined $time;
650              
651 250         467 for my $node ( map { @{ $_ } } grep { defined $_ } ( $self->sub_nodes ) ) {
  63         80  
  63         141  
  250         631  
652 84   50     159 $time -= ( $node->total_inclusive_time || 0 );
653             }
654              
655 250         545 for my $plan ( map { @{ $_ } } grep { defined $_ } ( $self->subplans ) ) {
  33         58  
  33         85  
  250         555  
656 34   100     89 $time -= ( $plan->total_inclusive_time || 0 );
657             }
658              
659             # Apply fix from ->exclusive_fix
660 250         504 $time += $self->exclusive_fix;
661              
662             # ignore negative times - these come from rounding errors on nodes with loops > 1.
663 250 100       576 return 0 if $time < 0;
664              
665 249         896 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 8 my $self = shift;
676              
677 3 50       8 return unless $self->buffers;
678              
679 3         16 my @nodes = grep { $_->buffers } $self->all_subnodes;
  4         9  
680 3 50       17 return $self->buffers if 0 == scalar @nodes;
681              
682 3         44 my $sub_node_buffers = $nodes[ 0 ]->buffers;
683 3         6 shift @nodes;
684 3         17 for my $n ( @nodes ) {
685 1         7 $sub_node_buffers = $sub_node_buffers + $n->buffers;
686             }
687              
688 3         9 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 77 my $self = shift;
699 57         107 my @nodes = ();
700 57 100       97 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  29         50  
701 57 100       108 push @nodes, @{ $self->initplans } if $self->initplans;
  2         3  
702 57 100       90 push @nodes, @{ $self->subplans } if $self->subplans;
  4         9  
703 57 50       103 push @nodes, values %{ $self->ctes } if $self->ctes;
  0         0  
704 57         160 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 3128     3128 1 4357 my $self = shift;
715 3128         4279 my @nodes = ();
716 3128 100       4960 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  1310         2140  
717 3128 100       5227 push @nodes, @{ $self->initplans } if $self->initplans;
  113         203  
718 3128 100       5148 push @nodes, @{ $self->subplans } if $self->subplans;
  60         119  
719 3128 100       5111 push @nodes, values %{ $self->ctes } if $self->ctes;
  75         139  
720              
721 3128         6838 return map { $_, $_->all_recursive_subnodes } @nodes;
  2158         4097  
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 11249 my $self = shift;
734 20         36 my @nodes = ();
735 20         26 my $current = $self;
736 20         32 while ( my $next = $current->parent ) {
737 79         106 unshift @nodes, $next;
738 79         117 $current = $next;
739             }
740 20         47 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 2405     2405 1 3540 my $self = shift;
751              
752 2405 100 66     4608 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 608 my $self = shift;
765 282         453 my $prefix = shift;
766 282 100       783 $prefix = '' unless defined $prefix;
767              
768 282 100       719 $prefix .= '-> ' if '' ne $prefix;
769 282         563 my $prefix_on_spaces = $prefix . " ";
770 282         1343 $prefix_on_spaces =~ s/[^ ]/ /g;
771              
772 282         737 my $heading_line = $self->type;
773              
774 282 100       698 if ( $self->scan_on ) {
775 140         354 my $S = $self->scan_on;
776 140 100       907 if ( $S->{ 'cte_name' } ) {
    100          
    100          
    100          
    100          
777 11         49 $heading_line .= " on " . $S->{ 'cte_name' };
778 11 100       45 $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       19 $heading_line .= " " . $S->{ 'function_alias' } if $S->{ 'function_alias' };
783             }
784             elsif ( $S->{ 'index_name' } ) {
785 42 100       117 if ( $S->{ 'table_name' } ) {
786 34         151 $heading_line .= " using " . $S->{ 'index_name' } . " on " . $S->{ 'table_name' };
787 34 100       123 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
788             }
789             else {
790 8         33 $heading_line .= " on " . $S->{ 'index_name' };
791             }
792             }
793             elsif ( $S->{ 'subquery_name' } ) {
794 1         6 $heading_line .= " on " . $S->{ 'subquery_name' },;
795             }
796             elsif ( $S->{ 'worktable_name' } ) {
797 4         12 $heading_line .= " on " . $S->{ 'worktable_name' },;
798 4 50       15 $heading_line .= " " . $S->{ 'worktable_alias' } if $S->{ 'worktable_alias' };
799             }
800             else {
801 79         234 $heading_line .= " on " . $S->{ 'table_name' };
802 79 100       297 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
803             }
804             }
805 282         716 $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       1004 if ( $self->is_analyzed ) {
807 231         436 my $inner;
808 231 100       713 if ( $self->never_executed ) {
    100          
809 6         13 $inner = 'never executed';
810             }
811             elsif ( defined $self->actual_time_last ) {
812 217         500 $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         26 $inner = sprintf 'actual rows=%s loops=%d', $self->actual_rows, $self->actual_loops;
816             }
817 231         2351 $heading_line .= " ($inner)";
818             }
819              
820 282         601 my @lines = ();
821              
822 282         699 push @lines, $prefix . $heading_line;
823 282 100       603 if ( $self->extra_info ) {
824 151         244 push @lines, $prefix_on_spaces . " " . $_ for @{ $self->extra_info };
  151         304  
825             }
826 282         1226 my $textual = join( "\n", @lines ) . "\n";
827 282 100       734 if ( $self->buffers ) {
828 62         166 my $buf_info = $self->buffers->as_text;
829 62         337 $buf_info =~ s/^/${prefix_on_spaces}/gm;
830 62         257 $textual .= $buf_info . "\n";
831             }
832              
833 282 100       713 if ( $self->cte_order ) {
834 9         21 for my $cte_name ( @{ $self->cte_order } ) {
  9         20  
835 11         53 $textual .= $prefix_on_spaces . "CTE " . $cte_name . "\n";
836 11         42 $textual .= $self->cte( $cte_name )->as_text( $prefix_on_spaces . " " );
837             }
838             }
839              
840 282 100       613 if ( $self->initplans ) {
841 11         24 for my $i ( 0 .. $#{ $self->initplans } ) {
  11         30  
842 12         29 my $ip = $self->initplans->[ $i ];
843 12         35 my $meta = $self->initplans_metainfo->[ $i ];
844 12         21 my $init_name;
845 12 100       37 if ( $meta ) {
846 8         52 $init_name = sprintf "InitPlan %d (returns %s)\n", $meta->{ 'name' }, $meta->{ 'returns' };
847             }
848             else {
849 4         6 $init_name = "InitPlan\n";
850             }
851 12         41 $textual .= $prefix_on_spaces . $init_name;
852 12         95 $textual .= $ip->as_text( $prefix_on_spaces . " " );
853             }
854             }
855 282 100       648 if ( $self->sub_nodes ) {
856 133         231 $textual .= $_->as_text( $prefix_on_spaces ) for @{ $self->sub_nodes };
  133         261  
857             }
858 282 100       689 if ( $self->subplans ) {
859 6         15 for my $ip ( @{ $self->subplans } ) {
  6         126  
860 7         37 $textual .= $prefix_on_spaces . "SubPlan\n";
861 7         116 $textual .= $ip->as_text( $prefix_on_spaces . " " );
862             }
863             }
864 282         1292 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 119 my $self = shift;
875 52         109 my $anonymizer = shift;
876              
877 52 100       159 if ( $self->scan_on ) {
878 26         57 $anonymizer->add( values %{ $self->scan_on } );
  26         74  
879             }
880              
881 52 100       183 if ( $self->cte_order ) {
882 2         13 $anonymizer->add( $self->{ 'cte_order' } );
883             }
884              
885 52 100       165 if ( $self->extra_info ) {
886 40         63 for my $line ( @{ $self->extra_info } ) {
  40         86  
887 83         11550 my $copy = $line;
888 83 100       287 if ( $copy =~ m{^Foreign File:\s+(\S.*?)\s*$} ) {
889 3         9 $anonymizer->add( $1 );
890 3         6 next;
891             }
892 80 100       766 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         180 my $prefix = $1;
894 44         160 my $lexer = $self->_make_lexer( $copy );
895 44         40819 while ( my $x = $lexer->() ) {
896 1867 50       676241 next unless ref $x;
897 1867 100       7670 $anonymizer->add( $x->[ 1 ] ) if $x->[ 0 ] =~ m{\A (?: STRING_LITERAL | QUOTED_IDENTIFIER | IDENTIFIER ) \z}x;
898             }
899             }
900             }
901              
902 52         5128 for my $key ( qw( sub_nodes initplans subplans ) ) {
903 156 100       468 next unless $self->{ $key };
904 30         66 $_->anonymize_gathering( $anonymizer ) for @{ $self->{ $key } };
  30         233  
905             }
906              
907 52 100       210 if ( $self->{ 'ctes' } ) {
908 2         10 $_->anonymize_gathering( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         24  
909             }
910 52         153 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   161 my $self = shift;
921 88         147 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         356 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         11639 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         664 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 121 my $self = shift;
1057 52         89 my $anonymizer = shift;
1058              
1059 52 100       159 if ( $self->scan_on ) {
1060 26         68 while ( my ( $key, $value ) = each %{ $self->scan_on } ) {
  67         136  
1061 41         291 $self->scan_on->{ $key } = $anonymizer->anonymized( $value );
1062             }
1063             }
1064              
1065 52 100       202 if ( $self->cte_order ) {
1066 2         6 my @new_order = ();
1067 2         6 for my $cte_name ( @{ $self->cte_order } ) {
  2         8  
1068 2         11 my $new_name = $anonymizer->anonymized( $cte_name );
1069 2         8 push @new_order, $new_name;
1070 2         14 $self->ctes->{ $new_name } = delete $self->{ 'ctes' }->{ $cte_name };
1071             }
1072 2         10 $self->cte_order( \@new_order );
1073             }
1074              
1075 52 100       164 if ( $self->extra_info ) {
1076 40         91 my @new_extra_info = ();
1077 40         78 for my $line ( @{ $self->extra_info } ) {
  40         110  
1078 83 100       366 if ( $line =~ m{^(Foreign File:\s+)(\S.*?)(\s*)$} ) {
1079 3         10 push @new_extra_info, $1 . $anonymizer->anonymized( $2 ) . $3;
1080 3         8 next;
1081             }
1082 80 100       816 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         92 push @new_extra_info, $line;
1084 36         223 next;
1085             }
1086 44         154 my $output = $1;
1087 44         147 my $lexer = $self->_make_lexer( $line );
1088 44         38044 while ( my $x = $lexer->() ) {
1089 1867 50       674356 if ( ref $x ) {
1090 1867 100       4996 if ( $x->[ 0 ] eq 'STRING_LITERAL' ) {
    50          
    100          
1091 17         101 $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         1933 $output .= $anonymizer->anonymized( $x->[ 1 ] );
1098             }
1099             else {
1100 1246         3239 $output .= $x->[ 1 ];
1101             }
1102             }
1103             else {
1104 0         0 $output .= $x;
1105             }
1106             }
1107 44         16940 push @new_extra_info, $output;
1108             }
1109 40         178 $self->{ 'extra_info' } = \@new_extra_info;
1110             }
1111              
1112 52         163 for my $key ( qw( sub_nodes initplans subplans ) ) {
1113 156 100       396 next unless $self->{ $key };
1114 30         58 $_->anonymize_substitute( $anonymizer ) for @{ $self->{ $key } };
  30         291  
1115             }
1116              
1117 52 100       148 if ( $self->{ 'ctes' } ) {
1118 2         50 $_->anonymize_substitute( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         30  
1119             }
1120 52         164 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