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   638 use v5.14;
  50         166  
2 50     50   263 use warnings;
  50         109  
  50         1274  
3 50     50   216 use utf8;
  50         94  
  50         404  
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.033
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   1470 use Type::Tiny::Role;
  50         100  
  50         2177  
55              
56             use Scalar::Util qw(blessed);
57 50     50   297 use Types::Standard qw(ArrayRef CodeRef Str Object InstanceOf Bool Num Int);
  50         97  
  50         2676  
58 50     50   306  
  50         106  
  50         413  
59             use Moo::Role;
60 50     50   58574
  50         90  
  50         304  
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 7550 $self->walk( prefix => sub {
79 13         27 my $a = shift;
80             my $level = shift;
81 23     23   25 my $parent = shift;
82 23         29 my $indent = ' ' x $level;
83 23         29 my @flags;
84 23         43 push(@flags, 'distinct') if ($a->distinct);
85 23         27 if (scalar(@{ $a->ordered })) {
86 23 100       387 my @orders;
87 23 100       137 foreach my $c (@{ $a->ordered }) {
  23         74  
88 1         2 my $dir = $c->ascending ? "↑" : "↓";
89 1         2 my $s = $dir . $c->expression->as_string;
  1         3  
90 1 50       4 push(@orders, $s);
91 1         5 }
92 1         3 push(@flags, "order: " . join('; ', @orders));
93             }
94 1         3 if (defined(my $cost = $a->cost)) {
95             push(@flags, "cost: $cost");
96 23 100       297 }
97 9         55 $string .= "-$indent " . $a->plan_as_string($level);
98             if (scalar(@flags)) {
99 23         161 $string .= ' (' . join(' ', @flags) . ")";
100 23 100       53 }
101 14         31 $string .= "\n";
102             });
103 23         54 return $string;
104 13         105 }
105 13         148  
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 16 }
116 8         32  
117 8         28 =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 2554  
  4342         8381  
128 1464         1832 =item C<< subplans_of_type_are_variable_connected( $type ) >>
  5236         6843  
  2878         2656  
  2878         5516  
129 1464         3633  
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 34
142 7         15 =item C<< children_are_variable_connected( $type ) >>
143 7         24  
144 7         19 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 1155 # of children. Better indexing of the children by variables can speed
157 1120         1015 # this up.
  1120         1916  
158 1120         1745 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   1089
166 1127         1276 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       1561 }
172             }
173 1126         1057
174 1126         1853 #
175 2257         2258 my @remaining = keys %vars_by_child;
176 2257         1973 return 1 unless (scalar(@remaining));
  2257         3452  
177 3672         6984 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         2016 my $candidate = $remaining[$i];
183 1126 50       1478 my @candidate_vars = keys %{ $vars_by_child{$candidate} };
184 1126         1233 foreach my $var (@candidate_vars) {
185             if (exists $seen_vars{ $var }) {
186 1126         1283 foreach my $var (@candidate_vars) {
  1126         2486  
187 1126         1726 $seen_vars{$var}++;
188 1129         1454 }
189 1132         1155 # warn "connected with $var: " . $c[$candidate]->as_string;
190 1132         1077 splice(@remaining, $i, 1);
  1132         1992  
191 1132         1333 next LOOP;
192 1330 100       1730 }
193 1075         1080 }
194 1805         2002 }
195             # warn 'Not fully connected';
196             return 0;
197 1075         1177 }
198 1075         2538 # warn 'Fully connected';
199             return 1;
200             }
201             }
202              
203 54         223 use Moo::Role;
204             with 'Attean::API::Plan';
205             requires 'substitute_impl'; # $code = $plan->impl($model, $binding);
206 1072         3023
207             my $self = shift;
208             my $model = shift;
209             my $b = Attean::Result->new();
210             return $self->substitute_impl($model, $b);
211 50     50   58472 }
  50         118  
  50         229  
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   19962 if (exists $args{in_scope_variables}) {
  50         107  
  50         193  
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   22117
  50         130  
  50         304  
245 50     50   21989 # 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         100  
  50         193  
246             has 'expression' => (is => 'ro', isa => ConsumerOf['Attean::API::Expression'], required => 0, default => sub { Attean::ValueExpression->new( value => Attean::Literal->true ) });
247 50     50   34011 }
  50         107  
  50         198  
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