<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -64,6 +64,28 @@ sub referenced_blanks {
 	return uniq(@list);
 }
 
+=item C&lt;&lt; referenced_functions &gt;&gt;
+
+Returns a list of the Function URIs used in this algebra expression.
+
+=cut
+
+sub referenced_functions {
+	my $self	= shift;
+	my @list;
+	foreach my $arg ($self-&gt;construct_args) {
+		if (blessed($arg)) {
+			if ($arg-&gt;isa('RDF::Query::Expression::Function')) {
+				push(@list, $arg-&gt;uri);
+			} elsif ($arg-&gt;isa('RDF::Query::Algebra')) {
+				my @funcs	= $arg-&gt;referenced_functions;
+				push(@list, @funcs);
+			}
+		}
+	}
+	return uniq(@list);
+}
+
 =item C&lt;&lt; check_duplicate_blanks &gt;&gt;
 
 Returns true if blank nodes respect the SPARQL rule of no blank-label re-use</diff>
      <filename>RDF-Query/lib/RDF/Query/Algebra.pm</filename>
    </modified>
    <modified>
      <diff>@@ -31,6 +31,7 @@ our ($VERSION, $debug, $lang, $languri);
 BEGIN {
 	$debug		= 0;
 	$VERSION	= do { my $REV = (qw$Revision: 121 $)[1]; sprintf(&quot;%0.3f&quot;, 1 + ($REV/1000)) };
+	our %SERVICE_BLOOM_IGNORE	= ('http://dbpedia.org/sparql' =&gt; 1);	# by default, assume dbpedia doesn't implement k:bloom().
 }
 
 ######################################################################
@@ -199,6 +200,8 @@ sub execute {
 		
 		my $handled	= 0;
 		
+		our %SERVICE_BLOOM_IGNORE;	# keep track of which service calls throw an error so we don't keep trying it...
+		
 		### cooperate with ::Algebra::Service so that if we've already got a stream
 		### of results from previous patterns, and the next pattern is a remote
 		### service call, we can try to send along a bloom filter function.
@@ -206,26 +209,27 @@ sub execute {
 		### function), then fall back on making the call without the filter.
 		try {
 			if ($stream and $triple-&gt;isa('RDF::Query::Algebra::Service')) {
-				my $m		= $stream-&gt;materialize;
-				
-				my @vars	= $triple-&gt;referenced_variables;
-				my %svars	= map { $_ =&gt; 1 } $stream-&gt;binding_names;
-				my $var		= RDF::Query::Node::Variable-&gt;new( first { $svars{ $_ } } @vars );
-				
-				my $f		= RDF::Query::Algebra::Service-&gt;bloom_filter_for_iterator( $query, $bridge, $bound, $m, $var, 0.001 );
-				
-				my $new;
-				try {
+				unless ($SERVICE_BLOOM_IGNORE{ $triple-&gt;endpoint-&gt;uri_value }) {
+	# 				local($RDF::Trine::Iterator::debug)	= 1;
+					$stream		= $stream-&gt;materialize;
+					my $m		= $stream;
+					
+					my @vars	= $triple-&gt;referenced_variables;
+					my %svars	= map { $_ =&gt; 1 } $stream-&gt;binding_names;
+					my $var		= RDF::Query::Node::Variable-&gt;new( first { $svars{ $_ } } @vars );
+					
+					my $f		= RDF::Query::Algebra::Service-&gt;bloom_filter_for_iterator( $query, $bridge, $bound, $m, $var, 0.001 );
+					
 					my $pattern	= $triple-&gt;add_bloom( $var, $f );
-					$new	= $pattern-&gt;execute( $query, $bridge, $bound, $context, %args );
+					my $new	= $pattern-&gt;execute( $query, $bridge, $bound, $context, %args );
 					throw RDF::Query::Error unless ($new);
-				} otherwise {
-					warn &quot;*** Wasn't able to use :bloom as a FILTER restriction in SERVICE call.\n&quot; if ($debug);
-					$new	= $triple-&gt;execute( $query, $bridge, $bound, $context, %args );
-				};
-				$stream	= RDF::Trine::Iterator::Bindings-&gt;join_streams( $m, $new, %args );
-				$handled	= 1;
+					$stream	= $self-&gt;join_bnode_streams( $m, $new, $query, $bridge, $bound );
+					$handled	= 1;
+				}
 			}
+		} otherwise {
+			$SERVICE_BLOOM_IGNORE{ $triple-&gt;endpoint-&gt;uri_value }	= 1;
+			warn &quot;*** Wasn't able to use k:bloom as a FILTER restriction in SERVICE call.\n&quot; if ($debug);
 		};
 		
 		unless ($handled) {
@@ -245,6 +249,108 @@ sub execute {
 	return $stream;
 }
 
+=item C&lt;&lt; join_bnode_streams ( $streamA, $streamB, $query, $bridge ) &gt;&gt;
+
+A modified inner-loop join that relies on there being bnode identity hints in
+the data returned by C&lt;&lt; $streamB-&gt;extra_result_data &gt;&gt;. These hints are
+combined with locally computed identity values for the items from C&lt;&lt; $streamA &gt;&gt;
+and the streams are merged using a natural join where equality is computed
+either on direct node equality or on any intersection of the identity hints.
+
+The identity hints and locally computed identity values are computed using
+Functional and InverseFunctional property values using N3 syntax. For example,
+a blank node '(r1)' might have identity hints using foaf:mbox_sha1sum such as:
+
+  $extra_result_data = {
+      'bnode-map' =&gt; [ {
+          '(r1)' =&gt; ['!&lt;http://xmlns.com/foaf/0.1/mbox_sha1sum&gt;&quot;26fb6400147dcccfda59717ff861db9cb97ac5ec&quot;']
+      } ]
+  };
+
+=cut
+
+sub join_bnode_streams {
+	my $self	= shift;
+	my $astream	= shift;
+	my $bstream	= shift;
+	my $query	= shift;
+	my $bridge	= shift;
+	
+	Carp::confess unless ($astream-&gt;isa('RDF::Trine::Iterator::Bindings'));
+	Carp::confess unless ($bstream-&gt;isa('RDF::Trine::Iterator::Bindings'));
+	
+	################################################
+	### BNODE MAP STUFF
+	my $b_extra	= $bstream-&gt;extra_result_data || {};
+	my (%b_map);
+	foreach my $h (@{ $b_extra-&gt;{'bnode-map'} || [] }) {
+		foreach my $id (keys %$h) {
+			my @values	= @{ $h-&gt;{ $id } };
+			push( @{ $b_map{ $id } }, @values );
+		}
+	}
+	my $b_map	= (%b_map) ? \%b_map : undef;
+	################################################
+	
+	my @names	= uniq( map { $_-&gt;binding_names() } ($astream, $bstream) );
+	my $a		= $astream-&gt;project( @names );
+	my $b		= $bstream-&gt;project( @names );
+	
+	my @results;
+	my @data	= $b-&gt;get_all();
+	no warnings 'uninitialized';
+	while (my $rowa = $a-&gt;next) {
+		LOOP: foreach my $rowb (@data) {
+			warn &quot;[--JOIN--] &quot; . join(' ', map { my $row = $_; '{' . join(', ', map { join('=', $_, ($row-&gt;{$_}) ? $row-&gt;{$_}-&gt;as_string : '(undef)') } (keys %$row)) . '}' } ($rowa, $rowb)) . &quot;\n&quot; if ($debug);
+			my %keysa	= map {$_=&gt;1} (keys %$rowa);
+			my @shared	= grep { $keysa{ $_ } } (keys %$rowb);
+			foreach my $key (@shared) {
+				my $val_a	= $rowa-&gt;{ $key };
+				my $val_b	= $rowb-&gt;{ $key };
+				my $defined	= 0;
+				foreach my $n ($val_a, $val_b) {
+					$defined++ if (defined($n));
+				}
+				if ($defined == 2) {
+					my $equal	= $val_a-&gt;equal( $val_b );
+					if (not $equal) {
+						my $names	= $b_map-&gt;{ $val_b-&gt;as_string };
+						if ($names) {
+							my $bnames	= Set::Scalar-&gt;new( @{ $names } );
+							my $anames	= Set::Scalar-&gt;new( RDF::Query::Algebra::Service-&gt;_names_for_node( $val_a, $query, $bridge, {} ) );
+							if ($debug) {
+								warn &quot;anames: $anames\n&quot;;
+								warn &quot;bnames: $bnames\n&quot;;
+							}
+							if (my $int = $anames-&gt;intersection( $bnames )) {
+								warn &quot;node equality based on $int&quot; if ($debug);
+								$equal	= 1;
+							}
+						}
+					}
+					
+					unless ($equal) {
+						warn &quot;can't join because mismatch of $key (&quot; . join(' &lt;==&gt; ', map {$_-&gt;as_string} ($val_a, $val_b)) . &quot;)&quot; if ($debug);
+						next LOOP;
+					}
+				}
+			}
+			
+			my $row	= { (map { $_ =&gt; $rowa-&gt;{$_} } grep { defined($rowa-&gt;{$_}) } keys %$rowa), (map { $_ =&gt; $rowb-&gt;{$_} } grep { defined($rowb-&gt;{$_}) } keys %$rowb) };
+			if ($debug) {
+				warn &quot;JOINED:\n&quot;;
+				foreach my $key (keys %$row) {
+					warn &quot;$key\t=&gt; &quot; . $row-&gt;{ $key }-&gt;as_string . &quot;\n&quot;;
+				}
+			}
+			push(@results, $row);
+		}
+	}
+	
+	my $args	= $astream-&gt;_args;
+	return $astream-&gt;_new( \@results, 'bindings', \@names, %$args );
+}
+
 1;
 
 __END__</diff>
      <filename>RDF-Query/lib/RDF/Query/Algebra/GroupGraphPattern.pm</filename>
    </modified>
    <modified>
      <diff>@@ -118,7 +118,7 @@ sub add_bloom {
 	
 	my $pattern	= $self-&gt;pattern;
 	my $iri		= RDF::Query::Node::Resource-&gt;new('http://kasei.us/code/rdf-query/functions/bloom');
-	warn &quot;Adding a bloom filter (with &quot; . $bloom-&gt;key_count . &quot; items) function to a remote query&quot;;
+	warn &quot;Adding a bloom filter (with &quot; . $bloom-&gt;key_count . &quot; items) function to a remote query&quot; if ($debug);
 	my $frozen	= $bloom-&gt;freeze;
 	my $literal	= RDF::Query::Node::Literal-&gt;new( $frozen );
 	my $expr	= RDF::Query::Expression::Function-&gt;new( $iri, $var, $literal );
@@ -268,8 +268,14 @@ sub execute {
 		throw RDF::Query::Error -text =&gt; &quot;SERVICE query couldn't get remote content: &quot; . $resp-&gt;status_line;
 	}
 	my $content		= $resp-&gt;content;
-	warn $content;
-	my $stream		= RDF::Trine::Iterator-&gt;from_string( $content );
+	my $stream		= smap {
+						my $bindings	= $_;
+						return undef unless ($bindings);
+						my %cast	= map {
+										$_ =&gt; RDF::Query::Model::RDFTrine::_cast_to_local( $bindings-&gt;{ $_ } )
+									} (keys %$bindings);
+						return \%cast;
+					} RDF::Trine::Iterator-&gt;from_string( $content );
 	return $stream;
 }
 </diff>
      <filename>RDF-Query/lib/RDF/Query/Algebra/Service.pm</filename>
    </modified>
    <modified>
      <diff>@@ -16,8 +16,9 @@ use strict;
 use warnings;
 no warnings 'redefine';
 
-use Scalar::Util qw(blessed reftype looks_like_number);
+use Scalar::Util qw(blessed reftype refaddr looks_like_number);
 
+use RDF::Query;
 use RDF::Query::Model::RDFTrine;
 use RDF::Query::Error qw(:try);
 
@@ -731,20 +732,18 @@ $RDF::Query::functions{&quot;http://kasei.us/2007/09/functions/warn&quot;}	= sub {
 		my $query	= shift;
 		my $bridge	= shift;
 		my $stream	= shift;
-		if ($query-&gt;{_query_cache}{ $BLOOM_URL }{ 'nodemap' }) {
-			foreach my $map (@{ $query-&gt;{_query_cache}{ $BLOOM_URL }{ 'nodemap' } }) {
-				my ($node, $string)	= @$map;
-				warn &quot;adding node map: &quot; . $node-&gt;as_string . &quot;: &quot; . $string . &quot;\n&quot;;
-			}
-		}
+		warn &quot;bloom filter got result stream\n&quot; if ($debug);
+		my $nodemap	= $query-&gt;{_query_cache}{ $BLOOM_URL }{ 'nodemap' };
+		$stream-&gt;add_extra_result_data('bnode-map', $nodemap);
 	}
-	RDF::Query-&gt;add_hook('http://kasei.us/code/rdf-query/hooks/function_init', sub {
+	push( @{ $RDF::Query::hooks{ 'http://kasei.us/code/rdf-query/hooks/function_init' } }, sub {
 		my $query		= shift;
 		my $function	= shift;
-		if ($function eq $BLOOM_URL) {
+		if ($function-&gt;uri_value eq $BLOOM_URL) {
+			$query-&gt;{_query_cache}{ $BLOOM_URL }{ 'nodemap' }	||= {};
 			$query-&gt;add_hook_once( 'http://kasei.us/code/rdf-query/hooks/post-execute', \&amp;_BLOOM_ADD_NODE_MAP_TO_STREAM, &quot;${BLOOM_URL}#add_node_map&quot; );
 		}
-	});
+	} );
 	$RDF::Query::functions{&quot;http://kasei.us/code/rdf-query/functions/bloom&quot;}	= sub {
 		my $query	= shift;
 		my $bridge	= shift;
@@ -767,7 +766,8 @@ $RDF::Query::functions{&quot;http://kasei.us/2007/09/functions/warn&quot;}	= sub {
 			my $ok	= $bloom-&gt;check( $string );
 			warn &quot;-&gt; ok\n&quot; if ($ok and $debug);
 			if ($ok) {
-				push( @{ $query-&gt;{_query_cache}{ $BLOOM_URL }{ 'nodemap' } }, [ $value, $string ] );
+				my $nodemap	= $query-&gt;{_query_cache}{ $BLOOM_URL }{ 'nodemap' };
+				push( @{ $nodemap-&gt;{ $value-&gt;as_string } }, $string );
 				return RDF::Query::Node::Literal-&gt;new('true', undef, 'http://www.w3.org/2001/XMLSchema#boolean');
 			}
 		}</diff>
      <filename>RDF-Query/lib/RDF/Query/Functions.pm</filename>
    </modified>
    <modified>
      <diff>@@ -6,50 +6,93 @@ use Test::More;
 use lib qw(. t);
 BEGIN { require &quot;models.pl&quot;; }
 
-my @files;
-my @models	= test_models( @files );
-
+my $tests	= 25;
 eval { require LWP::Simple };
 if ($@) {
 	plan skip_all =&gt; &quot;LWP::Simple is not available for loading &lt;http://...&gt; URLs&quot;;
 	return;
 } elsif (not exists $ENV{RDFQUERY_NO_NETWORK}) {
-	plan tests =&gt; 14;
+	plan tests =&gt; $tests;
+} elsif (not exists $ENV{RDFQUERY_DEV_TESTS}) {
+	plan skip_all =&gt; 'Developer tests. Set RDFQUERY_DEV_TESTS to run these tests.';
+	return;
 } else {
 	plan skip_all =&gt; 'No network. Unset RDFQUERY_NO_NETWORK to run these tests.';
 	return;
 }
 
-my $loaded	= use_ok( 'RDF::Query' );
-BAIL_OUT( 'RDF::Query not loaded' ) unless ($loaded);
+use RDF::Query;
 
 
-SKIP: {
-	unless ($ENV{RDFQUERY_DEV_TESTS}) {
-		skip &quot;developer network tests&quot;, 4;
-	}
+{
+	print &quot;# join using default graph (local rdf) and remote SERVICE (kasei.us), joining on bnode\n&quot;;
+	my $file	= URI::file-&gt;new_abs( 'data/bnode-person.rdf' );
 	my $query	= RDF::Query-&gt;new( &lt;&lt;&quot;END&quot;, undef, undef, 'sparqlp' );
 		PREFIX foaf: &lt;http://xmlns.com/foaf/0.1/&gt;
-		SELECT DISTINCT *
-		FROM &lt;http://kasei.us/about/foaf.xrdf&gt;
+		PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
+		SELECT DISTINCT ?name ?nick
+		FROM &lt;$file&gt;
 		WHERE {
 			?p a foaf:Person ; foaf:name ?name .
-			FILTER &lt;http://kasei.us/code/rdf-query/functions/bloom&gt;( ?p, &quot;BAcEMTIzNAQEBAgRDUJsb29tOjpGaWx0ZXIDCAAAAAoEAAAAAAgAAABibGFua3ZlYweamZmZmZmpPwoAAABlcnJvcl9yYXRlCggAAAFAAAgCAAYAAABmaWx0ZXIIhA4AAABudW1faGFzaF9mdW5jcwiBCQAAAGtleV9jb3VudAi/DQAAAGZpbHRlcl9sZW5ndGgIiggAAABjYXBhY2l0eQQCBAAAAAoRMC4zNzkxOTU0NDEyOTczOTQKETAuMjQ0MjQ5NjM2ODQ2NjkzChEwLjg1MTQ5ODA5NTE3NjMxMQoSMC4wNzgyMTI0NDE5NjY1MjA5BQAAAHNhbHRz&quot; ) .
+			SERVICE &lt;http://kasei.us/sparql&gt; {
+				?p foaf:nick ?nick
+			}
 		}
 END
 	my $stream	= $query-&gt;execute();
 	isa_ok( $stream, 'RDF::Trine::Iterator' );
-	my $d	= $stream-&gt;next;
-	isa_ok( $d, 'HASH' );
-	isa_ok( $d-&gt;{ name }, 'RDF::Trine::Node::Literal' );
-	is( $d-&gt;{name}-&gt;literal_value, 'Gregory Todd Williams', 'expected value passed through bloom filter' );
+	my $count	= 0;
+	while (my $d = $stream-&gt;next) {
+		isa_ok( $d-&gt;{nick}, 'RDF::Query::Node::Literal' );
+		like( $d-&gt;{name}-&gt;literal_value, qr/^(Adam Pisoni|Gregory Todd Williams)$/, 'got name from local file (joined on a bnode)' );
+		like( $d-&gt;{nick}-&gt;literal_value, qr/^(wonko)$/, 'got nick from SERVICE (joined on a bnode)' );
+		$count++;
+	}
+	is( $count, 1, 'expected result count' );
 }
 
-
-SKIP: {
-	unless ($ENV{RDFQUERY_DEV_TESTS}) {
-		skip &quot;developer network tests&quot;, 3;
+{
+	my $file	= URI::file-&gt;new_abs( 'data/bnode-person.rdf' );
+	
+	### This filter contains greg and adam, identified by a primaryTopic page and an email sha1sum, respectively:
+	### !&lt;http://xmlns.com/foaf/0.1/mbox_sha1sum&gt;&quot;26fb6400147dcccfda59717ff861db9cb97ac5ec&quot;
+	### ^&lt;http://xmlns.com/foaf/0.1/primaryTopic&gt;&lt;http://kasei.us/&gt;
+	my $sparql	= &lt;&lt;&quot;END&quot;;
+		PREFIX foaf: &lt;http://xmlns.com/foaf/0.1/&gt;
+		PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
+		PREFIX k: &lt;http://kasei.us/code/rdf-query/functions/&gt;
+		SELECT DISTINCT *
+		FROM &lt;$file&gt;
+		WHERE {
+			?p a foaf:Person ; foaf:name ?name .
+			FILTER k:bloom( ?p, &quot;AAAAAgAAAB0AAAACAAAACgAAAAQAAAAFZJREZxMt9AekxG8jP9HulJISKVbFzR2rP9boeVledzCy\\nIqE/jWf8NvjRYRUwLjAwMQ==\\n&quot; ) .
+		}
+END
+	{
+		print &quot;# bgp using default graph (local rdf) with k:bloom FILTER produces bnode identity hints in XML results\n&quot;;
+		my $query	= RDF::Query-&gt;new( $sparql, undef, undef, 'sparqlp' );
+		my $stream	= $query-&gt;execute();
+		my $xml		= $stream-&gt;as_xml;
+		like( $xml, qr#&lt;extrakey id=&quot;[^&quot;]+&quot;&gt;!&amp;lt;http://xmlns.com/foaf/0.1/mbox_sha1sum&gt;&amp;quot;26fb6400147dcccfda59717ff861db9cb97ac5ec&amp;quot;&lt;/extrakey&gt;#sm, 'xml serialization has good looking bnode map' );
 	}
+	{
+		print &quot;# bgp using default graph (local rdf) with k:bloom FILTER\n&quot;;
+		my $query	= RDF::Query-&gt;new( $sparql, undef, undef, 'sparqlp' );
+		my $stream	= $query-&gt;execute();
+		isa_ok( $stream, 'RDF::Trine::Iterator' );
+		
+		my $count	= 0;
+		while (my $d = $stream-&gt;next) {
+			isa_ok( $d-&gt;{name}, 'RDF::Query::Node::Literal' );
+			like( $d-&gt;{name}-&gt;literal_value, qr/^(Adam Pisoni|Gregory Todd Williams)$/, 'expected name passed from person passing through bloom filter' );
+			$count++;
+		}
+		is( $count, 2, 'expected result count' );
+	}
+}
+
+{
+	print &quot;# join using default graph (remote rdf) and remote SERVICE (kasei.us), joining on IRI\n&quot;;
 	my $query	= RDF::Query-&gt;new( &lt;&lt;&quot;END&quot;, undef, undef, 'sparqlp' );
 		PREFIX foaf: &lt;http://xmlns.com/foaf/0.1/&gt;
 		SELECT DISTINCT *
@@ -68,22 +111,14 @@ END
 	isa_ok( $stream, 'RDF::Trine::Iterator' );
 	my $d	= $stream-&gt;next;
 	isa_ok( $d, 'HASH' );
-	is_deeply( $d, {
-		'p' =&gt; bless( [
-			'URI',
-			'http://kasei.us/about/foaf.xrdf#greg',
-		], 'RDF::Trine::Node::Resource' ),
-		'name' =&gt; bless( [
-			'LITERAL',
-			'Gregory Todd Williams'
-		], 'RDF::Trine::Node::Literal' )
-	} );
+	isa_ok( $d-&gt;{p}, 'RDF::Trine::Node::Resource' );
+	is( $d-&gt;{p}-&gt;uri_value, 'http://kasei.us/about/foaf.xrdf#greg', 'expected person uri' );
+	isa_ok( $d-&gt;{name}, 'RDF::Trine::Node::Literal' );
+	is( $d-&gt;{name}-&gt;literal_value, 'Gregory Todd Williams', 'expected person name' );
 }
 
-SKIP: {
-	unless ($ENV{RDFQUERY_DEV_TESTS}) {
-		skip &quot;developer network tests&quot;, 3;
-	}
+{
+	print &quot;# join using default graph (remote rdf) and remote SERVICE (dbpedia), joining on IRI\n&quot;;
 	my $query	= RDF::Query-&gt;new( &lt;&lt;&quot;END&quot;, undef, undef, 'sparqlp' );
 		PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
 		SELECT DISTINCT *
@@ -95,48 +130,17 @@ SKIP: {
 			}
 			SERVICE &lt;http://dbpedia.org/sparql&gt; {
 				?thing a &lt;http://dbpedia.org/class/yago/Island109316454&gt;
+				FILTER( REGEX( STR(?thing), &quot;http://dbpedia.org/resource/V&quot; ) ) .
 			}
 		}
 END
 	my $stream	= $query-&gt;execute();
 	isa_ok( $stream, 'RDF::Trine::Iterator' );
 	my $d	= $stream-&gt;next;
-	
-	isa_ok( $d, 'HASH' );
-	is_deeply( $d, {
-		'label' =&gt; bless( [
-			'LITERAL',
-			'Vancouver Island',
-			'en',
-			undef
-		], 'RDF::Trine::Node::Literal' ),
-		'thing' =&gt; bless( [
-			'URI',
-			'http://dbpedia.org/resource/Vancouver_Island'
-		], 'RDF::Trine::Node::Resource' )
-	} );
-}
-
-
-SKIP: {
-	unless ($ENV{RDFQUERY_DEV_TESTS}) {
-		skip &quot;developer network tests&quot;, 3;
-	}
-	my $query	= RDF::Query-&gt;new( &lt;&lt;&quot;END&quot;, undef, undef, 'sparqlp' );
-PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
-SELECT *
-WHERE {
-	SERVICE &lt;http://dbpedia.org/sparql&gt; {
-		?thing a &lt;http://dbpedia.org/class/yago/Island109316454&gt;
-	}
-}
-END
-	my $stream	= $query-&gt;execute();
-	isa_ok( $stream, 'RDF::Trine::Iterator' );
-	my $d	= $stream-&gt;next;
 	isa_ok( $d, 'HASH' );
-	
-	my @values	= values %$d;
-	isa_ok( $values[0], 'RDF::Trine::Node' );
+	isa_ok( $d-&gt;{label}, 'RDF::Trine::Node::Literal' );
+	is( $d-&gt;{label}-&gt;literal_value, 'Vancouver Island', 'expected (island) name' );
+	is( $d-&gt;{label}-&gt;literal_value_language, 'en', 'expected (island) name language' );
+	isa_ok( $d-&gt;{thing}, 'RDF::Trine::Node::Resource' );
+	is( $d-&gt;{thing}-&gt;uri_value, 'http://dbpedia.org/resource/Vancouver_Island', 'expected (island) uri' );
 }
-</diff>
      <filename>RDF-Query/t/31-service.t</filename>
    </modified>
    <modified>
      <diff>@@ -39,7 +39,7 @@ use XML::Twig;
 use Data::Dumper;
 use Carp qw(carp);
 use List::MoreUtils qw(uniq);
-use Scalar::Util qw(blessed reftype);
+use Scalar::Util qw(blessed reftype refaddr);
 
 use RDF::Trine::Node;
 
@@ -183,6 +183,7 @@ sub from_string {
 	my $value;
 	my @results;
 	my $result	= {};
+	my (%extrakeys, %extra);
 	
 	my $twig	= XML::Twig-&gt;new(
 		twig_handlers =&gt; {
@@ -197,6 +198,8 @@ sub from_string {
 							$value	= RDF::Trine::Node::Literal-&gt;new( $_-&gt;text, $lang, $dt )
 						},
 			boolean		=&gt; sub { $bool	= ($_-&gt;text eq 'true') ? 1 : 0 },
+			extra		=&gt; sub { push( @{ $extra{ $_-&gt;att('name') } }, { %extrakeys } ); %extrakeys = (); },
+			extrakey	=&gt; sub { push(@{ $extrakeys{ $_-&gt;att('id') } }, $_-&gt;text); },
 		},
 	);
 	$twig-&gt;parse( $string );
@@ -204,7 +207,13 @@ sub from_string {
 	if (defined($bool)) {
 		return RDF::Trine::Iterator::Boolean-&gt;new( [$bool] );
 	} else {
-		return RDF::Trine::Iterator::Bindings-&gt;new( \@results, \@vars );
+		my $bindings	= RDF::Trine::Iterator::Bindings-&gt;new( \@results, \@vars, extra_result_data =&gt; \%extra );
+# 		foreach my $tag (keys %extra) {
+# 			foreach my $value (@{ $extra{ $tag } }) {
+# 				$bindings-&gt;add_extra_result_data( $tag, $value );
+# 			}
+# 		}
+		return $bindings;
 	}
 }
 
@@ -216,8 +225,8 @@ Returns the next item in the stream.
 
 =cut
 
-sub next { $_[0]-&gt;next_result }
-sub next_result {
+sub next_result { $_[0]-&gt;next }
+sub next {
 	my $self	= shift;
 	return if ($self-&gt;{_finished});
 	
@@ -227,14 +236,6 @@ sub next_result {
 		$self-&gt;{_finished}	= 1;
 	}
 
-	my $args	= $self-&gt;_args;
-# 	if ($args-&gt;{named}) {
-# 		if ($self-&gt;_bridge-&gt;supports('named_graph')) {
-# 			my $bridge	= $self-&gt;_bridge;
-# 			$args-&gt;{context}	= $bridge-&gt;get_context( $self-&gt;{_stream}, %$args );
-# 		}
-# 	}
-	
 	$self-&gt;{_open}	= 1;
 	$self-&gt;{_row}	= $value;
 	return $value;
@@ -295,22 +296,6 @@ sub close {
 	return;
 }
 
-# =item C&lt;&lt; context &gt;&gt;
-# 
-# Returns the context node of the current result (if applicable).
-# 
-# =cut
-# 
-# sub context {
-# 	my $self	= shift;
-# 	my $args	= $self-&gt;_args;
-# 	my $bridge	= $args-&gt;{bridge};
-# 	my $stream	= $self-&gt;{_stream};
-# 	my $context	= $bridge-&gt;get_context( $stream, %$args );
-# 	return $context;
-# }
-
-
 =item C&lt;&lt; concat ( $stream ) &gt;&gt;
 
 Returns a new stream resulting from the concatenation of the referant and the
@@ -388,6 +373,27 @@ sub join_streams {
 	Carp::confess unless ($astream-&gt;isa('RDF::Trine::Iterator::Bindings'));
 	Carp::confess unless ($bstream-&gt;isa('RDF::Trine::Iterator::Bindings'));
 	
+	################################################
+	### BNODE MAP STUFF
+	my $a_extra	= $astream-&gt;extra_result_data || {};
+	my $b_extra	= $bstream-&gt;extra_result_data || {};
+	my (%a_map, %b_map);
+	foreach my $h (@{ $a_extra-&gt;{'bnode-map'} || [] }) {
+		foreach my $id (keys %$h) {
+			my @values	= @{ $h-&gt;{ $id } };
+			push( @{ $a_map{ $id } }, @values );
+		}
+	}
+	foreach my $h (@{ $b_extra-&gt;{'bnode-map'} || [] }) {
+		foreach my $id (keys %$h) {
+			my @values	= @{ $h-&gt;{ $id } };
+			push( @{ $b_map{ $id } }, @values );
+		}
+	}
+	my $a_map	= (%a_map) ? \%a_map : undef;
+	my $b_map	= (%b_map) ? \%b_map : undef;
+	################################################
+	
 	my @names	= uniq( map { $_-&gt;binding_names() } ($astream, $bstream) );
 	my $a		= $astream-&gt;project( @names );
 	my $b		= $bstream-&gt;project( @names );
@@ -397,7 +403,7 @@ sub join_streams {
 	no warnings 'uninitialized';
 	while (my $rowa = $a-&gt;next) {
 		LOOP: foreach my $rowb (@data) {
-			warn &quot;[--JOIN--] &quot; . join(' ', map { my $row = $_; '{' . join(', ', map { join('=',$_,$row-&gt;{$_}-&gt;as_string) } (keys %$row)) . '}' } ($rowa, $rowb)) . &quot;\n&quot; if ($debug);
+			warn &quot;[--JOIN--] &quot; . join(' ', map { my $row = $_; '{' . join(', ', map { join('=', $_, ($row-&gt;{$_}) ? $row-&gt;{$_}-&gt;as_string : '(undef)') } (keys %$row)) . '}' } ($rowa, $rowb)) . &quot;\n&quot; if ($debug);
 			my %keysa	= map {$_=&gt;1} (keys %$rowa);
 			my @shared	= grep { $keysa{ $_ } } (keys %$rowb);
 			foreach my $key (@shared) {
@@ -408,7 +414,35 @@ sub join_streams {
 					$defined++ if (defined($n));
 				}
 				if ($defined == 2) {
-					unless ($val_a-&gt;equal($val_b)) {
+					my $equal	= $val_a-&gt;equal( $val_b );
+					if (not $equal) {
+						my $query 	= $args{ query };
+						my $bridge	= $args{ bridge };
+						if ($query and $bridge) {
+							warn 'join values and bnode maps: ' . Dumper($val_a, $val_b, $a_map, $b_map) if ($a_map or $b_map);
+							if ($a_map and $val_a-&gt;isa('RDF::Trine::Node::Blank')) {
+								my $anames	= Set::Scalar-&gt;new( @{ $a_map{ $val_a-&gt;blank_identifier } } );
+								my $bnames	= Set::Scalar-&gt;new( RDF::Query::Algebra::Service-&gt;_names_for_node( $val_b, $query, $bridge, {} ) );
+								if (my $int = $anames-&gt;intersection( $bnames )) {
+									warn &quot;node equality based on $int&quot;;
+									$equal	= 1;
+								}
+							} elsif ($b_map and $val_b-&gt;isa('RDF::Trine::Node::Blank')) {
+								my $bnames	= Set::Scalar-&gt;new( @{ $b_map{ $val_b-&gt;blank_identifier } } );
+								my $anames	= Set::Scalar-&gt;new( RDF::Query::Algebra::Service-&gt;_names_for_node( $val_a, $query, $bridge, {} ) );
+								warn &quot;anames: $anames\n&quot;;
+								warn &quot;bnames: $bnames\n&quot;;
+								if (my $int = $anames-&gt;intersection( $bnames )) {
+									warn &quot;node equality based on $int&quot;;
+									$equal	= 1;
+								}
+							}
+						} else {
+							Carp::cluck &quot;no query,bridge in args&quot; if ($debug);
+						}
+					}
+					
+					unless ($equal) {
 						warn &quot;can't join because mismatch of $key (&quot; . join(' &lt;==&gt; ', map {$_-&gt;as_string} ($val_a, $val_b)) . &quot;)&quot; if ($debug);
 						next LOOP;
 					}
@@ -491,7 +525,7 @@ sub construct_args {
 	my $self	= shift;
 	my $type	= $self-&gt;type;
 	my $args	= $self-&gt;_args || {};
-	return ($type, []);
+	return ($type, [], %$args);
 }
 
 =begin private
@@ -550,6 +584,24 @@ sub _stream {
 }
 
 
+=item C&lt;&lt; add_extra_result_data ( $tag, \%data ) &gt;&gt;
+
+=cut
+
+sub add_extra_result_data {
+	my $self	= shift;
+	my $tag		= shift;
+	my $data	= shift;
+	push( @{ $self-&gt;_args-&gt;{ extra_result_data }{ $tag } }, $data );
+}
+
+sub extra_result_data {
+	my $self	= shift;
+	my $args	= $self-&gt;_args;
+	my $extra	= $args-&gt;{ extra_result_data };
+	return $extra;
+}
+
 
 
 </diff>
      <filename>RDF-Trine/lib/RDF/Trine/Iterator.pm</filename>
    </modified>
    <modified>
      <diff>@@ -39,9 +39,8 @@ use RDF::Trine::Iterator qw(smap);
 use base qw(RDF::Trine::Iterator);
 
 our ($REVISION, $VERSION, $debug);
-use constant DEBUG	=&gt; 0;
 BEGIN {
-	$debug		= DEBUG;
+	$debug		= 0;
 	$REVISION	= do { my $REV = (qw$Revision: 293 $)[1]; sprintf(&quot;%0.3f&quot;, 1 + ($REV/1000)) };
 	$VERSION	= '1.000';
 }
@@ -149,7 +148,7 @@ sub join_streams {
 		my $a_sort		= join(',', $a-&gt;sorted_by);
 		my $b_sort		= join(',', $b-&gt;sorted_by);
 		
-		if (DEBUG) {
+		if ($debug) {
 			warn '---------------------------';
 			warn 'REQUESTED SORT in JOIN: ' . Dumper($req_sort);
 			warn 'JOIN STREAM SORTED BY: ' . Dumper($a_sort);
@@ -157,17 +156,17 @@ sub join_streams {
 		}
 		my $actual_sort;
 		if (substr( $a_sort, 0, length($req_sort) ) eq $req_sort) {
-			warn &quot;first stream is already sorted. using it in the outer loop.\n&quot; if (DEBUG);
+			warn &quot;first stream is already sorted. using it in the outer loop.\n&quot; if ($debug);
 		} elsif (substr( $b_sort, 0, length($req_sort) ) eq $req_sort) {
-			warn &quot;second stream is already sorted. using it in the outer loop.\n&quot; if (DEBUG);
+			warn &quot;second stream is already sorted. using it in the outer loop.\n&quot; if ($debug);
 			($a,$b)	= ($b,$a);
 		} else {
 			my $a_common	= join('!', $a_sort, $req_sort);
 			my $b_common	= join('!', $b_sort, $req_sort);
 			if ($a_common =~ qr[^([^!]+)[^!]*!\1]) {	# shared prefix between $a_sort and $req_sort?
-				warn &quot;first stream is closely sorted ($1).\n&quot; if (DEBUG);
+				warn &quot;first stream is closely sorted ($1).\n&quot; if ($debug);
 			} elsif ($b_common =~ qr[^([^!]+)[^!]*!\1]) {	# shared prefix between $b_sort and $req_sort?
-				warn &quot;second stream is closely sorted ($1).\n&quot; if (DEBUG);
+				warn &quot;second stream is closely sorted ($1).\n&quot; if ($debug);
 				($a,$b)	= ($b,$a);
 			}
 		}
@@ -175,7 +174,7 @@ sub join_streams {
 	}
 	
 	my $stream	= $self-&gt;SUPER::join_streams( $a, $b, %args );
-	warn &quot;JOINED stream is sorted by: &quot; . join(',', @join_sorted_by) . &quot;\n&quot; if (DEBUG);
+	warn &quot;JOINED stream is sorted by: &quot; . join(',', @join_sorted_by) . &quot;\n&quot; if ($debug);
 	$stream-&gt;{sorted_by}	= \@join_sorted_by;
 	return $stream;
 }
@@ -386,10 +385,29 @@ END
 		
 		last if ($max_result_size and ++$count &gt;= $max_result_size);
 	} continue { $self-&gt;next_result }
-	$xml	.= &lt;&lt;&quot;EOT&quot;;
-&lt;/results&gt;
-&lt;/sparql&gt;
-EOT
+	$xml	.= &quot;&lt;/results&gt;\n&quot;;
+	
+	if (my $extra = $self-&gt;extra_result_data) {
+		foreach my $tag (keys %$extra) {
+			$xml	.= qq[&lt;extra name=&quot;${tag}&quot;&gt;\n];
+			my $value	= $extra-&gt;{ $tag };
+			foreach my $e (@$value) {
+				foreach my $k (keys %$e) {
+					my $v		= $e-&gt;{ $k };
+					my @values	= @$v;
+					foreach ($k, @values) {
+						s/&amp;/&amp;amp;/g;
+						s/&lt;/&amp;lt;/g;
+						s/&quot;/&amp;quot;/g;
+					}
+					$xml	.= qq[\t&lt;extrakey id=&quot;$k&quot;&gt;] . join(',', @values) . qq[&lt;/extrakey&gt;\n];
+				}
+			}
+			$xml	.= &quot;&lt;/extra&gt;\n&quot;;
+		}
+	}
+	
+	$xml	.= &quot;&lt;/sparql&gt;\n&quot;;
 	return $xml;
 }
 </diff>
      <filename>RDF-Trine/lib/RDF/Trine/Iterator/Bindings.pm</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>7aaedae28f61e48f4eed20239c35ae39dfc70481</id>
    </parent>
    <parent>
      <id>ba42ed1f3e09aecf45dfc58827cba66f9fb76d88</id>
    </parent>
  </parents>
  <author>
    <name>Gregory Williams</name>
    <email>samofool@tiu.local</email>
  </author>
  <url>http://github.com/kasei/perlrdf/commit/e27a1d7a520ff42a5d079ed4cd6267e112f353cc</url>
  <id>e27a1d7a520ff42a5d079ed4cd6267e112f353cc</id>
  <committed-date>2008-03-10T19:12:38-07:00</committed-date>
  <authored-date>2008-03-10T19:12:38-07:00</authored-date>
  <message>Merge branch 'service-bnode-join'</message>
  <tree>6de85651d51d01688590513da4470508dce1cae1</tree>
  <committer>
    <name>Gregory Williams</name>
    <email>samofool@tiu.local</email>
  </committer>
</commit>
