module SPARQL; module Algebra
class Operator
##
# The SPARQL GraphPattern `project` operator.
#
# [9] SelectClause ::= 'SELECT' ( 'DISTINCT' | 'REDUCED' )? ( ( Var | ( '(' Expression 'AS' Var ')' ) )+ | '*' )
#
# ## Basic Projection
#
# @example SPARQL Grammar
# PREFIX :
# SELECT ?v {
# ?s :p ?v .
# FILTER (?v = 2)
# }
#
# @example SSE
# (prefix ((: ))
# (project (?v)
# (filter (= ?v 2)
# (bgp (triple ?s :p ?v)))))
#
# @example SPARQL Grammar (Sub select)
# SELECT (1 AS ?X ) {
# SELECT (2 AS ?Y ) {}
# }
#
# @example SSE (Sub select)
# (project (?X)
# (extend ((?X 1))
# (project (?Y)
# (extend ((?Y 2))
# (bgp)))))
#
# @example SPARQL Grammar (filter projection)
# PREFIX :
# ASK {
# {SELECT (GROUP_CONCAT(?o) AS ?g) WHERE {
# :a :p1 ?o
# }}
# FILTER(?g = "1 22" || ?g = "22 1")
# }
#
# @example SSE (filter projection)
# (prefix ((: ))
# (ask
# (filter
# (|| (= ?g "1 22") (= ?g "22 1"))
# (project (?g)
# (extend ((?g ??.0))
# (group () ((??.0 (group_concat ?o)))
# (bgp (triple :a :p1 ?o)))))) ))
#
# @see https://www.w3.org/TR/sparql11-query/#modProjection
class Project < Operator::Binary
include Query
NAME = [:project]
##
# Can only project in-scope variables.
#
# @return (see Algebra::Operator#initialize)
def validate!
if (group = descendants.detect {|o| o.is_a?(Group)})
raise ArgumentError, "project * on group is illegal" if operands.first.empty?
query_vars = operands.last.variables
variables.keys.each do |v|
raise ArgumentError,
"projecting #{v.to_sse} not projected from group" unless
query_vars.key?(v.to_sym)
end
end
super
end
##
# The projected variables.
#
# @return [Hash{Symbol => RDF::Query::Variable}]
def variables
operands(1).inject({}) {|hash, o| hash.merge(o.variables)}
end
##
# Executes this query on the given `queryable` graph or repository.
# Reduces the result set to the variables listed in the first operand
#
# If the first operand is empty, this indicates a `SPARQL *`, and all in-scope variables are projected.
#
# @param [RDF::Queryable] queryable
# the graph or repository to query
# @param [Hash{Symbol => Object}] options
# any additional keyword options
# @yield [solution]
# each matching solution
# @yieldparam [RDF::Query::Solution] solution
# @yieldreturn [void] ignored
# @return [RDF::Query::Solutions]
# the resulting solution sequence
# @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra
def execute(queryable, **options, &block)
@solutions = queryable.query(operands.last, depth: options[:depth].to_i + 1, **options)
@solutions.variable_names = self.variables.keys
@solutions = @solutions.project(*(operands.first)) unless operands.first.empty?
@solutions.each(&block) if block_given?
@solutions
end
##
# In-scope variables for a select are limited to those projected.
#
# @return [Hash{Symbol => RDF::Query::Variable}]
def variables
in_scope = operands.first.empty? ?
operands.last.variables.values :
operands.first
in_scope.inject({}) {|memo, v| memo.merge(v.variables)}
end
##
#
# Returns a partial SPARQL grammar for this operator.
#
# Extracts projections
#
# If there are already extensions or filters, then this is a sub-select.
#
# @return [String]
def to_sparql(**options)
vars = operands[0].empty? ? [:*] : operands[0]
if options[:extensions] || options[:filter_ops] || options[:project]
# Any of these options indicates we're in a sub-select
opts = options.dup.delete_if {|k,v| %I{extensions filter_ops project}.include?(k)}
content = operands.last.to_sparql(project: vars, **opts)
content = "{#{content}}" unless content.start_with?('{') && content.end_with?('}')
Operator.to_sparql(content, **options)
else
operands.last.to_sparql(project: vars, **options)
end
end
end # Project
end # Operator
end; end # SPARQL::Algebra