module SPARQL; module Algebra
class Operator
##
# The SPARQL Property Path `pathRange` (NonCountingPath) operator.
#
# Property path ranges allow specific path lenghts to be queried. The minimum range describes the minimum path length that will yield solutions, and the maximum range the maximum path that will be returned. A minumum of zero is similar to the `path?` operator. The maximum range of `*` is similar to the `path*` or `path+` operators.
#
# For example, the two queries are functionally equivalent:
#
# SELECT * WHERE {:a :p{1,2} :b}
#
# SELECT * WHERE {:a (:p/:p?) :b}
#
# [91] PathElt ::= PathPrimary PathMod?
# [93] PathMod ::= '*' | '?' | '+' | '{' INTEGER? (',' INTEGER?)? '}'
#
# @example SPARQL Grammar range with fixed length
# PREFIX :
# SELECT * WHERE {
# :a :p{2} ?z
# }
#
# @example SSE range with fixed length only
# (prefix ((: ))
# (path :a (pathRange 2 2 :p) ?z))
#
# @example SPARQL Grammar range with min only
# PREFIX :
# SELECT * WHERE {
# :a :p{1,} ?z
# }
#
# @example SSE range with min only
# (prefix ((: ))
# (path :a (pathRange 1 * :p) ?z))
#
# @example SPARQL Grammar range with max only
# PREFIX :
# SELECT * WHERE {
# :a :p{,2} ?z
# }
#
# @example SSE range with max only
# (prefix ((: ))
# (path :a (pathRange 0 2 :p) ?z))
#
# @example SPARQL Grammar range with min and max
# PREFIX :
# SELECT * WHERE {
# :a :p{1,2} ?z
# }
#
# @example SSE range with min and max
# (prefix ((: ))
# (path :a (pathRange 1 2 :p) ?z))
#
class PathRange < Operator::Ternary
include Query
NAME = :"pathRange"
##
# Initializes a new operator instance.
#
# @param [RDF::Literal::Integer] max
# the range minimum
# @param [RDF::Literal::Integer, Symbol] min
# the range maximum (may be `*`)
# @param [SPARQL::Operator] path
# the query
# @param [Hash{Symbol => Object}] options
# any additional options (see {Operator#initialize})
# @raise [TypeError] if any operand is invalid
# @raise [ArgumentError] range element is invalid
def initialize(min, max, path, **options)
raise ArgumentError, "expect min <= max {#{min},#{max}}" if
max.is_a?(RDF::Literal::Integer) && max < min
super
end
##
# Path with lower and upper bounds on lenghts:
#
# (path :a (pathRange 1 2 :p) :b)
# => (path)
#
# @param [RDF::Queryable] queryable
# the graph or repository to query
# @param [RDF::Query::Solutions] accumulator (RDF::Query::Solutions.new)
# For previous solutions to avoid duplicates.
# @param [Hash{Symbol => Object}] options
# any additional keyword options
# @option options [RDF::Term, RDF::Variable] :subject
# @option options [RDF::Term, RDF::Variable] :object
# @yield [solution]
# each matching solution
# @yieldparam [RDF::Query::Solution] solution
# @yieldreturn [void] ignored
# @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra
def execute(queryable,
accumulator: RDF::Query::Solutions.new,
index: RDF::Literal(0),
**options,
&block)
min, max, op = *operands
subject, object = options[:subject], options[:object]
debug(options) {"Path{#{min},#{max}} #{[subject, op, object].to_sse}"}
path_mm = if min.zero? && max.is_a?(RDF::Literal::Integer) && max.zero?
PathZero.new(op, **options)
else
# Build up a sequence
path_opt = nil
seq_len = min.to_i
if max.is_a?(RDF::Literal::Integer)
# min to max is a sequence of optional sequences
opt_len = (max - min).to_i
while opt_len > 0 do
path_opt = PathOpt.new(path_opt ? Seq.new(op, path_opt, **options) : op, **options)
opt_len -= 1
end
elsif seq_len > 0
path_opt = PathPlus.new(op, **options)
seq_len -= 1
else
path_opt = PathStar.new(op, **options)
end
# sequence ending in op, op+, op*, or path_opt
path_seq = nil
while seq_len > 0 do
path_seq = if path_opt
opt, path_opt = path_opt, nil
Seq.new(op, opt, **options)
elsif path_seq
Seq.new(op, path_seq, **options)
else
op
end
seq_len -= 1
end
path_seq || path_opt || op
end
debug(options) {"=> #{path_mm.to_sse}"}
# After this, path_mm may just be the original op, which can be a term, not an operator.
if path_mm.is_a?(RDF::Term)
path_mm = RDF::Query.new do |q|
q.pattern [subject, path_mm, object]
end
end
solutions = path_mm.execute(queryable, **options.merge(depth: options[:depth].to_i + 1)).uniq
debug(options) {"(path{#{min},#{max}})=> #{solutions.to_sxp}"}
solutions.each(&block) if block_given?
solutions
end
##
#
# Returns a partial SPARQL grammar for this operator.
#
# @return [String]
def to_sparql(**options)
min, max, path = operands
"(#{path.to_sparql(**options)})" +
if max == :*
"{#{min},}"
elsif min == max
"{#{min}}"
else
"{#{min},#{max}}"
end
end
end # PathStar
end # Operator
end; end # SPARQL::Algebra