diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aaedb94..9a760e16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: ruby: - 2.6 - 2.7 - - 3.0 + - "3.0" - 3.1 - ruby-head - jruby diff --git a/README.md b/README.md index 799045af..6df83b8f 100755 --- a/README.md +++ b/README.md @@ -281,6 +281,8 @@ a full set of RDF formats. ### Parsing a SSE to SPARQL query or update string to SPARQL + # Note: if the SSE uses extension functions, they either must be XSD casting functions, or custom functions which are registered extensions. (See [SPARQL Extension Functions](#sparql-extension-functions)) + query = SPARQL::Algebra.parse(%{(bgp (triple ?s ?p ?o))}) sparql = query.to_sparql #=> "SELECT * WHERE { ?s ?p ?o }" diff --git a/VERSION b/VERSION index 944880fa..e4604e3a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.2.1 diff --git a/examples/function_call.sse b/examples/function_call.sse new file mode 100644 index 00000000..5b10ff8a --- /dev/null +++ b/examples/function_call.sse @@ -0,0 +1,6 @@ +(prefix ((: ) + (rdf: ) + (xsd: )) + (project (?a ?v ?boolean) + (extend ((?boolean (xsd:boolean ?v))) + (bgp (triple ?a :p ?v))))) \ No newline at end of file diff --git a/lib/sparql/algebra.rb b/lib/sparql/algebra.rb index 8deea09b..a2636915 100644 --- a/lib/sparql/algebra.rb +++ b/lib/sparql/algebra.rb @@ -19,6 +19,24 @@ module SPARQL # # {RDF::Query} and {RDF::Query::Pattern} are used as primitives for `bgp` and `triple` expressions. # + # # Background + # + # The SPARQL Algebra, and the S-Expressions used to represent it, are based on those of [Jena](https://jena.apache.org/documentation/notes/sse.html). Generally, an S-Expression generated by this Gem can be used as an SSE input to Jena, or an SSE output from Jena can also be used as input to this Gem. + # + # S-Expressions generally follow a standardized nesting resulting from parsing the original SPARQL Grammar. The individual operators map to SPARQL Grammar productions, and in most cases, the SPARQL Grammar can be reproduced by turning the S-Expression back into SPARQL (see {SPARQL::Algebra::Operator#to_sparql}). The order of operations will typically be as follows: + # + # * {SPARQL::Algebra::Operator::Base} + # * {SPARQL::Algebra::Operator::Prefix} + # * {SPARQL::Algebra::Operator::Slice} + # * {SPARQL::Algebra::Operator::Distinct} + # * {SPARQL::Algebra::Operator::Reduced} + # * {SPARQL::Algebra::Operator::Project} + # * {SPARQL::Algebra::Operator::Order} + # * {SPARQL::Algebra::Operator::Filter} + # * {SPARQL::Algebra::Operator::Extend} + # * {SPARQL::Algebra::Operator::Group} + # * {SPARQL::Algebra::Query} (many classes implement Query) + # # # Queries # # require 'sparql/algebra' @@ -262,6 +280,7 @@ module SPARQL # * {SPARQL::Algebra::Operator::Extend} # * {SPARQL::Algebra::Operator::Filter} # * {SPARQL::Algebra::Operator::Floor} + # * {SPARQL::Algebra::Operator::FunctionCall} # * {SPARQL::Algebra::Operator::Graph} # * {SPARQL::Algebra::Operator::GreaterThan} # * {SPARQL::Algebra::Operator::GreaterThanOrEqual} @@ -339,11 +358,9 @@ module SPARQL # * {SPARQL::Algebra::Operator::With} # * {SPARQL::Algebra::Operator::Year} # - # TODO - # ==== - # * Operator#optimize needs to be completed and tested. # # @see http://www.w3.org/TR/sparql11-query/#sparqlAlgebra + # @see https://jena.apache.org/documentation/notes/sse.html module Algebra include RDF diff --git a/lib/sparql/algebra/expression.rb b/lib/sparql/algebra/expression.rb index 7e7a73b1..3aec2313 100644 --- a/lib/sparql/algebra/expression.rb +++ b/lib/sparql/algebra/expression.rb @@ -66,9 +66,11 @@ def self.open(filename, **options, &block) # # @param [Array] sse # a SPARQL S-Expression (SSE) form + # @param [Hash{Symbol => Object}] options + # any additional options (see {Operator#initialize}) # @return [Expression] - def self.for(*sse) - self.new(sse) + def self.for(*sse, **options) + self.new(sse, **options) end class << self; alias_method :[], :for; end @@ -86,6 +88,13 @@ def self.new(sse, **options) raise ArgumentError, "invalid SPARQL::Algebra::Expression form: #{sse.inspect}" unless sse.is_a?(Array) operator = Operator.for(sse.first, sse.length - 1) + + # If we don't find an operator, and sse.first is an extension IRI, use a function call + if !operator && sse.first.is_a?(RDF::URI) && self.extension?(sse.first) + operator = Operator.for(:function_call, sse.length) + sse.unshift(:function_call) + end + unless operator return case sse.first when Array @@ -115,11 +124,16 @@ def self.new(sse, **options) end debug(options) {"#{operator.inspect}(#{operands.map(&:inspect).join(',')})"} + logger = options[:logger] options.delete_if {|k, v| [:debug, :logger, :depth, :prefixes, :base_uri, :update, :validate].include?(k) } begin operator.new(*operands, **options) rescue ArgumentError => e - error(options) {"Operator=#{operator.inspect}: #{e}"} + if logger + logger.error("Operator=#{operator.inspect}: #{e}") + else + raise "Operator=#{operator.inspect}: #{e}" + end end end @@ -163,6 +177,17 @@ def self.extensions @extensions ||= {} end + ## + # Is an extension function available? + # + # It's either a registered extension, or an XSD casting function + # + # @param [RDF::URI] function + # @return [Boolean] + def self.extension?(function) + function.to_s.start_with?(RDF::XSD.to_s) || self.extensions[function] + end + ## # Invoke an extension function. # diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 6b8cce51..0c94d008 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -70,28 +70,12 @@ def to_sxp_bin # Returns a partial SPARQL grammar for this array. # # @param [String] delimiter (" ") + # If the first element is an IRI, treat it as an extension function # @return [String] - def to_sparql(delimiter: " ", **options) + def to_sparql(delimiter: " ", **options) map {|e| e.to_sparql(**options)}.join(delimiter) end - ## - # Evaluates the array using the given variable `bindings`. - # - # In this case, the Array has two elements, the first of which is - # an XSD datatype, and the second is the expression to be evaluated. - # The result is cast as a literal of the appropriate type - # - # @param [RDF::Query::Solution] bindings - # a query solution containing zero or more variable bindings - # @param [Hash{Symbol => Object}] options ({}) - # options passed from query - # @return [RDF::Term] - # @see SPARQL::Algebra::Expression.evaluate - def evaluate(bindings, **options) - SPARQL::Algebra::Expression.extension(*self.map {|o| o.evaluate(bindings, **options)}) - end - ## # If `#execute` is invoked, it implies that a non-implemented Algebra operator # is being invoked @@ -302,7 +286,6 @@ def to_sparql(**options) end end # RDF::Term - # Override RDF::Queryable to execute against SPARQL::Algebra::Query elements as well as RDF::Query and RDF::Pattern module RDF::Queryable alias_method :query_without_sparql, :query @@ -390,15 +373,17 @@ def to_sxp(prefixes: nil, base_uri: nil) # @param [Boolean] as_statement (false) serialize as < ... >, otherwise TRIPLE(...) # @return [String] def to_sparql(as_statement: false, **options) - return "TRIPLE(#{to_triple.to_sparql(as_statement: true, **options)})" unless as_statement - - to_triple.map do |term| - if term.is_a?(::RDF::Statement) - "<<" + term.to_sparql(**options) + ">>" - else - term.to_sparql(**options) - end - end.join(" ") + " ." + if as_statement + to_triple.map do |term| + if term.is_a?(::RDF::Statement) + "<<" + term.to_sparql(as_statement: true, **options) + ">>" + else + term.to_sparql(**options) + end + end.join(" ") + else + "TRIPLE(#{to_triple.to_sparql(as_statement: true, **options)})" + end end ## @@ -448,15 +433,34 @@ def to_sxp_bin ## # - # Returns a partial SPARQL grammar for this term. + # Returns a partial SPARQL grammar for this query. # # @param [Boolean] top_level (true) # Treat this as a top-level, generating SELECT ... WHERE {} + # @param [Array] filter_ops ([]) + # Filter Operations # @return [String] - def to_sparql(top_level: true, **options) - str = @patterns.map { |e| e.to_sparql(as_statement: true, top_level: false, **options) }.join("\n") + def to_sparql(top_level: true, filter_ops: [], **options) + str = @patterns.map { |e| e.to_sparql(as_statement: true, top_level: false, **options) }.join(". \n") str = "GRAPH #{graph_name.to_sparql(**options)} {\n#{str}\n}\n" if graph_name - top_level ? SPARQL::Algebra::Operator.to_sparql(str, **options) : str + if top_level + SPARQL::Algebra::Operator.to_sparql(str, filter_ops: filter_ops, **options) + else + # Filters + filter_ops.each do |op| + str << "\nFILTER (#{op.to_sparql(**options)}) ." + end + + # Extensons + extensions = options.fetch(:extensions, []) + extensions.each do |as, expression| + v = expression.to_sparql(as_statement: true, **options) + v = "<< #{v} >>" if expression.is_a?(RDF::Statement) + str << "\nBIND (" << v << " AS " << as.to_sparql(**options) << ") ." + end + str = "{#{str}}" unless filter_ops.empty? && extensions.empty? + str + end end ## diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index 15e9f374..7fd4983a 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -106,6 +106,7 @@ class Operator autoload :Coalesce, 'sparql/algebra/operator/coalesce' autoload :Desc, 'sparql/algebra/operator/desc' autoload :Exprlist, 'sparql/algebra/operator/exprlist' + autoload :FunctionCall, 'sparql/algebra/operator/function_call' autoload :GroupConcat, 'sparql/algebra/operator/group_concat' autoload :In, 'sparql/algebra/operator/in' autoload :NotIn, 'sparql/algebra/operator/notin' @@ -254,6 +255,7 @@ def self.for(name, arity = nil) when :asc then Asc when :desc then Desc when :exprlist then Exprlist + when :function_call then FunctionCall # Datasets when :dataset then Dataset @@ -336,31 +338,41 @@ def self.arity # Generate a top-level Grammar, using collected options # # @param [String] content + # @param [Operator] datasets ([]) + # @param [Operator] distinct (false) # @param [Hash{Symbol => Operator}] extensions # Variable bindings - # @param [Operator] distinct (false) # @param [Array] filter_ops ([]) # Filter Operations # @param [Integer] limit (nil) # @param [Array] group_ops ([]) + # @param [Array] having_ops ([]) # @param [Integer] offset (nil) # @param [Array] order_ops ([]) # Order Operations # @param [Array] project (%i(*)) # Terms to project # @param [Operator] reduced (false) + # @param [Operator] values_clause (nil) + # Top-level Values clause + # @param [Operator] where_clause (true) + # Emit 'WHERE' before GroupGraphPattern # @param [Hash{Symbol => Object}] options # @return [String] def self.to_sparql(content, + datasets: [], distinct: false, extensions: {}, filter_ops: [], group_ops: [], + having_ops: [], limit: nil, offset: nil, order_ops: [], project: %i(*), reduced: false, + values_clause: nil, + where_clause: true, **options) str = "" @@ -372,28 +384,41 @@ def self.to_sparql(content, str << project.map do |p| if expr = extensions.delete(p) + v = expr.to_sparql(as_statement: true, **options) + v = "<< #{v} >>" if expr.is_a?(RDF::Statement) + pp = p.to_sparql(**options) # Replace projected variables with their extension, if any - "(" + [expr, :AS, p].to_sparql(**options) + ")" + '(' + v + ' AS ' + pp + ')' else p.to_sparql(**options) end end.join(" ") + "\n" end - # Extensions + # DatasetClause + datasets.each do |ds| + str << "FROM #{ds.to_sparql(**options)}\n" + end + + # Bind extensions.each do |as, expression| - content << "\nBIND (#{expression.to_sparql(**options)} AS #{as.to_sparql(**options)}) ." + v = expression.to_sparql(as_statement: true, **options) + v = "<< #{v} >>" if expression.is_a?(RDF::Statement) + content << "\nBIND (" << v << " AS " << as.to_sparql(**options) << ") ." end - # Filters + # Filter filter_ops.each do |f| - content << "\nFILTER #{f.to_sparql(**options)} ." + content << "\nFILTER (#{f.to_sparql(**options)}) ." end - # Where clause - str << "WHERE {\n#{content}\n}\n" + # WhereClause / GroupGraphPattern + str << (where_clause ? "WHERE {\n#{content}\n}\n" : "{\n#{content}\n}\n") - # Group + ## + # SolutionModifier + # + # GroupClause unless group_ops.empty? ops = group_ops.map do |o| # Replace projected variables with their extension, if any @@ -404,14 +429,22 @@ def self.to_sparql(content, str << "GROUP BY #{ops.join(' ')}\n" end - # Order + # HavingClause + unless having_ops.empty? + str << "HAVING #{having_ops.to_sparql(**options)}" + end + + # OrderClause unless order_ops.empty? str << "ORDER BY #{order_ops.to_sparql(**options)}\n" end - # Offset and Limmit + # LimitOffsetClauses str << "OFFSET #{offset}\n" unless offset.nil? str << "LIMIT #{limit}\n" unless limit.nil? + + # Values Clause + str << values_clause.to_sparql(top_level: false, **options) if values_clause str end @@ -640,12 +673,8 @@ def optimize!(**options) # @return [SPARQL::Algebra::Expression] `self` def rewrite(&block) @operands = @operands.map do |op| - # Rewrite the operand - unless new_op = block.call(op) - # Not re-written, rewrite - new_op = op.respond_to?(:rewrite) ? op.rewrite(&block) : op - end - new_op + new_op = block.call(op) + new_op.respond_to?(:rewrite) ? new_op.rewrite(&block) : new_op end self end @@ -671,7 +700,6 @@ def to_sxp(prefixes: nil, base_uri: nil) end ## - # # Returns a partial SPARQL grammar for the operator. # # @return [String] diff --git a/lib/sparql/algebra/operator/abs.rb b/lib/sparql/algebra/operator/abs.rb index 60e2096e..c0401c56 100644 --- a/lib/sparql/algebra/operator/abs.rb +++ b/lib/sparql/algebra/operator/abs.rb @@ -5,7 +5,7 @@ class Operator # # [121] BuiltInCall ::= ... | 'ABS' '(' Expression ')' # - # @example SPARQL Query + # @example SPARQL Grammar # PREFIX : # SELECT * WHERE { # ?s :num ?num diff --git a/lib/sparql/algebra/operator/alt.rb b/lib/sparql/algebra/operator/alt.rb index 8f839809..8ea82176 100644 --- a/lib/sparql/algebra/operator/alt.rb +++ b/lib/sparql/algebra/operator/alt.rb @@ -79,7 +79,7 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - "#{operands.first.to_sparql(**options)}|#{operands.last.to_sparql(**options)}" + "(#{operands.first.to_sparql(**options)}|#{operands.last.to_sparql(**options)})" end end # Alt end # Operator diff --git a/lib/sparql/algebra/operator/avg.rb b/lib/sparql/algebra/operator/avg.rb index 774aa6d9..f57bc59e 100644 --- a/lib/sparql/algebra/operator/avg.rb +++ b/lib/sparql/algebra/operator/avg.rb @@ -54,7 +54,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "AVG(#{operands.to_sparql(**options)})" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + "AVG(#{'DISTINCT ' if distinct}#{args.to_sparql(**options)})" end end # Avg end # Operator diff --git a/lib/sparql/algebra/operator/bgp.rb b/lib/sparql/algebra/operator/bgp.rb index 1e3fe183..6d1704f8 100644 --- a/lib/sparql/algebra/operator/bgp.rb +++ b/lib/sparql/algebra/operator/bgp.rb @@ -7,12 +7,20 @@ class Operator # # @example SPARQL Grammar # PREFIX : - # SELECT * { :s :p :o } + # SELECT * { ?s ?p ?o } # # @example SSE # (prefix ((: )) # (bgp (triple ?s ?p ?o))) # + # @example SPARQL Grammar (sparql-star) + # PREFIX : + # SELECT * {<< :a :b :c >> :p1 :o1.} + # + # @example SSE (sparql-star) + # (prefix ((: )) + # (bgp (triple (triple :a :b :c) :p1 :o1))) + # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class BGP < Operator NAME = [:bgp] diff --git a/lib/sparql/algebra/operator/clear.rb b/lib/sparql/algebra/operator/clear.rb index ffda3ced..684989d3 100644 --- a/lib/sparql/algebra/operator/clear.rb +++ b/lib/sparql/algebra/operator/clear.rb @@ -8,12 +8,18 @@ class Operator # # [32] Clear ::= 'CLEAR' 'SILENT'? GraphRefAll # - # @example SPARQL Grammar + # @example SPARQL Grammar (SILENT DEFAULT) # CLEAR SILENT DEFAULT # - # @example SSE + # @example SSE (SILENT DEFAULT) # (update (clear silent default)) # + # @example SPARQL Grammar (IRI) + # CLEAR GRAPH + # + # @example SSE (IRI) + # (update (clear )) + # # @see https://www.w3.org/TR/sparql11-update/#clear class Clear < Operator include SPARQL::Algebra::Update @@ -70,7 +76,11 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - "CLEAR " + operands.to_sparql(**options) + silent = operands.first == :silent + str = "CLEAR " + str << "SILENT " if operands.first == :silent + str << "GRAPH " if operands.last.is_a?(RDF::URI) + str << operands.last.to_sparql(**options) end end # Clear end # Operator diff --git a/lib/sparql/algebra/operator/construct.rb b/lib/sparql/algebra/operator/construct.rb index 0b9845b2..c1278e75 100644 --- a/lib/sparql/algebra/operator/construct.rb +++ b/lib/sparql/algebra/operator/construct.rb @@ -99,7 +99,7 @@ def query_yields_statements? # @return [String] def to_sparql(**options) str = "CONSTRUCT {\n" + - operands[0].map { |e| e.to_sparql(as_statement: true, top_level: false, **options) }.join("\n") + + operands[0].map { |e| e.to_sparql(as_statement: true, top_level: false, **options) }.join(". \n") + "\n}\n" str << operands[1].to_sparql(top_level: true, project: nil, **options) diff --git a/lib/sparql/algebra/operator/count.rb b/lib/sparql/algebra/operator/count.rb index f7efcbeb..ef976399 100644 --- a/lib/sparql/algebra/operator/count.rb +++ b/lib/sparql/algebra/operator/count.rb @@ -11,11 +11,39 @@ class Operator # WHERE { ?S ?P ?O } # # @example SSE - # (prefix ((: )) - # (project (?C) - # (extend ((?C ??.0)) - # (group () ((??.0 (count ?O))) - # (bgp (triple ?S ?P ?O)))))) + # (prefix ((: )) + # (project (?C) + # (extend ((?C ??.0)) + # (group () ((??.0 (count ?O))) + # (bgp (triple ?S ?P ?O)))))) + # + # @example SPARQL Grammar (count(*)) + # PREFIX : + # + # SELECT (COUNT(*) AS ?C) + # WHERE { ?S ?P ?O } + # + # @example SSE (count(*)) + # (prefix + # ((: )) + # (project (?C) + # (extend ((?C ??.0)) + # (group () ((??.0 (count))) + # (bgp (triple ?S ?P ?O)))))) + # + # @example SPARQL Grammar (count(distinct *)) + # PREFIX : + # + # SELECT (COUNT(DISTINCT *) AS ?C) + # WHERE { ?S ?P ?O } + # + # @example SSE (count(distinct *)) + # (prefix + # ((: )) + # (project (?C) + # (extend ((?C ??.0)) + # (group () ((??.0 (count distinct))) + # (bgp (triple ?S ?P ?O)))))) # # @see https://www.w3.org/TR/sparql11-query/#defn_aggCount class Count < Operator @@ -39,7 +67,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "COUNT(#{operands.to_sparql(**options)})" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + "COUNT(#{'DISTINCT ' if distinct}#{args.empty? ? '*' : args.to_sparql(**options)})" end end # Count end # Operator diff --git a/lib/sparql/algebra/operator/create.rb b/lib/sparql/algebra/operator/create.rb index 7c69c9c7..1ef956f2 100644 --- a/lib/sparql/algebra/operator/create.rb +++ b/lib/sparql/algebra/operator/create.rb @@ -55,10 +55,11 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - *args, last = operands.dup - args += [:GRAPH, last] - - "CREATE " + args.to_sparql(**options) + silent = operands.first == :silent + str = "CREATE " + str << "SILENT " if operands.first == :silent + str << "GRAPH " if operands.last.is_a?(RDF::URI) + str << operands.last.to_sparql(**options) end end # Create end # Operator diff --git a/lib/sparql/algebra/operator/dataset.rb b/lib/sparql/algebra/operator/dataset.rb index 61d00bfd..cbcd31ef 100644 --- a/lib/sparql/algebra/operator/dataset.rb +++ b/lib/sparql/algebra/operator/dataset.rb @@ -73,7 +73,7 @@ class Operator # @example Dataset with two default data sources # # (prefix ((: )) - # (dataset ( ) || (= ?g )) # (graph ?g (bgp (triple ?s ?p ?o)))))) # - # @example Dataset with multiple named graphs + # + # @example SPARQL Grammar + # BASE + # PREFIX : + # + # SELECT * + # FROM + # { ?s ?p ?o } + # + # @example SSE + # (base + # (prefix ((: )) + # (dataset () + # (bgp (triple ?s ?p ?o))))) + # # @see https://www.w3.org/TR/sparql11-query/#specifyingDataset class Dataset < Binary include Query @@ -163,17 +177,11 @@ def execute(queryable, **options, &base) # # Returns a partial SPARQL grammar for this operator. # + # Extracts datasets + # # @return [String] def to_sparql(**options) - operands[0].each_with_object('') do |graph, str| - str << if graph.is_a?(Array) - "FROM #{graph[0].upcase} #{graph[1].to_sparql(**options)}\n" - else - "FROM #{graph.to_sparql(**options)}\n" - end - end.tap do |str| - str << operands[1].to_sparql(**options) - end + operands.last.to_sparql(datasets: operands.first, **options) end end # Dataset end # Operator diff --git a/lib/sparql/algebra/operator/delete.rb b/lib/sparql/algebra/operator/delete.rb index 228dbf37..bc65d76b 100644 --- a/lib/sparql/algebra/operator/delete.rb +++ b/lib/sparql/algebra/operator/delete.rb @@ -75,7 +75,9 @@ def execute(queryable, solutions: nil, **options) # # @return [String] def to_sparql(**options) - "DELETE {\n" + operands.first.to_sparql(as_statement: true, **options) + "\n}" + "DELETE {\n" + + operands.first.to_sparql(as_statement: true, delimiter: " .\n", **options) + + "\n}" end end # Delete end # Operator diff --git a/lib/sparql/algebra/operator/delete_data.rb b/lib/sparql/algebra/operator/delete_data.rb index 1511b80e..5794457d 100644 --- a/lib/sparql/algebra/operator/delete_data.rb +++ b/lib/sparql/algebra/operator/delete_data.rb @@ -54,7 +54,7 @@ def execute(queryable, **options) # @return [String] def to_sparql(**options) "DELETE DATA {\n" + - operands.first.to_sparql(as_statement: true, top_level: false, delimiter: "\n", **options) + + operands.first.to_sparql(as_statement: true, top_level: false, delimiter: ". \n", **options) + "\n}" end end # DeleteData diff --git a/lib/sparql/algebra/operator/delete_where.rb b/lib/sparql/algebra/operator/delete_where.rb index 83245769..26eec944 100644 --- a/lib/sparql/algebra/operator/delete_where.rb +++ b/lib/sparql/algebra/operator/delete_where.rb @@ -71,7 +71,7 @@ def execute(queryable, **options) # @return [String] def to_sparql(**options) "DELETE WHERE {\n" + - operands.first.to_sparql(as_statement: true, top_level: false, delimiter: "\n", **options) + + operands.first.to_sparql(as_statement: true, top_level: false, delimiter: ". \n", **options) + "\n}" end end # DeleteWhere diff --git a/lib/sparql/algebra/operator/distinct.rb b/lib/sparql/algebra/operator/distinct.rb index da99fbd2..3742ac9b 100644 --- a/lib/sparql/algebra/operator/distinct.rb +++ b/lib/sparql/algebra/operator/distinct.rb @@ -12,8 +12,8 @@ class Operator # WHERE { ?x ?p ?v } # # @example SSE - # (prefix ((xsd: ) - # (: )) + # (prefix ((: ) + # (xsd: )) # (distinct # (project (?v) # (bgp (triple ?x ?p ?v))))) diff --git a/lib/sparql/algebra/operator/divide.rb b/lib/sparql/algebra/operator/divide.rb index 370da362..a09d1cbf 100644 --- a/lib/sparql/algebra/operator/divide.rb +++ b/lib/sparql/algebra/operator/divide.rb @@ -60,7 +60,7 @@ def apply(left, right, **options) # # @return [String] def to_sparql(**options) - "#{operands.first.to_sparql(**options)} / #{operands.last.to_sparql(**options)}" + "(#{operands.first.to_sparql(**options)} / #{operands.last.to_sparql(**options)})" end end # Divide end # Operator diff --git a/lib/sparql/algebra/operator/drop.rb b/lib/sparql/algebra/operator/drop.rb index e610322a..a3ebc4a9 100644 --- a/lib/sparql/algebra/operator/drop.rb +++ b/lib/sparql/algebra/operator/drop.rb @@ -10,12 +10,18 @@ class Operator # # [33] Drop ::= 'DROP' 'SILENT'? GraphRefAll # - # @example SPARQL Grammar - # DROP DEFAULT + # @example SPARQL Grammar (SILENT DEFAULT) + # DROP SILENT DEFAULT # - # @example SSE + # @example SSE (SILENT DEFAULT) # (update - # (drop default)) + # (drop silent default)) + # + # @example SPARQL Grammar (IRI) + # DROP GRAPH + # + # @example SSE (IRI) + # (update (drop )) # # @see https://www.w3.org/TR/sparql11-update/#drop class Drop < Operator @@ -40,7 +46,6 @@ class Drop < Operator def execute(queryable, **options) debug(options) {"Drop"} silent = operands.first == :silent - silent = operands.first == :silent operands.shift if silent raise ArgumentError, "drop expected operand to be 'default', 'named', 'all', or an IRI" unless operands.length == 1 @@ -74,7 +79,11 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - "DROP " + operands.to_sparql(**options) + silent = operands.first == :silent + str = "DROP " + str << "SILENT " if operands.first == :silent + str << "GRAPH " if operands.last.is_a?(RDF::URI) + str << operands.last.to_sparql(**options) end end # Drop end # Operator diff --git a/lib/sparql/algebra/operator/encode_for_uri.rb b/lib/sparql/algebra/operator/encode_for_uri.rb index 23e382e3..b6fb4f84 100644 --- a/lib/sparql/algebra/operator/encode_for_uri.rb +++ b/lib/sparql/algebra/operator/encode_for_uri.rb @@ -9,15 +9,13 @@ class Operator # # @example SPARQL Grammar # PREFIX : - # PREFIX xsd: # SELECT ?s ?str (ENCODE_FOR_URI(?str) AS ?encoded) WHERE { # ?s :str ?str # } # # @example SSE - # (prefix - # ((: )) - # (project (?str ?encoded) + # (prefix ((: )) + # (project (?s ?str ?encoded) # (extend ((?encoded (encode_for_uri ?str))) # (bgp (triple ?s :str ?str))))) # diff --git a/lib/sparql/algebra/operator/exprlist.rb b/lib/sparql/algebra/operator/exprlist.rb index 3964c24a..0554ef95 100644 --- a/lib/sparql/algebra/operator/exprlist.rb +++ b/lib/sparql/algebra/operator/exprlist.rb @@ -8,6 +8,8 @@ class Operator # [72] ExpressionList ::= NIL | '(' Expression ( ',' Expression )* ')' # # @example SPARQL Grammar + # PREFIX : + # # SELECT ?v ?w # { # FILTER (?v = 2) @@ -17,7 +19,7 @@ class Operator # } # # @example SSE - # (prefix ((: )) + # (prefix ((: )) # (project (?v ?w) # (filter (exprlist (= ?v 2) (= ?w 3)) # (bgp diff --git a/lib/sparql/algebra/operator/extend.rb b/lib/sparql/algebra/operator/extend.rb index 9dd564f7..8d81976e 100644 --- a/lib/sparql/algebra/operator/extend.rb +++ b/lib/sparql/algebra/operator/extend.rb @@ -10,14 +10,47 @@ class Operator # @example SPARQL Grammar # SELECT ?z # { - # ?x ?o - # BIND(?o+1 AS ?z) + # ?x ?o + # BIND(?o+10 AS ?z) # } # # @example SSE # (project (?z) # (extend ((?z (+ ?o 10))) - # (bgp (triple ?s ?o)))) + # (bgp (triple ?x ?o)))) + # + # @example SPARQL Grammar (cast as boolean) + # PREFIX : + # PREFIX rdf: + # PREFIX xsd: + # SELECT ?a ?v (xsd:boolean(?v) AS ?boolean) + # WHERE { ?a :p ?v . } + # + # @example SSE (cast as boolean) + # (prefix ((: ) + # (rdf: ) + # (xsd: )) + # (project (?a ?v ?boolean) + # (extend ((?boolean (xsd:boolean ?v))) + # (bgp (triple ?a :p ?v))))) + # + # @example SPARQL Grammar (inner bind) + # PREFIX : + # + # SELECT ?z ?s1 + # { + # ?s ?p ?o . + # BIND(?o+1 AS ?z) + # ?s1 ?p1 ?z + # } + # + # @example SSE (inner bind) + # (prefix ((: )) + # (project (?z ?s1) + # (join + # (extend ((?z (+ ?o 1))) + # (bgp (triple ?s ?p ?o))) + # (bgp (triple ?s1 ?p1 ?z))))) # # @see https://www.w3.org/TR/sparql11-query/#evaluation class Extend < Operator::Binary diff --git a/lib/sparql/algebra/operator/filter.rb b/lib/sparql/algebra/operator/filter.rb index c7493135..26e1179e 100644 --- a/lib/sparql/algebra/operator/filter.rb +++ b/lib/sparql/algebra/operator/filter.rb @@ -89,7 +89,7 @@ def validate! # @return [String] def to_sparql(**options) filter_ops = operands.first.is_a?(Operator::Exprlist) ? operands.first.operands : [operands.first] - operands.last.to_sparql(filter_ops: filter_ops, **options) + str = operands.last.to_sparql(filter_ops: filter_ops, **options) end end # Filter end # Operator diff --git a/lib/sparql/algebra/operator/function_call.rb b/lib/sparql/algebra/operator/function_call.rb new file mode 100644 index 00000000..e1a7b013 --- /dev/null +++ b/lib/sparql/algebra/operator/function_call.rb @@ -0,0 +1,64 @@ + +module SPARQL; module Algebra + class Operator + ## + # The SPARQL `function_call` operator. + # + # [70] FunctionCall ::= iri ArgList + # + # @example SPARQL Grammar + # PREFIX xsd: + # SELECT * + # WHERE { ?s ?p ?o . FILTER xsd:integer(?o) } + # + # @example SSE + # (prefix + # ((xsd: )) + # (filter (xsd:integer ?o) + # (bgp (triple ?s ?p ?o)))) + # + # @see https://www.w3.org/TR/sparql11-query/#funcex-regex + # @see https://www.w3.org/TR/xpath-functions/#func-matches + class FunctionCall < Operator + include Evaluatable + + NAME = :function_call + + ## + # Invokes the function with the passed arguments. + # + # @param [RDF::IRI] iri + # Identifies the function + # @param [Array] args + # @return [RDF::Term] + def apply(iri, *args, **options) + args = RDF.nil == args.last ? args[0..-2] : args + SPARQL::Algebra::Expression.extension(iri, *args, **options) + end + + ## + # Returns the SPARQL S-Expression (SSE) representation of this expression. + # + # Remove the optional argument. + # + # @return [Array] `self` + # @see https://openjena.org/wiki/SSE + def to_sxp_bin + @operands.map(&:to_sxp_bin) + end + + ## + # + # Returns a partial SPARQL grammar for this operator. + # + # @return [String] + def to_sparql(**options) + iri, args = operands + iri.to_sparql(**options) + + '(' + + args.to_sparql(delimiter: ', ', **options) + + ')' + end + end # FunctionCall + end # Operator +end; end # SPARQL::Algebra diff --git a/lib/sparql/algebra/operator/graph.rb b/lib/sparql/algebra/operator/graph.rb index 3bb31ddd..1f700cfb 100644 --- a/lib/sparql/algebra/operator/graph.rb +++ b/lib/sparql/algebra/operator/graph.rb @@ -7,7 +7,7 @@ class Operator # # [58] GraphGraphPattern ::= 'GRAPH' VarOrIri GroupGraphPattern # - # @example SPARQL Grammar + # @example SPARQL Grammar (query) # PREFIX : # SELECT * { # GRAPH ?g { ?s ?p ?o } @@ -18,15 +18,50 @@ class Operator # (graph ?g # (bgp (triple ?s ?p ?o)))) # - # @example of a query + # @example SPARQL Grammar (named set of statements) + # PREFIX : + # SELECT * { + # GRAPH :g { :s :p :o } + # } + # + # @example SSE (named set of statements) # (prefix ((: )) + # (graph :g + # (bgp (triple :s :p :o)))) + # + # @example SPARQL Grammar (syntax-graph-05.rq) + # PREFIX : + # SELECT * + # WHERE + # { + # :x :p :z + # GRAPH ?g { :x :b ?a . GRAPH ?g2 { :x :p ?x } } + # } + # + # @example SSE (syntax-graph-05.rq) + # (prefix ((: )) + # (join + # (bgp (triple :x :p :z)) # (graph ?g - # (bgp (triple ?s ?p ?o)))) + # (join + # (bgp (triple :x :b ?a)) + # (graph ?g2 + # (bgp (triple :x :p ?x))))))) # - # @example named set of statements - # (prefix ((: )) - # (graph :g - # ((triple :s :p :o)))) + # @example SPARQL Grammar (pp06.rq) + # prefix ex: + # prefix in: + # + # select ?x where { + # graph ?g {in:a ex:p1/ex:p2 ?x} + # } + # + # @example SSE (syntax-graph-05.rq) + # (prefix ((ex: ) + # (in: )) + # (project (?x) + # (graph ?g + # (path in:a (seq ex:p1 ex:p2) ?x)))) # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Graph < Operator::Binary @@ -89,6 +124,21 @@ def execute(queryable, **options, &block) def rewrite(&block) self end + + ## + # + # Returns a partial SPARQL grammar for this operator. + # + # @param [Boolean] top_level (true) + # Treat this as a top-level, generating SELECT ... WHERE {} + # @return [String] + def to_sparql(top_level: true, **options) + query = operands.last.to_sparql(top_level: false, **options) + # Paths don't automatically get braces. + query = "{\n#{query}\n}" unless query.start_with?('{') + str = "GRAPH #{operands.first.to_sparql(**options)} " + query + top_level ? Operator.to_sparql(str, **options) : str + end end # Graph end # Operator end; end # SPARQL::Algebra diff --git a/lib/sparql/algebra/operator/group.rb b/lib/sparql/algebra/operator/group.rb index de834c59..15222055 100644 --- a/lib/sparql/algebra/operator/group.rb +++ b/lib/sparql/algebra/operator/group.rb @@ -25,6 +25,37 @@ class Operator # (group (?P) ((??.0 (count ?O))) # (bgp (triple ?S ?P ?O)))))) # + # @example SPARQL Grammar (HAVING aggregate) + # PREFIX : + # SELECT ?s (AVG(?o) AS ?avg) + # WHERE { ?s ?p ?o } + # GROUP BY ?s + # HAVING (AVG(?o) <= 2.0) + # + # @example SSE (HAVING aggregate) + # (prefix ((: )) + # (project (?s ?avg) + # (filter (<= ??.0 2.0) + # (extend ((?avg ??.0)) + # (group (?s) ((??.0 (avg ?o))) + # (bgp (triple ?s ?p ?o)))))) ) + # + # @example SPARQL Grammar (non-triveal filters) + # PREFIX : + # SELECT ?g (AVG(?p) AS ?avg) ((MIN(?p) + MAX(?p)) / 2 AS ?c) + # WHERE { ?g :p ?p . } + # GROUP BY ?g + # + # @example SSE (non-triveal filters) + # (prefix ((: )) + # (project (?g ?avg ?c) + # (extend ((?avg ??.0) (?c (/ (+ ??.1 ??.2) 2))) + # (group (?g) + # ((??.0 (avg ?p)) + # (??.1 (min ?p)) + # (??.2 (max ?p))) + # (bgp (triple ?g :p ?p)))) )) + # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Group < Operator include Query @@ -137,16 +168,56 @@ def validate! # # @param [Hash{Symbol => Operator}] extensions # Variable bindings + # @param [Array] filter_ops ([]) + # Filter Operations # @return [String] - def to_sparql(extensions: {}, **options) + def to_sparql(extensions: {}, filter_ops: [], **options) + having_ops = [] if operands.length > 2 + temp_bindings = operands[1].inject({}) {|memo, (var, op)| memo.merge(var => op)} # Replace extensions from temporary bindings - operands[1].each do |var, op| - ext_var = extensions.invert.fetch(var) - extensions[ext_var] = op + temp_bindings.each do |var, op| + # Update extensions using a temporarily bound variable with its binding + extensions = extensions.inject({}) do |memo, (ext_var, ext_op)| + if ext_op.is_a?(Operator) + # Try to recursivley replace variable within operator + new_op = ext_op.deep_dup.rewrite do |operand| + if operand.is_a?(Variable) && operand.to_sym == var.to_sym + op.dup + else + operand + end + end + memo.merge(ext_var => new_op) + elsif ext_op.is_a?(Variable) && ext_op.to_sym == var.to_sym + memo.merge(ext_var => op) + else + # Doesn't match this variable, so don't change + memo.merge(ext_var => ext_op) + end + end + + # Filter ops using temporary bindinds are used for HAVING clauses + filter_ops.each do |fop| + having_ops << fop if fop.descendants.include?(var) && !having_ops.include?(fop) + end + end + + # If used in a HAVING clause, it's not also a filter + filter_ops -= having_ops + + # Replace each operand in having using var with it's corresponding operation + having_ops = having_ops.map do |op| + op.dup.rewrite do |operand| + # Rewrite based on temporary bindings + temp_bindings.fetch(operand, operand) + end end end - operands.last.to_sparql(extensions: extensions, group_ops: operands.first, **options) + operands.last.to_sparql(extensions: extensions, + group_ops: operands.first, + having_ops: having_ops, + **options) end end # Group end # Operator diff --git a/lib/sparql/algebra/operator/group_concat.rb b/lib/sparql/algebra/operator/group_concat.rb index 4fc35088..27e1f1ca 100644 --- a/lib/sparql/algebra/operator/group_concat.rb +++ b/lib/sparql/algebra/operator/group_concat.rb @@ -16,6 +16,24 @@ class Operator # (group () ((??.0 (group_concat ?x))) # (bgp)))) # + # @example SPARQL Grammar (DISTINCT) + # SELECT (GROUP_CONCAT(DISTINCT ?x) AS ?y) {} + # + # @example SSE (DISTINCT) + # (project (?y) + # (extend ((?y ??.0)) + # (group () ((??.0 (group_concat distinct ?x))) + # (bgp)))) + # + # @example SPARQL Grammar (SEPARATOR) + # SELECT (GROUP_CONCAT(?x; SEPARATOR=';') AS ?y) {} + # + # @example SSE (SEPARATOR) + # (project (?y) + # (extend ((?y ??.0)) + # (group () ((??.0 (group_concat (separator ";") ?x))) + # (bgp)))) + # # @see https://www.w3.org/TR/sparql11-query/#defn_aggGroupConcat class GroupConcat < Operator include Aggregate @@ -63,7 +81,13 @@ def apply(enum, separator, **options) # # @return [String] def to_sparql(**options) - "GROUP_CONCAT(#{operands.to_sparql(delimiter: ', ', **options)})" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + separator = args.first.last if args.first.is_a?(Array) && args.first.first == :separator + args = args[1..-1] if separator + str = "GROUP_CONCAT(#{'DISTINCT ' if distinct}#{args.to_sparql(delimiter: ', ', **options)}" + str << "; SEPARATOR=#{separator.to_sparql}" if separator + str << ")" end end # GroupConcat end # Operator diff --git a/lib/sparql/algebra/operator/if.rb b/lib/sparql/algebra/operator/if.rb index eae31f27..9cb9568b 100644 --- a/lib/sparql/algebra/operator/if.rb +++ b/lib/sparql/algebra/operator/if.rb @@ -48,15 +48,15 @@ def evaluate(bindings, **options) rescue raise TypeError end - end # If - ## - # - # Returns a partial SPARQL grammar for this operator. - # - # @return [String] - def to_sparql(**options) - "IF(" + operands.to_sparql(delimiter: ', ', **options) + ")" - end - end # If + ## + # + # Returns a partial SPARQL grammar for this operator. + # + # @return [String] + def to_sparql(**options) + "IF(" + operands.to_sparql(delimiter: ', ', **options) + ")" + end + end # If + end # Operator end; end # SPARQL::Algebra diff --git a/lib/sparql/algebra/operator/insert.rb b/lib/sparql/algebra/operator/insert.rb index 7a096006..f9eb4ecb 100644 --- a/lib/sparql/algebra/operator/insert.rb +++ b/lib/sparql/algebra/operator/insert.rb @@ -69,7 +69,9 @@ def execute(queryable, solutions: nil, **options) # # @return [String] def to_sparql(**options) - "INSERT {\n" + operands.first.to_sparql(as_statement: true, **options) + "\n}" + "INSERT {\n" + + operands.first.to_sparql(as_statement: true, delimiter: " .\n", **options) + + "\n}" end end # Insert end # Operator diff --git a/lib/sparql/algebra/operator/insert_data.rb b/lib/sparql/algebra/operator/insert_data.rb index f4e66f15..86137965 100644 --- a/lib/sparql/algebra/operator/insert_data.rb +++ b/lib/sparql/algebra/operator/insert_data.rb @@ -54,7 +54,7 @@ def execute(queryable, **options) # @return [String] def to_sparql(**options) "INSERT DATA {\n" + - operands.first.to_sparql(as_statement: true, top_level: false, delimiter: "\n", **options) + + operands.first.to_sparql(as_statement: true, top_level: false, delimiter: ". \n", **options) + "\n}" end end # InsertData diff --git a/lib/sparql/algebra/operator/is_blank.rb b/lib/sparql/algebra/operator/is_blank.rb index a833a927..b0045d6c 100644 --- a/lib/sparql/algebra/operator/is_blank.rb +++ b/lib/sparql/algebra/operator/is_blank.rb @@ -13,8 +13,7 @@ class Operator # } # # @example SSE - # (prefix ((xsd: ) - # (: )) + # (prefix ((: )) # (project (?x ?v) # (filter (isBlank ?v) # (bgp (triple ?x :p ?v))))) diff --git a/lib/sparql/algebra/operator/is_iri.rb b/lib/sparql/algebra/operator/is_iri.rb index 8d49b16d..e4c3ed60 100644 --- a/lib/sparql/algebra/operator/is_iri.rb +++ b/lib/sparql/algebra/operator/is_iri.rb @@ -13,8 +13,7 @@ class Operator # } # # @example SSE - # (prefix ((xsd: ) - # (: )) + # (prefix ((: )) # (project (?x ?v) # (filter (isIRI ?v) # (bgp (triple ?x :p ?v))))) diff --git a/lib/sparql/algebra/operator/is_literal.rb b/lib/sparql/algebra/operator/is_literal.rb index fdd23447..0f16332a 100644 --- a/lib/sparql/algebra/operator/is_literal.rb +++ b/lib/sparql/algebra/operator/is_literal.rb @@ -13,8 +13,7 @@ class Operator # } # # @example SSE - # (prefix ((xsd: ) - # (: )) + # (prefix ((: )) # (project (?x ?v) # (filter (isLiteral ?v) # (bgp (triple ?x :p ?v))))) diff --git a/lib/sparql/algebra/operator/is_numeric.rb b/lib/sparql/algebra/operator/is_numeric.rb index b43076b0..ec3b6382 100644 --- a/lib/sparql/algebra/operator/is_numeric.rb +++ b/lib/sparql/algebra/operator/is_numeric.rb @@ -15,8 +15,7 @@ class Operator # } # # @example SSE - # (prefix ((xsd: ) - # (: )) + # (prefix ((: )) # (project (?x ?v) # (filter (isNumeric ?v) # (bgp (triple ?x :p ?v))))) diff --git a/lib/sparql/algebra/operator/join.rb b/lib/sparql/algebra/operator/join.rb index 2f2edd2c..dab09d5e 100644 --- a/lib/sparql/algebra/operator/join.rb +++ b/lib/sparql/algebra/operator/join.rb @@ -19,6 +19,22 @@ class Operator # (graph ?g # (bgp (triple ?s ?q ?v))))) # + # @example SPARQL Grammar (inline filter) + # PREFIX : + # ASK { + # :who :homepage ?homepage + # FILTER REGEX(?homepage, "^http://example.org/") + # :who :schoolHomepage ?schoolPage + # } + # + # @example SSE (inline filter) + # (prefix ((: )) + # (ask + # (filter (regex ?homepage "^http://example.org/") + # (join + # (bgp (triple :who :homepage ?homepage)) + # (bgp (triple :who :schoolHomepage ?schoolPage)))))) + # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Join < Operator::Binary include Query @@ -98,10 +114,28 @@ def optimize!(**options) # # @param [Boolean] top_level (true) # Treat this as a top-level, generating SELECT ... WHERE {} + # @param [Hash{Symbol => Operator}] extensions + # Variable bindings + # @param [Array] filter_ops ([]) + # Filter Operations # @return [String] - def to_sparql(top_level: true, **options) - str = operands.to_sparql(top_level: false, delimiter: "\n", **options) - top_level ? Operator.to_sparql(str, **options) : str + def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) + # If this is top-level, and the last operand is a Table (values), put the values at the outer-level + str = "{\n" + operands.first.to_sparql(top_level: false, extensions: {}, **options) + + # Any accrued filters go here. + filter_ops.each do |op| + str << "\nFILTER (#{op.to_sparql(**options)}) ." + end + + if top_level && operands.last.is_a?(Table) + str << "\n}" + options = options.merge(values_clause: operands.last) + else + str << "\n{\n" + operands.last.to_sparql(top_level: false, extensions: {}, **options) + "\n}\n}" + end + + top_level ? Operator.to_sparql(str, extensions: extensions, **options) : str end end # Join end # Operator diff --git a/lib/sparql/algebra/operator/lcase.rb b/lib/sparql/algebra/operator/lcase.rb index 125cfea2..b25f0e67 100644 --- a/lib/sparql/algebra/operator/lcase.rb +++ b/lib/sparql/algebra/operator/lcase.rb @@ -12,9 +12,8 @@ class Operator # } # # @example SSE - # (prefix - # ((: )) - # (project (?str ?lstr) + # (prefix ((: )) + # (project (?s ?lstr) # (extend ((?lstr (lcase ?str))) # (bgp (triple ?s :str ?str))))) # diff --git a/lib/sparql/algebra/operator/left_join.rb b/lib/sparql/algebra/operator/left_join.rb index c3eae45a..7dc20592 100644 --- a/lib/sparql/algebra/operator/left_join.rb +++ b/lib/sparql/algebra/operator/left_join.rb @@ -131,14 +131,27 @@ def optimize!(**options) # # @param [Boolean] top_level (true) # Treat this as a top-level, generating SELECT ... WHERE {} + # @param [Hash{Symbol => Operator}] extensions + # Variable bindings + # @param [Array] filter_ops ([]) + # Filter Operations # @return [String] - def to_sparql(top_level: true, **options) - str = operands[0].to_sparql(top_level: false, **options) + - "\nOPTIONAL { \n" + - operands[1].to_sparql(top_level: false, **options) + "\n" - str << 'FILTER (' + operands[2].to_sparql(**options) + ") \n" if operands[2] - str << '}' - top_level ? Operator.to_sparql(str, **options) : str + def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) + str = "{\n" + operands[0].to_sparql(top_level: false, extensions: {}, **options) + str << + "\nOPTIONAL {\n" + + operands[1].to_sparql(top_level: false, extensions: {}, **options) + case operands[2] + when SPARQL::Algebra::Operator::Exprlist + operands[2].operands.each do |op| + str << "\nFILTER (" + op.to_sparql(**options) + ")" + end + when nil + else + str << "\nFILTER (" + operands[2].to_sparql(**options) + ")" + end + str << "\n}}" + top_level ? Operator.to_sparql(str, filter_ops: filter_ops, extensions: extensions, **options) : str end end # LeftJoin end # Operator diff --git a/lib/sparql/algebra/operator/max.rb b/lib/sparql/algebra/operator/max.rb index 62ca8add..52fb3902 100644 --- a/lib/sparql/algebra/operator/max.rb +++ b/lib/sparql/algebra/operator/max.rb @@ -56,7 +56,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "MAX(" + operands.to_sparql(**options) + ")" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + "MAX(#{'DISTINCT ' if distinct}#{args.to_sparql(**options)})" end end # Max end # Operator diff --git a/lib/sparql/algebra/operator/min.rb b/lib/sparql/algebra/operator/min.rb index 20cc6169..48ab4c13 100644 --- a/lib/sparql/algebra/operator/min.rb +++ b/lib/sparql/algebra/operator/min.rb @@ -15,7 +15,7 @@ class Operator # (project (?min) # (extend ((?min ??.0)) # (group () ((??.0 (min ?o))) - # (bgp (triple ?s ?p ?o)))))) + # (bgp (triple ?s :dec ?o)))))) # # @see https://www.w3.org/TR/sparql11-query/#defn_aggMin class Min < Operator @@ -56,7 +56,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "MIN(" + operands.to_sparql(**options) + ")" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + "MIN(#{'DISTINCT ' if distinct}#{args.to_sparql(**options)})" end end # Min end # Operator diff --git a/lib/sparql/algebra/operator/minus.rb b/lib/sparql/algebra/operator/minus.rb index a858416a..ce48c453 100644 --- a/lib/sparql/algebra/operator/minus.rb +++ b/lib/sparql/algebra/operator/minus.rb @@ -14,6 +14,32 @@ class Operator # (triple ?s ?p ?o)) # (bgp (triple ?s ?q ?v))) # + # @example SPARQL Grammar (inline filter) + # PREFIX : + # SELECT (?s1 AS ?subset) (?s2 AS ?superset) + # WHERE { + # ?s2 a :Set . + # ?s1 a :Set . + # FILTER(?s1 != ?s2) + # MINUS { + # ?s1 a :Set . + # ?s2 a :Set . + # FILTER(?s1 != ?s2) + # } + # } + # + # @example SSE (inline filter) + # (prefix ((: )) + # (project (?subset ?superset) + # (extend ((?subset ?s1) (?superset ?s2)) + # (filter (!= ?s1 ?s2) + # (minus + # (bgp (triple ?s2 a :Set) (triple ?s1 a :Set)) + # (filter (!= ?s1 ?s2) + # (bgp + # (triple ?s1 a :Set) + # (triple ?s2 a :Set)))))))) + # # @see https://www.w3.org/TR/xpath-functions/#func-numeric-unary-minus # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Minus < Operator::Binary @@ -74,15 +100,29 @@ def optimize!(**options) # # Returns a partial SPARQL grammar for this operator. # + # @param [Hash{Symbol => Operator}] extensions + # Variable bindings + # @param [Array] filter_ops ([]) + # Filter Operations # @param [Boolean] top_level (true) # Treat this as a top-level, generating SELECT ... WHERE {} # @return [String] - def to_sparql(top_level: true, **options) - str = operands.first.to_sparql(top_level: false, **options) + "\n" - str << "MINUS {\n" - str << operands.last.to_sparql(top_level: false, **options) - str << "\n}" - top_level ? Operator.to_sparql(str, **options) : str + def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) + lhs, *rhs = operands + str = "{\n" + lhs.to_sparql(top_level: false, extensions: {}, **options) + + # Any accrued filters go here. + filter_ops.each do |op| + str << "\nFILTER (#{op.to_sparql(**options)}) ." + end + + rhs.each do |minus| + str << "\nMINUS {\n" + str << minus.to_sparql(top_level: false, extensions: {}, **options) + str << "\n}" + end + str << "}" + top_level ? Operator.to_sparql(str, extensions: extensions, **options) : str end end # Minus end # Operator diff --git a/lib/sparql/algebra/operator/multiply.rb b/lib/sparql/algebra/operator/multiply.rb index 77daba5b..03e15ca9 100644 --- a/lib/sparql/algebra/operator/multiply.rb +++ b/lib/sparql/algebra/operator/multiply.rb @@ -50,7 +50,7 @@ def apply(left, right, **options) # # @return [String] def to_sparql(**options) - "#{operands.first.to_sparql(**options)} * #{operands.last.to_sparql(**options)}" + "(#{operands.first.to_sparql(**options)} * #{operands.last.to_sparql(**options)})" end end # Multiply end # Operator diff --git a/lib/sparql/algebra/operator/notoneof.rb b/lib/sparql/algebra/operator/notoneof.rb index bdddc1b5..c397c11c 100644 --- a/lib/sparql/algebra/operator/notoneof.rb +++ b/lib/sparql/algebra/operator/notoneof.rb @@ -8,13 +8,13 @@ class Operator # @example SPARQL Grammar # PREFIX ex: # PREFIX in: - # ASK { in:b ^ex:p in:a } + # ASK { in:a !(ex:p1|ex:p2) ?x } # # @example SSE # (prefix ((ex: ) - # (in: )) + # (in: )) # (ask - # (path in:b (reverse ex:p) in:a))) + # (path in:a (notoneof ex:p1 ex:p2) ?x))) # # @see https://www.w3.org/TR/sparql11-query/#eval_negatedPropertySet class NotOneOf < Operator @@ -56,6 +56,15 @@ def execute(queryable, **options, &block) block.call(solution) end end + + ## + # + # Returns a partial SPARQL grammar for this operator. + # + # @return [String] + def to_sparql(**options) + "!(" + operands.to_sparql(delimiter: ' | ', **options) + ')' + end end # NotOneOf end # Operator end; end # SPARQL::Algebra diff --git a/lib/sparql/algebra/operator/order.rb b/lib/sparql/algebra/operator/order.rb index 4df4861d..a0a8bf85 100644 --- a/lib/sparql/algebra/operator/order.rb +++ b/lib/sparql/algebra/operator/order.rb @@ -17,6 +17,50 @@ class Operator # (order ((asc ?name)) # (bgp (triple ?x foaf:name ?name))))) # + # @example SPARQL Grammar (with builtin) + # PREFIX : + # SELECT ?s WHERE { + # ?s :p ?o . + # } + # ORDER BY str(?o) + # + # @example SSE (with builtin) + # (prefix ((: )) + # (project (?s) + # (order ((str ?o)) + # (bgp (triple ?s :p ?o))))) + # + # @example SPARQL Grammar (with bracketed expression) + # PREFIX : + # SELECT ?s WHERE { + # ?s :p ?o1 ; :q ?o2 . + # } ORDER BY (?o1 + ?o2) + # + # @example SSE (with bracketed expression) + # (prefix + # ((: )) + # (project (?s) + # (order ((+ ?o1 ?o2)) + # (bgp + # (triple ?s :p ?o1) + # (triple ?s :q ?o2))))) + # + # @example SPARQL Grammar (with function call) + # PREFIX : + # PREFIX xsd: + # SELECT * + # { ?s ?p ?o } + # ORDER BY + # DESC(?o+57) xsd:string(?o) ASC(?s) + # + # @example SSE (with function call) + # (prefix ((: ) + # (xsd: )) + # (order ((desc (+ ?o 57)) + # (xsd:string ?o) + # (asc ?s)) + # (bgp (triple ?s ?p ?o)))) + # # @see https://www.w3.org/TR/sparql11-query/#modOrderBy class Order < Operator::Binary include Query diff --git a/lib/sparql/algebra/operator/plus.rb b/lib/sparql/algebra/operator/plus.rb index 31c31ecc..5f4e975b 100644 --- a/lib/sparql/algebra/operator/plus.rb +++ b/lib/sparql/algebra/operator/plus.rb @@ -69,7 +69,7 @@ def apply(left, right = nil, **options) # # @return [String] def to_sparql(**options) - "#{operands.first.to_sparql(**options)} + #{operands.last.to_sparql(**options)}" + "(#{operands.first.to_sparql(**options)} + #{operands.last.to_sparql(**options)})" end end # Plus end # Operator diff --git a/lib/sparql/algebra/operator/project.rb b/lib/sparql/algebra/operator/project.rb index 60913997..902cb0e7 100644 --- a/lib/sparql/algebra/operator/project.rb +++ b/lib/sparql/algebra/operator/project.rb @@ -20,20 +20,37 @@ class Operator # (filter (= ?v 2) # (bgp (triple ?s :p ?v))))) # - # ## Sub select - # - # @example SPARQL Grammar + # @example SPARQL Grammar (Sub select) # SELECT (1 AS ?X ) { # SELECT (2 AS ?Y ) {} # } # - # @example SSE + # @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 @@ -77,6 +94,7 @@ def to_sparql(**options) # 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) diff --git a/lib/sparql/algebra/operator/reduced.rb b/lib/sparql/algebra/operator/reduced.rb index ae9daa6c..27590403 100644 --- a/lib/sparql/algebra/operator/reduced.rb +++ b/lib/sparql/algebra/operator/reduced.rb @@ -8,12 +8,12 @@ class Operator # @example SPARQL Grammar # PREFIX : # PREFIX xsd: - # SELECT DISTINCT ?v + # SELECT REDUCED ?v # WHERE { ?x ?p ?v } # # @example SSE - # (prefix ((xsd: ) - # (: )) + # (prefix ((: ) + # (xsd: )) # (reduced # (project (?v) # (bgp (triple ?x ?p ?v))))) diff --git a/lib/sparql/algebra/operator/regex.rb b/lib/sparql/algebra/operator/regex.rb index 4be5b901..ddd3fe30 100644 --- a/lib/sparql/algebra/operator/regex.rb +++ b/lib/sparql/algebra/operator/regex.rb @@ -6,8 +6,8 @@ class Operator # [122] RegexExpression ::= 'REGEX' '(' Expression ',' Expression ( ',' Expression )? ')' # # @example SPARQL Grammar - # PREFIX rdf: # PREFIX ex: + # PREFIX rdf: # SELECT ?val # WHERE { # ex:foo rdf:value ?val . diff --git a/lib/sparql/algebra/operator/reverse.rb b/lib/sparql/algebra/operator/reverse.rb index 430ae987..c40881bc 100644 --- a/lib/sparql/algebra/operator/reverse.rb +++ b/lib/sparql/algebra/operator/reverse.rb @@ -15,6 +15,17 @@ class Operator # (in: )) # (ask (path in:b (reverse ex:p) in:a))) # + # @example SPARQL Grammar + # prefix ex: + # prefix in: + # + # select * where { in:c ^(ex:p1/ex:p2) ?x } + # + # @example SSE + # (prefix ((ex: ) + # (in: )) + # (path in:c (reverse (seq ex:p1 ex:p2)) ?x)) + # # @see https://www.w3.org/TR/sparql11-query/#defn_evalPP_inverse class Reverse < Operator::Unary include Query @@ -65,7 +76,7 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - "^" + operands.first.to_sparql(**options) + "^(" + operands.first.to_sparql(**options) + ')' end end # Reverse end # Operator diff --git a/lib/sparql/algebra/operator/sample.rb b/lib/sparql/algebra/operator/sample.rb index e4e8900f..fcffe166 100644 --- a/lib/sparql/algebra/operator/sample.rb +++ b/lib/sparql/algebra/operator/sample.rb @@ -54,7 +54,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "SAMPLE(#{operands.to_sparql(**options)})" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + "SAMPLE(#{'DISTINCT ' if distinct}#{args.to_sparql(**options)})" end end # Sample end # Operator diff --git a/lib/sparql/algebra/operator/seq.rb b/lib/sparql/algebra/operator/seq.rb index e206e0b4..2c899fc1 100644 --- a/lib/sparql/algebra/operator/seq.rb +++ b/lib/sparql/algebra/operator/seq.rb @@ -83,7 +83,7 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - operands.to_sparql(delimiter: '/', **options) + '(' + operands.to_sparql(delimiter: '/', **options) + ')' end end # Seq end # Operator diff --git a/lib/sparql/algebra/operator/sequence.rb b/lib/sparql/algebra/operator/sequence.rb index 7e9c7c91..46efdd0b 100644 --- a/lib/sparql/algebra/operator/sequence.rb +++ b/lib/sparql/algebra/operator/sequence.rb @@ -4,8 +4,11 @@ class Operator ## # The SPARQL UPDATE `sequence` operator. # - # Sequences through each operand + # Sequences through each operand. # + # [103] CollectionPath ::= '(' GraphNodePath+ ')' + # + # @see https://www.w3.org/TR/sparql11-query/#collections class Sequence < Operator include SPARQL::Algebra::Update diff --git a/lib/sparql/algebra/operator/strlang.rb b/lib/sparql/algebra/operator/strlang.rb index 6162434e..1f5745c2 100644 --- a/lib/sparql/algebra/operator/strlang.rb +++ b/lib/sparql/algebra/operator/strlang.rb @@ -13,8 +13,7 @@ class Operator # } # # @example SSE - # (prefix - # ((: ) (xsd: )) + # (prefix ((: )) # (project (?s ?s2) # (extend ((?s2 (strlang ?str "en-US"))) # (filter (langMatches (lang ?str) "en") diff --git a/lib/sparql/algebra/operator/subtract.rb b/lib/sparql/algebra/operator/subtract.rb index e4309e7c..96622c97 100644 --- a/lib/sparql/algebra/operator/subtract.rb +++ b/lib/sparql/algebra/operator/subtract.rb @@ -51,7 +51,7 @@ def apply(left, right, **options) # # @return [String] def to_sparql(**options) - "#{operands.first.to_sparql(**options)} - #{operands.last.to_sparql(**options)}" + "(#{operands.first.to_sparql(**options)} - #{operands.last.to_sparql(**options)})" end end # Subtract end # Operator diff --git a/lib/sparql/algebra/operator/sum.rb b/lib/sparql/algebra/operator/sum.rb index 1222c4f5..ed957ae1 100644 --- a/lib/sparql/algebra/operator/sum.rb +++ b/lib/sparql/algebra/operator/sum.rb @@ -7,15 +7,15 @@ class Operator # # @example SPARQL Grammar # PREFIX : - # SELECT (SUM(?O) AS ?sum) + # SELECT (SUM(?o) AS ?sum) # WHERE { ?s :dec ?o } # # @example SSE - # (prefix ((: )) - # (project (?sum) - # (extend ((?sum ??.0)) - # (group () ((??.0 (sum ?o))) - # (bgp (triple ?s :dec ?o)))))) + # (prefix ((: )) + # (project (?sum) + # (extend ((?sum ??.0)) + # (group () ((??.0 (sum ?o))) + # (bgp (triple ?s :dec ?o)))))) # # @see https://www.w3.org/TR/sparql11-query/#defn_aggSum class Sum < Operator @@ -47,7 +47,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "SUM(" + operands.to_sparql(**options) + ")" + distinct = operands.first == :distinct + args = distinct ? operands[1..-1] : operands + "SUM(#{'DISTINCT ' if distinct}#{args.to_sparql(**options)})" end end # Sum end # Operator diff --git a/lib/sparql/algebra/operator/table.rb b/lib/sparql/algebra/operator/table.rb index 2924684f..013fea32 100644 --- a/lib/sparql/algebra/operator/table.rb +++ b/lib/sparql/algebra/operator/table.rb @@ -8,7 +8,7 @@ class Operator # # [28] ValuesClause ::= ( 'VALUES' DataBlock )? # - # @example SPARQL Grammar + # @example SPARQL Grammar (ValuesClause) # PREFIX dc: # PREFIX : # PREFIX ns: @@ -18,7 +18,7 @@ class Operator # } # VALUES ?book { :book1 } # - # @example SSE + # @example SSE (ValuesClause) # (prefix ((dc: ) # (: ) # (ns: )) @@ -27,8 +27,38 @@ class Operator # (bgp (triple ?book dc:title ?title) (triple ?book ns:price ?price)) # (table (vars ?book) (row (?book :book1)))) )) # + # @example SPARQL Grammar (empty query no values) + # SELECT * { } VALUES () { } + # + # @example SSE (empty query no values) + # (join (bgp) (table empty)) + # + # [61] InlineData ::= 'VALUES' DataBlock + # + # @example SPARQL Grammar (InlineData) + # PREFIX dc: + # PREFIX : + # PREFIX ns: + # + # SELECT ?book ?title ?price + # { + # VALUES ?book { :book1 } + # ?book dc:title ?title ; + # ns:price ?price . + # } + # + # @example SSE (InlineData) + # (prefix ((dc: ) + # (: ) + # (ns: )) + # (project (?book ?title ?price) + # (join + # (table (vars ?book) (row (?book :book1))) + # (bgp (triple ?book dc:title ?title) (triple ?book ns:price ?price))) )) + # # @example empty table # (table unit) + # # @see https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#inline-data class Table < Operator include Query @@ -67,22 +97,26 @@ def execute(queryable, **options, &block) # # Returns a partial SPARQL grammar for this operator. # + # @param [Boolean] top_level (true) + # Treat this as a top-level, generating SELECT ... WHERE {} # @return [String] - def to_sparql(**options) - str = "VALUES (#{operands.first[1..-1].map { |e| e.to_sparql(**options) }.join(' ')}) {\n" + def to_sparql(top_level: true, **options) + str = "VALUES (#{Array(operands.first)[1..-1].map { |e| e.to_sparql(**options) }.join(' ')}) {\n" operands[1..-1].each do |row| line = '(' row[1..-1].each do |col| - line << "#{col[1].to_sparql(**options)} " + v = col[1].to_sparql(as_statement: true, **options) + v = "<< #{v} >>" if col[1].is_a?(RDF::Statement) + line << v + ' ' end - line = line.chop + line = line.chomp(' ') line << ")\n" str << line end str << "}\n" - str + top_level ? Operator.to_sparql(str, **options) : str end end # Table end # Operator diff --git a/lib/sparql/algebra/operator/ucase.rb b/lib/sparql/algebra/operator/ucase.rb index a48d7269..6a974782 100644 --- a/lib/sparql/algebra/operator/ucase.rb +++ b/lib/sparql/algebra/operator/ucase.rb @@ -14,7 +14,7 @@ class Operator # @example SSE # (prefix # ((: )) - # (project (?str ?ustr) + # (project (?s ?ustr) # (extend ((?ustr (ucase ?str))) # (bgp (triple ?s :str ?str))))) # diff --git a/lib/sparql/algebra/operator/update.rb b/lib/sparql/algebra/operator/update.rb index a12e24e8..c0f28ef5 100644 --- a/lib/sparql/algebra/operator/update.rb +++ b/lib/sparql/algebra/operator/update.rb @@ -21,6 +21,27 @@ class Operator # (delete ((triple ?a foaf:knows ?b))) # (insert ((triple ?b foaf:knows ?a)))) )) # + # @example SPARQL Grammar (update multiple) + # PREFIX : + # PREFIX foaf: + # + # DELETE { ?a foaf:knows ?b . } + # WHERE { ?a foaf:knows ?b . } + # ; + # INSERT { ?b foaf:knows ?a . } + # WHERE { ?a foaf:knows ?b .} + # + # @example SSE (update multiple) + # (prefix ((: ) + # (foaf: )) + # (update + # (modify + # (bgp (triple ?a foaf:knows ?b)) + # (delete ((triple ?a foaf:knows ?b)))) + # (modify + # (bgp (triple ?a foaf:knows ?b)) + # (insert ((triple ?b foaf:knows ?a)))))) + # # @see https://www.w3.org/TR/sparql11-update/#graphUpdate class Update < Operator include SPARQL::Algebra::Update @@ -58,7 +79,7 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - str = operands.map { |e| e.to_sparql(**options) }.join("\n") + str = operands.map { |e| e.to_sparql(**options) }.join(";\n") end end # Update end # Operator diff --git a/lib/sparql/algebra/operator/using.rb b/lib/sparql/algebra/operator/using.rb index 84ce5462..f84e3dcd 100644 --- a/lib/sparql/algebra/operator/using.rb +++ b/lib/sparql/algebra/operator/using.rb @@ -28,6 +28,21 @@ class Operator # (bgp (triple :a foaf:knows ?s) (triple ?s ?p ?o))) # (delete ((triple ?s ?p ?o)))) )) # + # @example SPARQL Grammar (multiple clauses) + # PREFIX : + # + # INSERT { ?s ?p "q" } + # USING :g1 + # USING :g2 + # WHERE { ?s ?p ?o } + # + # @example SSE (multiple clauses) + # (prefix ((: )) + # (update + # (modify (using (:g1 :g2) + # (bgp (triple ?s ?p ?o))) + # (insert ((triple ?s ?p "q")))))) + # # @see https://www.w3.org/TR/sparql11-update/#add class Using < Operator include SPARQL::Algebra::Query @@ -61,7 +76,9 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - str = "USING #{operands.first.to_sparql(**options)}\n" + str = "\n" + operands.first.map do |op| + "USING #{op.to_sparql(**options)}\n" + end.join("") content = operands.last.to_sparql(top_level: false, **options) str << Operator.to_sparql(content, project: nil, **options) end diff --git a/lib/sparql/algebra/operator/with.rb b/lib/sparql/algebra/operator/with.rb index 9e51a1d2..04382351 100644 --- a/lib/sparql/algebra/operator/with.rb +++ b/lib/sparql/algebra/operator/with.rb @@ -85,7 +85,7 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - with, where, ops = operands + with, where, *ops = operands str = "WITH #{with.to_sparql(**options)}\n" # The content of the WHERE clause, may be USING diff --git a/lib/sparql/grammar/parser11.rb b/lib/sparql/grammar/parser11.rb index 93999a78..96a9d843 100644 --- a/lib/sparql/grammar/parser11.rb +++ b/lib/sparql/grammar/parser11.rb @@ -197,7 +197,7 @@ class Parser # [2] Query ::= Prologue # ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery ) production(:Query) do |input, data, callback| - query = data[:query].first + query = data[:query].first if data[:query] # Add prefix if data[:PrefixDecl] @@ -806,7 +806,7 @@ class Parser # [70] FunctionCall ::= iri ArgList production(:FunctionCall) do |input, data, callback| - add_prod_data(:Function, Array(data[:iri]) + data[:ArgList]) + add_prod_data(:Function, SPARQL::Algebra::Operator::FunctionCall.new(data[:iri], *data[:ArgList])) end # [71] ArgList ::= NIL @@ -1437,7 +1437,7 @@ class Parser production(:iriOrFunction) do |input, data, callback| if data.has_key?(:ArgList) # Function is (func arg1 arg2 ...) - add_prod_data(:Function, Array(data[:iri]) + data[:ArgList]) + add_prod_data(:Function, SPARQL::Algebra::Operator::FunctionCall.new(data[:iri], *data[:ArgList])) else input[:iri] = data[:iri] end diff --git a/sparql.gemspec b/sparql.gemspec index 38771f86..580f230e 100755 --- a/sparql.gemspec +++ b/sparql.gemspec @@ -22,12 +22,12 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.6' gem.requirements = [] - gem.add_runtime_dependency 'rdf', '~> 3.2' + gem.add_runtime_dependency 'rdf', '~> 3.2', '>= 3.2.3' gem.add_runtime_dependency 'rdf-aggregate-repo', '~> 3.2' gem.add_runtime_dependency 'ebnf', '~> 2.2' gem.add_runtime_dependency 'builder', '~> 3.2' gem.add_runtime_dependency 'logger', '~> 1.4' - gem.add_runtime_dependency 'sxp', '~> 1.2' + gem.add_runtime_dependency 'sxp', '~> 1.2', '>= 1.2.1' gem.add_runtime_dependency 'sparql-client', '~> 3.2' gem.add_runtime_dependency 'rdf-xsd', '~> 3.2' diff --git a/spec/algebra/expression_spec.rb b/spec/algebra/expression_spec.rb index 955c78c0..1f283a5f 100644 --- a/spec/algebra/expression_spec.rb +++ b/spec/algebra/expression_spec.rb @@ -10,124 +10,125 @@ { # String "(equal (xsd:string 'foo'^^xsd:string) xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::Literal.new("foo", datatype: RDF::XSD.string)]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::Literal.new("foo", datatype: RDF::XSD.string))), RDF::XSD.string), "(equal (xsd:string '1.0e10'^^xsd:double) xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::Literal.new(1.0e10)]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::Literal.new(1.0e10))), RDF::XSD.string), "(equal (xsd:string 'foo'^^xsd:integer) xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::Literal.new(1)]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::Literal.new(1))), RDF::XSD.string), "(equal (xsd:string '2011-02-20T00:00:00'^^xsd:dateTime) xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::Literal.new(DateTime.now)]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::Literal.new(DateTime.now))), RDF::XSD.string), "(equal (xsd:string 'foo'^^xsd:boolean) xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::Literal.new(true)]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::Literal.new(true))), RDF::XSD.string), "(equal (xsd:string ) xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::URI("foo")]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::URI("foo"))), RDF::XSD.string), "(equal (xsd:string 'foo') xsd:string)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.string, RDF::Literal.new("foo")]), RDF::XSD.string), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.string, RDF::Literal.new("foo"))), RDF::XSD.string), # Double "(equal (xsd:double '1.0e10'^^xsd:string) xsd:double)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new("1.0e10", datatype: RDF::XSD.string)]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new("1.0e10", datatype: RDF::XSD.string))), RDF::XSD.double), "(equal (xsd:double 'foo'^^xsd:string) xsd:double) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new("foo", datatype: RDF::XSD.string)]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new("foo", datatype: RDF::XSD.string))), RDF::XSD.double), "(equal (xsd:double '1.0e10'^^xsd:double) xsd:double)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new(1.0e10)]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new(1.0e10))), RDF::XSD.double), "(equal (xsd:double '1'^^xsd:integer) xsd:double)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new(1)]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new(1))), RDF::XSD.double), "(equal (xsd:double '2011-02-20T00:00:00'^^xsd:dateTime) xsd:double) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new(DateTime.now)]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new(DateTime.now))), RDF::XSD.double), "(equal (xsd:double 'foo'^^xsd:boolean) xsd:double)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new(true)]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new(true))), RDF::XSD.double), "(equal (xsd:double ) xsd:double) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::URI("foo")]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::URI("foo"))), RDF::XSD.double), "(equal (xsd:double '1') xsd:double)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new("1")]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new("1"))), RDF::XSD.double), "(equal (xsd:double 'foo') xsd:double) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.double, RDF::Literal.new("foo")]), RDF::XSD.double), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.double, RDF::Literal.new("foo"))), RDF::XSD.double), # Decimal "(equal (xsd:decimal '1.0'^^xsd:string) xsd:decimal)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new("1.0", datatype: RDF::XSD.string)]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new("1.0", datatype: RDF::XSD.string))), RDF::XSD.decimal), "(equal (xsd:decimal 'foo'^^xsd:string) xsd:decimal) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new("foo", datatype: RDF::XSD.string)]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new("foo", datatype: RDF::XSD.string))), RDF::XSD.decimal), "(equal (xsd:decimal '1.0e10'^^xsd:double) xsd:decimal)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal::Double.new("1.0e10")]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal::Double.new("1.0e10"))), RDF::XSD.decimal), "(equal (xsd:decimal '1'^^xsd:integer) xsd:decimal)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new(1)]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new(1))), RDF::XSD.decimal), "(equal (xsd:decimal '2011-02-20T00:00:00'^^xsd:dateTime) xsd:decimal) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new(DateTime.now)]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new(DateTime.now))), RDF::XSD.decimal), "(equal (xsd:decimal 'foo'^^xsd:boolean) xsd:decimal)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new(true)]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new(true))), RDF::XSD.decimal), "(equal (xsd:decimal ) xsd:decimal) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::URI("foo")]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::URI("foo"))), RDF::XSD.decimal), "(equal (xsd:decimal '1.0') xsd:decimal)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new("1.0")]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new("1.0"))), RDF::XSD.decimal), "(equal (xsd:decimal 'foo') xsd:decimal) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.decimal, RDF::Literal.new("foo")]), RDF::XSD.decimal), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.decimal, RDF::Literal.new("foo"))), RDF::XSD.decimal), # Integer "(equal (xsd:integer '1'^^xsd:string) xsd:integer)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new("1", datatype: RDF::XSD.string)]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new("1", datatype: RDF::XSD.string))), RDF::XSD.integer), "(equal (xsd:integer 'foo'^^xsd:string) xsd:integer) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new("foo", datatype: RDF::XSD.string)]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new("foo", datatype: RDF::XSD.string))), RDF::XSD.integer), "(equal (xsd:integer '1.0e10'^^xsd:double) xsd:integer)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal::Double.new("1.0e10")]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal::Double.new("1.0e10"))), RDF::XSD.integer), "(equal (xsd:integer '1'^^xsd:integer) xsd:integer)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new(1)]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new(1))), RDF::XSD.integer), "(equal (xsd:integer '2011-02-20T00:00:00'^^xsd:dateTime) xsd:integer) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new(DateTime.now)]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new(DateTime.now))), RDF::XSD.integer), "(equal (xsd:integer 'foo'^^xsd:boolean) xsd:integer)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new(true)]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new(true))), RDF::XSD.integer), "(equal (xsd:integer ) xsd:integer) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::URI("foo")]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::URI("foo"))), RDF::XSD.integer), "(equal (xsd:integer '1') xsd:integer)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new("1")]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new("1"))), RDF::XSD.integer), "(equal (xsd:integer 'foo') xsd:integer) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.integer, RDF::Literal.new("foo")]), RDF::XSD.integer), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.integer, RDF::Literal.new("foo"))), RDF::XSD.integer), # DateTime "(equal (xsd:dateTime '1'^^xsd:string) xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new("1", datatype: RDF::XSD.string)]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new("1", datatype: RDF::XSD.string))), RDF::XSD.dateTime), "(equal (xsd:dateTime 'foo'^^xsd:string) xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new("foo", datatype: RDF::XSD.string)]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new("foo", datatype: RDF::XSD.string))), RDF::XSD.dateTime), "(equal (xsd:dateTime '1.0e10'^^xsd:double) xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal::Double.new("1.0e10")]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal::Double.new("1.0e10"))), RDF::XSD.dateTime), "(equal (xsd:dateTime '2011-02-20T00:00:00'^^xsd:dateTime) xsd:dateTime)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new(DateTime.now)]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new(DateTime.now))), RDF::XSD.dateTime), "(equal (xsd:dateTime 'foo'^^xsd:boolean) xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new(true)]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new(true))), RDF::XSD.dateTime), "(equal (xsd:dateTime ) xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::URI("foo")]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::URI("foo"))), RDF::XSD.dateTime), "(equal (xsd:dateTime '1') xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new("1")]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new("1"))), RDF::XSD.dateTime), "(equal (xsd:dateTime 'foo') xsd:dateTime) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new("foo")]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new("foo"))), RDF::XSD.dateTime), "(equal (xsd:dateTime '2011-02-20T00:00:00'^^xsd:string) xsd:dateTime)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.dateTime, RDF::Literal.new("2011-02-20T00:00:00", datatype: RDF::XSD.string)]), RDF::XSD.dateTime), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.dateTime, RDF::Literal.new("2011-02-20T00:00:00", datatype: RDF::XSD.string))), RDF::XSD.dateTime), # Boolean "(equal (xsd:boolean '1'^^xsd:string) xsd:boolean)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new("1", datatype: RDF::XSD.string)]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new("1", datatype: RDF::XSD.string))), RDF::XSD.boolean), "(equal (xsd:boolean 'foo'^^xsd:string) xsd:boolean) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new("foo", datatype: RDF::XSD.string)]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new("foo", datatype: RDF::XSD.string))), RDF::XSD.boolean), "(equal (xsd:boolean '1.0e10'^^xsd:double) xsd:boolean)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal::Double.new("1.0e10")]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal::Double.new("1.0e10"))), RDF::XSD.boolean), "(equal (xsd:boolean '1'^^xsd:boolean) xsd:boolean)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new(1)]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new(1))), RDF::XSD.boolean), "(equal (xsd:boolean '2011-02-20T00:00:00'^^xsd:dateTime) xsd:boolean) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new(DateTime.now)]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new(DateTime.now))), RDF::XSD.boolean), "(equal (xsd:boolean 'foo'^^xsd:boolean) xsd:boolean)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new(true)]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new(true))), RDF::XSD.boolean), "(equal (xsd:boolean ) xsd:boolean) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::URI("foo")]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::URI("foo"))), RDF::XSD.boolean), "(equal (xsd:boolean '1') xsd:boolean)" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new("1")]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new("1"))), RDF::XSD.boolean), "(equal (xsd:boolean 'foo') xsd:boolean) raises TypeError" => - Operator::Equal.new(Operator::Datatype.new([RDF::XSD.boolean, RDF::Literal.new("foo")]), RDF::XSD.boolean), + Operator::Equal.new(Operator::Datatype.new(Operator::FunctionCall.new(RDF::XSD.boolean, RDF::Literal.new("foo"))), RDF::XSD.boolean), }.each do |spec, op| it spec do if spec =~ /raises/ expect { op.evaluate(RDF::Query::Solution.new) }.to raise_error(TypeError) else + op.evaluate(RDF::Query::Solution.new) expect(op.evaluate(RDF::Query::Solution.new)).to eq RDF::Literal::TRUE end end @@ -158,7 +159,8 @@ it "raises error unless function is registered" do expect { - [RDF::URI("func"), RDF::Literal("foo")].evaluate(RDF::Query::Solution.new) + Operator::FunctionCall.new(RDF::URI("func"), RDF::Literal("foo")). + evaluate(RDF::Query::Solution.new) }.to raise_error(TypeError) end @@ -168,7 +170,8 @@ did_yield = true expect(literal).to eq RDF::Literal("foo") end - [RDF::URI("func"), RDF::Literal("foo")].evaluate(RDF::Query::Solution.new) + Operator::FunctionCall.new(RDF::URI("func"), RDF::Literal("foo")). + evaluate(RDF::Query::Solution.new) expect(did_yield).to be_truthy end end @@ -191,11 +194,11 @@ }.each do |given, expected| if expected == TypeError it "raises TypeError given #{given.inspect}" do - expect {[RDF::XSD.dateTime, given].evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) + expect {Operator::FunctionCall.new(RDF::XSD.dateTime, given).evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) end else it "generates #{expected.inspect} given #{given.inspect}" do - expect([RDF::XSD.dateTime, given].evaluate(RDF::Query::Solution.new)).to eq expected + expect(Operator::FunctionCall.new(RDF::XSD.dateTime, given).evaluate(RDF::Query::Solution.new)).to eq expected end end end @@ -220,11 +223,11 @@ }.each do |given, expected| if expected == TypeError it "raises TypeError given #{given.inspect}" do - expect {[RDF::XSD.float, given].evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) + expect {Operator::FunctionCall.new(RDF::XSD.float, given).evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) end else it "generates #{expected.inspect} given #{given.inspect}" do - expect([RDF::XSD.float, given].evaluate(RDF::Query::Solution.new)).to eq expected + expect(Operator::FunctionCall.new(RDF::XSD.float, given).evaluate(RDF::Query::Solution.new)).to eq expected end end end @@ -249,11 +252,11 @@ }.each do |given, expected| if expected == TypeError it "raises TypeError given #{given.inspect}" do - expect {[RDF::XSD.double, given].evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) + expect {Operator::FunctionCall.new(RDF::XSD.double, given).evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) end else it "generates #{expected.inspect} given #{given.inspect}" do - expect([RDF::XSD.double, given].evaluate(RDF::Query::Solution.new)).to eq expected + expect(Operator::FunctionCall.new(RDF::XSD.double, given).evaluate(RDF::Query::Solution.new)).to eq expected end end end @@ -278,11 +281,11 @@ }.each do |given, expected| if expected == TypeError it "raises TypeError given #{given.inspect}" do - expect {[RDF::XSD.decimal, given].evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) + expect {Operator::FunctionCall.new(RDF::XSD.decimal, given).evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) end else it "generates #{expected.inspect} given #{given.inspect}" do - expect([RDF::XSD.decimal, given].evaluate(RDF::Query::Solution.new)).to eq expected + expect(Operator::FunctionCall.new(RDF::XSD.decimal, given).evaluate(RDF::Query::Solution.new)).to eq expected end end end @@ -307,11 +310,11 @@ }.each do |given, expected| if expected == TypeError it "raises TypeError given #{given.inspect}" do - expect {[RDF::XSD.integer, given].evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) + expect {Operator::FunctionCall.new(RDF::XSD.integer, given).evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) end else it "generates #{expected.inspect} given #{given.inspect}" do - expect([RDF::XSD.integer, given].evaluate(RDF::Query::Solution.new)).to eq expected + expect(Operator::FunctionCall.new(RDF::XSD.integer, given).evaluate(RDF::Query::Solution.new)).to eq expected end end end @@ -336,11 +339,11 @@ }.each do |given, expected| if expected == TypeError it "raises TypeError given #{given.inspect}" do - expect {[RDF::XSD.boolean, given].evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) + expect {Operator::FunctionCall.new(RDF::XSD.boolean, given).evaluate(RDF::Query::Solution.new)}.to raise_error(TypeError) end else it "generates #{expected.inspect} given #{given.inspect}" do - expect([RDF::XSD.boolean, given].evaluate(RDF::Query::Solution.new)).to eq expected + expect(Operator::FunctionCall.new(RDF::XSD.boolean, given).evaluate(RDF::Query::Solution.new)).to eq expected end end end diff --git a/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index 9fb7e5c3..76b09e3b 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -5,14 +5,19 @@ include SPARQL::Algebra -describe SPARQL::Algebra::Operator do - it "reproduces simple query" do - sxp = %{(prefix ((: )) - (bgp (triple :s :p :o)))} +shared_examples "SXP to SPARQL" do |name, sxp, **options| + it(name) do sse = SPARQL::Algebra.parse(sxp) sparql_result = sse.to_sparql - expect(sparql_result).to generate(sxp, resolve_iris: false, validate: true) + production = sparql_result.match?(/ASK|SELECT|CONSTRUCT|DESCRIBE/) ? :QueryUnit : :UpdateUnit + expect(sparql_result).to generate(sxp, resolve_iris: false, production: production, validate: true, **options) end +end + +describe SPARQL::Algebra::Operator do + it_behaves_like "SXP to SPARQL", "simple query", + %{(prefix ((: )) + (bgp (triple :s :p :o)))} context "Examples" do def self.read_examples @@ -20,12 +25,13 @@ def self.read_examples Dir.glob(File.expand_path("../../../lib/sparql/algebra/operator/*.rb", __FILE__)).each do |rb| op = File.basename(rb, ".rb") scanner = StringScanner.new(File.read(rb)) - while scanner.skip_until(/# @example SSE/) - ex = scanner.scan_until(/^\s+#\s*$/) - - # Trim off comment prefix - ex = ex.gsub(/^\s*#/, '') - (examples[op] ||= []) << ex + while scanner.skip_until(/# @example SPARQL Grammar(.*)$/) + ctx = scanner.matched.sub(/.*Grammar\s*/, '') + current = {} + current[:sparql] = scanner.scan_until(/# @example SSE.*$/).gsub(/^\s*#/, '').sub(/@example SSE.*$/, '') + current[:sxp] = scanner.scan_until(/^\s+#\s*$/).gsub(/^\s*#/, '') + current[:ctx] = ctx unless ctx.empty? + (examples[op] ||= []) << current end end examples @@ -33,18 +39,96 @@ def self.read_examples read_examples.each do |op, examples| describe "Operator #{op}:" do - examples.each do |sxp| - it(sxp) do - pending "not implemented yet" if %w( - - ).include?(op) - sse = SPARQL::Algebra.parse(sxp) - sparql_result = sse.to_sparql - production = sparql_result.match?(/ASK|SELECT|CONSTRUCT|DESCRIBE/) ? :QueryUnit : :UpdateUnit - expect(sparql_result).to generate(sxp, resolve_iris: false, production: production, validate: true) - end + examples.each do |example| + sxp, sparql, ctx = example[:sxp], example[:sparql], example[:ctx] + it_behaves_like "SXP to SPARQL", (ctx || ('sxp: ' + sxp)), sxp, logger: "Source:\n#{sparql}" end end end end + + context "Issues" do + it_behaves_like "SXP to SPARQL", "#39", + SPARQL.parse(%( + PREFIX obo: + + SELECT DISTINCT ?enst + FROM + WHERE { + + ?enst obo:SO_transcribed_from ?ensg . + } + LIMIT 10 + )).to_sxp + + # PREFIX obo: + # PREFIX taxon: + # PREFIX rdfs: + # PREFIX faldo: + # PREFIX dc: + # + # SELECT DISTINCT ?parent ?child ?child_label + # FROM + # WHERE { + # ?enst obo:SO_transcribed_from ?ensg . + # ?ensg a ?parent ; + # obo:RO_0002162 taxon:9606 ; + # faldo:location ?ensg_location ; + # dc:identifier ?child ; + # rdfs:label ?child_label . + # FILTER(CONTAINS(STR(?parent), "terms/ensembl/")) + # BIND(STRBEFORE(STRAFTER(STR(?ensg_location), "GRCh38/"), ":") AS ?chromosome) + # } + # VALUES ?chromosome { + # "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" + # "11" "12" "13" "14" "15" "16" "17" "18" "19" "20" "21" "22" + # "X" "Y" "MT" + # } + it_behaves_like "SXP to SPARQL", "#40", %{ + (prefix ((obo: ) + (taxon: ) + (rdfs: ) + (faldo: ) + (dc: )) + (dataset () + (distinct + (project (?parent ?child ?child_label) + (join + (filter (contains (str ?parent) "terms/ensembl/") + (extend ((?chromosome (strbefore (strafter (str ?ensg_location) "GRCh38/") ":"))) + (bgp + (triple ?enst obo:SO_transcribed_from ?ensg) + (triple ?ensg a ?parent) + (triple ?ensg obo:RO_0002162 taxon:9606) + (triple ?ensg faldo:location ?ensg_location) + (triple ?ensg dc:identifier ?child) + (triple ?ensg rdfs:label ?child_label)))) + (table (vars ?chromosome) + (row (?chromosome "1")) + (row (?chromosome "2")) + (row (?chromosome "3")) + (row (?chromosome "4")) + (row (?chromosome "5")) + (row (?chromosome "6")) + (row (?chromosome "7")) + (row (?chromosome "8")) + (row (?chromosome "9")) + (row (?chromosome "10")) + (row (?chromosome "11")) + (row (?chromosome "12")) + (row (?chromosome "13")) + (row (?chromosome "14")) + (row (?chromosome "15")) + (row (?chromosome "16")) + (row (?chromosome "17")) + (row (?chromosome "18")) + (row (?chromosome "19")) + (row (?chromosome "20")) + (row (?chromosome "21")) + (row (?chromosome "22")) + (row (?chromosome "X")) + (row (?chromosome "Y")) + (row (?chromosome "MT")))))))) + } + end end diff --git a/spec/grammar/examples_spec.rb b/spec/grammar/examples_spec.rb index 35fa3ff0..904966d4 100644 --- a/spec/grammar/examples_spec.rb +++ b/spec/grammar/examples_spec.rb @@ -39,5 +39,37 @@ def parse(query, **options) parser = SPARQL::Grammar::Parser.new(query) parser.parse(options[:update] ? :UpdateUnit: :QueryUnit) end + + context "Operator Examples" do + def self.read_operator_examples + examples = {} + Dir.glob(File.expand_path("../../../lib/sparql/algebra/operator/*.rb", __FILE__)).each do |rb| + op = File.basename(rb, ".rb") + scanner = StringScanner.new(File.read(rb)) + while scanner.skip_until(/# @example SPARQL Grammar(.*)$/) + current = {} + current[:sparql] = scanner.scan_until(/# @example SSE.*$/).gsub(/^\s*#/, '').sub(/@example SSE.*$/, '') + current[:sxp] = scanner.scan_until(/^\s+#\s*$/).gsub(/^\s*#/, '') + current[:prod] = current[:sxp].include?('(update') ? :UpdateUnit : :QueryUnit + (examples[op] ||= []) << current + end + end + examples + end + + read_operator_examples.each do |op, examples| + describe "Operator #{op}:" do + examples.each do |example| + sxp, sparql, production = example[:sxp], example[:sparql], example[:prod] + it(sparql) do + pending "not implemented yet" if %w( + + ).include?(op) + expect(sparql).to generate(sxp, resolve_iris: false, production: production, validate: true) + end + end + end + end + end end end \ No newline at end of file diff --git a/spec/grammar/parser_spec.rb b/spec/grammar/parser_spec.rb index cbbc47cc..b939e34f 100644 --- a/spec/grammar/parser_spec.rb +++ b/spec/grammar/parser_spec.rb @@ -13,10 +13,10 @@ def self.variable(id, distinguished = true) context "FunctionCall nonterminal" do { "('bar')" => [ - %q(("bar")), [RDF::URI("foo"), RDF::Literal("bar")] + %q(("bar")), SPARQL::Algebra::Expression[:function_call, RDF::URI("foo"), RDF::Literal("bar")] ], "()" => [ - %q(()), [RDF::URI("foo"), RDF["nil"]] + %q(()), SPARQL::Algebra::Expression[:function_call, RDF::URI("foo"), RDF["nil"]] ] }.each do |title, (input, output)| it title do |example| @@ -1750,7 +1750,7 @@ def self.variable(id, distinguished = true) %(FILTER REGEX ("foo", "bar")), [:filter, SPARQL::Algebra::Expression[:regex, RDF::Literal("foo"), RDF::Literal("bar")]] ], "" => [ - %(FILTER ("arg")), [:filter, [RDF::URI("fun"), RDF::Literal("arg")]] + %(FILTER ("arg")), [:filter, SPARQL::Algebra::Expression[:function_call, RDF::URI("fun"), RDF::Literal("arg")]] ], "bound" => [ %(FILTER BOUND (?e)), [:filter, SPARQL::Algebra::Expression[:bound, RDF::Query::Variable.new("e")]] diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 1ecf03b7..731c8392 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -11,21 +11,21 @@ case t.type when 'mf:QueryEvaluationTest' it "evaluates #{t.entry} - #{t.name}: #{t.comment}" do - case t.name - when 'Basic - Term 6', 'Basic - Term 7' + case t.entry + when 'term-6.rq', 'term-7.rq' skip "Decimal format changed in SPARQL 1.1" - when 'datatype-2 : Literals with a datatype' - skip "datatype now returns rdf:langString for language-tagged literals" - when /REDUCED/ + when 'reduced-1.rq', 'reduced-2.rq' skip "REDUCED equivalent to DISTINCT" - when 'Strings: Distinct', 'All: Distinct' + when 'distinct-1.rq' skip "More compact representation" + when 'q-datatype-2.rq' + skip "datatype now returns rdf:langString for language-tagged literals" when /sq03/ pending "Graph variable binding differences" - when /pp11|pp31/ + when 'pp11.rq', 'path-p2.rq' pending "Expects multiple equivalent property path solutions" - when 'date-1', /dawg-optional-filter-005-not-simplified/ - pending "Different results on unapproved tests" unless t.approved? + when 'date-1.rq', 'expr-5.rq' + pending "Different results on unapproved tests" unless t.name.include?('dawg-optional-filter-005-simplified') end t.logger = RDF::Spec.logger @@ -65,15 +65,11 @@ end when 'mf:PositiveSyntaxTest', 'mf:PositiveSyntaxTest11' it "positive syntax for #{t.entry} - #{t.name} - #{t.comment}" do - skip "Spurrious error on Ruby < 2.0" if t.name == 'syntax-bind-02.rq' - case t.name - when 'Basic - Term 7', 'syntax-lit-08.rq' + case t.entry + when 'term-7.rq', 'syntax-lit-08.rq' skip "Decimal format changed in SPARQL 1.1" when 'syntax-esc-04.rq', 'syntax-esc-05.rq' skip "PNAME_LN changed in SPARQL 1.1" - when 'dawg-optional-filter-005-simplified', 'dawg-optional-filter-005-not-simplified', - 'dataset-10' - pending 'New problem with different manifest processing?' end expect do SPARQL.parse(t.action.query_string, base_uri: t.base_uri, validate: true, logger: t.logger) @@ -81,11 +77,11 @@ end when 'mf:NegativeSyntaxTest', 'mf:NegativeSyntaxTest11' it "detects syntax error for #{t.entry} - #{t.name} - #{t.comment}" do - skip("Better Error Detection") if %w( + pending("Better Error Detection") if %w( agg08.rq agg09.rq agg10.rq agg11.rq agg12.rq syn-bad-pname-06.rq group06.rq group07.rq ).include?(t.entry) - skip("Better Error Detection") if %w( + pending("Better Error Detection") if %w( syn-bad-01.rq syn-bad-02.rq ).include?(t.entry) && man_name == 'syntax-query' expect do @@ -131,12 +127,90 @@ end end +shared_examples "to_sparql" do |id, label, comment, tests| + man_name = id.to_s.split("/")[-2] + describe [man_name, label, comment].compact.join(" - ") do + tests.each do |t| + next unless t.action + case t.type + when 'mf:QueryEvaluationTest', 'mf:PositiveSyntaxTest', 'mf:PositiveSyntaxTest11' + it "Round Trips #{t.entry} - #{t.name}: #{t.comment}" do + case t.entry + when 'syntax-expr-05.rq', 'syntax-order-05.rq', 'syntax-function-04.rq' + pending("Unregistered function calls") + when 'term-7.rq', 'syntax-lit-08.rq' + skip "Decimal format changed in SPARQL 1.1" + when 'syntax-esc-04.rq', 'syntax-esc-05.rq' + skip "PNAME_LN changed in SPARQL 1.1" + when 'syn-pp-in-collection.rq' + pending "CollectionPath" + when 'bind05.rq', 'bind08.rq', 'syntax-bind-02.rq', 'strbefore02.rq', + 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', + 'sq08.rq', 'sq12.rq', 'sq13.rq', + 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' + skip "Equivalent form" + when 'sq09.rq', 'sq14.rq' + pending("SubSelect") + when 'sparql-star-order-by.rq' + pending("OFFSET/LIMIT in sub-select") + end + t.logger = RDF::Spec.logger + t.logger.debug "Source:\n#{t.action.query_string}" + sse = SPARQL.parse(t.action.query_string, base_uri: t.base_uri) + sparql = sse.to_sparql(base_uri: t.base_uri) + expect(sparql).to generate(sse, + base_uri: t.base_uri, + resolve_iris: false, + production: :QueryUnit, + logger: t.logger) + end + when 'ut:UpdateEvaluationTest', 'mf:UpdateEvaluationTest', 'mf:PositiveUpdateSyntaxTest11' + it "Round Trips #{t.entry} - #{t.name}: #{t.comment}" do + case t.entry + when 'syntax-update-38.ru' + pending "empty query" + when 'large-request-01.ru' + skip "large request" + when 'syntax-update-26.ru', 'syntax-update-27.ru', 'syntax-update-28.ru', + 'syntax-update-36.ru' + pending("Whitespace in string tokens") + when 'insert-05a.ru', 'insert-data-same-bnode.ru', + 'insert-where-same-bnode.ru', 'insert-where-same-bnode2.ru' + skip "Equivalent form" + when 'delete-insert-04.ru' + pending("SubSelect") + end + t.logger = RDF::Spec.logger + t.logger.debug "Source:\n#{t.action.query_string}\n" + sse = SPARQL.parse(t.action.query_string, + base_uri: t.base_uri, + update: true) + sparql = sse.to_sparql(base_uri: t.base_uri) + expect(sparql).to generate(sse, + base_uri: t.base_uri, + resolve_iris: false, + production: :UpdateUnit, + logger: t.logger) + end + when 'mf:NegativeSyntaxTest', 'mf:NegativeSyntaxTest11', 'mf:NegativeUpdateSyntaxTest11' + # Do nothing. + else + it "??? #{t.entry} - #{t.name}" do + puts t.inspect + fail "Unknown test type #{t.type}" + end + end + end + end +end + describe SPARQL do BASE = "http://w3c.github.io/rdf-tests/sparql11/" describe "w3c dawg SPARQL 1.0 syntax tests" do SPARQL::Spec.sparql1_0_syntax_tests.each do |path| SPARQL::Spec::Manifest.open(path) do |man| it_behaves_like "SUITE", man.attributes['id'], man.label, man.comment, man.entries + it_behaves_like "to_sparql", man.attributes['id'], man.label, man.comment, man.entries end end end @@ -145,6 +219,7 @@ SPARQL::Spec.sparql1_0_tests.each do |path| SPARQL::Spec::Manifest.open(path) do |man| it_behaves_like "SUITE", man.attributes['id'], man.label, man.comment, man.entries + it_behaves_like "to_sparql", man.attributes['id'], man.label, man.comment, man.entries end end end @@ -153,6 +228,7 @@ SPARQL::Spec.sparql1_1_tests.each do |path| SPARQL::Spec::Manifest.open(path) do |man| it_behaves_like "SUITE", man.attributes['id'], man.label, man.comment, man.entries + it_behaves_like "to_sparql", man.attributes['id'], man.label, man.comment, man.entries end end end @@ -161,6 +237,7 @@ SPARQL::Spec.sparql_star_tests.each do |path| SPARQL::Spec::Manifest.open(path) do |man| it_behaves_like "SUITE", man.attributes['id'], man.label, man.comment, man.entries + it_behaves_like "to_sparql", man.attributes['id'], man.label, man.comment, man.entries end end end diff --git a/spec/support/matchers/generate.rb b/spec/support/matchers/generate.rb index ee27271b..ddb5fb1c 100644 --- a/spec/support/matchers/generate.rb +++ b/spec/support/matchers/generate.rb @@ -57,22 +57,22 @@ def normalize(obj) end failure_message do |input| - "Input : #{@input}\n" + + "Input:\n#{@input}\n" + case expected when String - "Expected : #{expected}\n" + "Expected:\n#{expected}\n" else - "Expected : #{expected.ai}\n" + - "Expected(sse): #{expected.to_sxp}\n" + "Expected:\n#{expected.ai}\n" + + "Expected(sse):\n#{expected.to_sxp}\n" end + case input when String - "Actual : #{actual}\n" + "Actual:\n#{actual}\n" else - "Actual : #{actual.ai}\n" + - "Actual(sse) : #{actual.to_sxp}\n" + "Actual:\n#{actual.ai}\n" + + "Actual(sse):\n#{actual.to_sxp}\n" end + (@exception ? "Exception: #{@exception}" : "") + - "Processing results:\n#{@debug.is_a?(Array) ? @debug.join("\n") : ''}" + "Processing results:\n#{options[:logger].to_s}" end end