sparql 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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