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 79     79   1114 use v5.18;
  79         322  
5 79     79   487 use strict;
  79         167  
  79         2212  
6 79     79   422 use warnings;
  79         156  
  79         4694  
7 79     79   439 use warnings qw( FATAL utf8 );
  79         149  
  79         4651  
8 79     79   484 use utf8;
  79         204  
  79         610  
9 79     79   3147 use open qw( :std :utf8 );
  79         169  
  79         568  
10 79     79   11723 use Unicode::Normalize qw( NFC );
  79         250  
  79         4990  
11 79     79   470 use Unicode::Collate;
  79         148  
  79         2612  
12 79     79   391 use Encode qw( decode );
  79         204  
  79         12465  
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 79     79   522 use Clone qw( clone );
  79         213  
  79         4870  
21 79     79   54584 use HOP::Lexer qw( string_lexer );
  79         269172  
  79         6280  
22 79     79   600 use Carp;
  79         148  
  79         5874  
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 79     79   531 no warnings 'recursion';
  79         137  
  79         786561  
27              
28             =head1 NAME
29              
30             Pg::Explain::Node - Class representing single node from query plan
31              
32             =head1 VERSION
33              
34             Version 2.9
35              
36             =cut
37              
38             our $VERSION = '2.9';
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 1764     1764 1 7829 sub id { my $self = shift; return $self->{ 'id' }; }
  1764         7668  
216 5742 100   5742 1 8279 sub actual_loops { my $self = shift; $self->{ 'actual_loops' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_loops' }; }
  5742         13269  
  5742         22425  
217 1454 50   1454 1 4992 sub actual_rows { my $self = shift; $self->{ 'actual_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_rows' }; }
  1454         3120  
  1454         4893  
218 1274 50   1274 1 1946 sub actual_time_first { my $self = shift; $self->{ 'actual_time_first' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_first' }; }
  1274         2485  
  1274         4517  
219 2965 50   2965 1 4225 sub actual_time_last { my $self = shift; $self->{ 'actual_time_last' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_last' }; }
  2965         5488  
  2965         9153  
220 1066 100   1066 1 1640 sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; }
  1066         2540  
  1066         2846  
221 6860 100   6860 1 12543 sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; }
  6860         12689  
  6860         16471  
222 1435 50   1435 1 2239 sub estimated_rows { my $self = shift; $self->{ 'estimated_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_rows' }; }
  1435         2988  
  1435         4017  
223 1601 50   1601 1 2421 sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_row_width' }; }
  1601         3485  
  1601         9354  
224 1435 50   1435 1 2141 sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; }
  1435         2858  
  1435         5545  
225 1435 50   1435 1 2112 sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; }
  1435         5611  
  1435         4715  
226 5294 100   5294 1 7928 sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; }
  5294         11267  
  5294         19260  
227 7346 100   7346 1 10055 sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; }
  7346         13960  
  7346         23726  
228 757 100   757 1 1172 sub initplans_metainfo { my $self = shift; $self->{ 'initplans_metainfo' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans_metainfo' }; }
  757         1777  
  757         2347  
229 750 100   750 1 1282 sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; }
  750         1747  
  750         4282  
230 1137 100   1137 1 1817 sub parent { my $self = shift; $self->{ 'parent' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'parent' }; }
  1137         3483  
  1137         6219  
231 3058 100   3058 1 4956 sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; }
  3058         7815  
  3058         14666  
232 10422 100   10422 1 17079 sub sub_nodes { my $self = shift; $self->{ 'sub_nodes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'sub_nodes' }; }
  10422         23266  
  10422         26152  
233 6003 100   6003 1 8587 sub subplans { my $self = shift; $self->{ 'subplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'subplans' }; }
  6003         11493  
  6003         13091  
234 11398 100   11398 1 115646 sub type { my $self = shift; $self->{ 'type' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'type' }; }
  11398         22498  
  11398         44016  
235 1419 100   1419 1 2100 sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; }
  1419         3112  
  1419         3975  
236 2406 100 50 2406 1 5430 sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; }
  2406         5915  
  2406         7441  
237 1395 100   1395 1 2189 sub buffers { my $self = shift; $self->{ 'buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'buffers' }; }
  1395         3203  
  1395         4226  
238 402 100 100 402 1 592 sub exclusive_fix { my $self = shift; $self->{ 'exclusive_fix' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'exclusive_fix' } // 0; }
  402         870  
  402         1738  
239              
240             =head2 new
241              
242             Object constructor.
243              
244             =cut
245              
246             sub new {
247 1580     1580 1 3291 my $class = shift;
248 1580         7493 my $self = bless { 'id' => $base_id++ }, $class;
249              
250 1580         5805 my %args;
251 1580 50       4235 if ( 0 == scalar @_ ) {
252 0         0 croak( 'Args should be passed as either hash or hashref' );
253             }
254 1580 50       5313 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 1580         22723 %args = @_;
267             }
268 1580 50       5762 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 1580         6624 for my $key ( qw( estimated_rows estimated_row_width estimated_startup_cost estimated_total_cost ) ) {
272 6320 100       17623 $args{ $key } = 0 unless defined $args{ $key };
273             }
274              
275 1580         6038 @{ $self }{ keys %args } = values %args;
  1580         13253  
276              
277 1580 100       5801 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 323         1040 $self->type( $1 );
285 323         1847 $self->scan_on( { 'table_name' => $2, } );
286 323 100       1504 $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         105 $self->type( $1 );
290 21         108 $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 132         567 $self->type( $1 );
294 132         1084 $self->scan_on(
295             {
296             'index_name' => $2,
297             'table_name' => $3,
298             }
299             );
300 132 100       634 $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         95 $self->type( $1 );
304 29         153 $self->scan_on( { 'cte_name' => $2, } );
305 29 100       125 $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         13 $self->type( $1 );
309 3         27 $self->scan_on( { 'worktable_name' => $2, } );
310 3 50       17 $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 12         64 $self->type( $1 );
314 12         74 $self->scan_on( { 'function_name' => $2, } );
315 12 100       164 $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         12 $self->type( $1 );
319 3         9 my $name = $2;
320 3         21 $name =~ s{\A"(.*)"\z}{$1};
321 3         20 $self->scan_on( { 'subquery_name' => $name, } );
322             }
323 1580         8495 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 1580     1580 1 2538 my $self = shift;
336 1580         2430 my $explain = shift;
337 1580 50       3774 if ( defined $explain ) {
338 1580         4164 $self->{ 'explain' } = $explain;
339 1580         4510 $explain->node( $self->id, $self );
340             }
341 1580         3690 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 1438     1438 1 2512 my $self = shift;
358 1438 100       3644 if ( $self->extra_info ) {
359 603         990 push @{ $self->extra_info }, @_;
  603         1243  
360             }
361             else {
362 835         2744 $self->extra_info( [ @_ ] );
363             }
364 1438         4785 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 76 my $self = shift;
408 35         153 my @nodes = map { $_->parent( $self ); $_ } @_;
  35         155  
  35         116  
409 35 100       160 if ( $self->subplans ) {
410 5         10 push @{ $self->subplans }, @nodes;
  5         18  
411             }
412             else {
413 30         117 $self->subplans( [ @nodes ] );
414             }
415 35         274 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 144 my $self = shift;
441 37         110 my ( $node, $node_info ) = @_;
442              
443 37 100       125 $self->initplans( [] ) unless $self->initplans;
444 37 100       117 $self->initplans_metainfo( [] ) unless $self->initplans_metainfo;
445              
446 37         134 $node->parent( $self );
447 37         66 push @{ $self->initplans }, $node;
  37         94  
448 37         72 push @{ $self->initplans_metainfo }, $node_info;
  37         91  
449 37         197 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 62 my $self = shift;
464 32         148 my ( $name, $cte ) = @_;
465 32         128 $cte->parent( $self );
466              
467 32 100       123 if ( $self->ctes ) {
468 9         25 $self->ctes->{ $name } = $cte;
469 9         17 push @{ $self->cte_order }, $name;
  9         22  
470             }
471             else {
472 23         124 $self->ctes( { $name => $cte } );
473 23         98 $self->cte_order( [ $name ] );
474             }
475 32         142 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         33 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 934     934 1 1659 my $self = shift;
515 934         2083 my @nodes = map { $_->parent( $self ); $_ } @_;
  934         3304  
  934         2586  
516 934 100       2548 if ( $self->sub_nodes ) {
517 250         447 push @{ $self->sub_nodes }, @nodes;
  250         675  
518             }
519             else {
520 684         2117 $self->sub_nodes( [ @nodes ] );
521             }
522 934         4093 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 545     545 1 1022 my $self = shift;
546 545         959 my $reply = {};
547              
548 545 50       1368 $reply->{ 'estimated_row_width' } = $self->estimated_row_width if defined $self->estimated_row_width;
549 545 50       1397 $reply->{ 'estimated_rows' } = $self->estimated_rows if defined $self->estimated_rows;
550 545 50       1360 $reply->{ 'estimated_startup_cost' } = 0 + $self->estimated_startup_cost if defined $self->estimated_startup_cost; # "0+" to remove .00 in case of integers
551 545 50       1388 $reply->{ 'estimated_total_cost' } = 0 + $self->estimated_total_cost if defined $self->estimated_total_cost; # "0+" to remove .00 in case of integers
552 545 100       1449 $reply->{ 'actual_loops' } = $self->actual_loops if defined $self->actual_loops;
553 545 100       1423 $reply->{ 'actual_rows' } = $self->actual_rows if defined $self->actual_rows;
554 545 100       1353 $reply->{ 'actual_time_first' } = 0 + $self->actual_time_first if defined $self->actual_time_first; # "0+" to remove .00 in case of integers
555 545 100       1433 $reply->{ 'actual_time_last' } = 0 + $self->actual_time_last if defined $self->actual_time_last; # "0+" to remove .00 in case of integers
556 545 50       1518 $reply->{ 'type' } = $self->type if defined $self->type;
557 545 100       1430 $reply->{ 'scan_on' } = clone( $self->scan_on ) if defined $self->scan_on;
558 545 100       1578 $reply->{ 'extra_info' } = clone( $self->extra_info ) if defined $self->extra_info;
559 545 100       1356 $reply->{ 'initplans_metainfo' } = clone( $self->initplans_metainfo ) if defined $self->initplans_metainfo;
560              
561 545         1392 $reply->{ 'is_analyzed' } = $self->is_analyzed;
562              
563 545 100       1394 $reply->{ 'sub_nodes' } = [ map { $_->get_struct } @{ $self->sub_nodes } ] if defined $self->sub_nodes;
  294         1085  
  236         545  
564 545 100       1316 $reply->{ 'initplans' } = [ map { $_->get_struct } @{ $self->initplans } ] if defined $self->initplans;
  33         114  
  30         63  
565 545 100       1246 $reply->{ 'subplans' } = [ map { $_->get_struct } @{ $self->subplans } ] if defined $self->subplans;
  24         79  
  21         57  
566              
567 545 100       1200 $reply->{ 'buffers' } = $self->buffers->get_struct() if $self->buffers;
568              
569 545 100       1346 $reply->{ 'cte_order' } = clone( $self->cte_order ) if defined $self->cte_order;
570 545 100       1191 if ( defined $self->ctes ) {
571 9         22 $reply->{ 'ctes' } = {};
572 9         18 while ( my ( $key, $cte_node ) = each %{ $self->ctes } ) {
  27         53  
573 18         45 my $struct = $cte_node->get_struct;
574 18         69 $reply->{ 'ctes' }->{ $key } = $struct;
575             }
576             }
577 545         2244 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 891 my $self = shift;
588 614 100       1297 return unless defined $self->actual_time_last;
589 610 50       1190 return unless defined $self->actual_loops;
590 610         1092 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 168     168 1 226 my $self = shift;
601 168 50       345 return unless defined $self->actual_time_last;
602 168 50       302 return unless defined $self->actual_loops;
603 168 100       351 return $self->actual_loops * $self->actual_rows if 1 == $self->workers;
604 1         4 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 358 my $self = shift;
629 251 100       438 return 0 unless $self->extra_info;
630 249         357 my $removed = 0;
631 249         359 for my $line ( @{ $self->extra_info } ) {
  249         409  
632 502 100       1608 next unless $line =~ m{^Rows Removed by (?:Conflict Filter|Filter|Index Recheck|Join Filter): (\d+)$};
633 250         720 $removed += $1;
634             }
635 249 100       503 return $self->actual_loops * $removed if 1 == $self->workers;
636 1         3 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 438 my $self = shift;
647              
648 251         478 my $time = $self->total_inclusive_time;
649 251 100       509 return unless defined $time;
650              
651 250         509 for my $node ( map { @{ $_ } } grep { defined $_ } ( $self->sub_nodes ) ) {
  63         60  
  63         114  
  250         604  
652 84   50     145 $time -= ( $node->total_inclusive_time || 0 );
653             }
654              
655 250         493 for my $plan ( map { @{ $_ } } grep { defined $_ } ( $self->subplans ) ) {
  33         54  
  33         118  
  250         524  
656 34   100     92 $time -= ( $plan->total_inclusive_time || 0 );
657             }
658              
659             # Apply fix from ->exclusive_fix
660 250         571 $time += $self->exclusive_fix;
661              
662             # ignore negative times - these come from rounding errors on nodes with loops > 1.
663 250 100       560 return 0 if $time < 0;
664              
665 249         925 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       11 return unless $self->buffers;
678              
679 3         14 my @nodes = grep { $_->buffers } $self->all_subnodes;
  4         9  
680 3 50       12 return $self->buffers if 0 == scalar @nodes;
681              
682 3         9 my $sub_node_buffers = $nodes[ 0 ]->buffers;
683 3         6 shift @nodes;
684 3         9 for my $n ( @nodes ) {
685 1         4 $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 78 my $self = shift;
699 57         79 my @nodes = ();
700 57 100       107 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  29         61  
701 57 100       154 push @nodes, @{ $self->initplans } if $self->initplans;
  2         5  
702 57 100       100 push @nodes, @{ $self->subplans } if $self->subplans;
  4         8  
703 57 50       133 push @nodes, values %{ $self->ctes } if $self->ctes;
  0         0  
704 57         181 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 3266     3266 1 4665 my $self = shift;
715 3266         4765 my @nodes = ();
716 3266 100       6076 push @nodes, @{ $self->sub_nodes } if $self->sub_nodes;
  1380         2521  
717 3266 100       7802 push @nodes, @{ $self->initplans } if $self->initplans;
  113         217  
718 3266 100       6038 push @nodes, @{ $self->subplans } if $self->subplans;
  60         2227  
719 3266 100       5964 push @nodes, values %{ $self->ctes } if $self->ctes;
  75         147  
720              
721 3266         8781 return map { $_, $_->all_recursive_subnodes } @nodes;
  2234         5123  
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 13177 my $self = shift;
734 20         17 my @nodes = ();
735 20         20 my $current = $self;
736 20         31 while ( my $next = $current->parent ) {
737 79         75 unshift @nodes, $next;
738 79         115 $current = $next;
739             }
740 20         64 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 2567     2567 1 3886 my $self = shift;
751              
752 2567 100 66     6308 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 345     345 1 609 my $self = shift;
765 345         605 my $prefix = shift;
766 345 100       1017 $prefix = '' unless defined $prefix;
767              
768 345 100       1019 $prefix .= '-> ' if '' ne $prefix;
769 345         682 my $prefix_on_spaces = $prefix . " ";
770 345         2147 $prefix_on_spaces =~ s/[^ ]/ /g;
771              
772 345         1054 my $heading_line = $self->type;
773              
774 345 100       887 if ( $self->scan_on ) {
775 168         365 my $S = $self->scan_on;
776 168 100       1151 if ( $S->{ 'cte_name' } ) {
    100          
    100          
    100          
    100          
777 11         28 $heading_line .= " on " . $S->{ 'cte_name' };
778 11 100       40 $heading_line .= " " . $S->{ 'cte_alias' } if $S->{ 'cte_alias' };
779             }
780             elsif ( $S->{ 'function_name' } ) {
781 5         18 $heading_line .= " on " . $S->{ 'function_name' };
782 5 50       27 $heading_line .= " " . $S->{ 'function_alias' } if $S->{ 'function_alias' };
783             }
784             elsif ( $S->{ 'index_name' } ) {
785 52 100       179 if ( $S->{ 'table_name' } ) {
786 44         129 $heading_line .= " using " . $S->{ 'index_name' } . " on " . $S->{ 'table_name' };
787 44 100       157 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
788             }
789             else {
790 8         23 $heading_line .= " on " . $S->{ 'index_name' };
791             }
792             }
793             elsif ( $S->{ 'subquery_name' } ) {
794 1         4 $heading_line .= " on " . $S->{ 'subquery_name' },;
795             }
796             elsif ( $S->{ 'worktable_name' } ) {
797 4         10 $heading_line .= " on " . $S->{ 'worktable_name' },;
798 4 50       31 $heading_line .= " " . $S->{ 'worktable_alias' } if $S->{ 'worktable_alias' };
799             }
800             else {
801 95         264 $heading_line .= " on " . $S->{ 'table_name' };
802 95 100       373 $heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' };
803             }
804             }
805 345         1058 $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 345 100       1150 if ( $self->is_analyzed ) {
807 294         465 my $inner;
808 294 100       815 if ( $self->never_executed ) {
    100          
809 6         15 $inner = 'never executed';
810             }
811             elsif ( defined $self->actual_time_last ) {
812 280         729 $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         19 $inner = sprintf 'actual rows=%s loops=%d', $self->actual_rows, $self->actual_loops;
816             }
817 294         1019 $heading_line .= " ($inner)";
818             }
819              
820 345         725 my @lines = ();
821              
822 345         1099 push @lines, $prefix . $heading_line;
823 345 100       859 if ( $self->extra_info ) {
824 184         363 push @lines, $prefix_on_spaces . " " . $_ for @{ $self->extra_info };
  184         390  
825             }
826 345         1284 my $textual = join( "\n", @lines ) . "\n";
827 345 100       999 if ( $self->buffers ) {
828 111         256 my $buf_info = $self->buffers->as_text;
829 111         716 $buf_info =~ s/^/${prefix_on_spaces}/gm;
830 111         331 $textual .= $buf_info . "\n";
831             }
832              
833 345 100       951 if ( $self->cte_order ) {
834 9         14 for my $cte_name ( @{ $self->cte_order } ) {
  9         30  
835 11         31 $textual .= $prefix_on_spaces . "CTE " . $cte_name . "\n";
836 11         57 $textual .= $self->cte( $cte_name )->as_text( $prefix_on_spaces . " " );
837             }
838             }
839              
840 345 100       856 if ( $self->initplans ) {
841 11         22 for my $i ( 0 .. $#{ $self->initplans } ) {
  11         28  
842 12         32 my $ip = $self->initplans->[ $i ];
843 12         36 my $meta = $self->initplans_metainfo->[ $i ];
844 12         19 my $init_name;
845 12 100       33 if ( $meta ) {
846 8         49 $init_name = sprintf "InitPlan %d (returns %s)\n", $meta->{ 'name' }, $meta->{ 'returns' };
847             }
848             else {
849 4         10 $init_name = "InitPlan\n";
850             }
851 12         34 $textual .= $prefix_on_spaces . $init_name;
852 12         87 $textual .= $ip->as_text( $prefix_on_spaces . " " );
853             }
854             }
855 345 100       928 if ( $self->sub_nodes ) {
856 168         281 $textual .= $_->as_text( $prefix_on_spaces ) for @{ $self->sub_nodes };
  168         353  
857             }
858 345 100       1204 if ( $self->subplans ) {
859 6         14 for my $ip ( @{ $self->subplans } ) {
  6         19  
860 7         23 $textual .= $prefix_on_spaces . "SubPlan\n";
861 7         39 $textual .= $ip->as_text( $prefix_on_spaces . " " );
862             }
863             }
864 345         1822 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 61     61 1 201 my $self = shift;
875 61         155 my $anonymizer = shift;
876              
877 61 100       231 if ( $self->scan_on ) {
878 30         117 $anonymizer->add( values %{ $self->scan_on } );
  30         115  
879             }
880              
881 61 100       390 if ( $self->cte_order ) {
882 2         21 $anonymizer->add( $self->{ 'cte_order' } );
883             }
884              
885 61 100       296 if ( $self->extra_info ) {
886 46         91 for my $line ( @{ $self->extra_info } ) {
  46         115  
887 93         20990 my $copy = $line;
888 93 100       414 if ( $copy =~ m{^Foreign File:\s+(\S.*?)\s*$} ) {
889 3         13 $anonymizer->add( $1 );
890 3         8 next;
891             }
892 90 100       971 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 49         184 my $prefix = $1;
894 49         252 my $lexer = $self->_make_lexer( $copy );
895 49         54649 while ( my $x = $lexer->() ) {
896 1951 50       989448 next unless ref $x;
897 1951 100       11509 $anonymizer->add( $x->[ 1 ] ) if $x->[ 0 ] =~ m{\A (?: STRING_LITERAL | QUOTED_IDENTIFIER | IDENTIFIER ) \z}x;
898             }
899             }
900             }
901              
902 61         8388 for my $key ( qw( sub_nodes initplans subplans ) ) {
903 183 100       558 next unless $self->{ $key };
904 35         105 $_->anonymize_gathering( $anonymizer ) for @{ $self->{ $key } };
  35         372  
905             }
906              
907 61 100       1271 if ( $self->{ 'ctes' } ) {
908 2         5 $_->anonymize_gathering( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         18  
909             }
910 61         261 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 98     98   198 my $self = shift;
921 98         203 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 98         487 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 98         20373 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 98         1059 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 61     61 1 141 my $self = shift;
1057 61         183 my $anonymizer = shift;
1058              
1059 61 100       281 if ( $self->scan_on ) {
1060 30         163 while ( my ( $key, $value ) = each %{ $self->scan_on } ) {
  78         175  
1061 48         253 $self->scan_on->{ $key } = $anonymizer->anonymized( $value );
1062             }
1063             }
1064              
1065 61 100       230 if ( $self->cte_order ) {
1066 2         6 my @new_order = ();
1067 2         5 for my $cte_name ( @{ $self->cte_order } ) {
  2         7  
1068 2         23 my $new_name = $anonymizer->anonymized( $cte_name );
1069 2         7 push @new_order, $new_name;
1070 2         11 $self->ctes->{ $new_name } = delete $self->{ 'ctes' }->{ $cte_name };
1071             }
1072 2         8 $self->cte_order( \@new_order );
1073             }
1074              
1075 61 100       241 if ( $self->extra_info ) {
1076 46         101 my @new_extra_info = ();
1077 46         84 for my $line ( @{ $self->extra_info } ) {
  46         138  
1078 93 100       469 if ( $line =~ m{^(Foreign File:\s+)(\S.*?)(\s*)$} ) {
1079 3         14 push @new_extra_info, $1 . $anonymizer->anonymized( $2 ) . $3;
1080 3         36 next;
1081             }
1082 90 100       992 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 41         104 push @new_extra_info, $line;
1084 41         116 next;
1085             }
1086 49         197 my $output = $1;
1087 49         252 my $lexer = $self->_make_lexer( $line );
1088 49         59020 while ( my $x = $lexer->() ) {
1089 1951 50       988040 if ( ref $x ) {
1090 1951 100       8491 if ( $x->[ 0 ] eq 'STRING_LITERAL' ) {
    50          
    100          
1091 21         118 $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 621         3101 $output .= $anonymizer->anonymized( $x->[ 1 ] );
1098             }
1099             else {
1100 1309         4869 $output .= $x->[ 1 ];
1101             }
1102             }
1103             else {
1104 0         0 $output .= $x;
1105             }
1106             }
1107 49         29810 push @new_extra_info, $output;
1108             }
1109 46         305 $self->{ 'extra_info' } = \@new_extra_info;
1110             }
1111              
1112 61         201 for my $key ( qw( sub_nodes initplans subplans ) ) {
1113 183 100       620 next unless $self->{ $key };
1114 35         198 $_->anonymize_substitute( $anonymizer ) for @{ $self->{ $key } };
  35         441  
1115             }
1116              
1117 61 100       188 if ( $self->{ 'ctes' } ) {
1118 2         7 $_->anonymize_substitute( $anonymizer ) for values %{ $self->{ 'ctes' } };
  2         23  
1119             }
1120 61         237 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-2023 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