sparql 1.1.6 → 1.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -6
- data/VERSION +1 -1
- data/lib/sparql/algebra/expression.rb +28 -2
- data/lib/sparql/algebra/extensions.rb +73 -0
- data/lib/sparql/algebra/operator.rb +73 -9
- data/lib/sparql/algebra/operator/alt.rb +58 -0
- data/lib/sparql/algebra/operator/extend.rb +15 -3
- data/lib/sparql/algebra/operator/filter.rb +18 -1
- data/lib/sparql/algebra/operator/group.rb +22 -1
- data/lib/sparql/algebra/operator/join.rb +16 -5
- data/lib/sparql/algebra/operator/left_join.rb +12 -1
- data/lib/sparql/algebra/operator/notin.rb +1 -1
- data/lib/sparql/algebra/operator/notoneof.rb +52 -0
- data/lib/sparql/algebra/operator/path.rb +49 -0
- data/lib/sparql/algebra/operator/path_opt.rb +112 -0
- data/lib/sparql/algebra/operator/path_plus.rb +99 -0
- data/lib/sparql/algebra/operator/path_star.rb +42 -0
- data/lib/sparql/algebra/operator/reverse.rb +54 -0
- data/lib/sparql/algebra/operator/seq.rb +73 -0
- data/lib/sparql/algebra/operator/sequence.rb +63 -0
- data/lib/sparql/algebra/operator/union.rb +12 -1
- data/lib/sparql/algebra/query.rb +1 -0
- data/lib/sparql/grammar/meta.rb +4593 -4582
- data/lib/sparql/grammar/parser11.rb +301 -107
- metadata +13 -4
@@ -91,7 +91,28 @@ module SPARQL; module Algebra
|
|
91
91
|
@solutions.each(&block) if block_given?
|
92
92
|
@solutions
|
93
93
|
end
|
94
|
-
|
94
|
+
|
95
|
+
# It is an error for aggregates to project variables with a name already used in other aggregate projections, or in the WHERE clause.
|
96
|
+
#
|
97
|
+
# It is also an error to project ungrouped variables
|
98
|
+
def validate!
|
99
|
+
group_vars = operand(0).map {|v| Array(v).first}
|
100
|
+
ext = first_ancestor(Extend)
|
101
|
+
extend_vars = ext ? ext.operand(0).map(&:first).select {|v| v.is_a?(RDF::Query::Variable)} : []
|
102
|
+
project = first_ancestor(Project)
|
103
|
+
# If not projecting, were are effectively projecting all variables in the query
|
104
|
+
project_vars = project ? project.operand(0) : operands.last.vars
|
105
|
+
|
106
|
+
available_vars = (extend_vars + group_vars).compact
|
107
|
+
|
108
|
+
# All variables must either be grouped or extended
|
109
|
+
unless (project_vars - available_vars).empty?
|
110
|
+
raise ArgumentError,
|
111
|
+
"projecting ungrouped/extended variables: #{(project_vars.compact - available_vars.compact).to_sse}"
|
112
|
+
end
|
113
|
+
super
|
114
|
+
end
|
115
|
+
|
95
116
|
##
|
96
117
|
# Returns an optimized version of this query.
|
97
118
|
#
|
@@ -39,22 +39,33 @@ module SPARQL; module Algebra
|
|
39
39
|
# eval(D(G), Join(P1, P2)) = Join(eval(D(G), P1), eval(D(G), P2))
|
40
40
|
#
|
41
41
|
# Generate solutions independently, merge based on solution compatibility
|
42
|
-
debug(options) {"Join"}
|
42
|
+
debug(options) {"Join #{operands.to_sse}"}
|
43
43
|
|
44
44
|
left = queryable.query(operand(0), options.merge(depth: options[:depth].to_i + 1))
|
45
|
-
debug(options) {"(join)=>(left) #{left.
|
45
|
+
debug(options) {"(join)=>(left) #{left.map(&:to_hash).to_sse}"}
|
46
46
|
|
47
47
|
right = queryable.query(operand(1), options.merge(depth: options[:depth].to_i + 1))
|
48
|
-
debug(options) {"(join)=>(right) #{right.
|
48
|
+
debug(options) {"(join)=>(right) #{right.map(&:to_hash).to_sse}"}
|
49
49
|
|
50
50
|
@solutions = RDF::Query::Solutions(left.map do |s1|
|
51
51
|
right.map { |s2| s2.merge(s1) if s2.compatible?(s1) }
|
52
52
|
end.flatten.compact)
|
53
|
-
debug(options) {"=> #{@solutions.
|
53
|
+
debug(options) {"(join)=> #{@solutions.map(&:to_hash).to_sse}"}
|
54
54
|
@solutions.each(&block) if block_given?
|
55
55
|
@solutions
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
|
+
# The same blank node label cannot be used in two different basic graph patterns in the same query
|
59
|
+
def validate!
|
60
|
+
left_nodes, right_nodes = operand(0).ndvars, operand(1).ndvars
|
61
|
+
|
62
|
+
unless (left_nodes.compact & right_nodes.compact).empty?
|
63
|
+
raise ArgumentError,
|
64
|
+
"sub-operands share non-distinguished variables: #{(left_nodes.compact & right_nodes.compact).to_sse}"
|
65
|
+
end
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
58
69
|
##
|
59
70
|
# Returns an optimized version of this query.
|
60
71
|
#
|
@@ -70,7 +70,18 @@ module SPARQL; module Algebra
|
|
70
70
|
@solutions.each(&block) if block_given?
|
71
71
|
@solutions
|
72
72
|
end
|
73
|
-
|
73
|
+
|
74
|
+
# The same blank node label cannot be used in two different basic graph patterns in the same query
|
75
|
+
def validate!
|
76
|
+
left_nodes, right_nodes = operand(0).ndvars, operand(1).ndvars
|
77
|
+
|
78
|
+
unless (left_nodes.compact & right_nodes.compact).empty?
|
79
|
+
raise ArgumentError,
|
80
|
+
"sub-operands share non-distinguished variables: #{(left_nodes.compact & right_nodes.compact).to_sse}"
|
81
|
+
end
|
82
|
+
super
|
83
|
+
end
|
84
|
+
|
74
85
|
##
|
75
86
|
# Returns an optimized version of this query.
|
76
87
|
#
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SPARQL; module Algebra
|
2
|
+
class Operator
|
3
|
+
##
|
4
|
+
# The SPARQL Property Path `notoneof` (NegatedPropertySet) operator.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# (notoneof ex:p1 ex:p2)
|
8
|
+
#
|
9
|
+
# @see http://www.w3.org/TR/sparql11-query/#eval_negatedPropertySet
|
10
|
+
class NotOneOf < Operator
|
11
|
+
include Query
|
12
|
+
|
13
|
+
NAME = :notoneof
|
14
|
+
|
15
|
+
##
|
16
|
+
# Equivalant to:
|
17
|
+
#
|
18
|
+
# (path (:x (noteoneof :p :q) :y))
|
19
|
+
# => (filter (notin ??p :p :q) (bgp (:x ??p :y)))
|
20
|
+
#
|
21
|
+
# @note all operands are terms, and not operators, so this can be done by filtering results usin
|
22
|
+
#
|
23
|
+
# @param [RDF::Queryable] queryable
|
24
|
+
# the graph or repository to query
|
25
|
+
# @param [Hash{Symbol => Object}] options
|
26
|
+
# any additional keyword options
|
27
|
+
# @option options [RDF::Term, RDF::Variable] :subject
|
28
|
+
# @option options [RDF::Term, RDF::Variable] :object
|
29
|
+
# @yield [solution]
|
30
|
+
# each matching solution
|
31
|
+
# @yieldparam [RDF::Query::Solution] solution
|
32
|
+
# @yieldreturn [void] ignored
|
33
|
+
# @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
|
34
|
+
def execute(queryable, options = {}, &block)
|
35
|
+
debug(options) {"NotOneOf #{operands.to_sse}"}
|
36
|
+
subject, object = options[:subject], options[:object]
|
37
|
+
|
38
|
+
v = RDF::Query::Variable.new
|
39
|
+
v.distinguished = false
|
40
|
+
bgp = RDF::Query.new do |q|
|
41
|
+
q.pattern [subject, v, object]
|
42
|
+
end
|
43
|
+
query = Filter.new(NotIn.new(v, *operands), bgp)
|
44
|
+
queryable.query(query, options.merge(depth: options[:depth].to_i + 1)) do |solution|
|
45
|
+
solution.bindings.delete(v.to_sym)
|
46
|
+
debug(options) {"(solution)-> #{solution.to_hash.to_sse}"}
|
47
|
+
block.call(solution)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end # NotOneOf
|
51
|
+
end # Operator
|
52
|
+
end; end # SPARQL::Algebra
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module SPARQL; module Algebra
|
2
|
+
class Operator
|
3
|
+
##
|
4
|
+
# The SPARQL Property Path `path` operator.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# (path :a (path+ :p) ?z)
|
8
|
+
#
|
9
|
+
# @see http://www.w3.org/TR/sparql11-query/#sparqlTranslatePathExpressions
|
10
|
+
class Path < Operator::Ternary
|
11
|
+
include Query
|
12
|
+
|
13
|
+
NAME = :path
|
14
|
+
|
15
|
+
##
|
16
|
+
# Finds solutions from `queryable` matching the path.
|
17
|
+
#
|
18
|
+
# @param [RDF::Queryable] queryable
|
19
|
+
# the graph or repository to query
|
20
|
+
# @param [Hash{Symbol => Object}] options
|
21
|
+
# any additional keyword options
|
22
|
+
# @yield [solution]
|
23
|
+
# each matching solution
|
24
|
+
# @yieldparam [RDF::Query::Solution] solution
|
25
|
+
# @yieldreturn [void] ignored
|
26
|
+
# @return [RDF::Query::Solutions]
|
27
|
+
# the resulting solution sequence
|
28
|
+
# @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
|
29
|
+
def execute(queryable, options = {}, &block)
|
30
|
+
debug(options) {"Path #{operands.to_sse}"}
|
31
|
+
subject, path_op, object = operands
|
32
|
+
|
33
|
+
@solutions = RDF::Query::Solutions.new
|
34
|
+
path_op.execute(queryable, options.merge(
|
35
|
+
subject: subject,
|
36
|
+
object: object,
|
37
|
+
context: options.fetch(:context, false),
|
38
|
+
depth: options[:depth].to_i + 1)
|
39
|
+
) do |solution|
|
40
|
+
@solutions << solution
|
41
|
+
end
|
42
|
+
debug(options) {"=> #{@solutions.inspect}"}
|
43
|
+
@solutions.uniq!
|
44
|
+
@solutions.each(&block) if block_given?
|
45
|
+
@solutions
|
46
|
+
end
|
47
|
+
end # Path
|
48
|
+
end # Operator
|
49
|
+
end; end # SPARQL::Algebra
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module SPARQL; module Algebra
|
2
|
+
class Operator
|
3
|
+
##
|
4
|
+
# The SPARQL Property Path `path?` (ZeroOrOnePath) operator.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# (path? :p)
|
8
|
+
#
|
9
|
+
# @see http://www.w3.org/TR/sparql11-query/#defn_evalPP_ZeroOrOnePath
|
10
|
+
class PathOpt < Operator::Unary
|
11
|
+
include Query
|
12
|
+
|
13
|
+
NAME = :path?
|
14
|
+
|
15
|
+
##
|
16
|
+
# Equivalent to:
|
17
|
+
#
|
18
|
+
# (path x (path? :p) y)
|
19
|
+
# => (union (bgp ((x :p y))) (filter (x = x) (solution x y)))
|
20
|
+
#
|
21
|
+
#
|
22
|
+
# @param [RDF::Queryable] queryable
|
23
|
+
# the graph or repository to query
|
24
|
+
# @param [Hash{Symbol => Object}] options
|
25
|
+
# any additional keyword options
|
26
|
+
# @option options [RDF::Term, RDF::Variable] :subject
|
27
|
+
# @option options [RDF::Term, RDF::Variable] :object
|
28
|
+
# @yield [solution]
|
29
|
+
# each matching solution
|
30
|
+
# @yieldparam [RDF::Query::Solution] solution
|
31
|
+
# @yieldreturn [void] ignored
|
32
|
+
# @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
|
33
|
+
def execute(queryable, options = {}, &block)
|
34
|
+
subject, object = options[:subject], options[:object]
|
35
|
+
debug(options) {"Path? #{[subject, operands, object].to_sse}"}
|
36
|
+
|
37
|
+
solutions = RDF::Query::Solutions.new
|
38
|
+
# Solutions where subject == object with no predicate
|
39
|
+
case
|
40
|
+
when subject.variable? && object.variable?
|
41
|
+
# Nodes is the set of all subjects and objects in queryable
|
42
|
+
# FIXME: should this be Queryable#enum_nodes?
|
43
|
+
# All subjects which are `object`
|
44
|
+
query = RDF::Query.new {|q| q.pattern(subject: subject)}
|
45
|
+
queryable.query(query, options) do |solution|
|
46
|
+
solution.merge!(object.to_sym => solution[subject])
|
47
|
+
debug(options) {"(solution-s0)-> #{solution.to_hash.to_sse}"}
|
48
|
+
solutions << solution
|
49
|
+
end if query.valid?
|
50
|
+
|
51
|
+
# All objects which are `object`
|
52
|
+
query = RDF::Query.new {|q| q.pattern(object: object)}
|
53
|
+
queryable.query(query, options) do |solution|
|
54
|
+
solution.merge!(subject.to_sym => solution[object])
|
55
|
+
debug(options) {"(solution-o0)-> #{solution.to_hash.to_sse}"}
|
56
|
+
solutions << solution
|
57
|
+
end if query.valid?
|
58
|
+
when subject.variable?
|
59
|
+
# All subjects which are `object`
|
60
|
+
query = RDF::Query.new {|q| q.pattern(subject: object)}
|
61
|
+
queryable.query(query, options) do |solution|
|
62
|
+
solution.merge!(subject.to_sym => object)
|
63
|
+
debug(options) {"(solution-s0)-> #{solution.to_hash.to_sse}"}
|
64
|
+
solutions << solution
|
65
|
+
end if query.valid?
|
66
|
+
|
67
|
+
# All objects which are `object`
|
68
|
+
query = RDF::Query.new {|q| q.pattern(object: object)}
|
69
|
+
queryable.query(query, options) do |solution|
|
70
|
+
solution.merge!(subject.to_sym => object)
|
71
|
+
debug(options) {"(solution-o0)-> #{solution.to_hash.to_sse}"}
|
72
|
+
solutions << solution
|
73
|
+
end if query.valid?
|
74
|
+
when object.variable?
|
75
|
+
# All subjects which are `subject`
|
76
|
+
query = RDF::Query.new {|q| q.pattern(subject: subject)}
|
77
|
+
queryable.query(query, options) do |solution|
|
78
|
+
solution.merge!(object.to_sym => subject)
|
79
|
+
debug(options) {"(solution-s0)-> #{solution.to_hash.to_sse}"}
|
80
|
+
solutions << solution
|
81
|
+
end if query.valid?
|
82
|
+
|
83
|
+
# All objects which are `subject
|
84
|
+
query = RDF::Query.new {|q| q.pattern(object: subject)}
|
85
|
+
queryable.query(query, options) do |solution|
|
86
|
+
solution.merge!(object.to_sym => subject)
|
87
|
+
debug(options) {"(solution-o0)-> #{solution.to_hash.to_sse}"}
|
88
|
+
solutions << solution
|
89
|
+
end if query.valid?
|
90
|
+
else
|
91
|
+
# Otherwise, if subject == object, an empty solution
|
92
|
+
solutions << RDF::Query::Solution.new if subject == object
|
93
|
+
end
|
94
|
+
|
95
|
+
# Solutions where predicate exists
|
96
|
+
query = if operand.is_a?(RDF::Term)
|
97
|
+
RDF::Query.new do |q|
|
98
|
+
q.pattern [subject, operand, object]
|
99
|
+
end
|
100
|
+
else
|
101
|
+
operand
|
102
|
+
end
|
103
|
+
|
104
|
+
# Recurse into query
|
105
|
+
solutions +=
|
106
|
+
queryable.query(query, options.merge(depth: options[:depth].to_i + 1))
|
107
|
+
solutions.each(&block) if block_given?
|
108
|
+
solutions
|
109
|
+
end
|
110
|
+
end # PathOpt
|
111
|
+
end # Operator
|
112
|
+
end; end # SPARQL::Algebra
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module SPARQL; module Algebra
|
2
|
+
class Operator
|
3
|
+
##
|
4
|
+
# The SPARQL Property Path `path+` (OneOrMorePath) operator.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# (path+ :p)
|
8
|
+
#
|
9
|
+
# @see http://www.w3.org/TR/sparql11-query/#defn_evalPP_OneOrMorePath
|
10
|
+
class PathPlus < Operator::Unary
|
11
|
+
include Query
|
12
|
+
|
13
|
+
NAME = :"path+"
|
14
|
+
|
15
|
+
##
|
16
|
+
# Match on simple relation of subject to object, and then recurse on solutions
|
17
|
+
#
|
18
|
+
# @param [RDF::Queryable] queryable
|
19
|
+
# the graph or repository to query
|
20
|
+
# @param [Hash{Symbol => Object}] options
|
21
|
+
# any additional keyword options
|
22
|
+
# @option options [RDF::Term, RDF::Variable] :subject
|
23
|
+
# @option options [RDF::Term, RDF::Variable] :object
|
24
|
+
# @yield [solution]
|
25
|
+
# each matching solution
|
26
|
+
# @yieldparam [RDF::Query::Solution] solution
|
27
|
+
# @yieldreturn [void] ignored
|
28
|
+
# @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
|
29
|
+
def execute(queryable, options = {}, &block)
|
30
|
+
subject, object = options[:subject], options[:object]
|
31
|
+
debug(options) {"Path+ #{[subject, operands, object].to_sse}"}
|
32
|
+
|
33
|
+
# This turns from
|
34
|
+
# (:x :p+ ?y)
|
35
|
+
# into
|
36
|
+
# (:x :p ?y) UNION solutions(:x :p ?y) do |soln|
|
37
|
+
# if :x.variable?
|
38
|
+
# (soln[:x] :p+ soln[:y])
|
39
|
+
# end
|
40
|
+
|
41
|
+
# Solutions where predicate exists
|
42
|
+
query = if operand.is_a?(RDF::Term)
|
43
|
+
RDF::Query.new do |q|
|
44
|
+
q.pattern [subject, operand, object]
|
45
|
+
end
|
46
|
+
else
|
47
|
+
operand
|
48
|
+
end
|
49
|
+
|
50
|
+
# Block is given on the first invocation, otherwise, results are returned. This is necessary to stop when an existing solution has already been found
|
51
|
+
cumulative_solutions = options.fetch(:accumulator, RDF::Query::Solutions.new)
|
52
|
+
|
53
|
+
# Keep track of solutions
|
54
|
+
# Recurse into query
|
55
|
+
immediate_solutions = []
|
56
|
+
queryable.query(query, options.merge(depth: options[:depth].to_i + 1)) do |solution|
|
57
|
+
immediate_solutions << solution
|
58
|
+
end
|
59
|
+
|
60
|
+
# For all solutions, if they are not in the accumulator, add them and recurse, otherwise skip
|
61
|
+
recursive_solutions = RDF::Query::Solutions.new
|
62
|
+
immediate_solutions.reject {|s| cumulative_solutions.include?(s)}.each do |solution|
|
63
|
+
debug(options) {"(immediate solution)-> #{solution.to_hash.to_sse}"}
|
64
|
+
|
65
|
+
# Recurse on subject, if is a variable
|
66
|
+
case
|
67
|
+
when subject.variable? && object.variable?
|
68
|
+
# Query starting with bound object as subject, but replace result with subject
|
69
|
+
rs = queryable.query(self, options.merge(
|
70
|
+
subject: solution[object],
|
71
|
+
accumulator: (cumulative_solutions + immediate_solutions),
|
72
|
+
depth: options[:depth].to_i + 1)).map {|s| s.merge(subject.to_sym => solution[subject])}
|
73
|
+
# Query starting with bound subject as object, but replace result with subject
|
74
|
+
ro = queryable.query(self, options.merge(
|
75
|
+
object: solution[subject],
|
76
|
+
accumulator: (cumulative_solutions + immediate_solutions),
|
77
|
+
depth: options[:depth].to_i + 1)).map {|s| s.merge(object.to_sym => solution[object])}
|
78
|
+
recursive_solutions += (rs + ro).uniq
|
79
|
+
when subject.variable?
|
80
|
+
recursive_solutions += queryable.query(self, options.merge(
|
81
|
+
object: solution[subject],
|
82
|
+
accumulator: (cumulative_solutions + immediate_solutions),
|
83
|
+
depth: options[:depth].to_i + 1)).uniq
|
84
|
+
when object.variable?
|
85
|
+
recursive_solutions += queryable.query(self, options.merge(
|
86
|
+
subject: solution[object],
|
87
|
+
accumulator: (cumulative_solutions + immediate_solutions),
|
88
|
+
depth: options[:depth].to_i + 1)).uniq
|
89
|
+
end
|
90
|
+
end
|
91
|
+
debug(options) {"(recursive solutions)-> #{recursive_solutions.map(&:to_hash).to_sse}"} unless recursive_solutions.empty?
|
92
|
+
|
93
|
+
solutions = (immediate_solutions + recursive_solutions).uniq
|
94
|
+
solutions.each(&block) if block_given? # Only at top-level
|
95
|
+
solutions
|
96
|
+
end
|
97
|
+
end # PathPlus
|
98
|
+
end # Operator
|
99
|
+
end; end # SPARQL::Algebra
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module SPARQL; module Algebra
|
2
|
+
class Operator
|
3
|
+
##
|
4
|
+
# The SPARQL Property Path `path*` (ZeroOrMorePath) operator.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# (path* :p)
|
8
|
+
#
|
9
|
+
# @see http://www.w3.org/TR/sparql11-query/#defn_evalPP_ZeroOrMorePath
|
10
|
+
class PathStar < Operator::Unary
|
11
|
+
include Query
|
12
|
+
|
13
|
+
NAME = :"path*"
|
14
|
+
|
15
|
+
##
|
16
|
+
# Path including zero length:
|
17
|
+
#
|
18
|
+
# (path :a (path* :p) :b)
|
19
|
+
# => (path :a (path? (path+ :p)) :b)
|
20
|
+
#
|
21
|
+
# @param [RDF::Queryable] queryable
|
22
|
+
# the graph or repository to query
|
23
|
+
# @param [Hash{Symbol => Object}] options
|
24
|
+
# any additional keyword options
|
25
|
+
# @option options [RDF::Term, RDF::Variable] :subject
|
26
|
+
# @option options [RDF::Term, RDF::Variable] :object
|
27
|
+
# @yield [solution]
|
28
|
+
# each matching solution
|
29
|
+
# @yieldparam [RDF::Query::Solution] solution
|
30
|
+
# @yieldreturn [void] ignored
|
31
|
+
# @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
|
32
|
+
def execute(queryable, options = {}, &block)
|
33
|
+
subject, object = options[:subject], options[:object]
|
34
|
+
debug(options) {"Path* #{[subject, operands, object].to_sse}"}
|
35
|
+
|
36
|
+
# (:x :p* :y) => (:x (:p+)? :y)
|
37
|
+
query = PathOpt.new(PathPlus.new(*operands))
|
38
|
+
query.execute(queryable, options.merge(depth: options[:depth].to_i + 1), &block)
|
39
|
+
end
|
40
|
+
end # PathStar
|
41
|
+
end # Operator
|
42
|
+
end; end # SPARQL::Algebra
|