File Coverage

blib/lib/Attean/SimpleQueryEvaluator.pm
Criterion Covered Total %
statement 419 867 48.3
branch 129 484 26.6
condition 42 220 19.0
subroutine 57 70 81.4
pod 2 4 50.0
total 649 1645 39.4


line stmt bran cond sub pod time code
1 3     3   4218 use v5.14;
  3         10  
2 3     3   14 use warnings;
  3         8  
  3         89  
3              
4             =head1 NAME
5              
6             Attean::SimpleQueryEvaluator - Simple query evaluator
7              
8             =head1 VERSION
9              
10             This document describes Attean::SimpleQueryEvaluator version 0.032
11              
12             =head1 SYNOPSIS
13              
14             use v5.14;
15             use Attean;
16             my $algebra = Attean->get_parser('SPARQL')->parse('SELECT * WHERE { ... }');
17             my $active_graph = Attean::IRI->new('http://example.org/');
18             my $e = Attean::SimpleQueryEvaluator->new( model => $model );
19             my $iter = $e->evaluate( $algebra, $active_graph );
20              
21             =head1 DESCRIPTION
22              
23             The Attean::SimpleQueryEvaluator class implements a simple query evaluator that,
24             given an L<Attean::API::Algebra|Attean::API::Query> and a L<Attean::API::Model>
25             object, evaluates the query represented by the algebra using data from the
26             model, and returns a query result.
27              
28             =head1 ATTRIBUTES
29              
30             =over 4
31              
32             =cut
33              
34 3     3   15 use Attean::Algebra;
  3         7  
  3         61  
35 3     3   17 use Attean::Expression;
  3         9  
  3         80  
36              
37             use Moo;
38 3     3   14 use Encode qw(encode);
  3         8  
  3         23  
39 3     3   973 use Attean::RDF;
  3         8  
  3         123  
40 3     3   16 use LWP::UserAgent;
  3         13  
  3         21  
41 3     3   2043 use Scalar::Util qw(blessed);
  3         8  
  3         88  
42 3     3   17 use List::Util qw(all any reduce);
  3         6  
  3         130  
43 3     3   18 use Types::Standard qw(ConsumerOf InstanceOf Bool Object);
  3         10  
  3         197  
44 3     3   19 use URI::Escape;
  3         5  
  3         21  
45 3     3   2148 use Attean::SPARQLClient;
  3         5  
  3         147  
46 3     3   28 use namespace::clean;
  3         6  
  3         50  
47 3     3   14  
  3         15  
  3         18  
48             =item C<< model >>
49              
50             The L<Attean::API::Model> object used for query evaluation.
51              
52             =cut
53              
54             has 'model' => (is => 'ro', isa => ConsumerOf['Attean::API::Model'], required => 1);
55            
56             =item C<< default_graph >>
57              
58             The L<Attean::API::IRI> object representing the default graph in the C<< model >>.
59             The default graph will be excluded from enumeration of graph names for query
60             features such as C<< GRAPH ?g {} >>.
61              
62             =cut
63              
64             has 'default_graph' => (is => 'ro', isa => ConsumerOf['Attean::API::IRI'], required => 1);
65              
66             has 'user_agent' => (is => 'rw', isa => InstanceOf['LWP::UserAgent'], default => sub { my $ua = LWP::UserAgent->new(); $ua->agent("Attean/$Attean::VERSION " . $ua->_agent); $ua });
67            
68             =item C<< request_signer >>
69              
70             If set, used to modify HTTP::Request objects used in evaluating SERVICE calls
71             before the request is made. This may be used to, for example, add cryptographic
72             signature headers to the request. The modification is performed by calling
73             C<< $request_signer->sign( $request ) >>.
74              
75             =cut
76              
77             has 'request_signer' => (is => 'rw', isa => Object);
78            
79             has 'ground_blanks' => (is => 'rw', isa => Bool, default => 0);
80            
81             =back
82              
83             =head1 METHODS
84              
85             =over 4
86              
87             =item C<< evaluate( $algebra, $active_graph ) >>
88              
89             Returns an L<Attean::API::Iterator> object with results produced by evaluating
90             the query C<< $algebra >> against the evaluator's C<< model >>, using the
91             supplied C<< $active_graph >>.
92              
93             =cut
94              
95             my $self = shift;
96             my $algebra = shift;
97 65     65 1 2602 my $active_graph = shift || Carp::confess "No active-graph passed to Attean::SimpleQueryEvaluator->evaluate";
98 65         87
99 65   33     127 Carp::confess "No algebra passed for evaluation" unless ($algebra);
100            
101 65 50       126 my $expr_eval = Attean::SimpleQueryEvaluator::ExpressionEvaluator->new( evaluator => $self );
102              
103 65         1029 my @children = @{ $algebra->children };
104             my ($child) = $children[0];
105 65         3362 if ($algebra->isa('Attean::Algebra::Query') or $algebra->isa('Attean::Algebra::Update')) {
  65         193  
106 65         127 return $self->evaluate($algebra->child, $active_graph, @_);
107 65 50 33     1295 } elsif ($algebra->isa('Attean::Algebra::BGP')) {
    100 66        
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    50          
    50          
    100          
    100          
    100          
    50          
    50          
    50          
    0          
108 0         0 my @triples = @{ $algebra->triples };
109             if (scalar(@triples) == 0) {
110 21         35 my $b = Attean::Result->new( bindings => {} );
  21         55  
111 21 50       45 return Attean::ListIterator->new(variables => [], values => [$b], item_type => 'Attean::API::Result');
112 0         0 } else {
113 0         0 my @iters;
114             my @new_vars;
115 21         50 my %blanks;
116             foreach my $t (@triples) {
117 21         0 push(@iters, $self->evaluate_pattern($t, $active_graph, \@new_vars, \%blanks));
118 21         34 }
119 24         184 while (scalar(@iters) > 1) {
120             my ($lhs, $rhs) = splice(@iters, 0, 2);
121 21         785 unshift(@iters, $lhs->join($rhs));
122 3         9 }
123 3         14 return shift(@iters)->map(sub { shift->project_complement(@new_vars) });
124             }
125 21     60   1471 } elsif ($algebra->isa('Attean::Algebra::Distinct') or $algebra->isa('Attean::Algebra::Reduced')) {
  60         156  
126             my %seen;
127             my $iter = $self->evaluate( $child, $active_graph );
128 1         3 return $iter->grep(sub {
129 1         3 my $r = shift;
130             my $str = $r->as_string;
131 4     4   7 my $ok = not($seen{ $str }) ? 1 : 0;
132 4         10 $seen{ $str }++;
133 4 100       13 return $ok;
134 4         18 });
135 4         18 } elsif ($algebra->isa('Attean::Algebra::Extend')) {
136 1         62 my $child = $algebra;
137             my @extends;
138 2         4 my %extends;
139 2         5 while ($child->isa('Attean::Algebra::Extend')) {
140             my $expr = $child->expression;
141 2         7 my $var = $child->variable->value;
142 2         7 $extends{ $var } = $expr;
143 2         7 unshift(@extends, $var);
144 2         5 ($child) = @{ $child->children };
145 2         4 }
146 2         4 return $self->evaluate( $child, $active_graph )->map(sub {
  2         10  
147             my $r = shift;
148             my %extension;
149 8     8   13 my %row_cache;
150 8         12 foreach my $var (@extends) {
151             my $expr = $extends{ $var };
152 8         13 my $val = $expr_eval->evaluate_expression( $expr, $r, $active_graph, \%row_cache );
153 8         15 if ($val->does('Attean::API::Binding')) {
154 8         23 # patterns need to be made ground to be bound as values (e.g. TriplePattern -> Triple)
155 8 50       362 $val = $val->ground($r);
156             }
157 0         0 # warn "Extend error: $@" if ($@);
158             $r = Attean::Result->new( bindings => { $var => $val } )->join($r) if ($val);
159             }
160 8 50       313 return $r;
161             });
162 8         33 } elsif ($algebra->isa('Attean::Algebra::Filter')) {
163 2         5 # TODO: Merge adjacent filter evaluation so that they can share a row_cache hash (as is done for Extend above)
164             my $expr = $algebra->expression;
165             my $iter = $self->evaluate( $child, $active_graph );
166 1         5 return $iter->grep(sub {
167 1         4 my $t = $expr_eval->evaluate_expression( $expr, shift, $active_graph, {} );
168             # if ($@) { warn "Filter evaluation: $@\n" };
169 4     4   13 return ($t ? $t->ebv : 0);
170             });
171 4 50       21 } elsif ($algebra->isa('Attean::Algebra::OrderBy')) {
172 1         55 local($Attean::API::Binding::ALLOW_IRI_COMPARISON) = 1;
173             my $iter = $self->evaluate( $child, $active_graph );
174 3         33 my @rows = $iter->elements;
175 3         11 my @cmps = @{ $algebra->comparators };
176 3         101 my @exprs = map { $_->expression } @cmps;
177 3         6 my @dirs = map { $_->ascending } @cmps;
  3         10  
178 3         7 my @sorted = map { $_->[0] } sort {
  4         10  
179 3         5 my ($ar, $avalues) = @$a;
  4         9  
180 12         19 my ($br, $bvalues) = @$b;
181 15         21 my $c = 0;
182 15         20 foreach my $i (0 .. $#cmps) {
183 15         19 my ($av, $bv) = map { $_->[$i] } ($avalues, $bvalues);
184 15         29 # Mirrors code in Attean::Plan::OrderBy->sort_rows
185 16         21 if (blessed($av) and $av->does('Attean::API::Binding') and (not(defined($bv)) or not($bv->does('Attean::API::Binding')))) {
  32         49  
186             $c = 1;
187 16 50 33     56 } elsif (blessed($bv) and $bv->does('Attean::API::Binding') and (not(defined($av)) or not($av->does('Attean::API::Binding')))) {
    50 0        
      33        
      33        
      0        
      33        
188 0         0 $c = -1;
189             } else {
190 0         0 $c = eval { $av ? $av->compare($bv) : 1 };
191             if ($@) {
192 16 50       603 $c = 1;
  16         40  
193 16 50       28 }
194 0         0 }
195             $c *= -1 if ($dirs[$i] == 0);
196             last unless ($c == 0);
197 16 100       32 }
198 16 100       28 $c
199             } map { my $r = $_; [$r, [map { $expr_eval->evaluate_expression( $_, $r, $active_graph, {} ) } @exprs]] } @rows;
200             return Attean::ListIterator->new( values => \@sorted, item_type => $iter->item_type, variables => $iter->variables);
201 3         7 } elsif ($algebra->isa('Attean::Algebra::Service')) {
  12         18  
  12         14  
  16         34  
202 3         66 my $endpoint = $algebra->endpoint->value;
203             my ($pattern) = @{ $algebra->children };
204 1         6 my $sparql = Attean::Algebra::Project->new( variables => [ map { variable($_) } $pattern->in_scope_variables ], children => [ $pattern ] )->as_sparql;
205 1         3 my $silent = $algebra->silent;
  1         4  
206 1         6 my $client = Attean::SPARQLClient->new(
  3         144  
207 1         26 endpoint => $endpoint,
208 1         21 silent => $silent,
209             user_agent => $self->user_agent,
210             request_signer => $self->request_signer,
211             );
212             return $client->query($sparql);
213             } elsif ($algebra->isa('Attean::Algebra::Graph')) {
214 1         302 my $graph = $algebra->graph;
215             return $self->evaluate($child, $graph) if ($graph->does('Attean::API::Term'));
216 3         10
217 3 100       11 my @iters;
218             my $graphs = $self->model->get_graphs();
219 1         24 my %vars;
220 1         22 while (my $g = $graphs->next) {
221 1         27 next if ($g->value eq $self->default_graph->value);
222 1         7 my $gr = Attean::Result->new( bindings => { $graph->value => $g } );
223 2 100       15 my $iter = $self->evaluate($child, $g)->map(sub { if (my $result = shift->join($gr)) { return $result } else { return } });
224 1         20 foreach my $v (@{ $iter->variables }) {
225 1 50   4   23 $vars{$v}++;
  4         13  
  4         15  
  0         0  
226 1         32 }
  1         17  
227 2         8 push(@iters, $iter);
228             }
229 1         5 return Attean::IteratorSequence->new( variables => [keys %vars], iterators => \@iters, item_type => 'Attean::API::Result' );
230             } elsif ($algebra->isa('Attean::Algebra::Group')) {
231 1         18 my @groupby = @{ $algebra->groupby };
232             my $iter = $self->evaluate($child, $active_graph);
233 0         0 my %groups;
  0         0  
234 0         0 while (my $r = $iter->next) {
235 0         0 my %vars;
236 0         0 my %row_cache;
237 0         0 my @group_terms = map { $expr_eval->evaluate_expression( $_, $r, $active_graph, \%row_cache ) } @groupby;
238             my $key = join(' ', map { blessed($_) ? $_->as_string : '' } @group_terms);
239 0         0 my %group_bindings;
  0         0  
240 0 0       0 foreach my $i (0 .. $#group_terms) {
  0         0  
241 0         0 my $v = $groupby[$i];
242 0         0 if (blessed($v) and $v->isa('Attean::ValueExpression') and $v->value->does('Attean::API::Variable') and $group_terms[$i]) {
243 0         0 $group_bindings{$v->value->value} = $group_terms[$i];
244 0 0 0     0 }
      0        
      0        
245 0         0 }
246             $groups{$key} = [Attean::Result->new( bindings => \%group_bindings ), []] unless (exists($groups{$key}));
247             push(@{ $groups{$key}[1] }, $r);
248 0 0       0 }
249 0         0 my @keys = keys %groups;
  0         0  
250             $groups{''} = [Attean::Result->new( bindings => {} ), []] if (scalar(@keys) == 0);
251 0         0 my $aggs = $algebra->aggregates;
252 0 0       0 my @results;
253 0         0 my %vars;
254 0         0 foreach my $key (keys %groups) {
255             my %row_cache;
256 0         0 my ($binding, $rows) = @{ $groups{$key} };
257 0         0 my $count = scalar(@$rows);
258 0         0 my %bindings;
  0         0  
259 0         0 foreach my $i (0 .. $#{ $aggs }) {
260 0         0 my $name = $aggs->[$i]->variable->value;
261 0         0 my $term = $expr_eval->evaluate_expression( $aggs->[$i], $rows, $active_graph, {} );
  0         0  
262 0         0 # warn "AGGREGATE error: $@" if ($@);
263 0         0 $vars{$name}++;
264             $bindings{ $name } = $term if ($term);
265 0         0 }
266 0 0       0 push(@results, Attean::Result->new( bindings => \%bindings )->join($binding));
267             }
268 0         0 return Attean::ListIterator->new(variables => [keys %vars], values => \@results, item_type => 'Attean::API::Result');
269             } elsif ($algebra->isa('Attean::Algebra::Join')) {
270 0         0 my ($lhs, $rhs) = map { $self->evaluate($_, $active_graph) } @children;
271             return $lhs->join($rhs);
272 2         8 } elsif ($algebra->isa('Attean::Algebra::LeftJoin')) {
  4         72  
273 2         73 my $expr = $algebra->expression;
274             my ($lhs_iter, $rhs_iter) = map { $self->evaluate($_, $active_graph) } @children;
275 0         0 my @rhs = $rhs_iter->elements;
276 0         0 my @results;
  0         0  
277 0         0 my %vars = map { $_ => 1 } (@{ $lhs_iter->variables }, @{ $rhs_iter->variables });
278 0         0 while (my $lhs = $lhs_iter->next) {
279 0         0 my $joined = 0;
  0         0  
  0         0  
  0         0  
280 0         0 foreach my $rhs (@rhs) {
281 0         0 if (my $j = $lhs->join($rhs)) {
282 0         0 if ($expr_eval->evaluate_expression( $expr, $j, $active_graph, {} )->ebv) {
283 0 0       0 $joined++;
284 0 0       0 push(@results, $j);
285 0         0 }
286 0         0 }
287             }
288             push(@results, $lhs) unless ($joined);
289             }
290 0 0       0 return Attean::ListIterator->new( variables => [keys %vars], values => \@results, item_type => 'Attean::API::Result');
291             } elsif ($algebra->isa('Attean::Algebra::Minus')) {
292 0         0 my ($lhsi, $rhs) = map { $self->evaluate($_, $active_graph) } @children;
293             my @rhs = $rhs->elements;
294 0         0 my @results;
  0         0  
295 0         0 while (my $lhs = $lhsi->next) {
296 0         0 my @compatible;
297 0         0 my @disjoint;
298 0         0 RHS: foreach my $rhs (@rhs) {
299             if (my $j = $lhs->join($rhs)) {
300 0         0 push(@compatible, 1);
301 0 0       0 } else {
302 0         0 push(@compatible, 0);
303             }
304 0         0
305             my $intersects = 0;
306             my %lhs_dom = map { $_ => 1 } $lhs->variables;
307 0         0 foreach my $rvar ($rhs->variables) {
308 0         0 if (exists $lhs_dom{$rvar}) {
  0         0  
309 0         0 $intersects = 1;
310 0 0       0 }
311 0         0 }
312             push(@disjoint, not($intersects));
313             }
314 0         0
315             my $count = scalar(@rhs);
316             my $keep = 1;
317 0         0 foreach my $i (0 .. $#rhs) {
318 0         0 $keep = 0 unless ($compatible[$i] == 0 or $disjoint[$i] == 1);
319 0         0 }
320 0 0 0     0
321             push(@results, $lhs) if ($keep);
322             }
323 0 0       0 return Attean::ListIterator->new( variables => $lhsi->variables, values => \@results, item_type => 'Attean::API::Result');
324             } elsif ($algebra->isa('Attean::Algebra::Path')) {
325 0         0 my $s = $algebra->subject;
326             my $path = $algebra->path;
327 24         50 my $o = $algebra->object;
328 24         41 my @children = @{ $path->children };
329 24         42 my ($child) = $children[0];
330 24         36
  24         54  
331 24         42 return $self->model->get_bindings( $s, $path->predicate, $o, $active_graph ) if ($path->isa('Attean::Algebra::PredicatePath'));
332             if ($path->isa('Attean::Algebra::InversePath')) {
333 24 100       131 my $path = Attean::Algebra::Path->new( subject => $o, path => $child, object => $s );
334 11 50 66     138 return $self->evaluate( $path, $active_graph );
    50          
    100          
    100          
    100          
    50          
335 0         0 } elsif ($path->isa('Attean::Algebra::AlternativePath')) {
336 0         0 my @children = @{ $path->children };
337             my @algebras = map { Attean::Algebra::Path->new( subject => $s, path => $_, object => $o ) } @children;
338 0         0 my @iters = map { $self->evaluate($_, $active_graph) } @algebras;
  0         0  
339 0         0 return Attean::IteratorSequence->new( iterators => \@iters, item_type => $iters[0]->item_type, variables => [$algebra->in_scope_variables] );
  0         0  
340 0         0 } elsif ($path->isa('Attean::Algebra::NegatedPropertySet')) {
  0         0  
341 0         0 my $preds = $path->predicates;
342             my %preds = map { $_->value => 1 } @$preds;
343 1         221 my $filter = $self->model->get_quads($s, undef, $o, $active_graph)->grep(sub {
344 1         6 my $q = shift;
  1         7  
345             my $p = $q->predicate;
346 2     2   4 return not exists $preds{ $p->value };
347 2         6 });
348 2         11 my %vars;
349 1         7 $vars{subject} = $s->value if ($s->does('Attean::API::Variable'));
350 1         31 $vars{object} = $o->value if ($o->does('Attean::API::Variable'));
351 1 50       3 return $filter->map(sub {
352 1 50       20 my $q = shift;
353             return unless $q;
354 1     1   3 my %bindings = map { $vars{$_} => $q->$_() } (keys %vars);
355 1 50       3 return Attean::Result->new( bindings => \%bindings );
356 1         4 }, 'Attean::API::Result', variables => [values %vars]);
  1         5  
357 1         18 } elsif ($path->isa('Attean::Algebra::SequencePath')) {
358 1         23 if (scalar(@children) == 1) {
359             my $path = Attean::Algebra::Path->new( subject => $s, path => $children[0], object => $o );
360 1 50       4 return $self->evaluate($path, $active_graph);
361 0         0 } else {
362 0         0 my @paths;
363             my $first = shift(@children);
364 1         3 my $join = Attean::Variable->new();
365 1         2 my @new_vars = ($join->value);
366 1         20 push(@paths, Attean::Algebra::Path->new( subject => $s, path => $first, object => $join ));
367 1         60 foreach my $i (0 .. $#children) {
368 1         18 my $newjoin = Attean::Variable->new();
369 1         4 my $obj = ($i == $#children) ? $o : $newjoin;
370 1         18 push(@new_vars, $newjoin->value);
371 1 50       49 push(@paths, Attean::Algebra::Path->new( subject => $join, path => $children[$i], object => $obj ));
372 1         4 $join = $newjoin;
373 1         16 }
374 1         3
375             while (scalar(@paths) > 1) {
376             my ($l, $r) = splice(@paths, 0, 2);
377 1         3 unshift(@paths, Attean::Algebra::Join->new( children => [$l, $r] ));
378 1         4 }
379 1         18 return $self->evaluate(shift(@paths), $active_graph)->map(sub { shift->project_complement(@new_vars) });
380             }
381 1     1   4 } elsif ($path->isa('Attean::Algebra::ZeroOrMorePath') or $path->isa('Attean::Algebra::OneOrMorePath')) {
  1         6  
382             if ($s->does('Attean::API::TermOrTriple') and $o->does('Attean::API::Variable')) {
383             my $v = {};
384 4 100 66     14 if ($path->isa('Attean::Algebra::ZeroOrMorePath')) {
    50 33        
    0 0        
385 3         76 $self->_ALP($active_graph, $s, $child, $v);
386 3 50       10 } else {
387 3         9 my $iter = $self->_eval($active_graph, $s, $child);
388             while (my $n = $iter->next) {
389 0         0 $self->_ALP($active_graph, $n, $child, $v);
390 0         0 }
391 0         0 }
392             my @results = map { Attean::Result->new( bindings => { $o->value => $_ } ) } (values %$v);
393             return Attean::ListIterator->new(variables => [$o->value], values => \@results, item_type => 'Attean::API::Result');
394 3         10 } elsif ($s->does('Attean::API::Variable') and $o->does('Attean::API::Variable')) {
  6         155  
395 3         111 my $nodes = $self->model->graph_nodes( $active_graph );
396             my @results;
397 1         72 while (my $t = $nodes->next) {
398 1         33 my $tr = Attean::Result->new( bindings => { $s->value => $t } );
399 1         4 my $p = Attean::Algebra::Path->new( subject => $t, path => $path, object => $o );
400 3         53 my $iter = $self->evaluate($p, $active_graph);
401 3         98 while (my $r = $iter->next) {
402 3         30 push(@results, $r->join($tr));
403 3         336 }
404 6         20 }
405             my %vars = map { $_ => 1 } ($s->value, $o->value);
406             return Attean::ListIterator->new(variables => [keys %vars], values => \@results, item_type => 'Attean::API::Result');
407 1         6 } elsif ($s->does('Attean::API::Variable') and $o->does('Attean::API::TermOrTriple')) {
  2         7  
408 1         21 my $pp = Attean::Algebra::InversePath->new( children => [$child] );
409             my $p = Attean::Algebra::Path->new( subject => $o, path => $pp, object => $s );
410 0         0 return $self->evaluate($p, $active_graph);
411 0         0 } else { # Term ZeroOrMorePath(path) Term
412 0         0 my $v = {};
413             $self->_ALP($active_graph, $s, $child, $v);
414 0         0 my @results;
415 0         0 foreach my $v (values %$v) {
416 0         0 return Attean::ListIterator->new(variables => [], values => [Attean::Result->new()], item_type => 'Attean::API::Result')
417 0         0 if ($v->equals($o));
418 0 0       0 }
419             return Attean::ListIterator->new(variables => [], values => [], item_type => 'Attean::API::Result');
420             }
421 0         0 } elsif ($path->isa('Attean::Algebra::ZeroOrOnePath')) {
422             my $path = Attean::Algebra::Path->new( subject => $s, path => $child, object => $o );
423             my @iters;
424 5         88 my %seen;
425 5         10 push(@iters, $self->evaluate( $path, $active_graph )->grep(sub { return not($seen{shift->as_string}++); }));
426             push(@iters, $self->_zeroLengthPath($s, $o, $active_graph));
427 5     1   14 my %vars;
  1         4  
428 5         163 foreach my $iter (@iters) {
429 5         453 $vars{$_}++ for (@{ $iter->variables });
430 5         11 }
431 10         37 return Attean::IteratorSequence->new( iterators => \@iters, item_type => 'Attean::API::Result', variables => [keys %vars] );
  10         148  
432             }
433 5         94 die "Unimplemented path type: $path";
434             } elsif ($algebra->isa('Attean::Algebra::Project')) {
435 0         0 my $iter = $self->evaluate( $child, $active_graph );
436             my @vars = map { $_->value } @{ $algebra->variables };
437 3         92 return $iter->map(sub {
438 3         98 my $r = shift;
  3         12  
  3         12  
439             my $b = { map { my $t = $r->value($_); $t ? ($_ => $t) : () } @vars };
440 9     9   15 return Attean::Result->new( bindings => $b );
441 9 50       14 }, undef, variables => \@vars); #->debug('Project result');
  9         20  
  9         45  
442 9         151 } elsif ($algebra->isa('Attean::Algebra::Slice')) {
443 3         19 my $iter = $self->evaluate( $child, $active_graph );
444             $iter = $iter->offset($algebra->offset) if ($algebra->offset > 0);
445 3         10 $iter = $iter->limit($algebra->limit) if ($algebra->limit >= 0);
446 3 100       113 return $iter;
447 3 100       18 } elsif ($algebra->isa('Attean::Algebra::Union')) {
448 3         71 my ($lhs, $rhs) = map { $self->evaluate($_, $active_graph) } @children;
449             return Attean::IteratorSequence->new(
450 0         0 iterators => [$lhs, $rhs],
  0         0  
451 0         0 item_type => 'Attean::API::Result',
452             variables => [$algebra->in_scope_variables]
453             );
454             } elsif ($algebra->isa('Attean::Algebra::Ask')) {
455             my $iter = $self->evaluate($child, $active_graph);
456             my $result = $iter->next;
457 0         0 return Attean::ListIterator->new(values => [$result ? Attean::Literal->true : Attean::Literal->false], item_type => 'Attean::API::Term');
458 0         0 } elsif ($algebra->isa('Attean::Algebra::Construct')) {
459 0 0       0 my $iter = $self->evaluate($child, $active_graph);
460             my $patterns = $algebra->triples;
461 1         4 use Data::Dumper;
462 1         35 my %seen;
463 3     3   12328 return Attean::CodeIterator->new(
  3         7  
  3         3786  
464 1         2 generator => sub {
465             my $r = $iter->next;
466             return unless ($r);
467 2     2   6 my %mapping = map { my $t = $r->value($_); $t ? ("?$_" => $t) : (); } ($r->variables);
468 2 100       6 my $mapper = Attean::TermMap->rewrite_map(\%mapping);
469 1 50       3 my @triples;
  2         5  
  2         9  
470 1         12 PATTERN: foreach my $p (@$patterns) {
471 1         44 my @terms = map {
472 1         3 ($_->does('Attean::API::TriplePattern'))
473             ? $_->as_triple
474 1 50       5 : $_
  3         43  
475             } $p->apply_map($mapper)->values;
476             unless (all { $_->does('Attean::API::TermOrTriple') } @terms) {
477             next PATTERN;
478 1 50       25 }
  3         25  
479 0         0 push(@triples, Attean::Triple->new(@terms));
480             }
481 1         37 return @triples;
482             },
483 1         31 item_type => 'Attean::API::Triple'
484             )->grep(sub { return not($seen{shift->as_string}++); });
485             } elsif ($algebra->isa('Attean::Algebra::Table')) {
486 1     1   23 my $vars = [map { $_->value } @{ $algebra->variables }];
  1         5  
487             return Attean::ListIterator->new(variables => $vars, values => $algebra->rows, item_type => 'Attean::API::Result');
488 0         0 }
  0         0  
  0         0  
489 0         0 die "Unimplemented algebra evaluation for: $algebra";
490             }
491 0         0  
492            
493             =item C<< evaluate_pattern( $pattern, $active_graph, \@new_vars, \%blanks ) >>
494              
495             Returns an L<Attean::API::Iterator> object with results produced by evaluating
496             the triple- or quad-pattern C<< $pattern >> against the evaluator's
497             C<< model >>, using the supplied C<< $active_graph >>.
498              
499             If the C<< ground_blanks >> option is false, replaces blank nodes in the
500             pattern with fresh variables before evaluation, and populates C<< %blanks >>
501             with pairs ($variable_name => $variable_node). Each new variable is also
502             appended to C<< @new_vars >> as it is created.
503              
504             =cut
505              
506             my $self = shift;
507             my $t = shift;
508             my $active_graph = shift || Carp::confess "No active-graph passed to Attean::SimpleQueryEvaluator->evaluate";
509 24     24 1 43 my $new_vars = shift;
510 24         34 my $blanks = shift;
511 24   33     51 my $q = $t->as_quad_pattern($active_graph);
512 24         27 my @values;
513 24         28 foreach my $v ($q->values) {
514 24         84 if (not($self->ground_blanks) and $v->does('Attean::API::Blank')) {
515 24         812 unless (exists $blanks->{$v->value}) {
516 24         68 $blanks->{$v->value} = Attean::Variable->new();
517 96 100 100     1351 push(@$new_vars, $blanks->{$v->value}->value);
518 2 100       56 }
519 1         15 push(@values, $blanks->{$v->value});
520 1         54 } else {
521             push(@values, $v);
522 2         6 }
523             }
524 94         2041 return $self->model->get_bindings( @values );
525             }
526            
527 24         112 my $self = shift;
528             my $graph = shift;
529             my $term = shift;
530             my $path = shift;
531 6     6   10 my $v = shift;
532 6         7 return if (exists $v->{ $term->as_string });
533 6         7 $v->{ $term->as_string } = $term;
534 6         8
535 6         8 my $iter = $self->_eval($graph, $term, $path);
536 6 50       16 while (my $n = $iter->next) {
537 6         326 $self->_ALP($graph, $n, $path, $v);
538             }
539 6         294 }
540 6         179
541 3         11 my $self = shift;
542             my $graph = shift;
543             my $term = shift;
544             my $path = shift;
545             my $pp = Attean::Algebra::Path->new( subject => $term, path => $path, object => variable('o') );
546 6     6   9 my $iter = $self->evaluate($pp, $graph);
547 6         8 my $terms = $iter->map(sub { shift->value('o') }, 'Attean::API::Term');
548 6         18 my %seen;
549 6         9 return $terms->grep(sub { not $seen{ shift->as_string }++ });
550 6         18 }
551 6         19
552 6     3   226 my $self = shift;
  3         13  
553 6         167 my $s = shift;
554 6     3   39 my $o = shift;
  3         11  
555             my $graph = shift;
556             my $s_term = ($s->does('Attean::API::TermOrTriple'));
557             my $o_term = ($o->does('Attean::API::TermOrTriple'));
558 5     5   9 if ($s_term and $o_term) {
559 5         8 my @r;
560 5         6 push(@r, Attean::Result->new()) if ($s->equals($o));
561 5         7 return Attean::ListIterator->new(variables => [], values => \@r, item_type => 'Attean::API::Result');
562 5         11 } elsif ($s_term) {
563 5         71 my $name = $o->value;
564 5 100 100     78 my $r = Attean::Result->new( bindings => { $name => $s } );
    100          
    100          
565 2         4 return Attean::ListIterator->new(variables => [$name], values => [$r], item_type => 'Attean::API::Result');
566 2 100       6 } elsif ($o_term) {
567 2         54 my $name = $s->value;
568             my $r = Attean::Result->new( bindings => { $name => $o } );
569 1         4 return Attean::ListIterator->new(variables => [$name], values => [$r], item_type => 'Attean::API::Result');
570 1         21 } else {
571 1         36 my @vars = map { $_->value } ($s, $o);
572             my $nodes = $self->model->graph_nodes( $graph );
573 1         4 return $nodes->map(
574 1         27 sub {
575 1         40 my $term = shift;
576             Attean::Result->new( bindings => { map { $_ => $term } @vars } );
577 1         3 },
  2         7  
578 1         8 'Attean::API::Result',
579             variables => \@vars
580             );
581 5     5   8 }
582 5         10 }
  10         99  
583             }
584 1         38  
585             use Moo;
586             use Attean::RDF;
587             use Scalar::Util qw(blessed);
588             use Types::Standard qw(InstanceOf);
589             use URI::Escape qw(uri_escape_utf8);
590             use Encode qw(encode);
591             use POSIX qw(ceil floor);
592 3     3   2557 use Digest;
  3         9  
  3         13  
593 3     3   934 use UUID::Tiny ':std';
  3         6  
  3         23  
594 3     3   2186 use List::MoreUtils qw(zip);
  3         8  
  3         105  
595 3     3   15 use DateTime::Format::W3CDTF;
  3         7  
  3         14  
596 3     3   1232 use I18N::LangTags;
  3         8  
  3         126  
597 3     3   18 use namespace::clean;
  3         6  
  3         108  
598 3     3   17
  3         6  
  3         30  
599 3     3   1700 has 'evaluator' => (is => 'ro', isa => InstanceOf['Attean::SimpleQueryEvaluator']);
  3         1507  
  3         84  
600 3     3   19 my $self = shift;
  3         7  
  3         577  
601 3     3   21 my $expr = shift;
  3         6  
  3         21  
602 3     3   2694 my $row = shift;
  3         8  
  3         62  
603 3     3   15 my $active_graph = shift;
  3         6  
  3         79  
604 3     3   13 my $row_cache = shift || {};
  3         8  
  3         16  
605             my $impl = $self->impl($expr, $active_graph);
606             return eval { $impl->($row, row_cache => $row_cache) };
607             }
608 53     53 0 1922
609 53         68 my $self = shift;
610 53         68 my $expr = shift;
611 53         64 my $active_graph = shift;
612 53   100     152 my $op = $expr->operator;
613 53         114 my $true = Attean::Literal->true;
614 53         82 my $false = Attean::Literal->false;
  53         92  
615             if ($expr->isa('Attean::ExistsExpression')) {
616             my $pattern = $expr->pattern;
617             return sub {
618 127     127 0 148 my $r = shift;
619 127         142 my $table = Attean::Algebra::Table->new( variables => [map { variable($_) } $r->variables], rows => [$r] );
620 127         155 my $join = Attean::Algebra::Join->new( children => [$table, $pattern] );
621 127         217 # TODO: substitute variables at top-level of EXISTS pattern
622 127         273 my $iter = $self->evaluator->evaluate($join, $active_graph);
623 127         219 return ($iter->next) ? $true : $false;
624 127 50       613 };
    100          
    50          
    100          
    50          
    50          
    50          
625 0         0 } elsif ($expr->isa('Attean::ValueExpression')) {
626             my $node = $expr->value;
627 0     0   0 if ($node->does('Attean::API::Variable')) {
628 0         0 return sub { return shift->value($node->value); };
  0         0  
629 0         0 } else {
630             return sub { return $node };
631 0         0 }
632 0 0       0 } elsif ($expr->isa('Attean::UnaryExpression')) {
633 0         0 my ($child) = @{ $expr->children };
634             my $impl = $self->impl($child, $active_graph);
635 88         151 if ($op eq '!') {
636 88 100       182 return sub {
637 29     29   440 my $term = $impl->(@_);
  29         69  
638             return ($term->ebv) ? $false : $true;
639 59     55   1355 }
  55         97  
640             } elsif ($op eq '-' or $op eq '+') {
641             return sub {
642 0         0 my $term = $impl->(@_);
  0         0  
643 0         0 die "TypeError $op" unless (blessed($term) and $term->does('Attean::API::NumericLiteral'));
644 0 0 0     0 my $v = $term->numeric_value;
    0          
645             return Attean::Literal->new( value => eval "$op$v", datatype => $term->datatype );
646 0     0   0 };
647 0 0       0 }
648             die "Unimplemented UnaryExpression evaluation: " . $expr->operator;
649 0         0 } elsif ($expr->isa('Attean::BinaryExpression')) {
650             my ($lhs, $rhs) = @{ $expr->children };
651 0     0   0 my ($lhsi, $rhsi) = map { $self->impl($_, $active_graph) } ($lhs, $rhs);
652 0 0 0     0 if ($op eq '&&') {
653 0         0 return sub {
654 0         0 my ($r, %args) = @_;
655 0         0 my $lbv = eval { $lhsi->($r, %args) };
656             my $rbv = eval { $rhsi->($r, %args) };
657 0         0 die "TypeError $op" unless ($lbv or $rbv);
658             return $false if (not($lbv) and not($rbv->ebv));
659 35         45 return $false if (not($rbv) and not($lbv->ebv));
  35         71  
660 35         61 die "TypeError $op" unless ($lbv and $rbv);
  70         131  
661 35 100       101 return ($lbv->ebv && $rbv->ebv) ? $true : $false;
    100          
    50          
    0          
    0          
662             }
663 9     9   20 } elsif ($op eq '||') {
664 9         15 return sub {
  9         23  
665 9         506 my ($r, %args) = @_;
  9         20  
666 9 100 100     556 my $lbv = eval { $lhsi->($r, %args) };
667 8 100 100     22 return $true if ($lbv and $lbv->ebv);
668 7 100 100     17 my $rbv = eval { $rhsi->($r, %args) };
669 6 100 100     49 die "TypeError $op" unless ($rbv);
670 4 100 100     13 return $true if ($rbv->ebv);
671             return $false if ($lbv);
672 9         37 die "TypeError $op";
673             }
674 9     9   21 } elsif ($op =~ m#^(?:[-+*/])$#) { # numeric operators: - + * /
675 9         13 return sub {
  9         24  
676 9 100 100     517 my ($r, %args) = @_;
677 6         12 ($lhs, $rhs) = map { $_->($r, %args) } ($lhsi, $rhsi);
  6         14  
678 6 100       446 for ($lhs, $rhs) { die "TypeError $op" unless (blessed($_) and $_->does('Attean::API::NumericLiteral')); }
679 4 100       11 my $lv = $lhs->numeric_value;
680 2 100       12 my $rv = $rhs->numeric_value;
681 1         17 return Attean::Literal->new( value => eval "$lv $op $rv", datatype => $lhs->binary_promotion_type($rhs, $op) );
682             };
683 9         34 } elsif ($op =~ /^!?=$/) {
684             return sub {
685 16     16   35 my ($r, %args) = @_;
686 16         27 ($lhs, $rhs) = map { $_->($r, %args) } ($lhsi, $rhsi);
  32         59  
687 16 100 66     29 for ($lhs, $rhs) { die "TypeError $op" unless (blessed($_) and $_->does('Attean::API::TermOrTriple')); }
  32         383  
688 15         204 my $ok;
689 15         43 if ($lhs->does('Attean::API::Binding')) {
690 15         551 $ok = $lhs->equals($rhs);
691 17         77 } else {
692             $ok = $lhs->equals($rhs);
693             }
694 0     0   0 $ok = not($ok) if ($op eq '!=');
695 0         0 return $ok ? $true : $false;
  0         0  
696 0 0 0     0 }
  0         0  
697 0         0 } elsif ($op =~ /^[<>]=?$/) {
698 0 0       0 return sub {
699 0         0 my ($r, %args) = @_;
700             ($lhs, $rhs) = map { $_->($r, %args) } ($lhsi, $rhsi);
701 0         0 for ($lhs, $rhs) {
702             die "TypeError $op" unless $_->does('Attean::API::TermOrTriple');
703 0 0       0 die "TypeError $op" if ($_->does('Attean::API::IRI')); # comparison of IRIs is only defined for `ORDER BY`, not for general expressions
704 0 0       0 }
705            
706 0         0 my $c = ($lhs->compare($rhs));
707             return $true if (($c < 0 and ($op =~ /<=?/)) or ($c > 0 and ($op =~ />=?/)) or ($c == 0 and ($op =~ /=/)));
708 0     0   0 return $false;
709 0         0 }
  0         0  
710 0         0 }
711 0 0       0 die "Unexpected operator evaluation: $op";
712 0 0       0 } elsif ($expr->isa('Attean::FunctionExpression')) {
713             my $func = $expr->operator;
714             my @children = map { $self->impl($_, $active_graph) } @{ $expr->children };
715 0         0 my %type_roles = qw(URI IRI IRI IRI BLANK Blank LITERAL Literal NUMERIC NumericLiteral TRIPLE Triple);
716 0 0 0     0 my %type_classes = qw(URI Attean::IRI IRI Attean::IRI STR Attean::Literal);
      0        
      0        
      0        
      0        
717 0         0 return sub {
718             my ($r, %args) = @_;
719 0         0 my $row_cache = $args{row_cache} || {};
720 0         0
721             if ($func eq 'IF') {
722 0         0 my $term = $children[0]->( $r, %args );
723 0         0 return ($term->ebv) ? $children[1]->( $r, %args ) : $children[2]->( $r, %args );
  0         0  
  0         0  
724 0         0 } elsif ($func eq 'IN' or $func eq 'NOTIN') {
725 0         0 ($true, $false) = ($false, $true) if ($func eq 'NOTIN');
726             my $child = shift(@children);
727 0     0   0 my $term = $child->( $r, %args );
728 0   0     0 foreach my $c (@children) {
729             if (my $value = eval { $c->( $r, %args ) }) {
730 0 0 0     0 return $true if ($term->equals($value));
    0          
    0          
731 0         0 }
732 0 0       0 }
733             return $false;
734 0 0       0 } elsif ($func eq 'COALESCE') {
735 0         0 foreach my $c (@children) {
736 0         0 my $t = eval { $c->( $r, %args ) };
737 0         0 next if ($@);
738 0 0       0 return $t if $t;
  0         0  
739 0 0       0 }
740             return;
741             }
742 0         0
743             my @operands = map { $_->( $r, %args ) } @children;
744 0         0 if ($func =~ /^(STR)$/) {
745 0         0 return $type_classes{$1}->new($operands[0]->value);
  0         0  
746 0 0       0 } elsif ($func =~ /^(SUBJECT|PREDICATE|OBJECT)$/) {
747 0 0       0 my $pos = lc($func);
748             my $term = $operands[0]->$pos();
749 0         0 return $term;
750             } elsif ($func =~ /^([UI]RI)$/) {
751             my @base = $expr->has_base ? (base => $expr->base) : ();
752 0         0 return $type_classes{$1}->new(value => $operands[0]->value, @base);
  0         0  
753 0 0 0     0 } elsif ($func eq 'BNODE') {
    0 0        
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
754 0         0 if (scalar(@operands)) {
755             my $name = $operands[0]->value;
756 0         0 if (my $b = $row_cache->{bnodes}{$name}) {
757 0         0 return $b;
758 0         0 } else {
759             my $b = Attean::Blank->new();
760 0 0       0 $row_cache->{bnodes}{$name} = $b;
761 0         0 return $b;
762             }
763 0 0       0 }
764 0         0 return Attean::Blank->new();
765 0 0       0 } elsif ($func eq 'LANG') {
766 0         0 die "TypeError: LANG" unless ($operands[0]->does('Attean::API::Literal'));
767             return Attean::Literal->new($operands[0]->language // '');
768 0         0 } elsif ($func eq 'LANGMATCHES') {
769 0         0 my ($lang, $match) = map { $_->value } @operands;
770 0         0 if ($match eq '*') {
771             # """A language-range of "*" matches any non-empty language-tag string."""
772             return ($lang ? $true : $false);
773 0         0 } else {
774             return (I18N::LangTags::is_dialect_of( $lang, $match )) ? $true : $false;
775 0 0       0 }
776 0   0     0 } elsif ($func eq 'DATATYPE') {
777             return $operands[0]->datatype;
778 0         0 } elsif ($func eq 'BOUND') {
  0         0  
779 0 0       0 return $operands[0] ? $true : $false;
780             } elsif ($func eq 'RAND') {
781 0 0       0 return Attean::Literal->new( value => rand(), datatype => 'http://www.w3.org/2001/XMLSchema#double' );
782             } elsif ($func eq 'ABS') {
783 0 0       0 return Attean::Literal->new( value => abs($operands[0]->value), $operands[0]->construct_args );
784             } elsif ($func =~ /^(?:CEIL|FLOOR)$/) {
785             my $v = $operands[0]->value;
786 0         0 return Attean::Literal->new( value => (($func eq 'CEIL') ? ceil($v) : floor($v)), $operands[0]->construct_args );
787             } elsif ($func eq 'ROUND') {
788 0 0       0 return Attean::Literal->new( value => sprintf('%.0f', (0.000000000000001 + $operands[0]->numeric_value)), $operands[0]->construct_args );
789             } elsif ($func eq 'CONCAT') {
790 0         0 my $all_lang = 1;
791             my $all_str = 1;
792 0         0 my $lang;
793             foreach my $n (@operands) {
794 0         0 die "CONCAT called with a non-literal argument" unless ($n->does('Attean::API::Literal'));
795 0 0       0 if ($n->datatype->value ne 'http://www.w3.org/2001/XMLSchema#string') {
796             die "CONCAT called with a datatyped-literal other than xsd:string";
797 0         0 } elsif ($n->language) {
798             $all_str = 0;
799 0         0 if (defined($lang) and $lang ne $n->language) {
800 0         0 $all_lang = 0;
801 0         0 } else {
802 0         0 $lang = $n->language;
803 0 0       0 }
804 0 0       0 } else {
    0          
805 0         0 $all_lang = 0;
806             $all_str = 0;
807 0         0 }
808 0 0 0     0 }
809 0         0 my %strtype;
810             if ($all_lang and $lang) {
811 0         0 $strtype{language} = $lang;
812             } elsif ($all_str) {
813             $strtype{datatype} = 'http://www.w3.org/2001/XMLSchema#string'
814 0         0 }
815 0         0 return Attean::Literal->new( value => join('', map { $_->value } @operands), %strtype );
816             } elsif ($func eq 'SUBSTR') {
817             my $str = shift(@operands);
818 0         0 my @args = map { $_->numeric_value } @operands;
819 0 0 0     0 my $v = scalar(@args == 1) ? substr($str->value, $args[0]-1) : substr($str->value, $args[0]-1, $args[1]);
    0          
820 0         0 return Attean::Literal->new( value => $v, $str->construct_args );
821             } elsif ($func eq 'STRLEN') {
822 0         0 return Attean::Literal->integer(length($operands[0]->value));
823             } elsif ($func eq 'REPLACE') {
824 0         0 my ($node, $pat, $rep) = @operands;
  0         0  
825             die "TypeError: REPLACE called without a literal arg1 term" unless (blessed($node) and $node->does('Attean::API::Literal'));
826 0         0 die "TypeError: REPLACE called without a literal arg2 term" unless (blessed($pat) and $pat->does('Attean::API::Literal'));
827 0         0 die "TypeError: REPLACE called without a literal arg3 term" unless (blessed($rep) and $rep->does('Attean::API::Literal'));
  0         0  
828 0 0       0 die "TypeError: REPLACE called with a datatyped (non-xsd:string) literal" if ($node->datatype and $node->datatype->value ne 'http://www.w3.org/2001/XMLSchema#string');
829 0         0 my ($value, $pattern, $replace) = map { $_->value } @operands;
830             die "EvaluationError: REPLACE called with unsafe ?{} match pattern" if (index($pattern, '(?{') != -1 or index($pattern, '(??{') != -1);
831 0         0 die "EvaluationError: REPLACE called with unsafe ?{} replace pattern" if (index($replace, '(?{') != -1 or index($replace, '(??{') != -1);
832            
833 0         0 $replace =~ s/\\/\\\\/g;
834 0 0 0     0 $replace =~ s/\$(\d+)/\$$1/g;
835 0 0 0     0 $replace =~ s/"/\\"/g;
836 0 0 0     0 $replace = qq["$replace"];
837 0 0 0     0 no warnings 'uninitialized';
838 0         0 $value =~ s/$pattern/"$replace"/eeg;
  0         0  
839 0 0 0     0 return Attean::Literal->new(value => $value, $node->construct_args);
840 0 0 0     0 } elsif ($func =~ /^[UL]CASE$/) {
841             return Attean::Literal->new( value => ($func eq 'UCASE' ? uc($operands[0]->value) : lc($operands[0]->value) ), $operands[0]->construct_args );
842 0         0 } elsif ($func eq 'ENCODE_FOR_URI') {
843 0         0 return Attean::Literal->new( uri_escape_utf8($operands[0]->value) );
844 0         0 } elsif ($func eq 'CONTAINS') {
845 0         0 my ($node, $pat) = @operands;
846 3     3   9209 my ($lit, $plit) = map { $_->value } @operands;
  3         7  
  3         8799  
847 0         0 die "TypeError: CONTAINS" if ($node->language and $pat->language and $node->language ne $pat->language);
  0         0  
848 0         0 return (index($lit, $plit) >= 0) ? $true : $false;
849             } elsif ($func eq 'STRSTARTS' or $func eq 'STRENDS') {
850 0 0       0 my ($lit, $plit) = map { $_->value } @operands;
851             if ($func eq 'STRENDS') {
852 0         0 my $pos = length($lit) - length($plit);
853             return (rindex($lit, $plit) == $pos) ? $true : $false;
854 0         0 } else {
855 0         0 return (index($lit, $plit) == 0) ? $true : $false;
  0         0  
856 0 0 0     0 }
      0        
857 0 0       0 } elsif ($func eq 'STRBEFORE' or $func eq 'STRAFTER') {
858             my ($node, $substr) = @operands;
859 0         0  
  0         0  
860 0 0       0 die "$func called without a literal arg1 term" unless (blessed($node) and $node->does('Attean::API::Literal'));
861 0         0 die "$func called without a literal arg2 term" unless (blessed($substr) and $substr->does('Attean::API::Literal'));
862 0 0       0 die "$func called with a datatyped (non-xsd:string) literal" if ($node->datatype and $node->datatype->value ne 'http://www.w3.org/2001/XMLSchema#string');
863              
864 0 0       0 my $lhs_simple = (not($node->language) and ($node->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string'));
865             my $rhs_simple = (not($substr->language) and ($substr->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string'));
866             if ($lhs_simple and $rhs_simple) {
867 0         0 # ok
868             } elsif ($node->language and $substr->language and $node->language eq $substr->language) {
869 0 0 0     0 # ok
870 0 0 0     0 } elsif ($node->language and $rhs_simple) {
871 0 0 0     0 # ok
872             } else {
873 0   0     0 die "$func called with literals that are not argument compatible";
874 0   0     0 }
875 0 0 0     0
    0 0        
    0 0        
      0        
876             my $value = $node->value;
877             my $match = $substr->value;
878             my $i = index($value, $match, 0);
879             if ($i < 0) {
880             return Attean::Literal->new('');
881             } else {
882 0         0 if ($func eq 'STRBEFORE') {
883             return Attean::Literal->new(value => substr($value, 0, $i), $node->construct_args);
884             } else {
885 0         0 return Attean::Literal->new(value => substr($value, $i+length($match)), $node->construct_args);
886 0         0 }
887 0         0 }
888 0 0       0 } elsif ($func =~ /^(?:YEAR|MONTH|DAY|HOURS|MINUTES)$/) {
889 0         0 my $method = lc($func =~ s/^(HOUR|MINUTE)S$/$1/r);
890             my $dt = $operands[0]->datetime;
891 0 0       0 return Attean::Literal->integer($dt->$method());
892 0         0 } elsif ($func eq 'SECONDS') {
893             my $dt = $operands[0]->datetime;
894 0         0 return Attean::Literal->decimal($dt->second());
895             } elsif ($func eq 'TZ' or $func eq 'TIMEZONE') {
896             my $dt = $operands[0]->datetime;
897             my $tz = $dt->time_zone;
898 0         0 if ($tz->is_floating) {
899 0         0 return Attean::Literal->new('') if ($func eq 'TZ');
900 0         0 die "TIMEZONE called with a dateTime without a timezone";
901             }
902 0         0 return Attean::Literal->new('Z') if ($func eq 'TZ' and $tz->is_utc);
903 0         0 if ($tz) {
904             my $offset = $tz->offset_for_datetime( $dt );
905 0         0 my $hours = 0;
906 0         0 my $minutes = 0;
907 0 0       0 my $minus = ($func eq 'TZ') ? '+' : '';
908 0 0       0 if ($offset < 0) {
909 0         0 $minus = '-';
910             $offset = -$offset;
911 0 0 0     0 }
912 0 0       0  
913 0         0 my $duration = "${minus}PT";
914 0         0 if ($offset >= 60*60) {
915 0         0 my $h = int($offset / (60*60));
916 0 0       0 $duration .= "${h}H" if ($h > 0);
917 0 0       0 $hours = int($offset / (60*60));
918 0         0 $offset = $offset % (60*60);
919 0         0 }
920             if ($offset >= 60) {
921             my $m = int($offset / 60);
922 0         0 $duration .= "${m}M" if ($m > 0);
923 0 0       0 $minutes = int($offset / 60);
924 0         0 $offset = $offset % 60;
925 0 0       0 }
926 0         0 my $seconds = int($offset);
927 0         0 my $s = int($offset);
928             $duration .= "${s}S" if ($s > 0 or $duration eq 'PT');
929 0 0       0
930 0         0 return ($func eq 'TZ')
931 0 0       0 ? Attean::Literal->new(sprintf('%s%02d:%02d', $minus, $hours, $minutes))
932 0         0 : Attean::Literal->new( value => $duration, datatype => "http://www.w3.org/2001/XMLSchema#dayTimeDuration");
933 0         0 } else {
934             return Attean::Literal->new('') if ($func eq 'TZ');
935 0         0 die "TIMEZONE called without a valid dateTime";
936 0         0 }
937 0 0 0     0 } elsif ($func eq 'NOW') {
938             my $value = DateTime::Format::W3CDTF->new->format_datetime( DateTime->now );
939 0 0       0 return Attean::Literal->new( value => $value, datatype => 'http://www.w3.org/2001/XMLSchema#dateTime' );
940             } elsif ($func =~ /^(?:STR)?UUID$/) {
941             return Attean::Literal->new(uc(uuid_to_string(create_uuid()))) if ($func eq 'STRUUID');
942             return Attean::IRI->new('urn:uuid:' . uc(uuid_to_string(create_uuid())));
943 0 0       0 } elsif ($func =~ /^(MD5|SHA1|SHA256|SHA384|SHA512)$/) {
944 0         0 my $hash = $func =~ s/SHA/SHA-/r;
945             my $digest = eval { Digest->new($hash)->add(encode('UTF-8', $operands[0]->value, Encode::FB_CROAK))->hexdigest };
946             return Attean::Literal->new($digest);
947 0         0 } elsif ($func eq 'STRLANG') {
948 0         0 my ($str, $lang) = @operands;
949             my @values = map { $_->value } @operands;
950 0 0       0 die "TypeError: STRLANG must be called with two plain literals" unless (blessed($str) and $str->does('Attean::API::Literal') and blessed($lang) and $lang->does('Attean::API::Literal'));
951 0         0 die "TypeError: STRLANG not called with a simple literal" unless ($str->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string' and not($str->language));
952             return Attean::Literal->new( value => $values[0], language => $values[1] );
953 0         0 } elsif ($func eq 'STRDT') {
954 0         0 die "TypeError: STRDT" unless ($operands[0]->does('Attean::API::Literal') and not($operands[0]->language));
  0         0  
955 0         0 if (my $dt = $operands[0]->datatype) {
956             die "TypeError: STRDT" unless ($dt->value eq 'http://www.w3.org/2001/XMLSchema#string');
957 0         0 }
958 0         0 die "TypeError: STRDT" unless ($operands[1]->does('Attean::API::IRI'));
  0         0  
959 0 0 0     0 my @values = map { $_->value } @operands;
      0        
      0        
960 0 0 0     0 return Attean::Literal->new( value => $values[0], datatype => $values[1] );
961 0         0 } elsif ($func eq 'SAMETERM') {
962             my ($a, $b) = @operands;
963 0 0 0     0 die "TypeError: SAMETERM" unless (blessed($operands[0]) and blessed($operands[1]));
964 0 0       0 if ($a->compare($b)) {
965 0 0       0 return $false;
966             }
967 0 0       0 if ($a->does('Attean::API::Binding')) {
968 0         0 my $ok = ($a->sameTerms($b));
  0         0  
969 0         0 return $ok ? $true : $false;
970             } else {
971 0         0 my $ok = ($a->value eq $b->value);
972 0 0 0     0 return $ok ? $true : $false;
973 0 0       0 }
974 0         0 } elsif ($func =~ /^IS([UI]RI|BLANK|LITERAL|NUMERIC|TRIPLE)$/) {
975             return $operands[0]->does("Attean::API::$type_roles{$1}") ? $true : $false;
976 0 0       0 } elsif ($func eq 'REGEX') {
977 0         0 my ($value, $pattern) = map { $_->value } @operands;
978 0 0       0 return ($value =~ /$pattern/) ? $true : $false;
979             } elsif ($func eq 'INVOKE') {
980 0         0 my $furi = shift(@operands)->value;
981 0 0       0 my $func = Attean->get_global_function($furi);
982             unless (ref($func)) {
983             die "No extension registered for <$furi>";
984 0 0       0 }
985             return $func->(@operands);
986 0         0 }
  0         0  
987 0 0       0 die "Unimplemented FunctionExpression evaluation: " . $expr->operator;
988             };
989 0         0 } elsif ($expr->isa('Attean::AggregateExpression')) {
990 0         0 my $agg = $expr->operator;
991 0 0       0 my ($child) = @{ $expr->children };
992 0         0 if ($agg eq 'COUNT') {
993             if ($child) {
994 0         0 my $impl = $self->impl($child, $active_graph);
995             return sub {
996 0         0 my ($rows, %args) = @_;
997 0         0 my @terms = grep { blessed($_) } map { $impl->($_, %args) } @{ $rows };
998             if ($expr->distinct) {
999 0         0 my %seen;
1000 0         0 @terms = grep { not($seen{$_->as_string}++) } @terms;
  0         0  
1001 0 0       0 }
    0          
    0          
1002 0 0       0 return Attean::Literal->integer(scalar(@terms));
1003 0         0 };
1004             } else {
1005 0     0   0 return sub {
1006 0         0 my ($rows, %args) = @_;
  0         0  
  0         0  
  0         0  
1007 0 0       0 return Attean::Literal->integer(scalar(@$rows));
1008 0         0 };
1009 0         0 }
  0         0  
1010             } elsif ($agg =~ /^(?:SAMPLE|MIN|MAX|SUM|AVG|GROUP_CONCAT)$/) {
1011 0         0 my $impl = $self->impl($child, $active_graph);
1012 0         0 if ($agg eq 'SAMPLE') {
1013             return sub {
1014             my ($rows, %args) = @_;
1015 0     0   0 return $impl->( shift(@$rows), %args )
1016 0         0 };
1017 0         0 } elsif ($agg eq 'MIN' or $agg eq 'MAX') {
1018             my $expect = ($agg eq 'MIN') ? 1 : -1;
1019             return sub {
1020 0         0 my ($rows, %args) = @_;
1021 0 0 0     0 my $extrema;
    0 0        
    0          
    0          
1022             foreach my $r (@$rows) {
1023 0     0   0 my $t = $impl->( $r, %args );
1024 0         0 return if (not($t) and $agg eq 'MIN'); # unbound is always minimal
1025 0         0 next if (not($t)); # unbound need not be considered for MAX
1026             $extrema = $t if (not($extrema) or $extrema->compare($t) == $expect);
1027 0 0       0 }
1028             return $extrema;
1029 0     0   0 };
1030 0         0 } elsif ($agg eq 'SUM' or $agg eq 'AVG') {
1031 0         0 return sub {
1032 0         0 my ($rows, %args) = @_;
1033 0 0 0     0 my $count = 0;
1034 0 0       0 my $sum = Attean::Literal->integer(0);
1035 0 0 0     0 my %seen;
1036             foreach my $r (@$rows) {
1037 0         0 my $term = $impl->( $r, %args );
1038 0         0 if ($expr->distinct) {
1039             next if ($seen{ $term->as_string }++);
1040             }
1041 0     0   0 if ($term->does('Attean::API::NumericLiteral')) {
1042 0         0 $count++;
1043 0         0 $sum = Attean::Literal->new( value => ($sum->numeric_value + $term->numeric_value), datatype => $sum->binary_promotion_type($term, '+') );
1044 0         0 } else {
1045 0         0 die "TypeError: AVG";
1046 0         0 }
1047 0 0       0 }
1048 0 0       0 if ($agg eq 'AVG') {
1049             $sum = not($count) ? undef : Attean::Literal->new( value => ($sum->numeric_value / $count), datatype => $sum->binary_promotion_type(Attean::Literal->integer($count), '/') );
1050 0 0       0 }
1051 0         0 return $sum;
1052 0         0 };
1053             } elsif ($agg eq 'GROUP_CONCAT') {
1054 0         0 my $sep = $expr->scalar_vars->{ 'seperator' } // ' ';
1055             return sub {
1056             my ($rows, %args) = @_;
1057 0 0       0 my %seen;
1058 0 0       0 my @strings;
1059             foreach my $r (@$rows) {
1060 0         0 my $term = eval { $impl->( $r, %args ) };
1061 0         0 if ($expr->distinct) {
1062             next if ($seen{ blessed($term) ? $term->as_string : '' }++);
1063 0   0     0 }
1064             push(@strings, $term->value // '');
1065 0     0   0 }
1066 0         0 return Attean::Literal->new(join($sep, sort @strings));
1067             };
1068 0         0 }
1069 0         0 } elsif ($agg eq 'CUSTOM') {
  0         0  
1070 0 0       0 my $iri = $expr->custom_iri;
1071 0 0       0 my $data = Attean->get_global_aggregate($iri);
    0          
1072             unless ($data) {
1073 0   0     0 die "No extension aggregate registered for <$iri>";
1074             }
1075 0         0 my $start = $data->{'start'};
1076 0         0 my $process = $data->{'process'};
1077             my $finalize = $data->{'finalize'};
1078              
1079 0         0 my $impl = $self->impl($child, $active_graph);
1080 0         0 return sub {
1081 0 0       0 my ($rows, %args) = @_;
1082 0         0 my $thunk = $start->();
1083             foreach my $r (@$rows) {
1084 0         0 my $t = $impl->( $r, %args );
1085 0         0 $process->($thunk, $t);
1086 0         0 }
1087             return $finalize->($thunk);
1088 0         0 };
1089             }
1090 0     0   0 die "Unimplemented AggregateExpression evaluation: " . $expr->operator;
1091 0         0 } elsif ($expr->isa('Attean::CastExpression')) {
1092 0         0 my ($child) = @{ $expr->children };
1093 0         0 my $impl = $self->impl( $child, $active_graph );
1094 0         0 my $type = $expr->datatype;
1095             return sub {
1096 0         0 my ($r, %args) = @_;
1097 0         0 my $term = $impl->($r, %args);
1098             # TODO: reformat syntax for xsd:double
1099 0         0 my $cast = Attean::Literal->new( value => $term->value, datatype => $type );
1100             return $cast->canonicalized_term if ($cast->does('Attean::API::CanonicalizingLiteral'));
1101 4         6 return $cast;
  4         38  
1102 4         19 }
1103 4         11 } else {
1104             Carp::confess "No impl for expression " . $expr->as_string;
1105 4     4   10 }
1106 4         11 }
1107             }
1108 4         83  
1109 4 50       327 1;
1110 0            
1111              
1112 4         17 =back
1113 0            
1114             =head1 BUGS
1115              
1116             Please report any bugs or feature requests to through the GitHub web interface
1117             at L<https://github.com/kasei/attean/issues>.
1118              
1119             =head1 SEE ALSO
1120              
1121              
1122              
1123             =head1 AUTHOR
1124              
1125             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
1126              
1127             =head1 COPYRIGHT
1128              
1129             Copyright (c) 2014--2022 Gregory Todd Williams.
1130             This program is free software; you can redistribute it and/or modify it under
1131             the same terms as Perl itself.
1132              
1133             =cut