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   616 use v5.14;
  50         157  
2 50     50   253 use warnings;
  50         104  
  50         1289  
3 50     50   235 use utf8;
  50         88  
  50         478  
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   1494 use Type::Tiny::Role;
  50         96  
  50         2165  
55              
56             use Scalar::Util qw(blessed);
57 50     50   279 use Types::Standard qw(ArrayRef CodeRef Str Object InstanceOf Bool Num Int);
  50         91  
  50         2624  
58 50     50   337  
  50         108  
  50         432  
59             use Moo::Role;
60 50     50   59122
  50         103  
  50         315  
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 6801 $self->walk( prefix => sub {
79 13         25 my $a = shift;
80             my $level = shift;
81 23     23   38 my $parent = shift;
82 23         31 my $indent = ' ' x $level;
83 23         31 my @flags;
84 23         54 push(@flags, 'distinct') if ($a->distinct);
85 23         30 if (scalar(@{ $a->ordered })) {
86 23 100       433 my @orders;
87 23 100       173 foreach my $c (@{ $a->ordered }) {
  23         85  
88 1         3 my $dir = $c->ascending ? "↑" : "↓";
89 1         3 my $s = $dir . $c->expression->as_string;
  1         4  
90 1 50       8 push(@orders, $s);
91 1         12 }
92 1         4 push(@flags, "order: " . join('; ', @orders));
93             }
94 1         5 if (defined(my $cost = $a->cost)) {
95             push(@flags, "cost: $cost");
96 23 100       321 }
97 9         107 $string .= "-$indent " . $a->plan_as_string($level);
98             if (scalar(@flags)) {
99 23         198 $string .= ' (' . join(' ', @flags) . ")";
100 23 100       69 }
101 14         43 $string .= "\n";
102             });
103 23         58 return $string;
104 13         129 }
105 13         200  
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 17 }
116 8         44  
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 2266  
  4342         9338  
128 1464         2155 =item C<< subplans_of_type_are_variable_connected( $type ) >>
  5236         7594  
  2878         2973  
  2878         6333  
129 1464         4108  
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 38
142 7         19 =item C<< children_are_variable_connected( $type ) >>
143 7         28  
144 7         28 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 1356 # of children. Better indexing of the children by variables can speed
157 1120         1183 # this up.
  1120         2184  
158 1120         2016 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   1287
166 1127         1540 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       2113 }
172             }
173 1126         1295
174 1126         2206 #
175 2257         2555 my @remaining = keys %vars_by_child;
176 2257         2227 return 1 unless (scalar(@remaining));
  2257         4069  
177 3672         7563 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         2200 my $candidate = $remaining[$i];
183 1126 50       1749 my @candidate_vars = keys %{ $vars_by_child{$candidate} };
184 1126         1436 foreach my $var (@candidate_vars) {
185             if (exists $seen_vars{ $var }) {
186 1126         1367 foreach my $var (@candidate_vars) {
  1126         2771  
187 1126         2036 $seen_vars{$var}++;
188 1130         1816 }
189 1135         1307 # warn "connected with $var: " . $c[$candidate]->as_string;
190 1135         1248 splice(@remaining, $i, 1);
  1135         2190  
191 1135         1533 next LOOP;
192 1345 100       2058 }
193 1076         1327 }
194 1752         2225 }
195             # warn 'Not fully connected';
196             return 0;
197 1076         1385 }
198 1076         2365 # warn 'Fully connected';
199             return 1;
200             }
201             }
202              
203 54         231 use Moo::Role;
204             with 'Attean::API::Plan';
205             requires 'substitute_impl'; # $code = $plan->impl($model, $binding);
206 1072         3714
207             my $self = shift;
208             my $model = shift;
209             my $b = Attean::Result->new();
210             return $self->substitute_impl($model, $b);
211 50     50   58022 }
  50         123  
  50         246  
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   20241 if (exists $args{in_scope_variables}) {
  50         124  
  50         245  
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   22450
  50         117  
  50         321  
245 50     50   22194 # 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         110  
  50         196  
246             has 'expression' => (is => 'ro', isa => ConsumerOf['Attean::API::Expression'], required => 0, default => sub { Attean::ValueExpression->new( value => Attean::Literal->true ) });
247 50     50   34953 }
  50         110  
  50         209  
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