#!/usr/bin/perl use strict; use warnings; use URI::file; use Test::More; use Scalar::Util qw(blessed); use RDF::Query::Node qw(iri); use RDF::Query::Error qw(:try); use lib qw(. t); BEGIN { require "models.pl"; } ################################################################################ # Log::Log4perl::init( \q[ # log4perl.category.rdf.query.plan.quad = TRACE, Screen # # log4perl.appender.Screen = Log::Log4perl::Appender::Screen # log4perl.appender.Screen.stderr = 0 # log4perl.appender.Screen.layout = Log::Log4perl::Layout::SimpleLayout # ] ); ################################################################################ my $file = 'data/foaf.xrdf'; my %named = map { $_ => URI::file->new_abs( File::Spec->rel2abs("data/named_graphs/$_") ) } qw(alice.rdf bob.rdf meta.rdf repeats1.rdf repeats2.rdf); my @models = test_models_and_classes($file); eval { require LWP::Simple }; if ($@) { plan skip_all => "LWP::Simple is not available for loading URLs"; return; } elsif (not exists $ENV{RDFQUERY_DEV_TESTS}) { plan skip_all => 'Developer tests. Set RDFQUERY_DEV_TESTS to run these tests.'; return; } use RDF::Query; use RDF::Trine::Namespace qw(rdf foaf); use RDF::Query::Plan; my $nil = RDF::Trine::Node::Nil->new(); ################################################################################ { { my $var = RDF::Trine::Node::Variable->new('s'); my $triple = RDF::Query::Plan::Quad->new( $var, $rdf->type, $foaf->Person, $nil ); my $plan = RDF::Query::Plan::Limit->new( 2, $triple ); is( _CLEAN_WS($plan->sse), '(limit 2 (quad ?s (nil)))', 'sse: triple' ) or die; } { my $var = RDF::Trine::Node::Variable->new('s'); my $triple = RDF::Query::Plan::Quad->new( $var, $rdf->type, $foaf->Person, $nil ); my $plan = RDF::Query::Plan::Offset->new( 2, $triple ); is( _CLEAN_WS($plan->sse), '(offset 2 (quad ?s (nil)))', 'sse: offset' ) or die; } { my $var = RDF::Trine::Node::Variable->new('p'); my $plan_a = RDF::Query::Plan::Quad->new( $var, $foaf->homepage, RDF::Trine::Node::Variable->new('page'), $nil ); my $plan_b = RDF::Query::Plan::Quad->new( $var, $foaf->name, RDF::Trine::Node::Variable->new('name'), $nil ); my $plan = RDF::Query::Plan::ThresholdUnion->new( 7, $plan_a, $plan_b ); is( _CLEAN_WS($plan->sse), '(threshold-union 7 (quad ?p ?page (nil)) (quad ?p ?name (nil)))', 'sse: threshold-union' ) or die; } { my $var = RDF::Trine::Node::Variable->new('p'); my $plan_a = RDF::Query::Plan::Quad->new( $var, $rdf->type, $foaf->Person, $nil ); my $plan_b = RDF::Query::Plan::Quad->new( $var, $foaf->homepage, RDF::Trine::Node::Variable->new('page'), $nil ); my $subplan = RDF::Query::Plan::Join::NestedLoop->new( $plan_a, $plan_b ); my $plan = RDF::Query::Plan::Service->new( 'http://kasei.us/sparql', $subplan, 0, 'PREFIX foaf: SELECT DISTINCT * WHERE { ?p a foaf:Person ; foaf:homepage ?page }' ); is( _CLEAN_WS($plan->sse), '(service "PREFIX foaf: SELECT DISTINCT * WHERE { ?p a foaf:Person ; foaf:homepage ?page }")', 'sse: service' ) or die; } } foreach my $data (@models) { my $model = $data->{modelobj}; print "\n#################################\n"; print "### Using model: $model\n\n"; unless ($model->isa('RDF::Trine::Model')) { $model = RDF::Trine::Model->new( RDF::Trine::Store->new_with_object( $model ) ); } my $parser = RDF::Trine::Parser->new('rdfxml'); foreach my $uri (values %named) { $parser->parse_url_into_model( "$uri", $model, context => iri("$uri") ); } my $context = RDF::Query::ExecutionContext->new( bound => {}, model => $model, ); { my $query = RDF::Query->new( <<"END", { lang => 'sparql11', force_no_optimization => 1 } ); # force_no_optimization because otherwise we'll get a model-optimized BGP instead of the bind-join PREFIX foaf: SELECT ?p ?name WHERE { ?p foaf:firstName ?name . } BINDINGS ?name { ("Gregory") ("Gary") } END my ($plan, $context) = $query->prepare(); is( _CLEAN_WS($plan->sse), '(project (p name) (bind-join (quad ?p ?name (nil)) (table (row [?name "Gregory"]) (row [?name "Gary"]))))', 'sse: constant' ) or die; } { my $parser = RDF::Query::Parser::SPARQL->new(); my $ns = { foaf => 'http://xmlns.com/foaf/0.1/' }; my ($bgp) = $parser->parse_pattern('{ ?p a foaf:Person }', undef, $ns)->patterns; my ($t) = $bgp->triples; my ($plan) = RDF::Query::Plan->generate_plans( $t, $context ); isa_ok( $plan, 'RDF::Query::Plan::Quad', 'quad algebra to plan' ); is( $plan->sse, '(quad ?p (nil))', 'sse: triple plan' ) or die; } { my $parser = RDF::Query::Parser::SPARQL->new(); my $ns = { foaf => 'http://xmlns.com/foaf/0.1/' }; my ($bgp) = $parser->parse_pattern('{ ?p a foaf:Person ; foaf:name ?name }', undef, $ns)->patterns; my ($plan) = RDF::Query::Plan->generate_plans( $bgp, $context ); isa_ok( $plan, 'RDF::Query::Plan::BasicGraphPattern', 'bgp algebra to plan' ); } { my $parser = RDF::Query::Parser::SPARQL->new(); my $parsed = $parser->parse( 'PREFIX foaf: SELECT * WHERE { ?p foaf:name ?name }' ); my $algebra = $parsed->{triples}[0]->pattern; my ($plan) = RDF::Query::Plan->generate_plans( $algebra, $context ); isa_ok( $plan, 'RDF::Query::Plan::Quad', 'ggp algebra to plan' ); } { my $parser = RDF::Query::Parser::SPARQL->new(); my $parsed = $parser->parse( 'PREFIX foaf: SELECT * WHERE { ?p foaf:name ?name } ORDER BY ?name' ); my $algebra = $parsed->{triples}[0]->pattern; my ($plan) = RDF::Query::Plan->generate_plans( $algebra, $context ); isa_ok( $plan, 'RDF::Query::Plan::Sort', 'sort algebra to plan' ); isa_ok( $plan->pattern, 'RDF::Query::Plan::Quad', 'triple algebra to plan' ); is( _CLEAN_WS($plan->sse), '(order (quad ?p ?name (nil)) ((asc ?name)))', 'sse: sort' ) or die; } { my $parser = RDF::Query::Parser::SPARQL->new(); my $parsed = $parser->parse( 'PREFIX foaf: SELECT * WHERE { ?p a foaf:Person ; foaf:name ?name . FILTER(?name = "Greg") }' ); my $algebra = $parsed->{triples}[0]->pattern; my @plans = RDF::Query::Plan->generate_plans( $algebra, $context ); my ($plan) = sort @plans; isa_ok( $plan, 'RDF::Query::Plan::Filter', 'filter algebra to plan' ); isa_ok( $plan->pattern, 'RDF::Query::Plan::BasicGraphPattern', 'bgp algebra to plan' ); is( _CLEAN_WS($plan->sse), '(filter (== ?name "Greg") (bgp (quad ?p (nil)) (quad ?p ?name (nil))))', 'sse: filter' ); } ############################################################################ { # simple triple my @triple = ( RDF::Trine::Node::Variable->new('p'), $rdf->type, $foaf->Person, ); my $plan = RDF::Query::Plan::Quad->new( @triple, $nil ); my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); $count++; } $plan->close; is( $count, 4, "expected result count for triple (?p a foaf:Person)" ); } { # repeats of the same variable in one triple my @triple = ( RDF::Trine::Node::Variable->new('type'), RDF::Trine::Node::Variable->new('type'), RDF::Trine::Node::Variable->new('obj'), ); my $plan = RDF::Query::Plan::Quad->new( @triple, $nil ); foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); $count++; } is( $count, 1, "expected result count for triple with repeated variables (pass $pass)" ); $plan->close; } } { # simple quad my @quad = ( RDF::Trine::Node::Variable->new('p'), $foaf->name, RDF::Trine::Node::Variable->new('name'), RDF::Trine::Node::Variable->new('c'), ); my $plan = RDF::Query::Plan::Quad->new( @quad ); is( _CLEAN_WS($plan->sse), '(quad ?p ?name ?c)', 'sse: quad' ) or die; my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); my $name = lc($row->{name}->literal_value); like( $row->{name}, qr#(Alice|Bob|Gary|Gregory|Liz|Lauren)#i, 'expected person name from named graph' ); unless (blessed($row->{c}) and $row->{c}->isa('RDF::Trine::Node::Nil')) { like( $row->{c}, qr#${name}[.]rdf>$#, 'graph name matches person name' ); } $count++; } $plan->close; is( $count, 6, "expected result count for quad (?p foaf:name ?name ?c)" ); } { # simple quad converted from algebra my $parser = RDF::Query::Parser::SPARQL->new(); my $parsed = $parser->parse( 'PREFIX foaf: SELECT * WHERE { GRAPH ?c { ?p foaf:name ?name } }' ); my $algebra = $parsed->{triples}[0]; my ($plan) = RDF::Query::Plan->generate_plans( $algebra, $context ); my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); my $name = lc($row->{name}->literal_value); like( $row->{name}, qr#(Alice|Bob)#i, 'expected person name from named graph' ); like( $row->{c}, qr#${name}[.]rdf>$#, 'graph name matches person name' ); $count++; } $plan->close; is( $count, 2, "expected result count for quad (?p foaf:name ?name ?c)" ); } { # repeats of the same variable in one quad my @quad = ( RDF::Query::Node::Variable->new('prop'), RDF::Query::Node::Variable->new('prop'), RDF::Query::Node::Variable->new('obj'), RDF::Query::Node::Variable->new('c'), ); my $plan = RDF::Query::Plan::Quad->new( @quad ); foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); like( $row->{prop}, qr[#(type|label)>$], 'expected property' ); $count++; } is( $count, 3, "expected result count for quad with repeated variables (pass $pass)" ); $plan->close; } } { # repeats of the same variable in one quad, constrained to a specific graph my @quad = ( RDF::Trine::Node::Variable->new('prop'), RDF::Trine::Node::Variable->new('prop'), RDF::Trine::Node::Variable->new('obj'), RDF::Trine::Node::Resource->new("$named{'repeats1.rdf'}"), ); my $plan = RDF::Query::Plan::Quad->new( @quad ); foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); like( $row->{prop}, qr[#type>$], 'expected property' ); $count++; } is( $count, 1, "expected result count for quad with repeated variables (pass $pass)" ); $plan->close; } } { # nested loop join my $var = RDF::Trine::Node::Variable->new('p'); my $plan_a = RDF::Query::Plan::Quad->new( $var, $foaf->homepage, RDF::Trine::Node::Variable->new('page'), RDF::Trine::Node::Nil->new() ); my $plan_b = RDF::Query::Plan::Quad->new( $var, $foaf->name, RDF::Trine::Node::Variable->new('name'), RDF::Trine::Node::Nil->new() ); my $plan = RDF::Query::Plan::Join::NestedLoop->new( $plan_a, $plan_b ); is( _CLEAN_WS($plan->sse), '(nestedloop-join (quad ?p ?page (nil)) (quad ?p ?name (nil)))', 'sse: nestedloop-join' ) or die; foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); like( $row->{p}, qr#^(_:|{page}, qr#^close; } } { # OPTIONAL nested loop join my $var = RDF::Trine::Node::Variable->new('p'); my $plan_a = RDF::Query::Plan::Quad->new( $var, $foaf->name, RDF::Trine::Node::Variable->new('name'), $nil ); my $plan_b = RDF::Query::Plan::Quad->new( $var, $foaf->homepage, RDF::Trine::Node::Variable->new('page'), $nil ); my $plan = RDF::Query::Plan::Join::PushDownNestedLoop->new( $plan_a, $plan_b, 1 ); is( _CLEAN_WS($plan->sse), '(bind-leftjoin (quad ?p ?name (nil)) (quad ?p ?page (nil)))', 'sse: bind-leftjoin' ) or die; foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); my @rows = $plan->get_all; my @names = map { $_->literal_value } grep { defined($_) } map { $_->{name} } @rows; my @pages = map { $_->uri_value } grep { defined($_) } map { $_->{page} } @rows; is( scalar(@names), 4, 'expected result count for names in OPTIONAL join' ); is( scalar(@pages), 2, 'expected result count for homepages in OPTIONAL join' ); $plan->close; } } { # union my $var = RDF::Trine::Node::Variable->new('s'); my $plan_a = RDF::Query::Plan::Quad->new( $var, $rdf->type, $foaf->Person, $nil ); my $plan_b = RDF::Query::Plan::Quad->new( $var, $rdf->type, $foaf->PersonalProfileDocument, $nil ); my $plan = RDF::Query::Plan::Union->new( $plan_a, $plan_b ); is( _CLEAN_WS($plan->sse), '(union (quad ?s (nil)) (quad ?s (nil)))' ) or die; foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { $count++; } is( $count, 5, "expected result count for union (pass $pass)" ); $plan->close; } } # SKIP: { # skip "network tests. Set RDFQUERY_NETWORK_TESTS to run these tests.", 2 unless (exists $ENV{RDFQUERY_NETWORK_TESTS}); # # my $var = RDF::Trine::Node::Variable->new('p'); # my $plan_a = RDF::Query::Plan::Triple->new( $var, $rdf->type, $foaf->Person ); # my $plan_b = RDF::Query::Plan::Triple->new( $var, $foaf->homepage, RDF::Trine::Node::Variable->new('page') ); # my $subplan = RDF::Query::Plan::Join::NestedLoop->new( $plan_a, $plan_b ); # my $plan = RDF::Query::Plan::Service->new( 'http://kasei.us/sparql', $subplan, 'PREFIX foaf: SELECT DISTINCT * WHERE { ?p a foaf:Person ; foaf:homepage ?page }' ); # # foreach my $pass (1..2) { # my $count = 0; # $plan->execute( $context ); # while (my $row = $plan->next) { # if ($count == 0) { # isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); # } # $count++; # } # cmp_ok( $count, '>', 1, "positive result count for SERVICE plan (pass $pass)" ); # $plan->close; # } # } SKIP: { skip "network tests. Set RDFQUERY_NETWORK_TESTS to run these tests.", 2 unless (exists $ENV{RDFQUERY_NETWORK_TESTS}); my $parser = RDF::Query::Parser::SPARQL11->new(); my $parsed = $parser->parse( 'PREFIX foaf: SELECT * WHERE { SERVICE { ?p a foaf:Person ; foaf:homepage ?page } }' ); my $algebra = $parsed->{triples}[0]; my ($plan) = RDF::Query::Plan->generate_plans( $algebra, $context ); foreach my $pass (1..2) { my $skip = 2; my $count = 0; try { $plan->execute( $context ); while (my $row = $plan->next) { if ($count == 0) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); } $count++; } cmp_ok( $count, '>', 1, "positive result count for generated SERVICE plan (pass $pass)" ); $plan->close; $skip--; } catch RDF::Query::Error::ExecutionError with { my $e = shift; skip $e->text, $skip; }; } } { # project on ?p { ?s ?p foaf:Person } my $s = RDF::Trine::Node::Variable->new('s'); my $p = RDF::Trine::Node::Variable->new('p'); my $plan_a = RDF::Query::Plan::Quad->new( $s, $p, $foaf->Person, $nil ); my $plan = RDF::Query::Plan::Project->new( $plan_a, ['p'] ); is( _CLEAN_WS($plan->sse), '(project (p) (quad ?s ?p (nil)))', 'sse: project' ) or die; foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); my @keys = $row->variables; is_deeply( \@keys, ['p'], 'projected variable list' ); $count++; } is( $count, 4, "expected result count for project (pass $pass)" ); $plan->close; } } { # { _:s rdf:first [] ; ?p [] } my $s = RDF::Trine::Node::Variable->new('__s'); my $f = RDF::Trine::Node::Variable->new('__f'); my $r = RDF::Trine::Node::Variable->new('__r'); my $p = RDF::Trine::Node::Variable->new('p'); my $plan_a = RDF::Query::Plan::Quad->new( $s, $rdf->first, $f, $nil ); my $plan_b = RDF::Query::Plan::Quad->new( $s, $p, $r, $nil ); my $join = RDF::Query::Plan::Join::NestedLoop->new( $plan_a, $plan_b ); my $proj = RDF::Query::Plan::Project->new( $join, ['p'] ); my $plan = RDF::Query::Plan::Distinct->new( $proj ); is( _CLEAN_WS($plan->sse), '(distinct (project (p) (nestedloop-join (quad ?__s ?__f (nil)) (quad ?__s ?p ?__r (nil)))))', 'sse: distinct-project-join' ) or die; foreach my $pass (1..2) { my $count = 0; $plan->execute( $context ); while (my $row = $plan->next) { isa_ok( $row, 'RDF::Query::VariableBindings', 'variable bindings' ); like( $row->{p}, qr[^($], 'expected predicate URI' ); $count++; } is( $count, 2, "expected result count for distinct (pass $pass)" ); $plan->close; } } { # simple variable sort with numerics my $s = RDF::Trine::Node::Variable->new('__s'); my $var = RDF::Trine::Node::Variable->new('v'); my $triple = RDF::Query::Plan::Quad->new( $s, $rdf->first, $var, $nil ); my $proj = RDF::Query::Plan::Project->new( $triple, ['v'] ); foreach my $data ([0 => [1,2,3]], [1 => [3,2,1]]) { my ($order, $expect) = @$data; my $dir = ($order) ? 'DESC' : 'ASC'; my $plan = RDF::Query::Plan::Sort->new( $proj, [$var, $order] ); my $count = 0; $plan->execute( $context ); my @rows = $plan->get_all; my @values = map { $_->{ v }->literal_value } @rows; is_deeply( \@values, $expect, "expected $dir sort values on numerics" ); $plan->close; } } { # simple variable sort with plain literals my $s = RDF::Trine::Node::Variable->new('__s'); my $var = RDF::Trine::Node::Variable->new('v'); my $triple = RDF::Query::Plan::Quad->new( $s, $foaf->name, $var, $nil ); my $proj = RDF::Query::Plan::Project->new( $triple, ['v'] ); my $expr = RDF::Query::Expression::Function->new( 'sparql:str', $var ); my @expect = ('Gary P', 'Gregory Todd Williams', 'Lauren B', 'Liz F'); foreach my $data ([0 => [@expect]], [1 => [reverse @expect]]) { my ($order, $expect) = @$data; my $dir = ($order) ? 'DESC' : 'ASC'; my $plan = RDF::Query::Plan::Sort->new( $proj, [$expr, $order] ); my $count = 0; $plan->execute( $context ); my @rows = $plan->get_all; my @values = map { $_->{ v }->literal_value } @rows; is_deeply( \@values, $expect, "expected $dir sort values on plain literals" ); $plan->close; } } { # sort with multiple expressions my $s = RDF::Trine::Node::Variable->new('__s'); my $s2 = RDF::Trine::Node::Variable->new('__s2'); my $var = RDF::Trine::Node::Variable->new('v'); my $name = RDF::Trine::Node::Variable->new('name'); my $plan_a = RDF::Query::Plan::Quad->new( $s, $rdf->first, $var, $nil ); my $plan_b = RDF::Query::Plan::Quad->new( $s2, $foaf->name, $name, $nil ); my $join = RDF::Query::Plan::Join::NestedLoop->new( $plan_a, $plan_b ); my $proj = RDF::Query::Plan::Project->new( $join, [qw(v name)] ); my $expr1 = RDF::Query::Expression::Binary->new( '*', RDF::Query::Expression::Function->new( 'http://www.w3.org/2001/XMLSchema#integer', $var ), RDF::Query::Node::Literal->new('-1', undef, 'http://www.w3.org/2001/XMLSchema#integer') ); my $expr2 = RDF::Query::Expression::Function->new( 'sparql:str', $name ); my $plan = RDF::Query::Plan::Sort->new( $proj, [$expr1, 0], [$expr2, 1] ); my @expect_v = (3,2,1); my @expect_name = reverse('Gary P', 'Gregory Todd Williams', 'Lauren B', 'Liz F'); $plan->execute( $context ); my @rows = $plan->get_all; my @v = map { $_->{ v }->literal_value } @rows; my @names = map { $_->{ name }->literal_value } @rows; is_deeply( \@v, [map { ($_)x4 } @expect_v], "expected primary sort values on numerics" ); is_deeply( \@names, [(@expect_name)x3], "expected secondary sort values on plain literals" ); $plan->close; } { # filter my $parser = RDF::Query::Parser::SPARQL->new(); my $ns = { foaf => 'http://xmlns.com/foaf/0.1/' }; my ($algebra) = $parser->parse_pattern('{ ?p a foaf:Person ; foaf:firstName ?name . FILTER(?name = "Gregory") }', undef, $ns); my ($join) = RDF::Query::Plan->generate_plans( $algebra, $context ); my $plan = RDF::Query::Plan::Project->new( $join, [qw(name)] ); $plan->execute( $context ); my @rows = $plan->get_all; my @v = map { $_->{ name }->literal_value } @rows; is_deeply( \@v, ['Gregory'], "expected filtered values on literals" ); $plan->close; } { # construct my $parser = RDF::Query::Parser::SPARQL->new(); my $ns = { foaf => 'http://xmlns.com/foaf/0.1/' }; my ($algebra) = $parser->parse_pattern('{ ?p foaf:firstName ?name }', undef, $ns); my ($pat) = RDF::Query::Plan->generate_plans( $algebra, $context ); my $st = RDF::Trine::Statement->new( RDF::Trine::Node::Variable->new('p'), $foaf->name, RDF::Trine::Node::Variable->new('name') ); my $plan = RDF::Query::Plan::Construct->new( $pat, [ $st ] ); is( _CLEAN_WS($plan->sse), '(construct (quad ?p ?name (nil)) ((triple ?p ?name)))', 'sse: construct' ) or die; $plan->execute( $context ); my $count = 0; while (my $row = $plan->next) { isa_ok( $row, 'RDF::Trine::Statement' ); my $pred = $row->predicate(); my $obj = $row->object(); is( $pred->as_string, '', 'expected CONSTRUCT predicate' ); like( $obj->as_string, qr<"(.+)">, 'expected CONSTRUCT object' ); $count++; } is( $count, 4, 'expected CONSTRUCT triple count' ); $plan->close; } } done_testing; sub _CLEAN_WS { my $string = shift; for ($string) { s/\s+/ /g; } return $string; }