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   26211 use v5.14;
  50         166  
2 50     50   255 use warnings;
  50         99  
  50         1453  
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   22804 use Attean::Algebra;
  50         197  
  50         8258  
41 50     50   31663 use Attean::Plan;
  50         215  
  50         2645  
42 50     50   23674 use Attean::Expression;
  50         248  
  50         2469  
43              
44             use Moo;
45 50     50   390 use Encode qw(encode);
  50         123  
  50         254  
46 50     50   15630 use Attean::RDF;
  50         123  
  50         2453  
47 50     50   302 use Scalar::Util qw(blessed reftype);
  50         108  
  50         472  
48 50     50   38628 use List::Util qw(reduce);
  50         117  
  50         2527  
49 50     50   312 use List::MoreUtils qw(all any);
  50         102  
  50         2460  
50 50     50   317 use Types::Standard qw(Int ConsumerOf InstanceOf);
  50         123  
  50         367  
51 50     50   49352 use URI::Escape;
  50         125  
  50         347  
52 50     50   32907 use Algorithm::Combinatorics qw(subsets);
  50         121  
  50         2607  
53 50     50   301 use List::Util qw(min);
  50         118  
  50         1809  
54 50     50   254 use Math::Cartesian::Product;
  50         102  
  50         1621  
55 50     50   280 use namespace::clean;
  50         114  
  50         1761  
56 50     50   281  
  50         97  
  50         407  
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 29 my $c = $self->counter;
78 15         26 $self->counter($c+1);
79 15         288 return sprintf('.%s-%d', $type, $c);
80 15         292 }
81 15         586  
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 2463 }
92 26         112
93 25         462 =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 266 my $default_graphs = shift;
106 134         197 my %args = @_;
107 134         214
108 134         220 if ($model->does('Attean::API::CostPlanner')) {
109 134         188 my @plans = $model->plans_for_algebra($algebra, $self, $active_graphs, $default_graphs, %args);
110 134         278 if (@plans) {
111             return @plans; # trust that the model knows better than us what plans are best
112 134 50       517 } else {
113 134         2753 $self->log->info("*** Model did not provide plans: $model");
114 134 100       359 }
115 2         8 }
116            
117 132         2455 Carp::confess "No algebra passed for evaluation" unless ($algebra);
118            
119             # TODO: propagate interesting orders
120             my $interesting = [];
121 132 50       15612
122             my @children = @{ $algebra->children };
123             my ($child) = $children[0];
124 132         244 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         200 } elsif ($algebra->isa('Attean::Algebra::BGP')) {
  132         480  
127 132         250 my $triples = $algebra->triples;
128 132 100 66     2749 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         39 my %blanks;
130             foreach my $i (0 .. $#triples) {
131 69         209 my $t = $triples[$i];
132 69         199 my @nodes = $t->values;
133 69         131 my $changed = 0;
134 69         210 foreach (@nodes) {
135 100         185 if ($_->does('Attean::API::Blank')) {
136 100         341 $changed++;
137 100         207 my $id = $_->value;
138 100         177 unless (exists $blanks{$id}) {
139 300 100       4013 $blanks{$id} = Attean::Variable->new(value => $self->new_temporary('blank'));
140 6         81 }
141 6         24 $_ = $blanks{$id};
142 6 50       21 }
143 6         36 }
144            
145 6         309 if ($changed) {
146             my $new = Attean::TriplePattern->new(@nodes);
147             $triples[$i] = $new;
148             }
149 100 100       1568 }
150 6         98 my $bgp = Attean::Algebra::BGP->new( triples => \@triples );
151 6         210 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         1322 return @plans;
155             } elsif ($algebra->isa('Attean::Algebra::Join')) {
156 69         237 return $self->group_join_plans($model, $active_graphs, $default_graphs, $interesting, map {
  100         356  
157             [$self->plans_for_algebra($_, $model, $active_graphs, $default_graphs, %args)]
158 68         503 } @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         32 my @dist;
  22         266  
162             foreach my $p (@plans) {
163             if ($p->distinct) {
164 6         148 push(@dist, $p);
165 6         17 } else {
166 6         14 my @vars = @{ $p->in_scope_variables };
167 38 100       4300 my $cmps = $p->ordered;
168 12         74 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         152 push(@dist, Attean::Plan::Unique->new(children => [$p], distinct => 1, ordered => $p->ordered));
  26         80  
171 26         59 } else {
172 26 50       84 # 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         390 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         548 my $w = Attean::TreeRewriter->new(types => ['Attean::API::DirectedAcyclicGraph']);
182             $w->register_pre_handler(sub {
183             my ($t, $parent, $thunk) = @_;
184 6         23 if ($t->isa('Attean::ExistsExpression')) {
185 6         84 my $pattern = $t->pattern;
186             my $plan = $self->plan_for_algebra($pattern, $model, $active_graphs, $default_graphs, @_);
187 12     12   525 unless ($plan->does('Attean::API::BindingSubstitutionPlan')) {
188 12 50       141 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         39 $expr = $rewritten;
200 6         372 }
201 6         57
202 6 50       18 my $var = $self->new_temporary('filter');
203 0         0 my %exprs = ($var => $expr);
204            
205             my @plans;
206 6         20 foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
207 6         21 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         30 my $filtered = Attean::Plan::EBVFilter->new(children => [$plan], variable => $expr->value->value, distinct => $distinct, ordered => $ordered);
211 6         141 push(@plans, $filtered);
212 6         45 } else {
213 6 100 66     53 my @vars = ($var);
214 3         87 my @inscope = ($var, @{ $plan->in_scope_variables });
215 3         625 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         11 my $filtered = Attean::Plan::EBVFilter->new(children => [$extend], variable => $var, distinct => 0, ordered => $ordered);
218 3         6 my $proj = $self->new_projection($filtered, $distinct, @{ $plan->in_scope_variables });
  3         14  
219 3         7 push(@plans, $proj);
  3         63  
  3         10  
220 3         179 }
221 3         1194 }
222 3         587 return @plans;
  3         40  
223 3         1275 } elsif ($algebra->isa('Attean::Algebra::OrderBy')) {
224             # TODO: no-op if already ordered
225             my @cmps = @{ $algebra->comparators };
226 6         103 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         17  
230 3         27  
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         19 # TODO: extend this to handle more than one comparator, so long as they are *all* just variables (and not complex expressions)
233 3         51 # 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     77 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         51 my $ordered = Attean::Plan::OrderBy->new(children => [$plan], variables => [$var], ascending => $ascending, distinct => $distinct, ordered => \@cmps);
  3         15  
239 3         6 push(@plans, $ordered);
  3         55  
  3         11  
240 3         168 } else {
241 3         12 my @vars = (@{ $plan->in_scope_variables }, keys %$exprs);
242 3         37 my @pvars = map { Attean::Variable->new($_) } @{ $plan->in_scope_variables };
243 3         765 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         24 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         30 my @plans = map {
265 8         15 ($vars_key eq join(' ', sort @{ $_->in_scope_variables }))
  14         49  
  8         25  
266 8         39 ? $_ # 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       220 return @plans;
  14         756  
  14         98  
270             } elsif ($algebra->isa('Attean::Algebra::Graph')) {
271             my $graph = $algebra->graph;
272             if ($graph->does('Attean::API::Term')) {
273 8         1193 if (my $available = $args{available_graphs}) {
274             # the list of available graphs has been restricted, and this
275 10         34 # graph is not available so return an empty table plan.
276 10 100       38 unless (any { $_->equals($graph) } @$available) {
277 5 100       95 my $plan = Attean::Plan::Table->new( variables => [], rows => [], distinct => 0, ordered => [] );
278             return $plan;
279             }
280 2 100   2   18 }
  2         11  
281 1         29 return $self->plans_for_algebra($child, $model, [$graph], $default_graphs, %args);
282 1         175 } else {
283             my $gvar = $graph->value;
284             my $graphs = $model->get_graphs;
285 4         73 my @plans;
286             my %vars = map { $_ => 1 } $child->in_scope_variables;
287 5         121 $vars{ $gvar }++;
288 5         135 my @vars = keys %vars;
289 5         133
290 5         28 my %available;
  3         177  
291 5         98 if (my $available = $args{available_graphs}) {
292 5         15 foreach my $a (@$available) {
293             $available{ $a->value }++;
294 5         7 }
295 5 100       22 $graphs = $graphs->grep(sub { $available{ $_->value } });
296 2         7 }
297 4         18
298             my @branches;
299 2     4   26 my %ignore = map { $_->value => 1 } @$default_graphs;
  4         28  
300             while (my $graph = $graphs->next) {
301             next if $ignore{ $graph->value };
302 5         74 my %exprs = ($gvar => Attean::ValueExpression->new(value => $graph));
303 5         13 # TODO: rewrite $child pattern here to replace any occurrences of the variable $gvar to $graph
  2         11  
304 5         35 my @plans = map {
305 3 50       8 Attean::Plan::Extend->new(children => [$_], expressions => \%exprs, distinct => 0, ordered => $_->ordered);
306 3         385 } $self->plans_for_algebra($child, $model, [$graph], $default_graphs, %args);
307             push(@branches, \@plans);
308             }
309 3         19
  3         64  
310             if (scalar(@branches) == 1) {
311 3         727 @plans = @{ shift(@branches) };
312             } else {
313             cartesian { push(@plans, Attean::Plan::Union->new(children => [@_], distinct => 0, ordered => [])) } @branches;
314 5 100       17 }
315 1         2 return @plans;
  1         3  
316             }
317 4     4   38 } elsif ($algebra->isa('Attean::Algebra::Table')) {
  4         213  
318             my $rows = $algebra->rows;
319 5         767 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         24 my $plan = Attean::Plan::Service->new(
338 4         19 request_signer => $self->request_signer,
339 4         25 endpoint => $endpoint,
340 4         137 silent => $silent,
341 4         341 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         1035 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         15  
372 1         4 my @plans;
373 1         3 foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
374 1         5 my $extend = Attean::Plan::Extend->new(children => [$plan], expressions => \%exprs, distinct => 0, ordered => $plan->ordered);
375             push(@plans, $extend);
376 1         75 }
377 1         86 return @plans;
378 4         75 } elsif ($algebra->isa('Attean::Algebra::Group')) {
379 4         861 my $aggs = $algebra->aggregates;
380             my $groups = $algebra->groupby;
381 1         8 my %exprs;
382             foreach my $expr (@$aggs) {
383 1         3 my $var = $expr->variable->value;
384 1         3 $exprs{$var} = $expr;
385 1         2 }
386 1         3 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         1 }
391 1         49 return @plans;
392 4         62 } elsif ($algebra->isa('Attean::Algebra::Ask')) {
393 4         1070 my @plans;
394             foreach my $plan ($self->plans_for_algebra($child, $model, $active_graphs, $default_graphs, %args)) {
395 1         5 return Attean::Plan::Exists->new(children => [$plan], distinct => 1, ordered => []);
396             }
397 4         15 return @plans;
398 4         147 } 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         6 }
498 1         2 return @plans;
499 1         3 } elsif ($algebra->isa('Attean::Algebra::Clear')) {
500 1         13 my $plan_class = $algebra->drop ? 'Attean::Plan::Drop' : 'Attean::Plan::Clear';
501             my $target = $algebra->target;
502 1         234 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 24 my %pvars = map { $_ => 1 } @vars;
735 10         18 my @porder;
736 10         17 CMP: foreach my $cmp (@{ $order }) {
737 10         27 my @cmpvars = $self->_comparator_referenced_variables($cmp);
738 10         28 foreach my $v (@cmpvars) {
739 10         21 unless ($pvars{ $v }) {
  16         541  
740             # projection is dropping a variable used in this comparator
741 10         452 # so we lose any remaining ordering that the sub-plan had.
  16         43  
742 10         17 last CMP;
743 10         19 }
  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         151 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 160 if (scalar(@triples)) {
772 69         118 my @plans = $self->joins_for_plan_alternatives($model, $active, $default, $interesting, @triples);
773 69         106 my @triples = @{ $bgp->triples };
774 69         109
775 69         110 # If the BGP does not contain any blanks, then the results are
776 69         134 # guaranteed to be distinct. Otherwise, we have to assume they're
777 69         147 # not distinct.
778             my $distinct = 1;
779 69 100       180 LOOP: foreach my $t (@triples) {
780 62         952 foreach my $b ($t->values_consuming_role('Attean::API::Blank')) {
781 61         122 $distinct = 0;
  61         210  
782             last LOOP;
783             }
784             foreach my $b ($t->values_consuming_role('Attean::API::Variable')) {
785             if ($b->value =~ /^[.]/) {
786 61         150 # variable names starting with a dot represent placeholders introduced during query planning (with C<new_temporary>)
787 61         131 # they are not projectable, and so may cause an otherwise distinct result to become non-distinct
788 97         304 $distinct = 0;
789 0         0 last LOOP;
790 0         0 }
791             }
792 97         1661 }
793 141 100       1632
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         19 # distinct, but that shouldn't matter to the rest of the planning
797 6         15 # process.)
798             if ($distinct) {
799             foreach my $p (@plans) {
800             $p->distinct(1);
801             }
802             }
803              
804             return @plans;
805             } else {
806 61 100       156 # The empty BGP is a special case -- it results in a single join-identity result
807 55         104 my $r = Attean::Result->new( bindings => {} );
808 119         3074 my $plan = Attean::Plan::Table->new( rows => [$r], variables => [], distinct => 1, ordered => [] );
809             return $plan;
810             }
811             }
812 61         1422  
813             =item C<< group_join_plans( $model, \@active_graphs, \@default_graphs, \@interesting_order, \@plansA, \@plansB, ... ) >>
814              
815 7         94 Returns a list of alternative plans for the join of a set of sub-plans.
816 7         289 The arguments C<@plansA>, C<@plansB>, etc. represent alternative plans for each
817 7         1417 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 37  
831 11         211 =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         2 produce results matching the L<Attean::API::TripleOrQuadPattern> $pattern in
846 1         2 the context of C<< $active_graphs >>.
847 1         2  
848 1         4 =cut
849 1         58  
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 166 my $distinct = 0; # TODO: is this pattern distinct? does it have blank nodes?
865 100         143
866 100         143 my @nodes = $pattern->values;
867 100         133 unless ($nodes[3]) {
868 100         368 $nodes[3] = $active_graphs;
  144         1650  
869 100         158 }
870 100         164 my $plan = Attean::Plan::Quad->new(
871 100         223 subject => $nodes[0],
872 144 50       467 predicate => $nodes[1],
873             object => $nodes[2],
874             graph => $nodes[3],
875 100         174 values => \@nodes,
876             distinct => $distinct,
877 100         271 ordered => [],
878 100 50       298 );
879 100         170 return $plan;
880             }
881 100         1905
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         11074  
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 2343 Carp::confess unless (reftype($lplans) eq 'ARRAY');
910 263         356 foreach my $lhs (@{ $lplans }) {
911 263         360 foreach my $rhs (@{ $rplans }) {
912 263         330 my @vars = (@{ $lhs->in_scope_variables }, @{ $rhs->in_scope_variables });
913 263         447 my %vars;
914 263         346 my %join_vars;
915 263         369 foreach my $v (@vars) {
916 263         401 if ($vars{$v}++) {
917 263         396 $join_vars{$v}++;
918 263         344 }
919             }
920 263         304 my @join_vars = keys %join_vars;
921 263 50       954
922 263         348 if ($left) {
  263         447  
923 353         13557 if (scalar(@join_vars) > 0) {
  353         528  
924 366         2275 push(@plans, Attean::Plan::HashJoin->new(children => [$lhs, $rhs], left => 1, expression => $expr, join_variables => \@join_vars, distinct => 0, ordered => []));
  366         1001  
  366         1003  
925 366         579 }
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         596 } elsif ($minus) {
928 1308 100       2523 # 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         597 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         826 # 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       846 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       776 }
944             }
945 311         5858
946 311         5637 return @plans;
947             }
948            
949            
950             my $self = shift;
951             my %vars;
952 366         7112 while (my $c = shift) {
953 366         60808 my $expr = $c->expression;
954             foreach my $v ($expr->in_scope_variables) {
955             $vars{$v}++;
956             }
957             }
958 263         38413 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   41 my $self = shift;
976 26         31 my $algebra = shift;
977 26         47 my ($exprs, $ascending, $svars);
978 26         42 my @cmps = @{ $algebra->comparators };
  78         142  
979 26         54 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         52 push(@svars, $var);
986 26         83 $ascending{$var} = $cmp->ascending;
987             $exprs{$var} = $cmp->expression;
988             }
989             return (\%exprs, \%ascending, \@svars);
990 3     3   10 }
991 3         6 }
992 3         6  
993 3         8 1;
  3         10  
994 3         26  
995              
996 3         0 =back
997 3         13  
998 3         11 =head1 BUGS
999 3         9  
1000 3         9 Please report any bugs or feature requests to through the GitHub web interface
1001 3         16 at L<https://github.com/kasei/attean/issues>.
1002 3         16  
1003             =head1 SEE ALSO
1004 3         12  
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