File Coverage

blib/lib/Attean/QueryPlanner.pm
Criterion Covered Total %
statement 352 609 57.8
branch 77 194 39.6
condition 8 38 21.0
subroutine 32 36 88.8
pod 10 10 100.0
total 479 887 54.0


line stmt bran cond sub pod time code
1 50     50   25385 use v5.14;
  50         169  
2 50     50   234 use warnings;
  50         90  
  50         1386  
3              
4             =head1 NAME
5              
6             Attean::QueryPlanner - Query planner
7              
8             =head1 VERSION
9              
10             This document describes Attean::QueryPlanner version 0.033
11              
12             =head1 SYNOPSIS
13              
14             use v5.14;
15             use Attean;
16             my $planner = Attean::QueryPlanner->new();
17             my $default_graphs = [ Attean::IRI->new('http://example.org/') ];
18             my $plan = $planner->plan_for_algebra( $algebra, $model, $default_graphs );
19             my $iter = $plan->evaluate($model);
20             while (my $result = $iter->next()) {
21             say $result->as_string;
22             }
23              
24             =head1 DESCRIPTION
25              
26             The Attean::QueryPlanner class is a base class implementing common behavior for
27             query planners. Subclasses will need to consume or compose the
28             L<Attean::API::JoinPlanner> role.
29              
30             Trivial sub-classes may consume L<Attean::API::NaiveJoinPlanner>, while more
31             complex planners may choose to implement complex join planning (e.g.
32             L<Attean::IDPQueryPlanner>).
33              
34             =head1 ATTRIBUTES
35              
36             =over 4
37              
38             =cut
39              
40 50     50   22496 use Attean::Algebra;
  50         191  
  50         8130  
41 50     50   30433 use Attean::Plan;
  50         218  
  50         2575  
42 50     50   22711 use Attean::Expression;
  50         206  
  50         2433  
43              
44             use Moo;
45 50     50   367 use Encode qw(encode);
  50         101  
  50         246  
46 50     50   15002 use Attean::RDF;
  50         115  
  50         2508  
47 50     50   295 use Scalar::Util qw(blessed reftype);
  50         107  
  50         433  
48 50     50   36730 use List::Util qw(reduce);
  50         110  
  50         2065  
49 50     50   277 use List::MoreUtils qw(all any);
  50         136  
  50         2315  
50 50     50   269 use Types::Standard qw(Int ConsumerOf InstanceOf);
  50         104  
  50         384  
51 50     50   46343 use URI::Escape;
  50         123  
  50         317  
52 50     50   31329 use Algorithm::Combinatorics qw(subsets);
  50         117  
  50         2607  
53 50     50   280 use List::Util qw(min);
  50         109  
  50         1648  
54 50     50   246 use Math::Cartesian::Product;
  50         111  
  50         1595  
55 50     50   296 use namespace::clean;
  50         110  
  50         1733  
56 50     50   251  
  50         108  
  50         362  
57             with 'Attean::API::QueryPlanner', 'MooX::Log::Any';
58             has 'counter' => (is => 'rw', isa => Int, default => 0);
59             has 'table_threshold' => (is => 'rw', isa => Int, default => 10);
60              
61             =back
62              
63             =head1 METHODS
64              
65             =over 4
66              
67             =item C<< new_temporary( $type ) >>
68              
69             Returns a new unique (in the context of the query planner) ID string that may
70             be used for things like fresh (temporary) variables. The C<< $type >> string is
71             used in the generated name to aid in identifying different uses for the names.
72              
73             =cut
74              
75             my $self = shift;
76             my $type = shift;
77 15     15 1 22 my $c = $self->counter;
78 15         23 $self->counter($c+1);
79 15         268 return sprintf('.%s-%d', $type, $c);
80 15         263 }
81 15         522  
82             =item C<< plan_for_algebra( $algebra, $model, \@active_graphs, \@default_graphs ) >>
83              
84             Returns the first plan returned from C<< plans_for_algebra >>.
85              
86             =cut
87              
88             my $self = shift;
89             my @plans = $self->plans_for_algebra(@_);
90             return shift(@plans);
91 26     26 1 2402 }
92 26         85
93 25         400 =item C<< plans_for_algebra( $algebra, $model, \@active_graphs, \@default_graphs ) >>
94              
95             Returns L<Attean::API::Plan> objects representing alternate query plans for
96             evaluating the query C<< $algebra >> against the C<< $model >>, using
97             the supplied C<< $active_graph >>.
98              
99             =cut
100              
101             my $self = shift;
102             my $algebra = shift;
103             my $model = shift;
104             my $active_graphs = shift;
105 134     134 1 226 my $default_graphs = shift;
106 134         166 my %args = @_;
107 134         172
108 134         165 if ($model->does('Attean::API::CostPlanner')) {
109 134         164 my @plans = $model->plans_for_algebra($algebra, $self, $active_graphs, $default_graphs, %args);
110 134         259 if (@plans) {
111             return @plans; # trust that the model knows better than us what plans are best
112 134 50       388 } else {
113 134         2445 $self->log->info("*** Model did not provide plans: $model");
114 134 100       324 }
115 2         9 }
116            
117 132         2497 Carp::confess "No algebra passed for evaluation" unless ($algebra);
118            
119             # TODO: propagate interesting orders
120             my $interesting = [];
121 132 50       14188
122             my @children = @{ $algebra->children };
123             my ($child) = $children[0];
124 132         232 if ($algebra->isa('Attean::Algebra::Query') or $algebra->isa('Attean::Algebra::Update')) {
125             return $self->plans_for_algebra($algebra->child, $model, $active_graphs, $default_graphs, %args);
126 132         177 } elsif ($algebra->isa('Attean::Algebra::BGP')) {
  132         446  
127 132         210 my $triples = $algebra->triples;
128 132 100 66     2289 my @triples = @$triples;
    100 66        
    100          
    100          
    100          
    100          
    50          
    50          
    100          
    100          
    50          
    100          
    50          
    50          
    100          
    100          
    100          
    50          
    50          
    50          
    0          
    0          
    0          
    0          
    0          
    0          
129 8         43 my %blanks;
130             foreach my $i (0 .. $#triples) {
131 69         162 my $t = $triples[$i];
132 69         135 my @nodes = $t->values;
133 69         92 my $changed = 0;
134 69         181 foreach (@nodes) {
135 100         159 if ($_->does('Attean::API::Blank')) {
136 100         275 $changed++;
137 100         187 my $id = $_->value;
138 100         180 unless (exists $blanks{$id}) {
139 300 100       3602 $blanks{$id} = Attean::Variable->new(value => $self->new_temporary('blank'));
140 6         74 }
141 6         20 $_ = $blanks{$id};
142 6 50       21 }
143 6         31 }
144            
145 6         310 if ($changed) {
146             my $new = Attean::TriplePattern->new(@nodes);
147             $triples[$i] = $new;
148             }
149 100 100       1462 }
150 6         97 my $bgp = Attean::Algebra::BGP->new( triples => \@triples );
151 6         192 my @plans = $self->bgp_join_plans($bgp, $model, $active_graphs, $default_graphs, $interesting, map {
152             [$self->access_plans($model, $active_graphs, $_)]
153             } @triples);
154 69         1206 return @plans;
155             } elsif ($algebra->isa('Attean::Algebra::Join')) {
156 69         205 return $self->group_join_plans($model, $active_graphs, $default_graphs, $interesting, map {
  100         300  
157             [$self->plans_for_algebra($_, $model, $active_graphs, $default_graphs, %args)]
158 68         443 } @children);
159             } elsif ($algebra->isa('Attean::Algebra::Distinct') or $algebra->isa('Attean::Algebra::Reduced')) {
160             my @plans = $self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args);
161 11         26 my @dist;
  22         159  
162             foreach my $p (@plans) {
163             if ($p->distinct) {
164 6         130 push(@dist, $p);
165 6         15 } else {
166 6         14 my @vars = @{ $p->in_scope_variables };
167 38 100       3999 my $cmps = $p->ordered;
168 12         70 if ($self->_comparators_are_stable_and_cover_vars($cmps, @vars)) {
169             # the plan has a stable ordering which covers all the variables, so we can just uniq the iterator
170 26         140 push(@dist, Attean::Plan::Unique->new(children => [$p], distinct => 1, ordered => $p->ordered));
  26         78  
171 26         42 } else {
172 26 50       77 # TODO: if the plan isn't distinct, but is ordered, we can use a batched implementation
173             push(@dist, Attean::Plan::HashDistinct->new(children => [$p], distinct => 1, ordered => $p->ordered));
174 0         0 }
175             }
176             }
177 26         362 return @dist;
178             } elsif ($algebra->isa('Attean::Algebra::Filter')) {
179             # TODO: simple range relation filters can be handled differently if that filter operates on a variable that is part of the ordering
180             my $expr = $algebra->expression;
181 6         468 my $w = Attean::TreeRewriter->new(types => ['Attean::API::DirectedAcyclicGraph']);
182             $w->register_pre_handler(sub {
183             my ($t, $parent, $thunk) = @_;
184 6         26 if ($t->isa('Attean::ExistsExpression')) {
185 6         79 my $pattern = $t->pattern;
186             my $plan = $self->plan_for_algebra($pattern, $model, $active_graphs, $default_graphs, @_);
187 12     12   486 unless ($plan->does('Attean::API::BindingSubstitutionPlan')) {
188 12 50       77 die 'Exists plan does not consume Attean::API::BindingSubstitutionPlan: ' . $plan->as_string;
189 0         0 }
190 0         0 my $new = Attean::ExistsPlanExpression->new(
191 0 0       0 plan => $plan,
192 0         0 );
193             return (1, 0, $new);
194 0         0 }
195             return (0, 1, $t);
196             });
197 0         0 my ($changed, $rewritten) = $w->rewrite($expr, {});
198             if ($changed) {
199 12         29 $expr = $rewritten;
200 6         313 }
201 6         52
202 6 50       18 my $var = $self->new_temporary('filter');
203 0         0 my %exprs = ($var => $expr);
204            
205             my @plans;
206 6         17 foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
207 6         26 my $distinct = $plan->distinct;
208             my $ordered = $plan->ordered;
209 6         10 if ($expr->isa('Attean::ValueExpression') and $expr->value->does('Attean::API::Variable')) {
210 6         18 my $filtered = Attean::Plan::EBVFilter->new(children => [$plan], variable => $expr->value->value, distinct => $distinct, ordered => $ordered);
211 6         80 push(@plans, $filtered);
212 6         50 } else {
213 6 100 66     41 my @vars = ($var);
214 3         95 my @inscope = ($var, @{ $plan->in_scope_variables });
215 3         514 my @pvars = map { Attean::Variable->new($_) } @{ $plan->in_scope_variables };
216             my $extend = Attean::Plan::Extend->new(children => [$plan], expressions => \%exprs, distinct => 0, ordered => $ordered);
217 3         9 my $filtered = Attean::Plan::EBVFilter->new(children => [$extend], variable => $var, distinct => 0, ordered => $ordered);
218 3         5 my $proj = $self->new_projection($filtered, $distinct, @{ $plan->in_scope_variables });
  3         11  
219 3         7 push(@plans, $proj);
  3         43  
  3         8  
220 3         171 }
221 3         832 }
222 3         600 return @plans;
  3         37  
223 3         1212 } elsif ($algebra->isa('Attean::Algebra::OrderBy')) {
224             # TODO: no-op if already ordered
225             my @cmps = @{ $algebra->comparators };
226 6         81 my ($exprs, $ascending, $svars) = $self->_order_by($algebra);
227             my @plans;
228             foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, interesting_order => $algebra->comparators, %args)) {
229 3         7 my $distinct = $plan->distinct;
  3         15  
230 3         24  
231 3         6 if (scalar(@cmps) == 1 and $cmps[0]->expression->isa('Attean::ValueExpression') and $cmps[0]->expression->value->does('Attean::API::Variable')) {
232 3         16 # TODO: extend this to handle more than one comparator, so long as they are *all* just variables (and not complex expressions)
233 3         45 # If we're sorting by just a variable name, don't bother creating new variables for the sort expressions, use the underlying variable directy
234             my @vars = @{ $plan->in_scope_variables };
235 3 50 33     66 my @pvars = map { Attean::Variable->new($_) } @{ $plan->in_scope_variables };
      33        
236             my $var = $cmps[0]->expression->value->value;
237             my $ascending = { $var => $cmps[0]->ascending };
238 3         48 my $ordered = Attean::Plan::OrderBy->new(children => [$plan], variables => [$var], ascending => $ascending, distinct => $distinct, ordered => \@cmps);
  3         16  
239 3         5 push(@plans, $ordered);
  3         45  
  3         9  
240 3         139 } else {
241 3         12 my @vars = (@{ $plan->in_scope_variables }, keys %$exprs);
242 3         32 my @pvars = map { Attean::Variable->new($_) } @{ $plan->in_scope_variables };
243 3         619 my $extend = Attean::Plan::Extend->new(children => [$plan], expressions => $exprs, distinct => 0, ordered => $plan->ordered);
244             my $ordered = Attean::Plan::OrderBy->new(children => [$extend], variables => $svars, ascending => $ascending, distinct => 0, ordered => \@cmps);
245 0         0 my $proj = $self->new_projection($ordered, $distinct, @{ $plan->in_scope_variables });
  0         0  
246 0         0 push(@plans, $proj);
  0         0  
  0         0  
247 0         0 }
248 0         0 }
249 0         0
  0         0  
250 0         0 return @plans;
251             } elsif ($algebra->isa('Attean::Algebra::LeftJoin')) {
252             my $l = [$self->plans_for_algebra($children[0], $model, $active_graphs, $default_graphs, %args)];
253             my $r = [$self->plans_for_algebra($children[1], $model, $active_graphs, $default_graphs, %args)];
254 3         17 return $self->join_plans($model, $active_graphs, $default_graphs, $l, $r, 'left', $algebra->expression);
255             } elsif ($algebra->isa('Attean::Algebra::Minus')) {
256 0         0 my $l = [$self->plans_for_algebra($children[0], $model, $active_graphs, $default_graphs, %args)];
257 0         0 my $r = [$self->plans_for_algebra($children[1], $model, $active_graphs, $default_graphs, %args)];
258 0         0 return $self->join_plans($model, $active_graphs, $default_graphs, $l, $r, 'minus');
259             } elsif ($algebra->isa('Attean::Algebra::Project')) {
260 0         0 my $vars = $algebra->variables;
261 0         0 my @vars = map { $_->value } @{ $vars };
262 0         0 my $vars_key = join(' ', sort @vars);
263             my $distinct = 0;
264 8         32 my @plans = map {
265 8         14 ($vars_key eq join(' ', sort @{ $_->in_scope_variables }))
  14         50  
  8         19  
266 8         37 ? $_ # no-op if plan is already properly-projected
267 8         17 : $self->new_projection($_, $distinct, @vars)
268             } $self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args);
269 8 100       239 return @plans;
  14         667  
  14         92  
270             } elsif ($algebra->isa('Attean::Algebra::Graph')) {
271             my $graph = $algebra->graph;
272             if ($graph->does('Attean::API::Term')) {
273 8         1245 if (my $available = $args{available_graphs}) {
274             # the list of available graphs has been restricted, and this
275 10         36 # graph is not available so return an empty table plan.
276 10 100       32 unless (any { $_->equals($graph) } @$available) {
277 5 100       98 my $plan = Attean::Plan::Table->new( variables => [], rows => [], distinct => 0, ordered => [] );
278             return $plan;
279             }
280 2 100   2   18 }
  2         10  
281 1         26 return $self->plans_for_algebra($child, $model, [$graph], $default_graphs, %args);
282 1         179 } else {
283             my $gvar = $graph->value;
284             my $graphs = $model->get_graphs;
285 4         71 my @plans;
286             my %vars = map { $_ => 1 } $child->in_scope_variables;
287 5         115 $vars{ $gvar }++;
288 5         92 my @vars = keys %vars;
289 5         175
290 5         28 my %available;
  3         157  
291 5         96 if (my $available = $args{available_graphs}) {
292 5         18 foreach my $a (@$available) {
293             $available{ $a->value }++;
294 5         8 }
295 5 100       20 $graphs = $graphs->grep(sub { $available{ $_->value } });
296 2         6 }
297 4         18
298             my @branches;
299 2     4   29 my %ignore = map { $_->value => 1 } @$default_graphs;
  4         27  
300             while (my $graph = $graphs->next) {
301             next if $ignore{ $graph->value };
302 5         77 my %exprs = ($gvar => Attean::ValueExpression->new(value => $graph));
303 5         11 # TODO: rewrite $child pattern here to replace any occurrences of the variable $gvar to $graph
  2         10  
304 5         37 my @plans = map {
305 3 50       8 Attean::Plan::Extend->new(children => [$_], expressions => \%exprs, distinct => 0, ordered => $_->ordered);
306 3         388 } $self->plans_for_algebra($child, $model, [$graph], $default_graphs, %args);
307             push(@branches, \@plans);
308             }
309 3         17
  3         64  
310             if (scalar(@branches) == 1) {
311 3         723 @plans = @{ shift(@branches) };
312             } else {
313             cartesian { push(@plans, Attean::Plan::Union->new(children => [@_], distinct => 0, ordered => [])) } @branches;
314 5 100       16 }
315 1         3 return @plans;
  1         3  
316             }
317 4     4   48 } elsif ($algebra->isa('Attean::Algebra::Table')) {
  4         192  
318             my $rows = $algebra->rows;
319 5         720 my $vars = $algebra->variables;
320             my @vars = map { $_->value } @{ $vars };
321            
322 0         0 if (scalar(@$rows) < $self->table_threshold) {
323 0         0 return Attean::Plan::Table->new( variables => $vars, rows => $rows, distinct => 0, ordered => [] );
324 0         0 } else {
  0         0  
  0         0  
325             my $iter = Attean::ListIterator->new(
326 0 0       0 item_type => 'Attean::API::Result',
327 0         0 variables => \@vars,
328             values => $rows
329 0         0 );
330             return Attean::Plan::Iterator->new( iterator => $iter, distinct => 0, ordered => [] );
331             }
332             } elsif ($algebra->isa('Attean::Algebra::Service')) {
333             my $endpoint = $algebra->endpoint;
334 0         0 my $silent = $algebra->silent;
335             my $sparql = sprintf('SELECT * WHERE { %s }', $child->as_sparql);
336             my @vars = $child->in_scope_variables;
337 4         19 my $plan = Attean::Plan::Service->new(
338 4         18 request_signer => $self->request_signer,
339 4         16 endpoint => $endpoint,
340 4         95 silent => $silent,
341 4         252 sparql => $sparql,
342             distinct => 0,
343             in_scope_variables => \@vars,
344             ordered => []
345             );
346             return $plan;
347             } elsif ($algebra->isa('Attean::Algebra::Slice')) {
348             my $limit = $algebra->limit;
349             my $offset = $algebra->offset;
350 4         882 my @plans;
351             foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
352 0         0 my $vars = $plan->in_scope_variables;
353 0         0 push(@plans, Attean::Plan::Slice->new(children => [$plan], limit => $limit, offset => $offset, distinct => $plan->distinct, ordered => $plan->ordered));
354 0         0 }
355 0         0 return @plans;
356 0         0 } elsif ($algebra->isa('Attean::Algebra::Union')) {
357 0         0 # TODO: if both branches are similarly ordered, we can use Attean::Plan::Merge to keep the resulting plan ordered
358             my @vars = keys %{ { map { map { $_ => 1 } $_->in_scope_variables } @children } };
359 0         0 my @plansets = map { [$self->plans_for_algebra($_, $model, $active_graphs, $default_graphs, %args)] } @children;
360              
361             my @plans;
362 0         0 cartesian {
  0         0  
  0         0  
  0         0  
363 0         0 push(@plans, Attean::Plan::Union->new(children => \@_, distinct => 0, ordered => []))
  0         0  
364             } @plansets;
365 0         0 return @plans;
366             } elsif ($algebra->isa('Attean::Algebra::Extend')) {
367 0     0   0 my $var = $algebra->variable->value;
368 0         0 my $expr = $algebra->expression;
369 0         0 my %exprs = ($var => $expr);
370             my @vars = $algebra->in_scope_variables;
371 1         4  
372 1         3 my @plans;
373 1         2 foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
374 1         4 my $extend = Attean::Plan::Extend->new(children => [$plan], expressions => \%exprs, distinct => 0, ordered => $plan->ordered);
375             push(@plans, $extend);
376 1         63 }
377 1         73 return @plans;
378 4         61 } elsif ($algebra->isa('Attean::Algebra::Group')) {
379 4         747 my $aggs = $algebra->aggregates;
380             my $groups = $algebra->groupby;
381 1         4 my %exprs;
382             foreach my $expr (@$aggs) {
383 1         9 my $var = $expr->variable->value;
384 1         4 $exprs{$var} = $expr;
385 1         2 }
386 1         2 my @plans;
387 1         4 foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
388 1         3 my $extend = Attean::Plan::Aggregate->new(children => [$plan], aggregates => \%exprs, groups => $groups, distinct => 0, ordered => []);
389             push(@plans, $extend);
390 1         2 }
391 1         36 return @plans;
392 4         51 } elsif ($algebra->isa('Attean::Algebra::Ask')) {
393 4         941 my @plans;
394             foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
395 1         4 return Attean::Plan::Exists->new(children => [$plan], distinct => 1, ordered => []);
396             }
397 4         24 return @plans;
398 4         117 } elsif ($algebra->isa('Attean::Algebra::Path')) {
399 4         54 my $s = $algebra->subject;
400             my $path = $algebra->path;
401 0         0 my $o = $algebra->object;
402            
403 0         0 my @algebra = $self->simplify_path($s, $path, $o);
404 0         0
405 0         0 my @join;
406             if (scalar(@algebra)) {
407 0         0 my @triples;
408             while (my $pa = shift(@algebra)) {
409 0         0 if ($pa->isa('Attean::TriplePattern')) {
410 0 0 0     0 push(@triples, $pa);
    0          
    0          
411 0         0 } else {
412 0         0 if (scalar(@triples)) {
413 0 0       0 push(@join, Attean::Algebra::BGP->new( triples => [@triples] ));
414 0         0 @triples = ();
415             }
416 0 0       0 push(@join, $pa);
417 0         0 }
418 0         0 }
419             if (scalar(@triples)) {
420 0         0 push(@join, Attean::Algebra::BGP->new( triples => [@triples] ));
421             }
422            
423 0 0       0 my @vars = $algebra->in_scope_variables;
424 0         0
425             my @joins = $self->group_join_plans($model, $active_graphs, $default_graphs, $interesting, map {
426             [$self->plans_for_algebra($_, $model, $active_graphs, $default_graphs, %args)]
427 0         0 } @join);
428            
429             my @plans;
430 0         0 foreach my $j (@joins) {
  0         0  
431             push(@plans, Attean::Plan::Project->new(children => [$j], variables => [map { Attean::Variable->new($_) } @vars], distinct => 0, ordered => []));
432             }
433 0         0 return @plans;
434 0         0
435 0         0 } elsif ($path->isa('Attean::Algebra::ZeroOrMorePath') or $path->isa('Attean::Algebra::OneOrMorePath')) {
  0         0  
436             my $skip = $path->isa('Attean::Algebra::OneOrMorePath') ? 1 : 0;
437 0         0 my $begin = Attean::Variable->new(value => $self->new_temporary('pp'));
438             my $end = Attean::Variable->new(value => $self->new_temporary('pp'));
439             my $s_var = $s->does('Attean::API::Variable');
440 0 0       0 my $o_var = $o->does('Attean::API::Variable');
441 0         0
442 0         0 my $child = $path->children->[0];
443 0         0 my $a;
444 0         0 if ($s_var and not($o_var)) {
445             my $inv = Attean::Algebra::InversePath->new( children => [$child] );
446 0         0 $a = Attean::Algebra::Path->new( subject => $end, path => $inv, object => $begin );
447 0         0 } else {
448 0 0 0     0 $a = Attean::Algebra::Path->new( subject => $begin, path => $child, object => $end );
449 0         0 }
450 0         0 my @cplans = $self->plans_for_algebra($a, $model, $active_graphs, $default_graphs, %args);
451             my @plans;
452 0         0 foreach my $cplan (@cplans) {
453             my $plan = Attean::Plan::ALPPath->new(
454 0         0 subject => $s,
455 0         0 children => [$cplan],
456 0         0 object => $o,
457 0         0 graph => $active_graphs,
458             skip => $skip,
459             step_begin => $begin,
460             step_end => $end,
461             distinct => 0,
462             ordered => []
463             );
464             push(@plans, $plan);
465             }
466             return @plans;
467             } elsif ($path->isa('Attean::Algebra::ZeroOrOnePath')) {
468 0         0 my $a = Attean::Algebra::Path->new( subject => $s, path => $path->children->[0], object => $o );
469             my @children = $self->plans_for_algebra($a, $model, $active_graphs, $default_graphs, %args);
470 0         0 my @plans;
471             foreach my $plan (@children) {
472 0         0 push(@plans, Attean::Plan::ZeroOrOnePath->new(
473 0         0 subject => $s,
474 0         0 children => [$plan],
475 0         0 object => $o,
476 0         0 graph => $active_graphs,
477             distinct => 0,
478             ordered => []
479             ));
480             }
481             return @plans;
482             } else {
483             die "Cannot simplify property path $path: " . $algebra->as_string;
484             }
485 0         0 } elsif ($algebra->isa('Attean::Algebra::Construct')) {
486             my @children = $self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args);
487 0         0 my @plans;
488             foreach my $plan (@children) {
489             push(@plans, Attean::Plan::Construct->new(triples => $algebra->triples, children => [$plan], distinct => 0, ordered => []));
490 0         0 }
491 0         0 return @plans;
492 0         0 } elsif ($algebra->isa('Attean::Algebra::Describe')) {
493 0         0 my @children = $self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args);
494             my @plans;
495 0         0 foreach my $plan (@children) {
496             push(@plans, Attean::Plan::Describe->new(terms => $algebra->terms, graph => $active_graphs, children => [$plan], distinct => 0, ordered => []));
497 1         4 }
498 1         1 return @plans;
499 1         2 } elsif ($algebra->isa('Attean::Algebra::Clear')) {
500 1         10 my $plan_class = $algebra->drop ? 'Attean::Plan::Drop' : 'Attean::Plan::Clear';
501             my $target = $algebra->target;
502 1         196 if ($target eq 'GRAPH') {
503             return Attean::Plan::Clear->new(graphs => [$algebra->graph]);
504 0 0       0 } else {
505 0         0 my %default = map { $_->value => 1 } @$active_graphs;
506 0 0       0 my $graphs = $model->get_graphs;
507 0         0 my @graphs;
508             while (my $graph = $graphs->next) {
509 0         0 if ($target eq 'ALL') {
  0         0  
510 0         0 push(@graphs, $graph);
511 0         0 } else {
512 0         0 if ($target eq 'DEFAULT' and $default{ $graph->value }) {
513 0 0       0 push(@graphs, $graph);
514 0         0 } elsif ($target eq 'NAMED' and not $default{ $graph->value }) {
515             push(@graphs, $graph);
516 0 0 0     0 }
    0 0        
517 0         0 }
518             }
519 0         0 return $plan_class->new(graphs => \@graphs);
520             }
521             } elsif ($algebra->isa('Attean::Algebra::Add')) {
522             my $triple = triplepattern(variable('s'), variable('p'), variable('o'));
523 0         0 my $child;
524             my $default_source = 0;
525             if (my $from = $algebra->source) {
526 0         0 ($child) = $self->access_plans( $model, $active_graphs, $triple->as_quad_pattern($from) );
527 0         0 } else {
528 0         0 $default_source++;
529 0 0       0 my $bgp = Attean::Algebra::BGP->new( triples => [$triple] );
530 0         0 ($child) = $self->plans_for_algebra($bgp, $model, $active_graphs, $default_graphs, %args);
531             }
532 0         0
533 0         0 my $dest;
534 0         0 my $default_dest = 0;
535             if (my $g = $algebra->destination) {
536             $dest = $triple->as_quad_pattern($g);
537 0         0 } else {
538 0         0 $default_dest++;
539 0 0       0 $dest = $triple->as_quad_pattern($default_graphs->[0]);
540 0         0 }
541              
542 0         0  
543 0         0 my @plans;
544             my $run_update = 1;
545             if ($default_dest and $default_source) {
546             $run_update = 0;
547 0         0 } elsif ($default_dest or $default_source) {
548 0         0 #
549 0 0 0     0 } elsif ($algebra->source->equals($algebra->destination)) {
    0 0        
    0          
550 0         0 $run_update = 0;
551             }
552            
553             if ($run_update) {
554 0         0 if ($algebra->drop_destination) {
555             my @graphs = $algebra->has_destination ? $algebra->destination : @$default_graphs;
556             unshift(@plans, Attean::Plan::Clear->new(graphs => [@graphs]));
557 0 0       0 }
558 0 0       0
559 0 0       0 push(@plans, Attean::Plan::TripleTemplateToModelQuadMethod->new(
560 0         0 graph => $default_graphs->[0],
561             order => ['add_quad'],
562             patterns => {'add_quad' => [$dest]},
563 0         0 children => [$child],
564             ));
565            
566             if ($algebra->drop_source) {
567             my @graphs = $algebra->has_source ? $algebra->source : @$default_graphs;
568             push(@plans, Attean::Plan::Clear->new(graphs => [@graphs]));
569             }
570 0 0       0 }
571 0 0       0 my $plan = (scalar(@plans) == 1) ? shift(@plans) : Attean::Plan::Sequence->new( children => \@plans );
572 0         0 return $plan;
573             } elsif ($algebra->isa('Attean::Algebra::Modify')) {
574             unless ($child) {
575 0 0       0 # This is an INSERT/DELETE DATA algebra with ground data and no pattern
576 0         0 $child = Attean::Algebra::BGP->new( triples => [] );
577             }
578 0 0       0
579             my $dataset = $algebra->dataset;
580 0         0 my @default = @{ $dataset->{default} || [] };
581             my @named = values %{ $dataset->{named} || {} };
582            
583 0         0 my @active_graphs = @$active_graphs;
584 0 0       0 my @default_graphs = @$default_graphs;
  0         0  
585 0 0       0
  0         0  
586             if (scalar(@default) or scalar(@named)) {
587 0         0 # change the available named graphs
588 0         0 # change the active graph(s)
589             @active_graphs = @default;
590 0 0 0     0 @default_graphs = @default;
591             $args{ available_graphs } = [@named];
592             } else {
593 0         0 # no custom dataset
594 0         0 }
595 0         0
596             my @children = $self->plans_for_algebra($child, $model, \@active_graphs, \@default_graphs, %args);
597             my $i = $algebra->insert;
598             my $d = $algebra->delete;
599             my %patterns;
600 0         0 my @order;
601 0         0 if (scalar(@$d)) {
602 0         0 push(@order, 'remove_quad');
603 0         0 $patterns{ 'remove_quad' } = $d;
604             }
605 0 0       0 if (scalar(@$i)) {
606 0         0 push(@order, 'add_quad');
607 0         0 $patterns{ 'add_quad' } = $i;
608             }
609 0 0       0 return map {
610 0         0 Attean::Plan::TripleTemplateToModelQuadMethod->new(
611 0         0 graph => $default_graphs->[0],
612             order => \@order,
613             patterns => \%patterns,
614 0         0 children => [$_],
  0         0  
615             )
616             } @children;
617             } elsif ($algebra->isa('Attean::Algebra::Load')) {
618             my $pattern = triplepattern(variable('subject'), variable('predicate'), variable('object'));
619             my $load = Attean::Plan::Load->new( url => $algebra->url->value, silent => $algebra->silent );
620             my $graph = $algebra->has_graph ? $algebra->graph : $default_graphs->[0];
621             my $plan = Attean::Plan::TripleTemplateToModelQuadMethod->new(
622 0         0 graph => $graph,
623 0         0 order => ['add_quad'],
624 0 0       0 patterns => {'add_quad' => [$pattern]},
625 0         0 children => [$load],
626             );
627             return $plan;
628             } elsif ($algebra->isa('Attean::Algebra::Create')) {
629             return Attean::Plan::Sequence->new( children => [] );
630             } elsif ($algebra->isa('Attean::Algebra::Sequence')) {
631 0         0 my @plans;
632             foreach my $child (@{ $algebra->children }) {
633 0         0 my ($plan) = $self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args);
634             push(@plans, $plan);
635 0         0 }
636 0         0 return Attean::Plan::Sequence->new( children => \@plans );
  0         0  
637 0         0 }
638 0         0 die "Unimplemented algebra evaluation for: " . $algebra->as_string;
639             }
640 0         0
641             # sub plans_for_unbounded_path {
642 0         0 # my $self = shift;
643             # my $algebra = shift;
644             # my $model = shift;
645             # my $active_graphs = shift;
646             # my $default_graphs = shift;
647             # my %args = @_;
648             #
649             # my $s = $algebra->subject;
650             # my $path = $algebra->path;
651             # my $o = $algebra->object;
652             #
653             # return Attean::Plan::ALPPath->new(distinct => 0, ordered => []);
654             # }
655            
656             my $self = shift;
657             my @args = @_;
658            
659             my @bgptriples = map { @{ $_->triples } } grep { $_->isa('Attean::Algebra::BGP') } @args;
660             my @triples = grep { $_->isa('Attean::TriplePattern') } @args;
661 0     0   0 my @rest = grep { not $_->isa('Attean::Algebra::BGP') and not $_->isa('Attean::TriplePattern') } @args;
662 0         0 if (scalar(@rest) == 0) {
663             return Attean::Algebra::BGP->new( triples => [@bgptriples, @triples] );
664 0         0 } else {
  0         0  
  0         0  
  0         0  
665 0         0 my $p = Attean::Algebra::BGP->new( triples => [@bgptriples, @triples] );
  0         0  
666 0   0     0 while (scalar(@rest) > 0) {
  0         0  
667 0 0       0 $p = Attean::Algebra::Join->new( children => [$p, shift(@rest)] );
668 0         0 }
669             return $p;
670 0         0 }
671 0         0 }
672 0         0
673             =item C<< simplify_path( $subject, $path, $object ) >>
674 0         0  
675             Return a simplified L<Attean::API::Algebra> object corresponding to the given
676             property path.
677              
678             =cut
679              
680             my $self = shift;
681             my $s = shift;
682             my $path = shift;
683             my $o = shift;
684             if ($path->isa('Attean::Algebra::SequencePath')) {
685             my $jvar = Attean::Variable->new(value => $self->new_temporary('pp'));
686 0     0 1 0 my ($lhs, $rhs) = @{ $path->children };
687 0         0 my @paths;
688 0         0 push(@paths, $self->simplify_path($s, $lhs, $jvar));
689 0         0 push(@paths, $self->simplify_path($jvar, $rhs, $o));
690 0 0       0 return $self->_package(@paths);
    0          
    0          
    0          
    0          
691 0         0 } elsif ($path->isa('Attean::Algebra::InversePath')) {
692 0         0 my ($ipath) = @{ $path->children };
  0         0  
693 0         0 return $self->simplify_path($o, $ipath, $s);
694 0         0 } elsif ($path->isa('Attean::Algebra::PredicatePath')) {
695 0         0 my $pred = $path->predicate;
696 0         0 return Attean::TriplePattern->new($s, $pred, $o);
697             } elsif ($path->isa('Attean::Algebra::AlternativePath')) {
698 0         0 my ($l, $r) = @{ $path->children };
  0         0  
699 0         0 my $la = $self->_package($self->simplify_path($s, $l, $o));
700             my $ra = $self->_package($self->simplify_path($s, $r, $o));
701 0         0 return Attean::Algebra::Union->new( children => [$la, $ra] );
702 0         0 } elsif ($path->isa('Attean::Algebra::NegatedPropertySet')) {
703             my @preds = @{ $path->predicates };
704 0         0 my $pvar = Attean::Variable->new(value => $self->new_temporary('nps'));
  0         0  
705 0         0 my $pvar_e = Attean::ValueExpression->new( value => $pvar );
706 0         0 my $t = Attean::TriplePattern->new($s, $pvar, $o);
707 0         0 my @vals = map { Attean::ValueExpression->new( value => $_ ) } @preds;
708             my $expr = Attean::FunctionExpression->new( children => [$pvar_e, @vals], operator => 'notin' );
709 0         0 my $bgp = Attean::Algebra::BGP->new( triples => [$t] );
  0         0  
710 0         0 my $f = Attean::Algebra::Filter->new( children => [$bgp], expression => $expr );
711 0         0 return $f;
712 0         0 } else {
713 0         0 return;
  0         0  
714 0         0 }
715 0         0 }
716 0         0
717 0         0 =item C<< new_projection( $plan, $distinct, @variable_names ) >>
718              
719 0         0 Return a new L<< Attean::Plan::Project >> plan over C<< $plan >>, projecting
720             the named variables. C<< $disctinct >> should be true if the caller can
721             guarantee that the resulting plan will produce distinct results, false otherwise.
722              
723             This method takes care of computing plan metadata such as the resulting ordering.
724              
725             =cut
726              
727             my $self = shift;
728             my $plan = shift;
729             my $distinct = shift;
730             my @vars = @_;
731             my $order = $plan->ordered;
732             my @pvars = map { Attean::Variable->new($_) } @vars;
733            
734 10     10 1 21 my %pvars = map { $_ => 1 } @vars;
735 10         18 my @porder;
736 10         16 CMP: foreach my $cmp (@{ $order }) {
737 10         27 my @cmpvars = $self->_comparator_referenced_variables($cmp);
738 10         28 foreach my $v (@cmpvars) {
739 10         19 unless ($pvars{ $v }) {
  16         464  
740             # projection is dropping a variable used in this comparator
741 10         448 # so we lose any remaining ordering that the sub-plan had.
  16         38  
742 10         20 last CMP;
743 10         14 }
  10         24  
744 0         0 }
745 0         0
746 0 0       0 # all the variables used by this comparator are available after
747             # projection, so the resulting plan will continue to be ordered
748             # by this comparator
749 0         0 push(@porder, $cmp);
750             }
751            
752             return Attean::Plan::Project->new(children => [$plan], variables => \@pvars, distinct => $distinct, ordered => \@porder);
753             }
754              
755             =item C<< bgp_join_plans( $bgp, $model, \@active_graphs, \@default_graphs, \@interesting_order, \@plansA, \@plansB, ... ) >>
756 0         0  
757             Returns a list of alternative plans for the join of a set of triples.
758             The arguments C<@plansA>, C<@plansB>, etc. represent alternative plans for each
759 10         144 triple participating in the join.
760              
761             =cut
762              
763             my $self = shift;
764             my $bgp = shift;
765             my $model = shift;
766             my $active = shift;
767             my $default = shift;
768             my $interesting = shift;
769             my @triples = @_;
770            
771 69     69 1 123 if (scalar(@triples)) {
772 69         100 my @plans = $self->joins_for_plan_alternatives($model, $active, $default, $interesting, @triples);
773 69         94 my @triples = @{ $bgp->triples };
774 69         99
775 69         82 # If the BGP does not contain any blanks, then the results are
776 69         90 # guaranteed to be distinct. Otherwise, we have to assume they're
777 69         109 # not distinct.
778             my $distinct = 1;
779 69 100       160 LOOP: foreach my $t (@triples) {
780 62         787 foreach my $b ($t->values_consuming_role('Attean::API::Blank')) {
781 61         107 $distinct = 0;
  61         169  
782             last LOOP;
783             }
784             foreach my $b ($t->values_consuming_role('Attean::API::Variable')) {
785             if ($b->value =~ /^[.]/) {
786 61         99 # variable names starting with a dot represent placeholders introduced during query planning (with C<new_temporary>)
787 61         111 # they are not projectable, and so may cause an otherwise distinct result to become non-distinct
788 97         249 $distinct = 0;
789 0         0 last LOOP;
790 0         0 }
791             }
792 97         1480 }
793 141 100       1490
794             # Set the distinct flag on each of the top-level join plans that
795             # represents the entire BGP. (Sub-plans won't ever be marked as
796 6         13 # distinct, but that shouldn't matter to the rest of the planning
797 6         26 # process.)
798             if ($distinct) {
799             foreach my $p (@plans) {
800             $p->distinct(1);
801             }
802             }
803              
804             return @plans;
805             } else {
806 61 100       139 # The empty BGP is a special case -- it results in a single join-identity result
807 55         90 my $r = Attean::Result->new( bindings => {} );
808 119         2807 my $plan = Attean::Plan::Table->new( rows => [$r], variables => [], distinct => 1, ordered => [] );
809             return $plan;
810             }
811             }
812 61         1288  
813             =item C<< group_join_plans( $model, \@active_graphs, \@default_graphs, \@interesting_order, \@plansA, \@plansB, ... ) >>
814              
815 7         85 Returns a list of alternative plans for the join of a set of sub-plans.
816 7         266 The arguments C<@plansA>, C<@plansB>, etc. represent alternative plans for each
817 7         1267 sub-plan participating in the join.
818              
819             =cut
820              
821             my $self = shift;
822             return $self->joins_for_plan_alternatives(@_);
823             }
824            
825             =item C<< joins_for_plan_alternatives( $model, \@active_graphs, \@default_graphs, $interesting, \@plan_A, \@plan_B, ... ) >>
826              
827             Returns a list of alternative plans that may all be used to produce results
828             matching the join of C<< plan_A >>, C< plan_B >>, etc. Each plan array here
829             (e.g. C<< @plan_A >>) should contain equivalent plans.
830 11     11 1 22  
831 11         193 =cut
832              
833             my $self = shift;
834             my $model = shift;
835             my $active_graphs = shift;
836             my $default_graphs = shift;
837             my $interesting = shift;
838             my @args = @_; # each $args[$i] here is an array reference containing alternate plans for element $i
839             die "This query planner does not seem to consume a Attean::API::JoinPlanner role (which is necessary for query planning)";
840             }
841            
842             =item C<< access_plans( $model, $active_graphs, $pattern ) >>
843 1     1 1 2  
844 1         2 Returns a list of alternative L<Attean::API::Plan> objects that may be used to
845 1         3 produce results matching the L<Attean::API::TripleOrQuadPattern> $pattern in
846 1         2 the context of C<< $active_graphs >>.
847 1         3  
848 1         2 =cut
849 1         24  
850             # $pattern is a Attean::API::TripleOrQuadPattern object
851             # Return a Attean::API::Plan object that represents the evaluation of $pattern.
852             # e.g. different plans might represent different ways of producing the matches (table scan, index match, etc.)
853             my $self = shift;
854             my $model = shift;
855             my $active_graphs = shift;
856             my $pattern = shift;
857             my @vars = map { $_->value } $pattern->values_consuming_role('Attean::API::Variable');
858             my %vars;
859             my $dup = 0;
860             foreach my $v (@vars) {
861             $dup++ if ($vars{$v}++);
862             }
863            
864 100     100 1 165 my $distinct = 0; # TODO: is this pattern distinct? does it have blank nodes?
865 100         129
866 100         130 my @nodes = $pattern->values;
867 100         138 unless ($nodes[3]) {
868 100         280 $nodes[3] = $active_graphs;
  144         1475  
869 100         156 }
870 100         125 my $plan = Attean::Plan::Quad->new(
871 100         148 subject => $nodes[0],
872 144 50       404 predicate => $nodes[1],
873             object => $nodes[2],
874             graph => $nodes[3],
875 100         144 values => \@nodes,
876             distinct => $distinct,
877 100         199 ordered => [],
878 100 50       243 );
879 100         205 return $plan;
880             }
881 100         1775
882             =item C<< join_plans( $model, \@active_graphs, \@default_graphs, \@plan_left, \@plan_right, $type [, $expr] ) >>
883              
884             Returns a list of alternative plans for the join of one plan from C<< @plan_left >>
885             and one plan from C<< @plan_right >>. The join C<< $type >> must be one of
886             C<< 'inner' >>, C<< 'left' >>, or C<< 'minus' >>, indicating the join algorithm
887             to be used. If C<< $type >> is C<< 'left' >>, then the optional C<< $expr >>
888             may be used to supply a filter expression that should be used by the SPARQL
889             left-join algorithm.
890 100         9794  
891             =cut
892              
893             # $lhs and $rhs are both Attean::API::Plan objects
894             # Return a Attean::API::Plan object that represents the evaluation of $lhs ⋈ $rhs.
895             # The $left and $minus flags indicate the type of the join to be performed (⟕ and ▷, respectively).
896             # e.g. different plans might represent different join algorithms (nested loop join, hash join, etc.) or different orderings ($lhs ⋈ $rhs or $rhs ⋈ $lhs)
897             my $self = shift;
898             my $model = shift;
899             my $active_graphs = shift;
900             my $default_graphs = shift;
901             my $lplans = shift;
902             my $rplans = shift;
903             my $type = shift;
904             my $left = ($type eq 'left');
905             my $minus = ($type eq 'minus');
906             my $expr = shift;
907            
908             my @plans;
909 263     263 1 1620 Carp::confess unless (reftype($lplans) eq 'ARRAY');
910 263         277 foreach my $lhs (@{ $lplans }) {
911 263         306 foreach my $rhs (@{ $rplans }) {
912 263         274 my @vars = (@{ $lhs->in_scope_variables }, @{ $rhs->in_scope_variables });
913 263         293 my %vars;
914 263         264 my %join_vars;
915 263         316 foreach my $v (@vars) {
916 263         329 if ($vars{$v}++) {
917 263         318 $join_vars{$v}++;
918 263         275 }
919             }
920 263         254 my @join_vars = keys %join_vars;
921 263 50       719
922 263         275 if ($left) {
  263         414  
923 353         12883 if (scalar(@join_vars) > 0) {
  353         469  
924 366         1915 push(@plans, Attean::Plan::HashJoin->new(children => [$lhs, $rhs], left => 1, expression => $expr, join_variables => \@join_vars, distinct => 0, ordered => []));
  366         811  
  366         795  
925 366         525 }
926             push(@plans, Attean::Plan::NestedLoopJoin->new(children => [$lhs, $rhs], left => 1, expression => $expr, join_variables => \@join_vars, distinct => 0, ordered => $lhs->ordered));
927 366         472 } elsif ($minus) {
928 1308 100       2328 # we can't use a hash join for MINUS queries, because of the definition of MINUS having a special case for compatible results that have disjoint domains
929 363         488 push(@plans, Attean::Plan::NestedLoopJoin->new(children => [$lhs, $rhs], anti => 1, join_variables => \@join_vars, distinct => 0, ordered => $lhs->ordered));
930             } else {
931             if (scalar(@join_vars) > 0) {
932 366         700 # if there's shared variables (hopefully), we can also use a hash join
933             push(@plans, Attean::Plan::HashJoin->new(children => [$lhs, $rhs], join_variables => \@join_vars, distinct => 0, ordered => []));
934 366 50       707 push(@plans, Attean::Plan::HashJoin->new(children => [$rhs, $lhs], join_variables => \@join_vars, distinct => 0, ordered => []));
    50          
935 0 0       0 # } else {
936 0         0 # warn "No join vars for $lhs ⋈ $rhs";
937             }
938 0         0  
939             # nested loop joins work in all cases
940             push(@plans, Attean::Plan::NestedLoopJoin->new(children => [$lhs, $rhs], join_variables => \@join_vars, distinct => 0, ordered => $lhs->ordered));
941 0         0 push(@plans, Attean::Plan::NestedLoopJoin->new(children => [$rhs, $lhs], join_variables => \@join_vars, distinct => 0, ordered => $rhs->ordered));
942             }
943 366 100       640 }
944             }
945 311         5127
946 311         4800 return @plans;
947             }
948            
949            
950             my $self = shift;
951             my %vars;
952 366         6201 while (my $c = shift) {
953 366         52039 my $expr = $c->expression;
954             foreach my $v ($expr->in_scope_variables) {
955             $vars{$v}++;
956             }
957             }
958 263         33261 return keys %vars;
959             }
960            
961             my $self = shift;
962             my $cmps = shift;
963 0     0   0 my @vars = @_;
964 0         0 my %unseen = map { $_ => 1 } @vars;
965 0         0 foreach my $c (@$cmps) {
966 0         0 return 0 unless ($c->expression->is_stable);
967 0         0 foreach my $v ($self->_comparator_referenced_variables($c)) {
968 0         0 delete $unseen{$v};
969             }
970             }
971 0         0 my @keys = keys %unseen;
972             return (scalar(@keys) == 0);
973             }
974            
975 26     26   36 my $self = shift;
976 26         32 my $algebra = shift;
977 26         46 my ($exprs, $ascending, $svars);
978 26         37 my @cmps = @{ $algebra->comparators };
  78         122  
979 26         44 my %ascending;
980 0 0       0 my %exprs;
981 0         0 my @svars;
982 0         0 foreach my $i (0 .. $#cmps) {
983             my $var = $self->new_temporary('order');
984             my $cmp = $cmps[$i];
985 26         53 push(@svars, $var);
986 26         78 $ascending{$var} = $cmp->ascending;
987             $exprs{$var} = $cmp->expression;
988             }
989             return (\%exprs, \%ascending, \@svars);
990 3     3   8 }
991 3         6 }
992 3         7  
993 3         7 1;
  3         10  
994 3         9  
995              
996 3         0 =back
997 3         9  
998 3         12 =head1 BUGS
999 3         8  
1000 3         8 Please report any bugs or feature requests to through the GitHub web interface
1001 3         14 at L<https://github.com/kasei/attean/issues>.
1002 3         11  
1003             =head1 SEE ALSO
1004 3         11  
1005              
1006              
1007             =head1 AUTHOR
1008              
1009             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
1010              
1011             =head1 COPYRIGHT
1012              
1013             Copyright (c) 2014--2022 Gregory Todd Williams.
1014             This program is free software; you can redistribute it and/or modify it under
1015             the same terms as Perl itself.
1016              
1017             =cut