diff --git a/VERSION b/VERSION
index b347b11e..351227fc 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.2.3
+3.2.4
diff --git a/lib/sparql/algebra/expression.rb b/lib/sparql/algebra/expression.rb
index 05a2c94b..86aded60 100644
--- a/lib/sparql/algebra/expression.rb
+++ b/lib/sparql/algebra/expression.rb
@@ -140,7 +140,7 @@ def self.new(sse, parent_operator: nil, **options)
logger = options[:logger]
options.delete_if {|k, v| [:debug, :logger, :depth, :prefixes, :base_uri, :update, :validate].include?(k) }
begin
- # Due to confusiong over (triple) and special-case for (qtriple)
+ # Due to confusion over (triple) and special-case for (qtriple)
if operator == RDF::Query::Pattern
options = options.merge(quoted: true) if sse.first == :qtriple
elsif operator == Operator::Triple && PATTERN_PARENTS.include?(parent_operator)
diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb
index f3c0fee2..faadbc07 100644
--- a/lib/sparql/algebra/operator.rb
+++ b/lib/sparql/algebra/operator.rb
@@ -134,6 +134,7 @@ class Operator
autoload :Prefix, 'sparql/algebra/operator/prefix'
autoload :Project, 'sparql/algebra/operator/project'
autoload :Reduced, 'sparql/algebra/operator/reduced'
+ autoload :Service, 'sparql/algebra/operator/service'
autoload :Slice, 'sparql/algebra/operator/slice'
autoload :Table, 'sparql/algebra/operator/table'
autoload :Union, 'sparql/algebra/operator/union'
@@ -236,6 +237,7 @@ def self.for(name, arity = nil)
when :seconds then Seconds
when :seq then Seq
when :sequence then Sequence
+ when :service then Service
when :sha1 then SHA1
when :sha256 then SHA256
when :sha384 then SHA384
diff --git a/lib/sparql/algebra/operator/extend.rb b/lib/sparql/algebra/operator/extend.rb
index 2b7f989a..8f3ccf7f 100644
--- a/lib/sparql/algebra/operator/extend.rb
+++ b/lib/sparql/algebra/operator/extend.rb
@@ -84,6 +84,11 @@ def execute(queryable, **options, &block)
debug(options) {"Extend"}
@solutions = operand(1).execute(queryable, depth: options[:depth].to_i + 1, **options)
@solutions.each do |solution|
+ # Re-bind to bindings, if defined, as they might not be found in solution
+ options[:bindings].each_binding do |name, value|
+ solution[name] = value if operands.first.variables.include?(name)
+ end if options[:bindings] && operands.first.respond_to?(:variables)
+
debug(options) {"===> soln #{solution.to_h.inspect}"}
operand(0).each do |(var, expr)|
begin
diff --git a/lib/sparql/algebra/operator/filter.rb b/lib/sparql/algebra/operator/filter.rb
index 26e1179e..9e73c895 100644
--- a/lib/sparql/algebra/operator/filter.rb
+++ b/lib/sparql/algebra/operator/filter.rb
@@ -49,6 +49,11 @@ def execute(queryable, **options, &block)
opts = options.merge(queryable: queryable, depth: options[:depth].to_i + 1)
@solutions = RDF::Query::Solutions()
queryable.query(operands.last, depth: options[:depth].to_i + 1, **options) do |solution|
+ # Re-bind to bindings, if defined, as they might not be found in solution
+ options[:bindings].each_binding do |name, value|
+ solution[name] ||= value if operands.first.variables.include?(name)
+ end if options[:bindings] && operands.first.respond_to?(:variables)
+
begin
pass = boolean(operands.first.evaluate(solution, **opts)).true?
debug(options) {"(filter) #{pass.inspect} #{solution.to_h.inspect}"}
diff --git a/lib/sparql/algebra/operator/left_join.rb b/lib/sparql/algebra/operator/left_join.rb
index d069c679..378c50e6 100644
--- a/lib/sparql/algebra/operator/left_join.rb
+++ b/lib/sparql/algebra/operator/left_join.rb
@@ -66,6 +66,11 @@ def execute(queryable, **options, &block)
load_left = true
right.each do |s2|
s = s2.merge(s1)
+ # Re-bind to bindings, if defined, as they might not be found in solution
+ options[:bindings].each_binding do |name, value|
+ s[name] = value if filter.variables.include?(name)
+ end if options[:bindings] && filter.respond_to?(:variables)
+
expr = filter ? boolean(filter.evaluate(s)).true? : true rescue false
debug(options) {"===>(evaluate) #{s.inspect}"} if filter
diff --git a/lib/sparql/algebra/operator/service.rb b/lib/sparql/algebra/operator/service.rb
new file mode 100644
index 00000000..bb5b2de2
--- /dev/null
+++ b/lib/sparql/algebra/operator/service.rb
@@ -0,0 +1,86 @@
+module SPARQL; module Algebra
+ class Operator
+ ##
+ # The SPARQL Service operator.
+ #
+ # [59] ServiceGraphPattern ::= 'SERVICE' 'SILENT'? VarOrIri GroupGraphPattern
+ #
+ # @example SPARQL Grammar
+ # PREFIX :
+ #
+ # SELECT ?s ?o1 ?o2 {
+ # ?s ?p1 ?o1 .
+ # SERVICE {
+ # ?s ?p2 ?o2
+ # }
+ # }
+ #
+ # @example SSE
+ # (prefix ((: ))
+ # (project (?s ?o1 ?o2)
+ # (join
+ # (bgp (triple ?s ?p1 ?o1))
+ # (service :sparql
+ # (bgp (triple ?s ?p2 ?o2))))))
+ #
+ # @see https://www.w3.org/TR/sparql11-query/#QSynIRI
+ class Service < Operator
+ include Query
+
+ NAME = [:service]
+
+ ##
+ # Executes this query on the given `queryable` graph or repository.
+ # Really a pass-through, as this is a syntactic object used for providing
+ # graph_name for URIs.
+ #
+ # @param [RDF::Queryable] queryable
+ # the graph or repository to query
+ # @param [Hash{Symbol => Object}] options
+ # any additional keyword options
+ # @yield [solution]
+ # each matching solution, statement or boolean
+ # @yieldparam [RDF::Statement, RDF::Query::Solution, Boolean] solution
+ # @yieldreturn [void] ignored
+ # @return [RDF::Query::Solutions]
+ # the resulting solution sequence
+ # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra
+ def execute(queryable, **options, &block)
+ debug(options) {"Service"}
+ silent = operands.first == :silent
+ location, query = operands
+ query_sparql = query.to_sparql
+ debug(options) {"query: #{query_sparql}"}
+ raise NotImplementedError, "SERVICE operator not implemented"
+ end
+
+ ##
+ # Returns an optimized version of this query.
+ #
+ # Replace with the query with URIs having their lexical shortcut removed
+ #
+ # @return [Prefix] a copy of `self`
+ # @see SPARQL::Algebra::Expression#optimize
+ def optimize(**options)
+ operands.last.optimize(**options)
+ end
+
+ ##
+ #
+ # Returns a partial SPARQL grammar for this term.
+ #
+ # @return [String]
+ def to_sparql(**options)
+ silent = operands.first == :silent
+ ops = silent ? operands[1..-1] : operands
+ location, query = ops
+
+
+ str = "SERVICE "
+ str << "SILENT " if silent
+ str << location.to_sparql(**options) + " {" + query.to_sparql(**options) + "}"
+ str
+ end
+ end # Service
+ end # Operator
+end; end # SPARQL::Algebra
diff --git a/lib/sparql/grammar/parser11.rb b/lib/sparql/grammar/parser11.rb
index 06564002..2623095d 100644
--- a/lib/sparql/grammar/parser11.rb
+++ b/lib/sparql/grammar/parser11.rb
@@ -916,6 +916,19 @@ class Parser
end
end
+ # [59] ServiceGraphPattern ::= 'SERVICE' 'SILENT'? VarOrIri GroupGraphPattern
+ #
+ # Input from `data` is TODO.
+ # Output to prod_data is TODO.
+ production(:ServiceGraphPattern) do |input, data, callback|
+ args = []
+ args << :silent if data[:silent]
+ args << (data[:VarOrIri]).last
+ args << data.fetch(:query, [SPARQL::Algebra::Operator::BGP.new]).first
+ service = SPARQL::Algebra::Expression.for(:service, *args)
+ add_prod_data(:query, service)
+ end
+
# [60] Bind ::= 'BIND' '(' Expression 'AS' Var ')'
#
# Input from `data` is TODO.
diff --git a/sparql.gemspec b/sparql.gemspec
index 0e1f660d..fbbc7201 100755
--- a/sparql.gemspec
+++ b/sparql.gemspec
@@ -29,7 +29,7 @@ Gem::Specification.new do |gem|
gem.required_ruby_version = '>= 2.6'
gem.requirements = []
- gem.add_runtime_dependency 'rdf', '~> 3.2', '>= 3.2.7'
+ gem.add_runtime_dependency 'rdf', '~> 3.2', '>= 3.2.8'
gem.add_runtime_dependency 'rdf-aggregate-repo', '~> 3.2'
gem.add_runtime_dependency 'ebnf', '~> 2.2', '>= 2.3.1'
gem.add_runtime_dependency 'builder', '~> 3.2'
diff --git a/spec/algebra/query_spec.rb b/spec/algebra/query_spec.rb
index c68fd810..b10f9da2 100644
--- a/spec/algebra/query_spec.rb
+++ b/spec/algebra/query_spec.rb
@@ -503,6 +503,38 @@
end
end
+ context "with variable pre-binding" do
+ # Only test exceptions, as pre-binding is handled in RDF::Query.execute.
+ let(:graph) {RDF::Graph.new}
+
+ {
+ "extend unbound": {
+ query: %{(project (?z) (extend ((?z (+ ?o 1))) (bgp)))},
+ bindings: {o: RDF::Literal(1)},
+ result: [{z: RDF::Literal(2)}]
+ },
+ "filter unbound": {
+ query: %{(project (?this) (filter (= ?this ) (bgp)))},
+ bindings: {this: RDF::URI("http://example.org/this")},
+ result: [{this: RDF::URI("http://example.org/this")}]
+ },
+ "left_join unbound": {
+ query: %{
+ (project (?this)
+ (leftjoin (bgp) (bgp) (= ?this )))
+ },
+ bindings: {this: RDF::URI("http://example.org/this")},
+ result: [{this: RDF::URI("http://example.org/this")}]
+ },
+ }.each do |name, params|
+ it name do
+ query = SPARQL::Algebra::Expression.parse(params[:query])
+ bindings = RDF::Query::Solution.new(params[:bindings])
+ expect(query.execute(graph, bindings: bindings)).to have_result_set params[:result]
+ end
+ end
+ end
+
context "aggregates" do
let(:graph) {
RDF::Graph.new do |g|
diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb
index fa4cca19..4e2a72aa 100644
--- a/spec/suite_spec.rb
+++ b/spec/suite_spec.rb
@@ -175,6 +175,8 @@
skip "Equivalent form"
when 'sq09.rq', 'sq14.rq'
pending("SubSelect")
+ when 'service03.rq', 'service06.rq', 'syntax-service-01.rq'
+ pending("Service")
when 'sparql-star-order-by.rq'
pending("OFFSET/LIMIT in sub-select")
when 'compare_time-01.rq',