Class: SPARQL::Algebra::Operator::LeftJoin

Inherits:
SPARQL::Algebra::Operator show all
Includes:
Query
Defined in:
lib/sparql/algebra/operator/left_join.rb

Overview

The SPARQL GraphPattern leftjoin operator.

[57] OptionalGraphPattern ::= 'OPTIONAL' GroupGraphPattern

Examples:

SPARQL Grammar

PREFIX :    <http://example/>
SELECT * { 
  ?x :p ?v .
  OPTIONAL { 
    ?y :q ?w .
    FILTER(?v=2)
  }
}

SSE

(prefix ((: <http://example/>))
  (leftjoin
    (bgp (triple ?x :p ?v))
    (bgp (triple ?y :q ?w))
    (= ?v 2)))

See Also:

Constant Summary collapse

NAME =
[:leftjoin]

Constants inherited from SPARQL::Algebra::Operator

ARITY, IsURI, URI

Constants included from Expression

Expression::PATTERN_PARENTS

Instance Attribute Summary

Attributes included from Query

#solutions

Attributes inherited from SPARQL::Algebra::Operator

#operands

Instance Method Summary collapse

Methods included from Query

#each_solution, #empty?, #failed?, #graph_name=, #matched?, #query_yields_boolean?, #query_yields_solutions?, #query_yields_statements?, #unshift, #variables

Methods inherited from SPARQL::Algebra::Operator

#aggregate?, arity, #base_uri, base_uri, base_uri=, #bind, #boolean, #constant?, #deep_dup, #each_descendant, #eql?, #evaluatable?, evaluate, #executable?, #first_ancestor, for, #initialize, #inspect, #mergable?, #ndvars, #node?, #operand, #optimize!, #parent, #parent=, #prefixes, prefixes, prefixes=, #rewrite, #to_binary, to_sparql, #to_sxp, #to_sxp_bin, #variable?, #variables, #vars

Methods included from Expression

cast, #constant?, debug, #evaluate, extension, extension?, extensions, for, #invalid?, new, #node?, open, #optimize!, parse, register_extension, #to_sxp_bin, #valid?, #variable?

Constructor Details

This class inherits a constructor from SPARQL::Algebra::Operator

Instance Method Details

#execute(queryable, **options) {|solution| ... } ⇒ RDF::Query::Solutions

Executes each operand with queryable and performs the leftjoin operation by adding every solution from the left, merging compatible solutions from the right that match an optional filter.

Yields:

  • (solution)

    each matching solution

Yield Parameters:

Yield Returns:

  • (void)

    ignored

Raises:

  • (ArgumentError)

See Also:



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/sparql/algebra/operator/left_join.rb', line 49

def execute(queryable, **options, &block)
  filter = operand(2)

  raise ArgumentError,
    "leftjoin operator accepts at most two arguments with an optional filter" if
    operands.length < 2 || operands.length > 3

  debug(options) {"LeftJoin"}
  left = queryable.query(operand(0), **options.merge(depth: options[:depth].to_i + 1))
  debug(options) {"=>(leftjoin left) #{left.inspect}"}

  right = queryable.query(operand(1), **options.merge(depth: options[:depth].to_i + 1))
  debug(options) {"=>(leftjoin right) #{right.inspect}"}

  # LeftJoin(Ω1, Ω2, expr) =
  @solutions = RDF::Query::Solutions()
  left.each do |s1|
    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)

      # See https://github.com/w3c/rdf-tests/pull/83#issuecomment-1324220844 for @afs's discussion of the simplified/not-simplified issue.
      #
      # The difference is when simplification is applied. It matters for OPTIONAL because OPTIONAL { ... FILTER(...) } puts the filter into the LeftJoin expressions. In LeftJoin, the FILTER can see the left-hand-side variables. (SQL: LEFT JOIN ... ON ...)
      # 
      # For OPTIONAL { { ... FILTER(...) } }, the inner part is Join({}, {.... FILTER }).
      # 
      # if simplify happens while coming back up the tree generating algebra operations, it removes the join i.e. the inner of {{ }}, and passes "... FILTER()" to the OPTIONAL. The effect of the extra nesting in {{ }} is lost and it exposes the filter to the OPTIONAL rule.
      # 
      # if simplification happens as a step after the whole algebra is converted, this does not happen. Compiling the OPTIONAL see a join and the filter is not at the top level of the OPTIONAl block and so not handled in the LeftJoin.
      # 
      # Use case:
      # 
      # # Include name if person over 18
      # SELECT *
      # { ?person :age ?age 
      #    OPTIONAL { ?person :name ?name. FILTER(?age > 18) }
      # }
      # Hindsight: a better syntax would be call out if the filter needed access to the LHS.
      # 
      # OPTIONAL FILTER(....) { }
      # 
      # But we are where we are.
      # 
      # (a "no conditions on LeftJoin" approach would mean users having to duplicate parts of their query - possibly quite large parts.)
      expr = filter ? boolean(filter.evaluate(s)).true? : true rescue false
      debug(options) {"===>(evaluate) #{s.inspect}"} if filter

      if expr && s1.compatible?(s2)
        # { merge(μ1, μ2) | μ1 in Ω1 and μ2 in Ω2, and μ1 and μ2 are compatible and expr(merge(μ1, μ2)) is true }
        debug(options) {"=>(merge s1 s2) #{s.inspect}"}
        @solutions << s
        load_left = false   # Left solution added one or more times due to merge
      end
    end
    if load_left
      debug(options) {"=>(add) #{s1.inspect}"}
      @solutions << s1
    end
  end
  
  debug(options) {"=> #{@solutions.inspect}"}
  @solutions.each(&block) if block_given?
  @solutions
end

#optimize(**options) ⇒ Object

Optimizes this query.

If optimize operands, and if the first two operands are both Queries, replace with the unique sum of the query elements

FIXME



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/sparql/algebra/operator/left_join.rb', line 139

def optimize(**options)
  lhs, rhs, expr = operands.map {|o| o.optimize(**options) }
  expr = nil if expr.respond_to?(:true?) && expr.true?

  if lhs.empty? && rhs.empty?
    RDF::Query.new  # Empty query, expr doesn't matter
  elsif rhs.empty?
    # Expression doesn't matter, just use the first operand
    lhs
  elsif lhs.empty?
    # Result is the filter of the second operand if there is an expression
    # FIXME: doesn't seem to work
    #expr ? Filter.new(expr, rhs) : rhs
    self.dup
  else
    expr ? LeftJoin.new(rhs, lhs, expr) : LeftJoin.new(lhs, rhs)
  end
end

#to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) ⇒ String

Returns a partial SPARQL grammar for this operator.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/sparql/algebra/operator/left_join.rb', line 169

def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options)
  str = "{\n" + operands[0].to_sparql(top_level: false, extensions: {}, **options)
  str << 
    "\nOPTIONAL {\n" +
    operands[1].to_sparql(top_level: false, extensions: {}, **options)
  case operands[2]
  when SPARQL::Algebra::Operator::Exprlist
    operands[2].operands.each do |op|
      str << "\nFILTER (" + op.to_sparql(**options) + ")"
    end
  when nil
  else
    str << "\nFILTER (" + operands[2].to_sparql(**options) + ")"
  end
  str << "\n}}"
  top_level ? Operator.to_sparql(str, filter_ops: filter_ops, extensions: extensions, **options) : str
end

#validate!Object

The same blank node label cannot be used in two different basic graph patterns in the same query



120
121
122
123
124
125
126
127
128
# File 'lib/sparql/algebra/operator/left_join.rb', line 120

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