module SPARQL; module Algebra class Operator ## # The SPARQL GraphPattern `join` operator. # # [54] GroupGraphPatternSub ::= TriplesBlock? (GraphPatternNotTriples "."? TriplesBlock? )* # # @example SPARQL Grammar # PREFIX : # SELECT * { # ?s ?p ?o # GRAPH ?g { ?s ?q ?v } # } # # @example SSE # (prefix ((: )) # (join # (bgp (triple ?s ?p ?o)) # (graph ?g # (bgp (triple ?s ?q ?v))))) # # @example SPARQL Grammar (inline filter) # PREFIX : # ASK { # :who :homepage ?homepage # FILTER REGEX(?homepage, "^http://example.org/") # :who :schoolHomepage ?schoolPage # } # # @example SSE (inline filter) # (prefix ((: )) # (ask # (filter (regex ?homepage "^http://example.org/") # (join # (bgp (triple :who :homepage ?homepage)) # (bgp (triple :who :schoolHomepage ?schoolPage)))))) # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Join < Operator::Binary include Query NAME = [:join] ## # Executes each operand with `queryable` and performs the `join` operation # by creating a new solution set containing the `merge` of all solutions # from each set that are `compatible` with each other. # # @param [RDF::Queryable] queryable # the graph or repository to query # @param [Hash{Symbol => Object}] options # any additional keyword options # @yield [solution] # each matching solution # @yieldparam [RDF::Query::Solution] solution # @yieldreturn [void] ignored # @return [RDF::Query::Solutions] # the resulting solution sequence # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra # @see https://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Query/Solution#merge-instance_method # @see https://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Query/Solution#compatible%3F-instance_method def execute(queryable, **options, &block) # Join(Ω1, Ω2) = { merge(μ1, μ2) | μ1 in Ω1 and μ2 in Ω2, and μ1 and μ2 are compatible } # eval(D(G), Join(P1, P2)) = Join(eval(D(G), P1), eval(D(G), P2)) # # Generate solutions independently, merge based on solution compatibility debug(options) {"Join #{operands.to_sse}"} left = queryable.query(operand(0), depth: options[:depth].to_i + 1, **options) debug(options) {"(join)=>(left) #{left.map(&:to_h).to_sse}"} right = queryable.query(operand(1), depth: options[:depth].to_i + 1, **options) debug(options) {"(join)=>(right) #{right.map(&:to_h).to_sse}"} @solutions = RDF::Query::Solutions(left.map do |s1| right.map { |s2| s2.merge(s1) if s2.compatible?(s1) } end.flatten.compact) debug(options) {"(join)=> #{@solutions.map(&:to_h).to_sse}"} @solutions.each(&block) if block_given? @solutions end # The same blank node label cannot be used in two different basic graph patterns in the same query def validate! left_nodes, right_nodes = operand(0).ndvars, operand(1).ndvars unless (left_nodes.compact & right_nodes.compact).empty? raise ArgumentError, "sub-operands share non-distinguished variables: #{(left_nodes.compact & right_nodes.compact).to_sse}" end super end ## # Optimizes this query. # # Groups of one graph pattern (not a filter) become join(Z, A) and can be replaced by A. # The empty graph pattern Z is the identity for join: # Replace join(Z, A) by A # Replace join(A, Z) by A # # @return [Join, RDF::Query] `self` # @return [self] # @see SPARQL::Algebra::Expression#optimize! def optimize!(**options) ops = operands.map {|o| o.optimize(**options) }.select {|o| o.respond_to?(:empty?) && !o.empty?} @operands = ops self end ## # # Returns a partial SPARQL grammar for this operator. # # @param [Boolean] top_level (true) # Treat this as a top-level, generating SELECT ... WHERE {} # @param [Hash{Symbol => Operator}] extensions # Variable bindings # @param [Array] filter_ops ([]) # Filter Operations # @return [String] def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) # If this is top-level, and the last operand is a Table (values), put the values at the outer-level str = "{\n" + operands.first.to_sparql(top_level: false, extensions: {}, **options) # Any accrued filters go here. filter_ops.each do |op| str << "\nFILTER (#{op.to_sparql(**options)}) ." end if top_level && operands.last.is_a?(Table) str << "\n}" options = options.merge(values_clause: operands.last) else str << "\n{\n" + operands.last.to_sparql(top_level: false, extensions: {}, **options) + "\n}\n}" end top_level ? Operator.to_sparql(str, extensions: extensions, **options) : str end end # Join end # Operator end; end # SPARQL::Algebra