sparql 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. data/AUTHORS +3 -0
  2. data/CREDITS +0 -0
  3. data/README.markdown +103 -53
  4. data/UNLICENSE +24 -0
  5. data/VERSION +1 -0
  6. data/bin/sparql +87 -0
  7. data/lib/sparql.rb +105 -22
  8. data/lib/sparql/algebra.rb +369 -0
  9. data/lib/sparql/algebra/evaluatable.rb +37 -0
  10. data/lib/sparql/algebra/expression.rb +284 -0
  11. data/lib/sparql/algebra/extensions.rb +159 -0
  12. data/lib/sparql/algebra/operator.rb +492 -0
  13. data/lib/sparql/algebra/operator/add.rb +34 -0
  14. data/lib/sparql/algebra/operator/and.rb +65 -0
  15. data/lib/sparql/algebra/operator/asc.rb +29 -0
  16. data/lib/sparql/algebra/operator/ask.rb +46 -0
  17. data/lib/sparql/algebra/operator/base.rb +46 -0
  18. data/lib/sparql/algebra/operator/bgp.rb +26 -0
  19. data/lib/sparql/algebra/operator/bound.rb +48 -0
  20. data/lib/sparql/algebra/operator/compare.rb +84 -0
  21. data/lib/sparql/algebra/operator/construct.rb +85 -0
  22. data/lib/sparql/algebra/operator/dataset.rb +77 -0
  23. data/lib/sparql/algebra/operator/datatype.rb +42 -0
  24. data/lib/sparql/algebra/operator/desc.rb +17 -0
  25. data/lib/sparql/algebra/operator/describe.rb +71 -0
  26. data/lib/sparql/algebra/operator/distinct.rb +50 -0
  27. data/lib/sparql/algebra/operator/divide.rb +43 -0
  28. data/lib/sparql/algebra/operator/equal.rb +32 -0
  29. data/lib/sparql/algebra/operator/exprlist.rb +52 -0
  30. data/lib/sparql/algebra/operator/filter.rb +71 -0
  31. data/lib/sparql/algebra/operator/graph.rb +28 -0
  32. data/lib/sparql/algebra/operator/greater_than.rb +32 -0
  33. data/lib/sparql/algebra/operator/greater_than_or_equal.rb +33 -0
  34. data/lib/sparql/algebra/operator/is_blank.rb +35 -0
  35. data/lib/sparql/algebra/operator/is_iri.rb +37 -0
  36. data/lib/sparql/algebra/operator/is_literal.rb +36 -0
  37. data/lib/sparql/algebra/operator/join.rb +67 -0
  38. data/lib/sparql/algebra/operator/lang.rb +29 -0
  39. data/lib/sparql/algebra/operator/lang_matches.rb +53 -0
  40. data/lib/sparql/algebra/operator/left_join.rb +95 -0
  41. data/lib/sparql/algebra/operator/less_than.rb +32 -0
  42. data/lib/sparql/algebra/operator/less_than_or_equal.rb +32 -0
  43. data/lib/sparql/algebra/operator/minus.rb +31 -0
  44. data/lib/sparql/algebra/operator/multiply.rb +34 -0
  45. data/lib/sparql/algebra/operator/not.rb +35 -0
  46. data/lib/sparql/algebra/operator/not_equal.rb +26 -0
  47. data/lib/sparql/algebra/operator/or.rb +65 -0
  48. data/lib/sparql/algebra/operator/order.rb +69 -0
  49. data/lib/sparql/algebra/operator/plus.rb +31 -0
  50. data/lib/sparql/algebra/operator/prefix.rb +45 -0
  51. data/lib/sparql/algebra/operator/project.rb +46 -0
  52. data/lib/sparql/algebra/operator/reduced.rb +47 -0
  53. data/lib/sparql/algebra/operator/regex.rb +70 -0
  54. data/lib/sparql/algebra/operator/same_term.rb +46 -0
  55. data/lib/sparql/algebra/operator/slice.rb +60 -0
  56. data/lib/sparql/algebra/operator/str.rb +35 -0
  57. data/lib/sparql/algebra/operator/subtract.rb +32 -0
  58. data/lib/sparql/algebra/operator/union.rb +55 -0
  59. data/lib/sparql/algebra/query.rb +99 -0
  60. data/lib/sparql/algebra/sxp_extensions.rb +35 -0
  61. data/lib/sparql/algebra/version.rb +20 -0
  62. data/lib/sparql/extensions.rb +102 -0
  63. data/lib/sparql/grammar.rb +298 -0
  64. data/lib/sparql/grammar/lexer.rb +609 -0
  65. data/lib/sparql/grammar/parser.rb +1383 -0
  66. data/lib/sparql/grammar/parser/meta.rb +1801 -0
  67. data/lib/sparql/results.rb +220 -0
  68. data/lib/sparql/version.rb +20 -0
  69. metadata +232 -62
  70. data/Rakefile +0 -22
  71. data/coverage/index.html +0 -252
  72. data/coverage/lib-sparql-execute_sparql_rb.html +0 -621
  73. data/coverage/lib-sparql_rb.html +0 -622
  74. data/lib/sparql/execute_sparql.rb +0 -27
  75. data/lib/sparql/sparql.treetop +0 -159
  76. data/sparql.gemspec +0 -16
  77. data/spec/spec.opts +0 -2
  78. data/spec/spec_helper.rb +0 -24
  79. data/spec/unit/graph_parsing_spec.rb +0 -76
  80. data/spec/unit/iri_parsing_spec.rb +0 -46
  81. data/spec/unit/prefixed_names_parsing_spec.rb +0 -40
  82. data/spec/unit/primitives_parsing_spec.rb +0 -26
  83. data/spec/unit/sparql_parsing_spec.rb +0 -72
  84. data/spec/unit/variables_parsing_spec.rb +0 -36
@@ -0,0 +1,28 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL GraphPattern `graph` operator.
5
+ #
6
+ # This is a wrapper to add a `context` to the query.
7
+ #
8
+ # @example
9
+ # (prefix ((: <http://example/>))
10
+ # (graph ?g
11
+ # (bgp (triple ?s ?p ?o))))
12
+ #
13
+ # @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
14
+ class Graph < Operator::Binary
15
+ NAME = [:graph]
16
+ ##
17
+ # A `graph` is an RDF::Query with a context.
18
+ #
19
+ # @param [RDF::URI, RDF::Query::Variable] context
20
+ # @param [RDF::Query] bgp
21
+ # @return [RDF::Query]
22
+ def self.new(context, bgp)
23
+ bgp.context = context
24
+ bgp
25
+ end
26
+ end # Graph
27
+ end # Operator
28
+ end; end # SPARQL::Algebra
@@ -0,0 +1,32 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL relational `>` (greater than) comparison operator.
5
+ #
6
+ # @example
7
+ # (> ?x ?y)
8
+ #
9
+ # @see http://www.w3.org/TR/rdf-sparql-query/#OperatorMapping
10
+ # @see http://www.w3.org/TR/xpath-functions/#func-compare
11
+ # @see http://www.w3.org/TR/xpath-functions/#func-numeric-greater-than
12
+ # @see http://www.w3.org/TR/xpath-functions/#func-boolean-greater-than
13
+ # @see http://www.w3.org/TR/xpath-functions/#func-dateTime-greater-than
14
+ class GreaterThan < Compare
15
+ NAME = :>
16
+
17
+ ##
18
+ # Returns `true` if the first operand is greater than the second
19
+ # operand; returns `false` otherwise.
20
+ #
21
+ # @param [RDF::Literal] left
22
+ # a literal
23
+ # @param [RDF::Literal] right
24
+ # a literal
25
+ # @return [RDF::Literal::Boolean] `true` or `false`
26
+ # @raise [TypeError] if either operand is not a literal
27
+ def apply(left, right)
28
+ super
29
+ end
30
+ end # GreaterThan
31
+ end # Operator
32
+ end; end # SPARQL::Algebra
@@ -0,0 +1,33 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL relational `>=` (greater than or equal) comparison
5
+ # operator.
6
+ #
7
+ # @example
8
+ # (>= ?x ?y)
9
+ #
10
+ # @see http://www.w3.org/TR/rdf-sparql-query/#OperatorMapping
11
+ # @see http://www.w3.org/TR/xpath-functions/#func-compare
12
+ # @see http://www.w3.org/TR/xpath-functions/#func-numeric-greater-than
13
+ # @see http://www.w3.org/TR/xpath-functions/#func-boolean-greater-than
14
+ # @see http://www.w3.org/TR/xpath-functions/#func-dateTime-greater-than
15
+ class GreaterThanOrEqual < Compare
16
+ NAME = :>=
17
+
18
+ ##
19
+ # Returns `true` if the first operand is greater than or equal to the
20
+ # second operand; returns `false` otherwise.
21
+ #
22
+ # @param [RDF::Literal] left
23
+ # a literal
24
+ # @param [RDF::Literal] right
25
+ # a literal
26
+ # @return [RDF::Literal::Boolean] `true` or `false`
27
+ # @raise [TypeError] if either operand is not a literal
28
+ def apply(left, right)
29
+ super
30
+ end
31
+ end # GreaterThanOrEqual
32
+ end # Operator
33
+ end; end # SPARQL::Algebra
@@ -0,0 +1,35 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL `isBlank` operator.
5
+ #
6
+ # @example
7
+ # (prefix ((xsd: <http://www.w3.org/2001/XMLSchema#>)
8
+ # (: <http://example.org/things#>))
9
+ # (project (?x ?v)
10
+ # (filter (isBlank ?v)
11
+ # (bgp (triple ?x :p ?v)))))
12
+ #
13
+ # @see http://www.w3.org/TR/rdf-sparql-query/#func-isBlank
14
+ class IsBlank < Operator::Unary
15
+ include Evaluatable
16
+
17
+ NAME = :isBlank
18
+
19
+ ##
20
+ # Returns `true` if the operand is an `RDF::Node`, `false` otherwise.
21
+ #
22
+ # @param [RDF::Term] term
23
+ # an RDF term
24
+ # @return [RDF::Literal::Boolean] `true` or `false`
25
+ # @raise [TypeError] if the operand is not an RDF term
26
+ def apply(term)
27
+ case term
28
+ when RDF::Node then RDF::Literal::TRUE
29
+ when RDF::Term then RDF::Literal::FALSE
30
+ else raise TypeError, "expected an RDF::Term, but got #{term.inspect}"
31
+ end
32
+ end
33
+ end # IsBlank
34
+ end # Operator
35
+ end; end # SPARQL::Algebra
@@ -0,0 +1,37 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL `isIRI`/`isURI` operator.
5
+ #
6
+ # @example
7
+ # (prefix ((xsd: <http://www.w3.org/2001/XMLSchema#>)
8
+ # (: <http://example.org/things#>))
9
+ # (project (?x ?v)
10
+ # (filter (isIRI ?v)
11
+ # (bgp (triple ?x :p ?v)))))
12
+ #
13
+ # @see http://www.w3.org/TR/rdf-sparql-query/#func-isIRI
14
+ class IsIRI < Operator::Unary
15
+ include Evaluatable
16
+
17
+ NAME = [:isIRI, :isURI]
18
+
19
+ ##
20
+ # Returns `true` if the operand is an `RDF::URI`, `false` otherwise.
21
+ #
22
+ # @param [RDF::Term] term
23
+ # an RDF term
24
+ # @return [RDF::Literal::Boolean] `true` or `false`
25
+ # @raise [TypeError] if the operand is not an RDF term
26
+ def apply(term)
27
+ case term
28
+ when RDF::URI then RDF::Literal::TRUE
29
+ when RDF::Term then RDF::Literal::FALSE
30
+ else raise TypeError, "expected an RDF::Term, but got #{term.inspect}"
31
+ end
32
+ end
33
+
34
+ Operator::IsURI = IsIRI
35
+ end # IsIRI
36
+ end # Operator
37
+ end; end # SPARQL::Algebra
@@ -0,0 +1,36 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL `isLiteral` operator.
5
+ #
6
+ # @example
7
+ # (prefix ((xsd: <http://www.w3.org/2001/XMLSchema#>)
8
+ # (: <http://example.org/things#>))
9
+ # (project (?x ?v)
10
+ # (filter (isLiteral ?v)
11
+ # (bgp (triple ?x :p ?v)))))
12
+ #
13
+ # @see http://www.w3.org/TR/rdf-sparql-query/#func-isLiteral
14
+ class IsLiteral < Operator::Unary
15
+ include Evaluatable
16
+
17
+ NAME = :isLiteral
18
+
19
+ ##
20
+ # Returns `true` if the operand is an `RDF::Literal`, `false`
21
+ # otherwise.
22
+ #
23
+ # @param [RDF::Term] term
24
+ # an RDF term
25
+ # @return [RDF::Literal::Boolean] `true` or `false`
26
+ # @raise [TypeError] if the operand is not an RDF term
27
+ def apply(term)
28
+ case term
29
+ when RDF::Literal then RDF::Literal::TRUE
30
+ when RDF::Term then RDF::Literal::FALSE
31
+ else raise TypeError, "expected an RDF::Term, but got #{term.inspect}"
32
+ end
33
+ end
34
+ end # IsLiteral
35
+ end # Operator
36
+ end; end # SPARQL::Algebra
@@ -0,0 +1,67 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL GraphPattern `join` operator.
5
+ #
6
+ # @example
7
+ # (prefix ((: <http://example/>))
8
+ # (join
9
+ # (bgp (triple ?s ?p ?o))
10
+ # (graph ?g
11
+ # (bgp (triple ?s ?q ?v)))))
12
+ #
13
+ # @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
14
+ class Join < Operator::Binary
15
+ include Query
16
+
17
+ NAME = [:join]
18
+
19
+ ##
20
+ # Executes each operand with `queryable` and performs the `join` operation
21
+ # by creating a new solution set containing the `merge` of all solutions
22
+ # from each set that are `compatible` with each other.
23
+ #
24
+ # @param [RDF::Queryable] queryable
25
+ # the graph or repository to query
26
+ # @param [Hash{Symbol => Object}] options
27
+ # any additional keyword options
28
+ # @return [RDF::Query::Solutions]
29
+ # the resulting solution sequence
30
+ # @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
31
+ # @see http://rdf.rubyforge.org/RDF/Query/Solution.html#merge-instance_method
32
+ # @see http://rdf.rubyforge.org/RDF/Query/Solution.html#compatible%3F-instance_method
33
+ def execute(queryable, options = {})
34
+ # Join(Ω1, Ω2) = { merge(μ1, μ2) | μ1 in Ω1 and μ2 in Ω2, and μ1 and μ2 are compatible }
35
+ # eval(D(G), Join(P1, P2)) = Join(eval(D(G), P1), eval(D(G), P2))
36
+ #
37
+ # Generate solutions independently, merge based on solution compatibility
38
+ debug(options) {"Join"}
39
+ solutions1 = operand(0).execute(queryable, options.merge(:depth => options[:depth].to_i + 1)) || {}
40
+ debug(options) {"=>(left) #{solutions1.inspect}"}
41
+ solutions2 = operand(1).execute(queryable, options.merge(:depth => options[:depth].to_i + 1)) || {}
42
+ debug(options) {"=>(right) #{solutions2.inspect}"}
43
+ @solutions = solutions1.map do |s1|
44
+ solutions2.map { |s2| s2.merge(s1) if s2.compatible?(s1) }
45
+ end.flatten.compact
46
+ @solutions = RDF::Query::Solutions.new(@solutions)
47
+ debug(options) {"=> #{@solutions.inspect}"}
48
+ @solutions
49
+ end
50
+
51
+ ##
52
+ # Returns an optimized version of this query.
53
+ #
54
+ # Groups of one graph pattern (not a filter) become join(Z, A) and can be replaced by A.
55
+ # The empty graph pattern Z is the identity for join:
56
+ # Replace join(Z, A) by A
57
+ # Replace join(A, Z) by A
58
+ #
59
+ # @return [Join, RDF::Query] `self`
60
+ def optimize
61
+ ops = operands.map {|o| o.optimize }.select {|o| o.respond_to?(:empty?) && !o.empty?}
62
+ @operands = ops
63
+ self
64
+ end
65
+ end # Join
66
+ end # Operator
67
+ end; end # SPARQL::Algebra
@@ -0,0 +1,29 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL `lang` operator.
5
+ #
6
+ # @see http://www.w3.org/TR/rdf-sparql-query/#func-lang
7
+ class Lang < Operator::Unary
8
+ include Evaluatable
9
+
10
+ NAME = :lang
11
+
12
+ ##
13
+ # Returns the language tag of the operand, if it has one.
14
+ #
15
+ # If the operand has no language tag, returns `""`.
16
+ #
17
+ # @param [RDF::Literal] literal
18
+ # a literal
19
+ # @return [RDF::Literal] a simple literal
20
+ # @raise [TypeError] if the operand is not a literal
21
+ def apply(literal)
22
+ case literal
23
+ when RDF::Literal then RDF::Literal(literal.language.to_s)
24
+ else raise TypeError, "expected an RDF::Literal, but got #{literal.inspect}"
25
+ end
26
+ end
27
+ end # Lang
28
+ end # Operator
29
+ end; end # SPARQL::Algebra
@@ -0,0 +1,53 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL `langMatches` operator.
5
+ #
6
+ # @example
7
+ # (prefix ((: <http://example.org/#>))
8
+ # (filter (langMatches (lang ?v) "en-GB")
9
+ # (bgp (triple :x ?p ?v))))
10
+ #
11
+ # @see http://www.w3.org/TR/rdf-sparql-query/#func-langMatches
12
+ # @see http://tools.ietf.org/html/rfc4647#section-3.3.1
13
+ class LangMatches < Operator::Binary
14
+ include Evaluatable
15
+
16
+ NAME = :langMatches
17
+
18
+ ##
19
+ # Returns `true` if the language tag (the first operand) matches the
20
+ # language range (the second operand).
21
+ #
22
+ # @param [RDF::Literal] language_tag
23
+ # a simple literal containing a language tag
24
+ # @param [RDF::Literal] language_range
25
+ # a simple literal containing a language range, per
26
+ # [RFC 4647 section 2.1](http://tools.ietf.org/html/rfc4647#section-2.1)
27
+ # @return [RDF::Literal::Boolean] `true` or `false`
28
+ # @raise [TypeError] if either operand is unbound
29
+ # @raise [TypeError] if either operand is not a simple literal
30
+ def apply(language_tag, language_range)
31
+ raise TypeError, "expected a plain RDF::Literal for language_tag, but got #{language_tag.inspect}" unless language_tag.is_a?(RDF::Literal) && language_tag.plain?
32
+ language_tag = language_tag.to_s.downcase
33
+
34
+ raise TypeError, "expected a plain RDF::Literal for language_range, but got #{language_range.inspect}" unless language_range.is_a?(RDF::Literal) && language_range.plain?
35
+ language_range = language_range.to_s.downcase
36
+
37
+ case
38
+ # A language range of "*" matches any non-empty language tag.
39
+ when language_range.eql?('*')
40
+ RDF::Literal(!(language_tag.empty?))
41
+ # A language range matches a particular language tag if, in a
42
+ # case-insensitive comparison, it exactly equals the tag, ...
43
+ when language_tag.eql?(language_range)
44
+ RDF::Literal::TRUE
45
+ # ... or if it exactly equals a prefix of the tag such that the
46
+ # first character following the prefix is "-".
47
+ else
48
+ RDF::Literal(language_tag.start_with?(language_range + '-'))
49
+ end
50
+ end
51
+ end # LangMatches
52
+ end # Operator
53
+ end; end # SPARQL::Algebra
@@ -0,0 +1,95 @@
1
+ module SPARQL; module Algebra
2
+ class Operator
3
+ ##
4
+ # The SPARQL GraphPattern `leftjoin` operator.
5
+ #
6
+ # @example
7
+ # (prefix ((: <http://example/>))
8
+ # (leftjoin
9
+ # (bgp (triple ?x :p ?v))
10
+ # (bgp (triple ?y :q ?w))
11
+ # (= ?v 2)))
12
+ #
13
+ # @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
14
+ class LeftJoin < Operator
15
+ include Query
16
+
17
+ NAME = [:leftjoin]
18
+
19
+ ##
20
+ # Executes each operand with `queryable` and performs the `leftjoin` operation
21
+ # by adding every solution from the left, merging compatible solutions from the right
22
+ # that match an optional filter.
23
+ #
24
+ # @param [RDF::Queryable] queryable
25
+ # the graph or repository to query
26
+ # @param [Hash{Symbol => Object}] options
27
+ # any additional keyword options
28
+ # @return [RDF::Query::Solutions]
29
+ # the resulting solution sequence
30
+ # @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
31
+ # @see http://rdf.rubyforge.org/RDF/Query/Solution.html#merge-instance_method
32
+ # @see http://rdf.rubyforge.org/RDF/Query/Solution.html#compatible%3F-instance_method
33
+ def execute(queryable, options = {})
34
+ filter = operand(2)
35
+
36
+
37
+ debug(options) {"LeftJoin"}
38
+ left = operand(0).execute(queryable, options.merge(:depth => options[:depth].to_i + 1)) || {}
39
+ debug(options) {"=>(left) #{left.inspect}"}
40
+ right = operand(1).execute(queryable, options.merge(:depth => options[:depth].to_i + 1)) || {}
41
+ debug(options) {"=>(right) #{right.inspect}"}
42
+
43
+ # LeftJoin(Ω1, Ω2, expr) =
44
+ solutions = []
45
+ left.each do |s1|
46
+ load_left = true
47
+ right.each do |s2|
48
+ s = s2.merge(s1)
49
+ expr = filter ? boolean(filter.evaluate(s)).true? : true rescue false
50
+ debug(options) {"===>(evaluate) #{s.inspect}"} if filter
51
+
52
+ if expr && s1.compatible?(s2)
53
+ # { merge(μ1, μ2) | μ1 in Ω1 and μ2 in Ω2, and μ1 and μ2 are compatible and expr(merge(μ1, μ2)) is true }
54
+ debug(options) {"=>(merge s1 s2) #{s.inspect}"}
55
+ solutions << s
56
+ load_left = false # Left solution added one or more times due to merge
57
+ end
58
+ end
59
+ if load_left
60
+ debug(options) {"=>(add) #{s1.inspect}"}
61
+ solutions << s1
62
+ end
63
+ end
64
+
65
+ @solutions = RDF::Query::Solutions.new(solutions)
66
+ debug(options) {"=> #{@solutions.inspect}"}
67
+ @solutions
68
+ end
69
+
70
+ ##
71
+ # Returns an optimized version of this query.
72
+ #
73
+ # If optimize operands, and if the first two operands are both Queries, replace
74
+ # with the unique sum of the query elements
75
+ #
76
+ # @return [Union, RDF::Query] `self`
77
+ def optimize
78
+ ops = operands.map {|o| o.optimize }.select {|o| o.respond_to?(:empty?) && !o.empty?}
79
+ expr = ops.pop unless ops.last.executable?
80
+ expr = nil if expr.respond_to?(:true?) && expr.true?
81
+
82
+ # ops now is one or two executable operators
83
+ # expr is a filter expression, which may have been optimized to 'true'
84
+ case ops.length
85
+ when 0
86
+ RDF::Query.new # Empty query, expr doesn't matter
87
+ when 1
88
+ expr ? Filter.new(expr, ops.first) : ops.first
89
+ else
90
+ expr ? LeftJoin(ops[0], ops[1], expr) : LeftJoin(ops[0], ops[1])
91
+ end
92
+ end
93
+ end # LeftJoin
94
+ end # Operator
95
+ end; end # SPARQL::Algebra