File Coverage

blib/lib/Attean/QueryPlanner.pm
Criterion Covered Total %
statement 355 612 58.0
branch 77 194 39.6
condition 8 38 21.0
subroutine 33 37 89.1
pod 10 10 100.0
total 483 891 54.2


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