File Coverage

blib/lib/Attean/API/Plan.pm
Criterion Covered Total %
statement 105 109 96.3
branch 14 16 87.5
condition n/a
subroutine 19 20 95.0
pod 5 6 83.3
total 143 151 94.7


line stmt bran cond sub pod time code
1 50     50   630 use v5.14;
  50         160  
2 50     50   268 use warnings;
  50         89  
  50         1269  
3 50     50   237 use utf8;
  50         101  
  50         401  
4              
5             =head1 NAME
6              
7             Attean::API::Plan - Query plan
8              
9             =head1 VERSION
10              
11             This document describes Attean::API::Plan version 0.032
12              
13             =head1 DESCRIPTION
14              
15             The Attean::API::Plan role defines a common API for all query plans.
16              
17             =head1 ATTRIBUTES
18              
19             =over 4
20              
21             =item C<< cost >>
22              
23             =item C<< distinct >>
24              
25             =item C<< item_type >>
26              
27             =item C<< in_scope_variables >>
28              
29             =item C<< ordered >>
30              
31             =back
32              
33             =head1 REQUIRED METHODS
34              
35             The following methods are required by the L<Attean::API::Plan> role:
36              
37             =over 4
38              
39             =item C<< impl( $model ) >>
40              
41             Returns a code reference that when called (without arguments), returns an
42             L<Attean::API::Iterator> object.
43              
44             =back
45              
46             =head1 METHODS
47              
48             =over 4
49              
50             =item C<< has_cost >>
51              
52             =cut
53              
54 50     50   1525 use Type::Tiny::Role;
  50         106  
  50         2240  
55              
56             use Scalar::Util qw(blessed);
57 50     50   297 use Types::Standard qw(ArrayRef CodeRef Str Object InstanceOf Bool Num Int);
  50         113  
  50         2735  
58 50     50   325  
  50         136  
  50         468  
59             use Moo::Role;
60 50     50   59368
  50         106  
  50         326  
61             has 'cost' => (is => 'rw', isa => Int, predicate => 'has_cost');
62             has 'distinct' => (is => 'rw', isa => Bool, required => 1, default => 0);
63             has 'item_type' => (is => 'ro', isa => Str, required => 1, default => 'Attean::API::Result');
64             has 'in_scope_variables' => (is => 'ro', isa => ArrayRef[Str], required => 1);
65             has 'ordered' => (is => 'ro', isa => ArrayRef, required => 1, default => sub { [] });
66            
67             requires 'impl';
68             requires 'plan_as_string';
69              
70             =item C<< as_string >>
71              
72             Returns a tree-structured string representation of this plan, including children.
73              
74             =cut
75            
76             my $self = shift;
77             my $string = '';
78 13     13 1 6799 $self->walk( prefix => sub {
79 13         20 my $a = shift;
80             my $level = shift;
81 23     23   32 my $parent = shift;
82 23         33 my $indent = ' ' x $level;
83 23         29 my @flags;
84 23         44 push(@flags, 'distinct') if ($a->distinct);
85 23         33 if (scalar(@{ $a->ordered })) {
86 23 100       412 my @orders;
87 23 100       144 foreach my $c (@{ $a->ordered }) {
  23         72  
88 1         3 my $dir = $c->ascending ? "↑" : "↓";
89 1         2 my $s = $dir . $c->expression->as_string;
  1         4  
90 1 50       4 push(@orders, $s);
91 1         7 }
92 1         3 push(@flags, "order: " . join('; ', @orders));
93             }
94 1         4 if (defined(my $cost = $a->cost)) {
95             push(@flags, "cost: $cost");
96 23 100       302 }
97 9         63 $string .= "-$indent " . $a->plan_as_string($level);
98             if (scalar(@flags)) {
99 23         158 $string .= ' (' . join(' ', @flags) . ")";
100 23 100       60 }
101 14         50 $string .= "\n";
102             });
103 23         60 return $string;
104 13         105 }
105 13         162  
106             =item C<< evaluate( $model ) >>
107              
108             Evaluates this plan and returns the resulting iterator.
109              
110             =cut
111              
112             my $self = shift;
113             my $impl = $self->impl(@_);
114             return $impl->();
115 8     8 1 14 }
116 8         29  
117 8         26 =item C<< in_scope_variables_union( @plans ) >>
118              
119             Returns the set union of C<< in_scope_variables >> of the given plan objects.
120              
121             =cut
122              
123             my @plans = grep { blessed($_) } @_;
124             my %vars = map { $_ => 1 } map { @{ $_->in_scope_variables } } @plans;
125             return keys %vars;
126             }
127 1464     1464 1 2961  
  4342         9444  
128 1464         2077 =item C<< subplans_of_type_are_variable_connected( $type ) >>
  5236         7744  
  2878         3068  
  2878         6421  
129 1464         4188  
130             Returns true if the subpatterns of the given C<< $type >> are all connected
131             through their C<< in_scope_variables >>, false otherwise (implying a cartesian
132             product if the connecting plans perform some form of join.
133              
134             =cut
135              
136             my $self = shift;
137             my @types = @_;
138             my @c = $self->subpatterns_of_type(@types);
139             return $self->_plans_are_variable_connected(@c);
140             }
141 7     7 1 31
142 7         15 =item C<< children_are_variable_connected( $type ) >>
143 7         24  
144 7         20 Returns true if the children of this plan are all connected
145             through their C<< in_scope_variables >>, false otherwise (implying a cartesian
146             product if this plan performs some form of join.
147              
148             =cut
149              
150             my $self = shift;
151             my @c = @{ $self->children };
152             return $self->_plans_are_variable_connected(@c);
153             }
154            
155             # TODO: In the worst case, this is going to run in O(n^2) in the number
156 1120     1120 1 1390 # of children. Better indexing of the children by variables can speed
157 1120         1200 # this up.
  1120         2259  
158 1120         2078 my $self = shift;
159             my @c = @_;
160             # warn "===========================\n";
161             # foreach my $c (@c) {
162             # warn $c->as_string;
163             # }
164             return 1 unless (scalar(@c));
165 1127     1127   1193
166 1127         1481 my %vars_by_child;
167             foreach my $i (0 .. $#c) {
168             my $c = $c[$i];
169             foreach my $var (@{ $c->in_scope_variables }) {
170             $vars_by_child{$i}{$var}++;
171 1127 100       1892 }
172             }
173 1126         1249
174 1126         2251 #
175 2257         2659 my @remaining = keys %vars_by_child;
176 2257         2264 return 1 unless (scalar(@remaining));
  2257         4335  
177 3672         7768 my $current = shift(@remaining);
178             # warn 'Starting with ' . $c[$current]->as_string;
179             my %seen_vars = %{ $vars_by_child{$current} };
180             LOOP: while (scalar(@remaining)) {
181             foreach my $i (0 .. $#remaining) {
182 1126         2220 my $candidate = $remaining[$i];
183 1126 50       1678 my @candidate_vars = keys %{ $vars_by_child{$candidate} };
184 1126         1514 foreach my $var (@candidate_vars) {
185             if (exists $seen_vars{ $var }) {
186 1126         1250 foreach my $var (@candidate_vars) {
  1126         2707  
187 1126         2224 $seen_vars{$var}++;
188 1129         1694 }
189 1134         1488 # warn "connected with $var: " . $c[$candidate]->as_string;
190 1134         1159 splice(@remaining, $i, 1);
  1134         2460  
191 1134         1553 next LOOP;
192 1365 100       2112 }
193 1075         1326 }
194 1800         2188 }
195             # warn 'Not fully connected';
196             return 0;
197 1075         1292 }
198 1075         2384 # warn 'Fully connected';
199             return 1;
200             }
201             }
202              
203 54         265 use Moo::Role;
204             with 'Attean::API::Plan';
205             requires 'substitute_impl'; # $code = $plan->impl($model, $binding);
206 1072         3735
207             my $self = shift;
208             my $model = shift;
209             my $b = Attean::Result->new();
210             return $self->substitute_impl($model, $b);
211 50     50   59463 }
  50         143  
  50         235  
212             }
213              
214             use Moo::Role;
215              
216 0     0 0   with 'Attean::API::Plan';
217 0          
218 0           around 'BUILDARGS' => sub {
219 0           my $orig = shift;
220             my $class = shift;
221             my %args = @_;
222             my @vars = Attean::API::Plan->in_scope_variables_union( @{ $args{children} } );
223            
224 50     50   20429 if (exists $args{in_scope_variables}) {
  50         137  
  50         225  
225             Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
226             }
227             $args{in_scope_variables} = [@vars];
228              
229             return $orig->( $class, %args );
230             };
231             }
232              
233             use Types::Standard qw(CodeRef);
234             use Types::Standard qw(ArrayRef Str ConsumerOf Bool);
235              
236             use Moo::Role;
237              
238             with 'Attean::API::Plan', 'Attean::API::BinaryQueryTree';
239             with 'Attean::API::UnionScopeVariablesPlan';
240            
241             has 'join_variables' => (is => 'ro', isa => ArrayRef[Str], required => 1);
242             has 'anti' => (is => 'ro', isa => Bool, default => 0); # is this an anti-join
243             has 'left' => (is => 'ro', isa => Bool, default => 0); # is this a left, outer-join
244 50     50   23199
  50         163  
  50         305  
245 50     50   22727 # if this is a left, outer-join, this is the filter expression that acts as part of the join operation (see the SPARQL semantics for LeftJoin for more details)
  50         107  
  50         207  
246             has 'expression' => (is => 'ro', isa => ConsumerOf['Attean::API::Expression'], required => 0, default => sub { Attean::ValueExpression->new( value => Attean::Literal->true ) });
247 50     50   35113 }
  50         121  
  50         218  
248              
249             1;
250              
251              
252             =back
253              
254             =head1 BUGS
255              
256             Please report any bugs or feature requests to through the GitHub web interface
257             at L<https://github.com/kasei/attean/issues>.
258              
259             =head1 SEE ALSO
260              
261              
262              
263             =head1 AUTHOR
264              
265             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
266              
267             =head1 COPYRIGHT
268              
269             Copyright (c) 2014--2022 Gregory Todd Williams.
270             This program is free software; you can redistribute it and/or modify it under
271             the same terms as Perl itself.
272              
273             =cut