Release 0.97

1. Allow parsing of plans inside psql generated frames
2. Provide a way to easily get all subnodes, including subnodes of
   subnodes
3. Provide a way to easily get parents of given node
4. Remove unnecessary Data::Dumper loads in tests
5. Assume plans should be in UTF-8
parent d5b1c5d8
Revision history for Pg-Explain
0.97 2020/04/03
- Allow parsing of plans inside psql generated frames
- Provide a way to easily get all subnodes, including subnodes of subnodes
- Provide a way to easily get parents of given node
- Remove unnecessary Data::Dumper loads in tests
- Assume plans should be in UTF-8
0.96 2020/03/23
- Performance optimizations
- Fix handling of certains misaligned plans
......
......@@ -237,6 +237,67 @@ t/48-line-wrapped-plans.d/4.expect
t/48-line-wrapped-plans.d/4.plan
t/48-line-wrapped-plans.t
t/49-tid-scans.t
t/50-psql-frames.d/json-0-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/json-0-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/json-0-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/json-0-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/json-0-unaligned-ascii-double-double-double.plan
t/50-psql-frames.d/json-1-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/json-1-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/json-1-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/json-1-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/json-2-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/json-2-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/json-2-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/json-2-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/json-2-aligned-unicode-single-double-double.plan
t/50-psql-frames.d/json-2-aligned-unicode-single-double-single.plan
t/50-psql-frames.d/text-0-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/text-0-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/text-0-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/text-0-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/text-0-unaligned-ascii-double-double-double.plan
t/50-psql-frames.d/text-1-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/text-1-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/text-1-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/text-2-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/text-2-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/text-2-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/text-2-aligned-unicode-single-double-double.plan
t/50-psql-frames.d/text-2-aligned-unicode-single-double-single.plan
t/50-psql-frames.d/xml-0-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/xml-0-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/xml-0-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/xml-0-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/xml-0-unaligned-ascii-double-double-double.plan
t/50-psql-frames.d/xml-1-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/xml-1-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/xml-1-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/xml-1-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/xml-2-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/xml-2-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/xml-2-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/xml-2-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/xml-2-aligned-unicode-single-double-double.plan
t/50-psql-frames.d/xml-2-aligned-unicode-single-double-single.plan
t/50-psql-frames.d/yaml-0-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-0-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-0-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/yaml-0-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/yaml-0-unaligned-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-1-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-1-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-1-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/yaml-1-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/yaml-2-aligned-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-2-aligned-old-ascii-double-double-double.plan
t/50-psql-frames.d/yaml-2-aligned-unicode-double-double-double.plan
t/50-psql-frames.d/yaml-2-aligned-unicode-double-double-single.plan
t/50-psql-frames.d/yaml-2-aligned-unicode-single-double-double.plan
t/50-psql-frames.d/yaml-2-aligned-unicode-single-double-single.plan
t/50-psql-frames.t
t/51-all-recursive-subnodes.t
t/52-all-parents.t
t/99-manifest.t
t/perlcriticrc
t/perltidyrc
......
......@@ -42,39 +42,39 @@
"provides" : {
"Pg::Explain" : {
"file" : "lib/Pg/Explain.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::Analyzer" : {
"file" : "lib/Pg/Explain/Analyzer.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::From" : {
"file" : "lib/Pg/Explain/From.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::FromJSON" : {
"file" : "lib/Pg/Explain/FromJSON.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::FromText" : {
"file" : "lib/Pg/Explain/FromText.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::FromXML" : {
"file" : "lib/Pg/Explain/FromXML.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::FromYAML" : {
"file" : "lib/Pg/Explain/FromYAML.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::Node" : {
"file" : "lib/Pg/Explain/Node.pm",
"version" : "0.96"
"version" : "0.97"
},
"Pg::Explain::StringAnonymizer" : {
"file" : "lib/Pg/Explain/StringAnonymizer.pm",
"version" : "0.96"
"version" : "0.97"
}
},
"release_status" : "stable",
......@@ -83,6 +83,6 @@
"http://dev.perl.org/licenses/"
]
},
"version" : "0.96",
"version" : "0.97",
"x_serialization_backend" : "JSON::PP version 4.02"
}
......@@ -19,31 +19,31 @@ name: Pg-Explain
provides:
Pg::Explain:
file: lib/Pg/Explain.pm
version: '0.96'
version: '0.97'
Pg::Explain::Analyzer:
file: lib/Pg/Explain/Analyzer.pm
version: '0.96'
version: '0.97'
Pg::Explain::From:
file: lib/Pg/Explain/From.pm
version: '0.96'
version: '0.97'
Pg::Explain::FromJSON:
file: lib/Pg/Explain/FromJSON.pm
version: '0.96'
version: '0.97'
Pg::Explain::FromText:
file: lib/Pg/Explain/FromText.pm
version: '0.96'
version: '0.97'
Pg::Explain::FromXML:
file: lib/Pg/Explain/FromXML.pm
version: '0.96'
version: '0.97'
Pg::Explain::FromYAML:
file: lib/Pg/Explain/FromYAML.pm
version: '0.96'
version: '0.97'
Pg::Explain::Node:
file: lib/Pg/Explain/Node.pm
version: '0.96'
version: '0.97'
Pg::Explain::StringAnonymizer:
file: lib/Pg/Explain/StringAnonymizer.pm
version: '0.96'
version: '0.97'
requires:
Clone: '0'
Digest::SHA: '0'
......@@ -54,5 +54,5 @@ requires:
perl: '5.010'
resources:
license: http://dev.perl.org/licenses/
version: '0.96'
version: '0.97'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
package Pg::Explain;
use v5.6;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use autodie;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use Carp;
use Clone qw( clone );
use autodie;
use Data::Dumper;
use Pg::Explain::StringAnonymizer;
use Pg::Explain::FromText;
......@@ -17,11 +33,11 @@ Pg::Explain - Object approach at reading explain analyze output
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......@@ -141,8 +157,36 @@ sub source_filtered {
my $self = shift;
my $source = $self->source;
# Remove frames around, handles |, ║, │
$source =~ s/^(\||║|│)(.*)\1\r?\n/$2\n/gm;
# Remove separator lines from various types of borders
$source =~ s/^\+-+\+\r?\n//gm;
$source =~ s/^(-|─|═)\1+\r?\n//gm;
$source =~ s/^(├|╟|╠|╞)(─|═)\2*(┤|╢|╣|╡)\r?\n//gm;
# Remove more horizontal lines
$source =~ s/^\+-+\+\r?\n//gm;
$source =~ s/^└(─)+┘\r?\n//gm;
$source =~ s/^╚(═)+╝\r?\n//gm;
$source =~ s/^┌(─)+┐\r?\n//gm;
$source =~ s/^╔(═)+╗\r?\n//gm;
# Remove quotes around lines, both ' and "
$source =~ s/^(["'])(.*)\1\r?\n/$2\n/gm;
# Remove "+" line continuations
$source =~ s/\s*\+\r?\n/\n/g;
# Remove "↵" line continuations
$source =~ s/↵\r?\n/\n/g;
# Remove "query plan" header
$source =~ s/^\s*QUERY PLAN\s*\r?\n//m;
# Remove rowcount
$source =~ s/^\(\d+ rows?\)(\r?\n|\z)//gm;
return $source;
}
......@@ -182,7 +226,12 @@ sub new {
$self->_read_source_from_file();
}
elsif ( $args{ 'source' } ) {
$self->{ 'source' } = $args{ 'source' };
if ( Encode::is_utf8( $args{ 'source' } ) ) {
$self->{ 'source' } = $args{ 'source' };
}
else {
$self->{ 'source' } = decode( 'UTF-8', $args{ 'source' } );
}
}
else {
croak( 'One of (source, source_file) parameters has to be provided)' );
......
package Pg::Explain::Analyzer;
use v5.6;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use autodie;
use Carp;
use Data::Dumper;
......@@ -11,11 +27,11 @@ Pg::Explain::Analyzer - Some helper methods to analyze explains
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......
package Pg::Explain::From;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use Pg::Explain::Node;
use Carp;
......@@ -9,11 +26,11 @@ Pg::Explain::From - Base class for parsers of non-text explain formats.
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......@@ -111,7 +128,8 @@ sub make_node_from {
'actual_loops' => $struct->{ 'Actual Loops' },
);
$new_node->explain( $self->explain );
if ( $struct->{ 'Actual Loops' } == 0 ) {
if ( !$struct->{ 'Actual Loops' } ) {
$new_node->never_executed( 1 );
}
......
package Pg::Explain::FromJSON;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use base qw( Pg::Explain::From );
use JSON;
use Carp;
......@@ -10,11 +27,11 @@ Pg::Explain::FromJSON - Parser for explains in JSON format
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......
package Pg::Explain::FromText;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use Carp;
use Data::Dumper;
use Pg::Explain::Node;
......@@ -10,11 +27,11 @@ Pg::Explain::FromText - Parser for text based explains
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......
package Pg::Explain::FromXML;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use base qw( Pg::Explain::From );
use XML::Simple;
use Carp;
......@@ -10,11 +27,11 @@ Pg::Explain::FromXML - Parser for explains in XML format
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......
package Pg::Explain::FromYAML;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use base qw( Pg::Explain::From );
use YAML;
use Carp;
......@@ -10,11 +27,11 @@ Pg::Explain::FromYAML - Parser for explains in YAML format
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......
package Pg::Explain::Node;
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use v5.14;
use strict;
use warnings;
use warnings qw( FATAL utf8 );
use utf8;
use open qw( :std :utf8 );
use Unicode::Normalize qw( NFC );
use Unicode::Collate;
use Encode qw( decode );
if ( grep /\P{ASCII}/ => @ARGV ) {
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
}
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
use Clone qw( clone );
use HOP::Lexer qw( string_lexer );
use Carp;
use warnings;
use strict;
=head1 NAME
......@@ -12,11 +27,11 @@ Pg::Explain::Node - Class representing single node from query plan
=head1 VERSION
Version 0.96
Version 0.97
=cut
our $VERSION = '0.96';
our $VERSION = '0.97';
=head1 SYNOPSIS
......@@ -156,6 +171,10 @@ For more details, check ->add_cte method description.
Returns true if given node was not executed, according to plan.
=head2 parent
Parent node of current node, or undef if it's top node.
=head2 explain
Returns Pg::Explain for this node.
......@@ -166,22 +185,23 @@ sub actual_loops { my $self = shift; $self->{ 'actual_loops' }
sub actual_rows { my $self = shift; $self->{ 'actual_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_rows' }; }
sub actual_time_first { my $self = shift; $self->{ 'actual_time_first' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_first' }; }
sub actual_time_last { my $self = shift; $self->{ 'actual_time_last' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_last' }; }
sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; }
sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; }
sub estimated_rows { my $self = shift; $self->{ 'estimated_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_rows' }; }
sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_row_width' }; }
sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; }
sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; }
sub explain { my $self = shift; $self->{ 'explain' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'explain' }; }
sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; }
sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; }
sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; }
sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; }
sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; }
sub explain { my $self = shift; $self->{ 'explain' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'explain' }; }
sub parent { my $self = shift; $self->{ 'parent' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'parent' }; }
sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; }
sub sub_nodes { my $self = shift; $self->{ 'sub_nodes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'sub_nodes' }; }
sub subplans { my $self = shift; $self->{ 'subplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'subplans' }; }
sub type { my $self = shift; $self->{ 'type' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'type' }; }
sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; }
sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; }
sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; }
sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; }
=head2 new
......@@ -332,12 +352,13 @@ Example of plan with subplan:
=cut
sub add_subplan {
my $self = shift;
my $self = shift;
my @nodes = map { $_->parent( $self ); $_ } @_;
if ( $self->subplans ) {
push @{ $self->subplans }, @_;
push @{ $self->subplans }, @nodes;
}
else {
$self->subplans( [ @_ ] );
$self->subplans( [ @nodes ] );
}
return;
}
......@@ -362,12 +383,13 @@ Example of plan with initplan:
=cut
sub add_initplan {
my $self = shift;
my $self = shift;
my @nodes = map { $_->parent( $self ); $_ } @_;
if ( $self->initplans ) {
push @{ $self->initplans }, @_;
push @{ $self->initplans }, @nodes;
}
else {
$self->initplans( [ @_ ] );
$self->initplans( [ @nodes ] );
}
return;
}
......@@ -385,6 +407,8 @@ Since we need order (ctes are stored unordered, in hash), there is also $node->c
sub add_cte {
my $self = shift;
my ( $name, $cte ) = @_;
$cte->parent( $self );
if ( $self->ctes ) {
$self->ctes->{ $name } = $cte;
push @{ $self->cte_order }, $name;
......@@ -432,12 +456,13 @@ Node 'Limit' has 1 sub_plan, whi