From 63de00590550905008ac3c788423521bd1ffaaea Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 3 Jan 2022 10:49:13 -0800 Subject: [PATCH 01/38] Test that the algebra grammar examples produce the stated SSE. --- lib/sparql/algebra/operator/bgp.rb | 2 +- lib/sparql/algebra/operator/count.rb | 10 +++--- lib/sparql/algebra/operator/distinct.rb | 4 +-- lib/sparql/algebra/operator/encode_for_uri.rb | 6 ++-- lib/sparql/algebra/operator/exprlist.rb | 4 ++- lib/sparql/algebra/operator/extend.rb | 6 ++-- lib/sparql/algebra/operator/is_blank.rb | 3 +- lib/sparql/algebra/operator/is_iri.rb | 3 +- lib/sparql/algebra/operator/is_literal.rb | 3 +- lib/sparql/algebra/operator/is_numeric.rb | 3 +- lib/sparql/algebra/operator/lcase.rb | 5 ++- lib/sparql/algebra/operator/min.rb | 2 +- lib/sparql/algebra/operator/reduced.rb | 6 ++-- lib/sparql/algebra/operator/regex.rb | 2 +- lib/sparql/algebra/operator/strlang.rb | 3 +- lib/sparql/algebra/operator/sum.rb | 12 +++---- lib/sparql/algebra/operator/ucase.rb | 2 +- spec/algebra/to_sparql_spec.rb | 3 -- spec/grammar/examples_spec.rb | 32 +++++++++++++++++++ spec/support/matchers/generate.rb | 14 ++++---- 20 files changed, 74 insertions(+), 51 deletions(-) diff --git a/lib/sparql/algebra/operator/bgp.rb b/lib/sparql/algebra/operator/bgp.rb index 1e3fe183..5a039666 100644 --- a/lib/sparql/algebra/operator/bgp.rb +++ b/lib/sparql/algebra/operator/bgp.rb @@ -7,7 +7,7 @@ class Operator # # @example SPARQL Grammar # PREFIX : - # SELECT * { :s :p :o } + # SELECT * { ?s ?p ?o } # # @example SSE # (prefix ((: )) diff --git a/lib/sparql/algebra/operator/count.rb b/lib/sparql/algebra/operator/count.rb index f7efcbeb..f895dc64 100644 --- a/lib/sparql/algebra/operator/count.rb +++ b/lib/sparql/algebra/operator/count.rb @@ -11,11 +11,11 @@ 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)))))) # # @see https://www.w3.org/TR/sparql11-query/#defn_aggCount class Count < Operator 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/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..6e6d1ae9 100644 --- a/lib/sparql/algebra/operator/extend.rb +++ b/lib/sparql/algebra/operator/extend.rb @@ -10,14 +10,14 @@ 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)))) # # @see https://www.w3.org/TR/sparql11-query/#evaluation class Extend < Operator::Binary 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/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/min.rb b/lib/sparql/algebra/operator/min.rb index 20cc6169..d9664dea 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 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/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/sum.rb b/lib/sparql/algebra/operator/sum.rb index 1222c4f5..6332a8a9 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 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/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index 9fb7e5c3..0f7907d5 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -35,9 +35,6 @@ def self.read_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 diff --git a/spec/grammar/examples_spec.rb b/spec/grammar/examples_spec.rb index 35fa3ff0..f05ccf25 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/)[0..-14].gsub(/^\s*#/, '') + 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/support/matchers/generate.rb b/spec/support/matchers/generate.rb index ee27271b..cd130eaf 100644 --- a/spec/support/matchers/generate.rb +++ b/spec/support/matchers/generate.rb @@ -57,20 +57,20 @@ 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") : ''}" From 04dd9cf713182d822f7b49baad8a874f7fddf00d Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 4 Jan 2022 14:52:26 -0800 Subject: [PATCH 02/38] Detect an extension function use within BIND or FILTER when serializing back to SPARQL. --- lib/sparql/algebra/extensions.rb | 19 +++++++++++++++---- lib/sparql/algebra/operator.rb | 8 ++++++-- spec/support/matchers/generate.rb | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 6b8cce51..7385cff7 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -70,16 +70,27 @@ def to_sxp_bin # Returns a partial SPARQL grammar for this array. # # @param [String] delimiter (" ") + # @param [Boolean] parse_extensions (false) + # If the first element is an IRI, treat it as an extension function # @return [String] - def to_sparql(delimiter: " ", **options) - map {|e| e.to_sparql(**options)}.join(delimiter) + def to_sparql(delimiter: " ", parse_extensions: false, **options) + if parse_extensions && first.is_a?(RDF::URI) + extension, *args = self + extension.to_sparql(**options) + + '(' + + args.to_sparql(delimiter: ', ', **options) + + ')' + else + map {|e| e.to_sparql(**options)}.join(delimiter) + end 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. + # In this case, the Array has two or more elements, the first of which is + # an IRI identifying a built-in function, and the remainder are exaluated + # as aruments to that function. # The result is cast as a literal of the appropriate type # # @param [RDF::Query::Solution] bindings diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index 15e9f374..ff6d73e3 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -382,12 +382,16 @@ def self.to_sparql(content, # Extensions extensions.each do |as, expression| - content << "\nBIND (#{expression.to_sparql(**options)} AS #{as.to_sparql(**options)}) ." + content << "\nBIND (" << + expression.to_sparql(parse_extensions: true, **options) << + " AS " << + as.to_sparql(**options) << + ") ." end # Filters filter_ops.each do |f| - content << "\nFILTER #{f.to_sparql(**options)} ." + content << "\nFILTER (#{f.to_sparql(parse_extensions: true, **options)}) ." end # Where clause diff --git a/spec/support/matchers/generate.rb b/spec/support/matchers/generate.rb index cd130eaf..ddb5fb1c 100644 --- a/spec/support/matchers/generate.rb +++ b/spec/support/matchers/generate.rb @@ -73,6 +73,6 @@ def normalize(obj) "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 From 2fd8f3cddda7e5bb55c16ff93d00721e508986ed Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 5 Jan 2022 15:58:00 -0800 Subject: [PATCH 03/38] Use a serializer helper for Function Call syntax when generating SPARQL. --- lib/sparql/algebra.rb | 35 +++++++++++++++ lib/sparql/algebra/extensions.rb | 13 +----- lib/sparql/algebra/operator.rb | 5 +-- lib/sparql/algebra/operator/asc.rb | 6 ++- lib/sparql/algebra/operator/desc.rb | 6 ++- lib/sparql/algebra/operator/extend.rb | 18 ++++++++ lib/sparql/algebra/operator/filter.rb | 15 +++++++ lib/sparql/algebra/operator/group.rb | 2 + lib/sparql/algebra/operator/order.rb | 53 +++++++++++++++++++++- spec/algebra/to_sparql_spec.rb | 64 ++++++++++++++++++++++----- spec/grammar/examples_spec.rb | 2 +- 11 files changed, 190 insertions(+), 29 deletions(-) diff --git a/lib/sparql/algebra.rb b/lib/sparql/algebra.rb index 8deea09b..591cba27 100644 --- a/lib/sparql/algebra.rb +++ b/lib/sparql/algebra.rb @@ -426,6 +426,41 @@ def Variable(name) module_function :Variable Variable = RDF::Query::Variable + + ## + # The serializer helper is used to help manage transformations from SSE back to the SPARQL Grammar + module SerializerHelper + + # Reserialize an Array used as a Function Call + class FunctionCall + # The name of the function + # @return [RDF::URI] + attr_accessor :iri + + # The arguments to the function + # @return [Array] + attr_reader :args + + ## + # @param [RDF::URI] iri + # @param [Array] args + def initialize(iri, *args) + args.pop if RDF.nil == args.last + @iri, @args = iri, args + end + + ## + # Returns a partial SPARQL grammar for the function call. + # + # @return [String] + def to_sparql(**options) + iri.to_sparql(**options) + + '(' + + args.to_sparql(delimiter: ', ', **options) + + ')' + end + end + end # SerializerHelper end # Algebra end # SPARQL diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 7385cff7..94775e81 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -70,19 +70,10 @@ def to_sxp_bin # Returns a partial SPARQL grammar for this array. # # @param [String] delimiter (" ") - # @param [Boolean] parse_extensions (false) # If the first element is an IRI, treat it as an extension function # @return [String] - def to_sparql(delimiter: " ", parse_extensions: false, **options) - if parse_extensions && first.is_a?(RDF::URI) - extension, *args = self - extension.to_sparql(**options) + - '(' + - args.to_sparql(delimiter: ', ', **options) + - ')' - else - map {|e| e.to_sparql(**options)}.join(delimiter) - end + def to_sparql(delimiter: " ", **options) + map {|e| e.to_sparql(**options)}.join(delimiter) end ## diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index ff6d73e3..d09e190d 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -383,7 +383,7 @@ def self.to_sparql(content, # Extensions extensions.each do |as, expression| content << "\nBIND (" << - expression.to_sparql(parse_extensions: true, **options) << + expression.to_sparql(**options) << " AS " << as.to_sparql(**options) << ") ." @@ -391,7 +391,7 @@ def self.to_sparql(content, # Filters filter_ops.each do |f| - content << "\nFILTER (#{f.to_sparql(parse_extensions: true, **options)}) ." + content << "\nFILTER (#{f.to_sparql(**options)}) ." end # Where clause @@ -675,7 +675,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/asc.rb b/lib/sparql/algebra/operator/asc.rb index 460a54aa..359ddfbe 100644 --- a/lib/sparql/algebra/operator/asc.rb +++ b/lib/sparql/algebra/operator/asc.rb @@ -44,7 +44,11 @@ def evaluate(bindings, **options) # # @return [String] def to_sparql(**options) - "ASC(#{operands.last.to_sparql(**options)})" + expression = operands.last.is_a?(Array) ? + SerializerHelper::FunctionCall.new(*operands.last) : + operands.last + + "ASC(#{expression.to_sparql(**options)})" end end # Asc end # Operator diff --git a/lib/sparql/algebra/operator/desc.rb b/lib/sparql/algebra/operator/desc.rb index 7fe23cd1..015646f8 100644 --- a/lib/sparql/algebra/operator/desc.rb +++ b/lib/sparql/algebra/operator/desc.rb @@ -29,7 +29,11 @@ class Desc < Operator::Asc # # @return [String] def to_sparql(**options) - "DESC(#{operands.last.to_sparql(**options)})" + expression = operands.last.is_a?(Array) ? + SerializerHelper::FunctionCall.new(*operands.last) : + operands.last + + "DESC(#{expression.to_sparql(**options)})" end end # Desc end # Operator diff --git a/lib/sparql/algebra/operator/extend.rb b/lib/sparql/algebra/operator/extend.rb index 6e6d1ae9..42039da1 100644 --- a/lib/sparql/algebra/operator/extend.rb +++ b/lib/sparql/algebra/operator/extend.rb @@ -19,6 +19,22 @@ class Operator # (extend ((?z (+ ?o 10))) # (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 + # (prefix ((: ) + # (rdf: ) + # (xsd: )) + # (project (?a ?v ?boolean) + # (extend ((?boolean (xsd:boolean ?v))) + # (bgp (triple ?a :p ?v))))) + # + # # @see https://www.w3.org/TR/sparql11-query/#evaluation class Extend < Operator::Binary include Query @@ -91,6 +107,8 @@ def validate! # @return [String] def to_sparql(**options) extensions = operands.first.inject({}) do |memo, (as, expression)| + # Individual entries may be function calls + expression = SerializerHelper::FunctionCall.new(*expression) if expression.is_a?(Array) memo.merge(as => expression) end diff --git a/lib/sparql/algebra/operator/filter.rb b/lib/sparql/algebra/operator/filter.rb index c7493135..0e55b74d 100644 --- a/lib/sparql/algebra/operator/filter.rb +++ b/lib/sparql/algebra/operator/filter.rb @@ -17,6 +17,17 @@ class Operator # (filter (= ?v 2) # (bgp (triple ?s ?v)))) # + # @example SPARQL Grammar (Using a Function Call) + # 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/#evaluation class Filter < Operator::Binary include Query @@ -89,6 +100,10 @@ def validate! # @return [String] def to_sparql(**options) filter_ops = operands.first.is_a?(Operator::Exprlist) ? operands.first.operands : [operands.first] + # Individual entries may be function calls + filter_ops = filter_ops.map do |op| + op.is_a?(Array) ? SerializerHelper::FunctionCall.new(*op) : op + end operands.last.to_sparql(filter_ops: filter_ops, **options) end end # Filter diff --git a/lib/sparql/algebra/operator/group.rb b/lib/sparql/algebra/operator/group.rb index de834c59..02786da4 100644 --- a/lib/sparql/algebra/operator/group.rb +++ b/lib/sparql/algebra/operator/group.rb @@ -143,6 +143,8 @@ def to_sparql(extensions: {}, **options) # Replace extensions from temporary bindings operands[1].each do |var, op| ext_var = extensions.invert.fetch(var) + # Individual ops may be function calls + op = SerializerHelper::FunctionCall.new(*op) if op.is_a?(Array) extensions[ext_var] = op end end diff --git a/lib/sparql/algebra/operator/order.rb b/lib/sparql/algebra/operator/order.rb index 4df4861d..5ddff910 100644 --- a/lib/sparql/algebra/operator/order.rb +++ b/lib/sparql/algebra/operator/order.rb @@ -17,6 +17,53 @@ 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 + # (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 + # (prefix + # ((: )) + # (project (?s) + # (order ((+ ?o1 ?o2)) + # (bgp + # (triple ?s :p ?o1) + # (triple ?s :q ?o2))))) + # + # @example SPARQL Grammar (with function call) + # PREFIX : + # SELECT * + # { ?s ?p ?o } + # ORDER BY + # DESC(?o+57) :func2(?o) ASC(?s) + # + # PREFIX : + # SELECT ?s WHERE { + # ?s :p ?o1 ; :q ?o2 . + # } ORDER BY (?o1 + ?o2) + # + # @example SSE + # (prefix ((: )) + # (order ((desc (+ ?o 57)) + # (:func2 ?o) + # (asc ?s)) + # (bgp (triple ?s ?p ?o)))) + # # @see https://www.w3.org/TR/sparql11-query/#modOrderBy class Order < Operator::Binary include Query @@ -73,7 +120,11 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - operands.last.to_sparql(order_ops: operands.first, **options) + # Individual entries may be function calls + order_ops = operands.first.map do |op| + op.is_a?(Array) ? SerializerHelper::FunctionCall.new(*op) : op + end + operands.last.to_sparql(order_ops: order_ops, **options) end end # Order end # Operator diff --git a/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index 0f7907d5..ea343bb0 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 "to_sparql" do |name, sxp| + 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) end +end + +describe SPARQL::Algebra::Operator do + it_behaves_like "to_sparql", "simple query", + %{(prefix ((: )) + (bgp (triple :s :p :o)))} context "Examples" do def self.read_examples @@ -34,14 +39,51 @@ def self.read_examples read_examples.each do |op, examples| describe "Operator #{op}:" do examples.each do |sxp| - it(sxp) do - 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 + it_behaves_like "to_sparql", sxp, sxp end end end end + + context "Issues" do + it_behaves_like "to_sparql", "#39", + SPARQL.parse(%( + PREFIX obo: + + SELECT DISTINCT ?enst + FROM + WHERE { + + ?enst obo:SO_transcribed_from ?ensg . + } + LIMIT 10 + )).to_sxp + + it_behaves_like "to_sparql", "#40", + SPARQL.parse(%( + 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" + } + } + )).to_sxp + end end diff --git a/spec/grammar/examples_spec.rb b/spec/grammar/examples_spec.rb index f05ccf25..f1f304f8 100644 --- a/spec/grammar/examples_spec.rb +++ b/spec/grammar/examples_spec.rb @@ -46,7 +46,7 @@ def self.read_operator_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/) + while scanner.skip_until(/# @example SPARQL Grammar(.*)$/) current = {} current[:sparql] = scanner.scan_until(/# @example SSE/)[0..-14].gsub(/^\s*#/, '') current[:sxp] = scanner.scan_until(/^\s+#\s*$/).gsub(/^\s*#/, '') From e2cea1b0c103b34478338e75d69dd84487f4a53a Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 5 Jan 2022 15:58:29 -0800 Subject: [PATCH 04/38] Add brackets around infix operators when generating SPARQL. --- lib/sparql/algebra/operator/divide.rb | 2 +- lib/sparql/algebra/operator/multiply.rb | 2 +- lib/sparql/algebra/operator/plus.rb | 2 +- lib/sparql/algebra/operator/subtract.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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/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/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 From 9277703bb7e13bdc44c97a6242e80d7d4494c251 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 5 Jan 2022 16:24:42 -0800 Subject: [PATCH 05/38] Delay emitting FROM to Operator to_sparql wrapper. Fixes #39. --- lib/sparql/algebra/operator.rb | 7 ++++++ lib/sparql/algebra/operator/dataset.rb | 30 ++++++++++++++++---------- lib/sparql/algebra/operator/order.rb | 5 ----- spec/algebra/to_sparql_spec.rb | 14 ++++++------ 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index d09e190d..879f700f 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -338,6 +338,7 @@ def self.arity # @param [String] content # @param [Hash{Symbol => Operator}] extensions # Variable bindings + # @param [Operator] datasets ([]) # @param [Operator] distinct (false) # @param [Array] filter_ops ([]) # Filter Operations @@ -352,6 +353,7 @@ def self.arity # @param [Hash{Symbol => Object}] options # @return [String] def self.to_sparql(content, + datasets: [], distinct: false, extensions: {}, filter_ops: [], @@ -394,6 +396,11 @@ def self.to_sparql(content, content << "\nFILTER (#{f.to_sparql(**options)}) ." end + # Datasets + datasets.each do |ds| + str << "FROM #{ds.to_sparql(**options)}\n" + end + # Where clause str << "WHERE {\n#{content}\n}\n" 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/order.rb b/lib/sparql/algebra/operator/order.rb index 5ddff910..50c7bbe6 100644 --- a/lib/sparql/algebra/operator/order.rb +++ b/lib/sparql/algebra/operator/order.rb @@ -51,11 +51,6 @@ class Operator # { ?s ?p ?o } # ORDER BY # DESC(?o+57) :func2(?o) ASC(?s) - # - # PREFIX : - # SELECT ?s WHERE { - # ?s :p ?o1 ; :q ?o2 . - # } ORDER BY (?o1 + ?o2) # # @example SSE # (prefix ((: )) diff --git a/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index ea343bb0..c8e0fb3c 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -5,7 +5,7 @@ include SPARQL::Algebra -shared_examples "to_sparql" do |name, sxp| +shared_examples "SXP to SPARQL" do |name, sxp| it(name) do sse = SPARQL::Algebra.parse(sxp) sparql_result = sse.to_sparql @@ -15,7 +15,7 @@ end describe SPARQL::Algebra::Operator do - it_behaves_like "to_sparql", "simple query", + it_behaves_like "SXP to SPARQL", "simple query", %{(prefix ((: )) (bgp (triple :s :p :o)))} @@ -39,14 +39,14 @@ def self.read_examples read_examples.each do |op, examples| describe "Operator #{op}:" do examples.each do |sxp| - it_behaves_like "to_sparql", sxp, sxp + it_behaves_like "SXP to SPARQL", sxp, sxp end end end end context "Issues" do - it_behaves_like "to_sparql", "#39", + it_behaves_like "SXP to SPARQL", "#39", SPARQL.parse(%( PREFIX obo: @@ -59,7 +59,7 @@ def self.read_examples LIMIT 10 )).to_sxp - it_behaves_like "to_sparql", "#40", + it_behaves_like "SXP to SPARQL", "#40", SPARQL.parse(%( PREFIX obo: PREFIX taxon: @@ -84,6 +84,8 @@ def self.read_examples "X" "Y" "MT" } } - )).to_sxp + )).to_sxp do + before {pending} + end end end From 7e7c0fd3afb3a03d72aca6d4092755cf0a8ce151 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 6 Jan 2022 16:44:41 -0800 Subject: [PATCH 06/38] Replace the `SerializerHelper` with a `FunctionCall` builtin and use it whenn parsing instead of the array form for function calls/extension functions. Also removes `Array#evaluate` When parsing SSE, look for registered extensions to find the `FunctionCall` operator. --- README.md | 2 + examples/function_call.sse | 6 + lib/sparql/algebra.rb | 36 +---- lib/sparql/algebra/expression.rb | 34 ++--- lib/sparql/algebra/operator.rb | 2 + lib/sparql/algebra/operator/asc.rb | 6 +- lib/sparql/algebra/operator/desc.rb | 6 +- lib/sparql/algebra/operator/extend.rb | 2 - lib/sparql/algebra/operator/filter.rb | 15 --- lib/sparql/algebra/operator/function_call.rb | 64 +++++++++ lib/sparql/algebra/operator/group.rb | 2 - lib/sparql/algebra/operator/order.rb | 16 +-- lib/sparql/grammar/parser11.rb | 4 +- spec/algebra/expression_spec.rb | 135 ++++++++++--------- spec/grammar/parser_spec.rb | 6 +- 15 files changed, 176 insertions(+), 160 deletions(-) create mode 100644 examples/function_call.sse create mode 100644 lib/sparql/algebra/operator/function_call.rb 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/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 591cba27..1b00f3bd 100644 --- a/lib/sparql/algebra.rb +++ b/lib/sparql/algebra.rb @@ -262,6 +262,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} @@ -426,41 +427,6 @@ def Variable(name) module_function :Variable Variable = RDF::Query::Variable - - ## - # The serializer helper is used to help manage transformations from SSE back to the SPARQL Grammar - module SerializerHelper - - # Reserialize an Array used as a Function Call - class FunctionCall - # The name of the function - # @return [RDF::URI] - attr_accessor :iri - - # The arguments to the function - # @return [Array] - attr_reader :args - - ## - # @param [RDF::URI] iri - # @param [Array] args - def initialize(iri, *args) - args.pop if RDF.nil == args.last - @iri, @args = iri, args - end - - ## - # Returns a partial SPARQL grammar for the function call. - # - # @return [String] - def to_sparql(**options) - iri.to_sparql(**options) + - '(' + - args.to_sparql(delimiter: ', ', **options) + - ')' - end - end - end # SerializerHelper end # Algebra end # SPARQL diff --git a/lib/sparql/algebra/expression.rb b/lib/sparql/algebra/expression.rb index 7e7a73b1..b26f5a97 100644 --- a/lib/sparql/algebra/expression.rb +++ b/lib/sparql/algebra/expression.rb @@ -86,6 +86,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 @@ -163,6 +170,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. # @@ -325,22 +343,6 @@ def optimize!(**options) self end - ## - # Evaluates this expression using the given variable `bindings`. - # - # This is the default implementation, which simply returns `self`. - # Subclasses can override this method in order to implement something - # more useful. - # - # @param [RDF::Query::Solution] bindings - # a query solution containing zero or more variable bindings - # @param [Hash{Symbol => Object}] options ({}) - # options passed from query - # @return [Expression] `self` - def evaluate(bindings, **options) - self - end - ## # Returns the SPARQL S-Expression (SSE) representation of this expression. # diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index 879f700f..bcf26a1c 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 diff --git a/lib/sparql/algebra/operator/asc.rb b/lib/sparql/algebra/operator/asc.rb index 359ddfbe..460a54aa 100644 --- a/lib/sparql/algebra/operator/asc.rb +++ b/lib/sparql/algebra/operator/asc.rb @@ -44,11 +44,7 @@ def evaluate(bindings, **options) # # @return [String] def to_sparql(**options) - expression = operands.last.is_a?(Array) ? - SerializerHelper::FunctionCall.new(*operands.last) : - operands.last - - "ASC(#{expression.to_sparql(**options)})" + "ASC(#{operands.last.to_sparql(**options)})" end end # Asc end # Operator diff --git a/lib/sparql/algebra/operator/desc.rb b/lib/sparql/algebra/operator/desc.rb index 015646f8..7fe23cd1 100644 --- a/lib/sparql/algebra/operator/desc.rb +++ b/lib/sparql/algebra/operator/desc.rb @@ -29,11 +29,7 @@ class Desc < Operator::Asc # # @return [String] def to_sparql(**options) - expression = operands.last.is_a?(Array) ? - SerializerHelper::FunctionCall.new(*operands.last) : - operands.last - - "DESC(#{expression.to_sparql(**options)})" + "DESC(#{operands.last.to_sparql(**options)})" end end # Desc end # Operator diff --git a/lib/sparql/algebra/operator/extend.rb b/lib/sparql/algebra/operator/extend.rb index 42039da1..6dd613eb 100644 --- a/lib/sparql/algebra/operator/extend.rb +++ b/lib/sparql/algebra/operator/extend.rb @@ -107,8 +107,6 @@ def validate! # @return [String] def to_sparql(**options) extensions = operands.first.inject({}) do |memo, (as, expression)| - # Individual entries may be function calls - expression = SerializerHelper::FunctionCall.new(*expression) if expression.is_a?(Array) memo.merge(as => expression) end diff --git a/lib/sparql/algebra/operator/filter.rb b/lib/sparql/algebra/operator/filter.rb index 0e55b74d..c7493135 100644 --- a/lib/sparql/algebra/operator/filter.rb +++ b/lib/sparql/algebra/operator/filter.rb @@ -17,17 +17,6 @@ class Operator # (filter (= ?v 2) # (bgp (triple ?s ?v)))) # - # @example SPARQL Grammar (Using a Function Call) - # 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/#evaluation class Filter < Operator::Binary include Query @@ -100,10 +89,6 @@ def validate! # @return [String] def to_sparql(**options) filter_ops = operands.first.is_a?(Operator::Exprlist) ? operands.first.operands : [operands.first] - # Individual entries may be function calls - filter_ops = filter_ops.map do |op| - op.is_a?(Array) ? SerializerHelper::FunctionCall.new(*op) : op - end operands.last.to_sparql(filter_ops: filter_ops, **options) end end # Filter 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/group.rb b/lib/sparql/algebra/operator/group.rb index 02786da4..de834c59 100644 --- a/lib/sparql/algebra/operator/group.rb +++ b/lib/sparql/algebra/operator/group.rb @@ -143,8 +143,6 @@ def to_sparql(extensions: {}, **options) # Replace extensions from temporary bindings operands[1].each do |var, op| ext_var = extensions.invert.fetch(var) - # Individual ops may be function calls - op = SerializerHelper::FunctionCall.new(*op) if op.is_a?(Array) extensions[ext_var] = op end end diff --git a/lib/sparql/algebra/operator/order.rb b/lib/sparql/algebra/operator/order.rb index 50c7bbe6..a9835ab5 100644 --- a/lib/sparql/algebra/operator/order.rb +++ b/lib/sparql/algebra/operator/order.rb @@ -46,16 +46,18 @@ class Operator # (triple ?s :q ?o2))))) # # @example SPARQL Grammar (with function call) - # PREFIX : + # PREFIX : + # PREFIX xsd: # SELECT * # { ?s ?p ?o } # ORDER BY - # DESC(?o+57) :func2(?o) ASC(?s) + # DESC(?o+57) xsd:string(?o) ASC(?s) # # @example SSE - # (prefix ((: )) + # (prefix ((: ) + # (xsd: )) # (order ((desc (+ ?o 57)) - # (:func2 ?o) + # (xsd:string ?o) # (asc ?s)) # (bgp (triple ?s ?p ?o)))) # @@ -115,11 +117,7 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - # Individual entries may be function calls - order_ops = operands.first.map do |op| - op.is_a?(Array) ? SerializerHelper::FunctionCall.new(*op) : op - end - operands.last.to_sparql(order_ops: order_ops, **options) + operands.last.to_sparql(order_ops: operands.first, **options) end end # Order end # Operator diff --git a/lib/sparql/grammar/parser11.rb b/lib/sparql/grammar/parser11.rb index 93999a78..4a675ab9 100644 --- a/lib/sparql/grammar/parser11.rb +++ b/lib/sparql/grammar/parser11.rb @@ -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/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/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")]] From 85fd5bfbda60314081ca58dae7c5119c15bac039 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 6 Jan 2022 16:45:30 -0800 Subject: [PATCH 07/38] Remove `#evaluate` from extensions, so that only reasonably evaluatable things respond. --- lib/sparql/algebra/extensions.rb | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 94775e81..9292e6a4 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -76,24 +76,6 @@ 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 or more elements, the first of which is - # an IRI identifying a built-in function, and the remainder are exaluated - # as aruments to that function. - # 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 From d694e550f6629432bd09796146107ca0909b4119 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Fri, 7 Jan 2022 14:43:49 -0800 Subject: [PATCH 08/38] Intercept filter_ops when serializing JOIN operator back to SPARQL. Inherited filters should come between joined operands. --- lib/sparql/algebra/extensions.rb | 11 +++++++- lib/sparql/algebra/operator.rb | 32 +++++++++++++++--------- lib/sparql/algebra/operator/abs.rb | 2 +- lib/sparql/algebra/operator/extend.rb | 2 +- lib/sparql/algebra/operator/join.rb | 30 ++++++++++++++++++++-- lib/sparql/algebra/operator/left_join.rb | 25 ++++++++++++------ lib/sparql/algebra/operator/order.rb | 6 ++--- lib/sparql/algebra/operator/table.rb | 28 +++++++++++++++++++-- spec/algebra/to_sparql_spec.rb | 2 +- spec/grammar/examples_spec.rb | 2 +- 10 files changed, 109 insertions(+), 31 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 9292e6a4..598c05bb 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -440,7 +440,16 @@ def to_sxp_bin def to_sparql(top_level: true, **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, **options) + else + filter_ops = options.delete(:filter_ops) || [] + filter_ops.each do |op| + str << "\nFILTER (#{op.to_sparql(**options)}) ." + end + str = "{#{str}}" unless filter_ops.empty? + str + end end ## diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index bcf26a1c..4741e7cb 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -352,6 +352,8 @@ def self.arity # @param [Array] project (%i(*)) # Terms to project # @param [Operator] reduced (false) + # @param [Operator] where_clause (true) + # Emit 'WHERE' before GroupGraphPattern # @param [Hash{Symbol => Object}] options # @return [String] def self.to_sparql(content, @@ -365,6 +367,7 @@ def self.to_sparql(content, order_ops: [], project: %i(*), reduced: false, + where_clause: true, **options) str = "" @@ -384,7 +387,12 @@ def self.to_sparql(content, 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) << @@ -393,20 +401,18 @@ def self.to_sparql(content, ") ." end - # Filters + # Filter filter_ops.each do |f| content << "\nFILTER (#{f.to_sparql(**options)}) ." end - # Datasets - datasets.each do |ds| - str << "FROM #{ds.to_sparql(**options)}\n" - 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 @@ -417,12 +423,14 @@ def self.to_sparql(content, str << "GROUP BY #{ops.join(' ')}\n" end - # Order + # HavingClause + + # 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? str 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/extend.rb b/lib/sparql/algebra/operator/extend.rb index 6dd613eb..888461c6 100644 --- a/lib/sparql/algebra/operator/extend.rb +++ b/lib/sparql/algebra/operator/extend.rb @@ -26,7 +26,7 @@ class Operator # SELECT ?a ?v (xsd:boolean(?v) AS ?boolean) # WHERE { ?a :p ?v . } # - # @example SSE + # @example SSE (cast as boolean) # (prefix ((: ) # (rdf: ) # (xsd: )) diff --git a/lib/sparql/algebra/operator/join.rb b/lib/sparql/algebra/operator/join.rb index 2f2edd2c..14a60a5e 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,9 +114,19 @@ def optimize!(**options) # # @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 = operands.to_sparql(top_level: false, delimiter: "\n", **options) + def to_sparql(top_level: true, filter_ops: [], **options) + str = "{\n" + operands.first.to_sparql(top_level: false, **options) + + # Any accrued filters go here. + filter_ops.each do |op| + str << "\nFILTER (#{op.to_sparql(**options)}) ." + end + + str << "\n" + operands.last.to_sparql(top_level: false, **options) + "\n}" + top_level ? Operator.to_sparql(str, **options) : str end end # Join diff --git a/lib/sparql/algebra/operator/left_join.rb b/lib/sparql/algebra/operator/left_join.rb index c3eae45a..5422b9fc 100644 --- a/lib/sparql/algebra/operator/left_join.rb +++ b/lib/sparql/algebra/operator/left_join.rb @@ -131,14 +131,25 @@ def optimize!(**options) # # @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 = 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: [], **options) + str = "{\n" + operands[0].to_sparql(top_level: false, **options) + str << + "\nOPTIONAL {\n" + + operands[1].to_sparql(top_level: false, **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, **options) : str end end # LeftJoin end # Operator diff --git a/lib/sparql/algebra/operator/order.rb b/lib/sparql/algebra/operator/order.rb index a9835ab5..a0a8bf85 100644 --- a/lib/sparql/algebra/operator/order.rb +++ b/lib/sparql/algebra/operator/order.rb @@ -24,7 +24,7 @@ class Operator # } # ORDER BY str(?o) # - # @example SSE + # @example SSE (with builtin) # (prefix ((: )) # (project (?s) # (order ((str ?o)) @@ -36,7 +36,7 @@ class Operator # ?s :p ?o1 ; :q ?o2 . # } ORDER BY (?o1 + ?o2) # - # @example SSE + # @example SSE (with bracketed expression) # (prefix # ((: )) # (project (?s) @@ -53,7 +53,7 @@ class Operator # ORDER BY # DESC(?o+57) xsd:string(?o) ASC(?s) # - # @example SSE + # @example SSE (with function call) # (prefix ((: ) # (xsd: )) # (order ((desc (+ ?o 57)) diff --git a/lib/sparql/algebra/operator/table.rb b/lib/sparql/algebra/operator/table.rb index 2924684f..005b8ed6 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,32 @@ class Operator # (bgp (triple ?book dc:title ?title) (triple ?book ns:price ?price)) # (table (vars ?book) (row (?book :book1)))) )) # + # [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 diff --git a/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index c8e0fb3c..e82b38da 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -25,7 +25,7 @@ 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/) + while scanner.skip_until(/# @example SSE.*$/) ex = scanner.scan_until(/^\s+#\s*$/) # Trim off comment prefix diff --git a/spec/grammar/examples_spec.rb b/spec/grammar/examples_spec.rb index f1f304f8..904966d4 100644 --- a/spec/grammar/examples_spec.rb +++ b/spec/grammar/examples_spec.rb @@ -48,7 +48,7 @@ def self.read_operator_examples scanner = StringScanner.new(File.read(rb)) while scanner.skip_until(/# @example SPARQL Grammar(.*)$/) current = {} - current[:sparql] = scanner.scan_until(/# @example SSE/)[0..-14].gsub(/^\s*#/, '') + 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 From 87aed89b6fb356634bc1184ee1b0ce8aa23056ea Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Fri, 7 Jan 2022 16:12:12 -0800 Subject: [PATCH 09/38] More complicated graph examples and to_sparql serialization. --- lib/sparql/algebra/operator/graph.rb | 48 +++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/lib/sparql/algebra/operator/graph.rb b/lib/sparql/algebra/operator/graph.rb index 3bb31ddd..a20d11ec 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,34 @@ class Operator # (graph ?g # (bgp (triple ?s ?p ?o)))) # - # @example of a query - # (prefix ((: )) - # (graph ?g - # (bgp (triple ?s ?p ?o)))) + # @example SPARQL Grammar (named set of statements) + # PREFIX : + # SELECT * { + # GRAPH :g { :s :p :o } + # } # - # @example named set of statements + # @example SSE (named set of statements) # (prefix ((: )) - # (graph :g - # ((triple :s :p :o)))) + # (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 + # (join + # (bgp (triple :x :b ?a)) + # (graph ?g2 + # (bgp (triple :x :p ?x))))))) # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Graph < Operator::Binary @@ -89,6 +108,19 @@ 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) + str = "GRAPH #{operands.first.to_sparql(**options)} " + + operands.last.to_sparql(top_level: false, **options) + top_level ? Operator.to_sparql(str, **options) : str + end end # Graph end # Operator end; end # SPARQL::Algebra From 5efef5669093dae9cefdc61b62f07618937313bb Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Fri, 7 Jan 2022 16:12:39 -0800 Subject: [PATCH 10/38] Run to_sparql over SPARQL 1.0 test cases. --- spec/suite_spec.rb | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 1ecf03b7..96fe8fcc 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -131,12 +131,63 @@ 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.name + when 'syntax-expr-05.rq', 'syntax-order-05.rq', 'syntax-function-04.rq' + pending("Unregistered function calls") + when 'Basic - Term 7', '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" + 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, + production: :QueryUnit) + 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 + 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, + production: :UpdateUnit) + sparql = sse.to_sparql(base_uri: t.base_uri) + expect(sparql).to generate(sse, resolve_iris: false, production: :UpdateUnit, logger: t.logger) + end + when 'mf:NegativeSyntaxTest' + # 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 +196,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 +205,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 +214,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 From 24e28674c59750ca632f7eb9ac1023ffcf9e661e Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 12:34:01 -0800 Subject: [PATCH 11/38] Improve Algebra documentation, with specific references to Jena. --- lib/sparql/algebra.rb | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/sparql/algebra.rb b/lib/sparql/algebra.rb index 1b00f3bd..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' @@ -340,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 From a49bf32dd094282edc94bcf6c27637f4b2b5dabd Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 14:08:20 -0800 Subject: [PATCH 12/38] Update Expression.for to take options, and potentially use for logging errors. Otherwise, if there is no logger, raise the error. --- lib/sparql/algebra/expression.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/sparql/algebra/expression.rb b/lib/sparql/algebra/expression.rb index b26f5a97..9b7e3b82 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 @@ -122,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 From e5eba51539acf833bca68bcbb8dc8da74935a086 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 14:14:51 -0800 Subject: [PATCH 13/38] Handle the COUNT(*) to_sparql case. --- lib/sparql/algebra/operator/count.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/sparql/algebra/operator/count.rb b/lib/sparql/algebra/operator/count.rb index f895dc64..5a86a271 100644 --- a/lib/sparql/algebra/operator/count.rb +++ b/lib/sparql/algebra/operator/count.rb @@ -17,6 +17,20 @@ class Operator # (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)))))) + # # @see https://www.w3.org/TR/sparql11-query/#defn_aggCount class Count < Operator include Aggregate @@ -39,7 +53,7 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "COUNT(#{operands.to_sparql(**options)})" + "COUNT(#{operands.empty? ? '*' : operands.to_sparql(**options)})" end end # Count end # Operator From 82af164644fd2bd40734aabe902e394f4fb294f6 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 14:50:58 -0800 Subject: [PATCH 14/38] Handle BIND within a join or left_join operator. Fixes #40. --- lib/sparql/algebra/extensions.rb | 17 ++++- lib/sparql/algebra/operator.rb | 4 +- lib/sparql/algebra/operator/extend.rb | 17 +++++ lib/sparql/algebra/operator/join.rb | 10 ++- lib/sparql/algebra/operator/left_join.rb | 10 ++- spec/algebra/to_sparql_spec.rb | 96 +++++++++++++++++------- spec/suite_spec.rb | 4 +- 7 files changed, 116 insertions(+), 42 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 598c05bb..d92b2938 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -432,7 +432,7 @@ 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 {} @@ -443,11 +443,22 @@ def to_sparql(top_level: true, **options) if top_level SPARQL::Algebra::Operator.to_sparql(str, **options) else - filter_ops = options.delete(:filter_ops) || [] + # Filters + filter_ops = options.fetch(:filter_ops, []) filter_ops.each do |op| str << "\nFILTER (#{op.to_sparql(**options)}) ." end - str = "{#{str}}" unless filter_ops.empty? + + # Extensons + extensions = options.fetch(:extensions, []) + extensions.each do |as, expression| + str << "\nBIND (" << + expression.to_sparql(**options) << + " 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 4741e7cb..c008b529 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -338,10 +338,10 @@ def self.arity # Generate a top-level Grammar, using collected options # # @param [String] content - # @param [Hash{Symbol => Operator}] extensions - # Variable bindings # @param [Operator] datasets ([]) # @param [Operator] distinct (false) + # @param [Hash{Symbol => Operator}] extensions + # Variable bindings # @param [Array] filter_ops ([]) # Filter Operations # @param [Integer] limit (nil) diff --git a/lib/sparql/algebra/operator/extend.rb b/lib/sparql/algebra/operator/extend.rb index 888461c6..8d81976e 100644 --- a/lib/sparql/algebra/operator/extend.rb +++ b/lib/sparql/algebra/operator/extend.rb @@ -33,7 +33,24 @@ class Operator # (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/join.rb b/lib/sparql/algebra/operator/join.rb index 14a60a5e..b223af2b 100644 --- a/lib/sparql/algebra/operator/join.rb +++ b/lib/sparql/algebra/operator/join.rb @@ -114,20 +114,22 @@ 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, filter_ops: [], **options) - str = "{\n" + operands.first.to_sparql(top_level: false, **options) + def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) + 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 - str << "\n" + operands.last.to_sparql(top_level: false, **options) + "\n}" + str << "\n" + operands.last.to_sparql(top_level: false, extensions: {}, **options) + "\n}" - top_level ? Operator.to_sparql(str, **options) : str + top_level ? Operator.to_sparql(str, extensions: extensions, **options) : str end end # Join end # Operator diff --git a/lib/sparql/algebra/operator/left_join.rb b/lib/sparql/algebra/operator/left_join.rb index 5422b9fc..7dc20592 100644 --- a/lib/sparql/algebra/operator/left_join.rb +++ b/lib/sparql/algebra/operator/left_join.rb @@ -131,14 +131,16 @@ 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, filter_ops: [], **options) - str = "{\n" + operands[0].to_sparql(top_level: false, **options) + 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, **options) + operands[1].to_sparql(top_level: false, extensions: {}, **options) case operands[2] when SPARQL::Algebra::Operator::Exprlist operands[2].operands.each do |op| @@ -149,7 +151,7 @@ def to_sparql(top_level: true, filter_ops: [], **options) str << "\nFILTER (" + operands[2].to_sparql(**options) + ")" end str << "\n}}" - top_level ? Operator.to_sparql(str, filter_ops: filter_ops, **options) : str + top_level ? Operator.to_sparql(str, filter_ops: filter_ops, extensions: extensions, **options) : str end end # LeftJoin end # Operator diff --git a/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index e82b38da..e031f6ca 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -59,33 +59,75 @@ def self.read_examples LIMIT 10 )).to_sxp - it_behaves_like "SXP to SPARQL", "#40", - SPARQL.parse(%( - 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" - } - } - )).to_sxp do - before {pending} + # 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) + (filter (contains (str ?parent) "terms/ensembl/") + (join + (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"))))))))) + } do + #before {pending} end end end diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 96fe8fcc..23533247 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -165,11 +165,11 @@ t.logger.debug "Source:\n#{t.action.query_string}\n" sse = SPARQL.parse(t.action.query_string, base_uri: t.base_uri, - production: :UpdateUnit) + update: true) sparql = sse.to_sparql(base_uri: t.base_uri) expect(sparql).to generate(sse, resolve_iris: false, production: :UpdateUnit, logger: t.logger) end - when 'mf:NegativeSyntaxTest' + when 'mf:NegativeSyntaxTest', 'mf:NegativeSyntaxTest11' # Do nothing. else it "??? #{t.entry} - #{t.name}" do From 44c2c3c416dec3fd13eb2dde1b468dd5b1b141f4 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 14:59:33 -0800 Subject: [PATCH 15/38] Using clauses only take a single IRI. --- lib/sparql/algebra/operator/using.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 From 14209a83470e1196feef3ecf9bf27b934999910b Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 15:18:14 -0800 Subject: [PATCH 16/38] Fix Operator.if to_sparql. --- lib/sparql/algebra/operator/if.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 From bd8e78a2f697c67b9e154f3745a654c529492041 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 15:41:42 -0800 Subject: [PATCH 17/38] Fix GraphRef case in clear and drop to_sparql --- lib/sparql/algebra/operator/clear.rb | 13 ++++++++++--- lib/sparql/algebra/operator/drop.rb | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/sparql/algebra/operator/clear.rb b/lib/sparql/algebra/operator/clear.rb index ffda3ced..f7b662cf 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,8 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - "CLEAR " + operands.to_sparql(**options) + "CLEAR #{'GRAPH ' if operands.last.is_a?(RDF::URI)}" + + operands.to_sparql(**options) end end # Clear end # Operator diff --git a/lib/sparql/algebra/operator/drop.rb b/lib/sparql/algebra/operator/drop.rb index e610322a..7449e871 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 @@ -74,7 +80,8 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - "DROP " + operands.to_sparql(**options) + "DROP #{'GRAPH ' if operands.last.is_a?(RDF::URI)}" + + operands.to_sparql(**options) end end # Drop end # Operator From e3859d536f53145d7c4293a9b19e4b19c0d4af84 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 8 Jan 2022 15:50:29 -0800 Subject: [PATCH 18/38] Separate update clauses with ';'. --- lib/sparql/algebra/operator/update.rb | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 From 1f86f63a2b45f386e6c3726819260b67735cbb3b Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 9 Jan 2022 16:35:04 -0800 Subject: [PATCH 19/38] GROUP BY/HAVING forms for to_sparql. --- lib/sparql/algebra/operator.rb | 13 ++++--- lib/sparql/algebra/operator/group.rb | 44 ++++++++++++++++++++-- spec/algebra/to_sparql_spec.rb | 4 +- spec/suite_spec.rb | 56 +++++++++++++++++----------- 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index c008b529..e930ff83 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -346,6 +346,7 @@ def self.arity # Filter Operations # @param [Integer] limit (nil) # @param [Array] group_ops ([]) + # @param [Array] having_ops ([]) # @param [Integer] offset (nil) # @param [Array] order_ops ([]) # Order Operations @@ -362,6 +363,7 @@ def self.to_sparql(content, extensions: {}, filter_ops: [], group_ops: [], + having_ops: [], limit: nil, offset: nil, order_ops: [], @@ -424,6 +426,9 @@ def self.to_sparql(content, end # HavingClause + unless having_ops.empty? + str << "HAVING #{having_ops.to_sparql(**options)}" + end # OrderClause unless order_ops.empty? @@ -661,12 +666,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 diff --git a/lib/sparql/algebra/operator/group.rb b/lib/sparql/algebra/operator/group.rb index de834c59..9fbf59c5 100644 --- a/lib/sparql/algebra/operator/group.rb +++ b/lib/sparql/algebra/operator/group.rb @@ -25,6 +25,21 @@ 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)))))) ) + # # @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra class Group < Operator include Query @@ -137,16 +152,39 @@ 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| + temp_bindings.each do |var, op| ext_var = extensions.invert.fetch(var) extensions[ext_var] = op + + # 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/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index e031f6ca..5e1220ad 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -126,8 +126,6 @@ def self.read_examples (row (?chromosome "X")) (row (?chromosome "Y")) (row (?chromosome "MT"))))))))) - } do - #before {pending} - end + } end end diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 23533247..ef2a856f 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 @@ -139,19 +135,29 @@ case t.type when 'mf:QueryEvaluationTest', 'mf:PositiveSyntaxTest', 'mf:PositiveSyntaxTest11' it "Round Trips #{t.entry} - #{t.name}: #{t.comment}" do - case t.name + case t.entry when 'syntax-expr-05.rq', 'syntax-order-05.rq', 'syntax-function-04.rq' pending("Unregistered function calls") - when 'Basic - Term 7', 'syntax-lit-08.rq' + 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 'bind05.rq', 'bind08.rq', 'syntax-bind-02.rq', 'strbefore02.rq' + skip "Equivalent form" + when 'subset-01.rq', 'subset-02.rq', 'set-equals-1.rq', 'subset-03.rq' + pending("TODO Minus") + when 'exists03.rq', 'exists04.rq', 'exists05.rq' + skip('TODO Exists') + when 'syntax-aggregate-02.rq', 'syntax-aggregate-14.rq', 'syntax-aggregate-15.rq' + pending("TODO Aggregates") + when 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', 'agg-groupconcat-3.rq', + 'agg-sample-01.rq', 'sq03.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', + 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' + pending("TODO SubSelect") 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, - production: :QueryUnit) + 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, @@ -161,6 +167,12 @@ end when 'ut:UpdateEvaluationTest', 'mf:UpdateEvaluationTest', 'mf:PositiveUpdateSyntaxTest11' it "Round Trips #{t.entry} - #{t.name}: #{t.comment}" do + case t.entry + when 'insert-05a.ru', 'insert-data-same-bnode.ru', + 'insert-where-same-bnode.ru', 'insert-where-same-bnode2.ru', + '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, From 9a7aedcf8de942c466a462cf838261cec58c4422 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 9 Jan 2022 17:18:53 -0800 Subject: [PATCH 20/38] CLEAR/CREATE/DROP SILENT serialization. --- lib/sparql/algebra/operator/clear.rb | 7 +++++-- lib/sparql/algebra/operator/create.rb | 9 +++++---- lib/sparql/algebra/operator/drop.rb | 8 +++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/sparql/algebra/operator/clear.rb b/lib/sparql/algebra/operator/clear.rb index f7b662cf..684989d3 100644 --- a/lib/sparql/algebra/operator/clear.rb +++ b/lib/sparql/algebra/operator/clear.rb @@ -76,8 +76,11 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - "CLEAR #{'GRAPH ' if operands.last.is_a?(RDF::URI)}" + - 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/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/drop.rb b/lib/sparql/algebra/operator/drop.rb index 7449e871..a3ebc4a9 100644 --- a/lib/sparql/algebra/operator/drop.rb +++ b/lib/sparql/algebra/operator/drop.rb @@ -46,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 @@ -80,8 +79,11 @@ def execute(queryable, **options) # # @return [String] def to_sparql(**options) - "DROP #{'GRAPH ' if operands.last.is_a?(RDF::URI)}" + - 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 From 321f9af173b2ed0835515a7c4203ec55cca528b1 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 9 Jan 2022 17:19:12 -0800 Subject: [PATCH 21/38] First operand of table may not be an array. --- lib/sparql/algebra/operator/table.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sparql/algebra/operator/table.rb b/lib/sparql/algebra/operator/table.rb index 005b8ed6..40af1b03 100644 --- a/lib/sparql/algebra/operator/table.rb +++ b/lib/sparql/algebra/operator/table.rb @@ -93,7 +93,7 @@ def execute(queryable, **options, &block) # # @return [String] def to_sparql(**options) - str = "VALUES (#{operands.first[1..-1].map { |e| e.to_sparql(**options) }.join(' ')}) {\n" + 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| From c148d4ac3ebed4888d406f6d18cc6d6769c93ba5 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 9 Jan 2022 17:19:29 -0800 Subject: [PATCH 22/38] WITH may have multiple trailing operands. --- lib/sparql/algebra/operator/with.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 94bd7a1ba3f4c6ce0035ff4e12a7f7f2af4dba9b Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 9 Jan 2022 17:19:56 -0800 Subject: [PATCH 23/38] Mark remaining SPARQL 1.1 to_sparql tests are pending. --- spec/suite_spec.rb | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index ef2a856f..cdab2878 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -154,6 +154,21 @@ 'agg-sample-01.rq', 'sq03.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' pending("TODO SubSelect") + when 'agg-err-01.rq' + pending "TODO key not found" + when 'pp06.rq', 'path-ng-01.rq', 'path-ng-02.rq' + pending "TODO graph name on property path" + when 'pp09.rq', 'pp10.rq', 'path-p2.rq', 'path-p4.rq' + pending "TODO property path grouping" + when 'syntax-bindings-02a.rq', 'syntax-bindings-03a.rq', 'syntax-bindings-05a.rq' + pending "TODO top-level values" + when 'syn-pname-05.rq', 'syn-pname-06.rq', 'syn-pname-07.rq', 'syn-codepoint-escape-01.rq', + '1val1STRING_LITERAL1_with_UTF8_boundaries.rq', '1val1STRING_LITERAL1_with_UTF8_boundaries_escaped.rq' + pending "TODO escaping" + when 'syn-pp-in-collection.rq' + pending "TODO runtime error and list representation" + when 'strafter02.rq ' + pending "TODO odd project multple bindings" end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" @@ -172,16 +187,30 @@ 'insert-where-same-bnode.ru', 'insert-where-same-bnode2.ru', 'delete-insert-04.ru' pending("SubSelect") + when 'delete-insert-04b.ru', 'delete-insert-05b.ru', 'delete-insert-05b.ru' + pending "TODO sub-joins" + when 'syntax-update-38.ru' + pending "empty query" + when 'large-request-01.ru' + skip "large request" end + pending("Whitespace in string tokens") if %w( + syntax-update-26.ru syntax-update-27.ru syntax-update-28.ru + syntax-update-36.ru + ).include?(t.entry) 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, resolve_iris: false, production: :UpdateUnit, logger: t.logger) + expect(sparql).to generate(sse, + base_uri: t.base_uri, + resolve_iris: false, + production: :UpdateUnit, + logger: t.logger) end - when 'mf:NegativeSyntaxTest', 'mf:NegativeSyntaxTest11' + when 'mf:NegativeSyntaxTest', 'mf:NegativeSyntaxTest11', 'mf:NegativeUpdateSyntaxTest11' # Do nothing. else it "??? #{t.entry} - #{t.name}" do @@ -217,7 +246,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 + it_behaves_like "to_sparql", man.attributes['id'], man.label, man.comment, man.entries end end end From 86854379991ab76895992f73b825f663d67c8626 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 10 Jan 2022 10:42:28 -0800 Subject: [PATCH 24/38] Try to force Ruby 3.0 in CI. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b17b025a20b4113eb49686fd91be4b788adb22d8 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 10 Jan 2022 11:28:02 -0800 Subject: [PATCH 25/38] Fix MINUS to_sparql with filters. --- lib/sparql/algebra/extensions.rb | 7 ++-- lib/sparql/algebra/operator/minus.rb | 52 ++++++++++++++++++++++++---- spec/suite_spec.rb | 2 -- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index d92b2938..4667af8e 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -436,15 +436,16 @@ def to_sxp_bin # # @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) + 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 if top_level - SPARQL::Algebra::Operator.to_sparql(str, **options) + SPARQL::Algebra::Operator.to_sparql(str, filter_ops: filter_ops, **options) else # Filters - filter_ops = options.fetch(:filter_ops, []) filter_ops.each do |op| str << "\nFILTER (#{op.to_sparql(**options)}) ." end 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/spec/suite_spec.rb b/spec/suite_spec.rb index cdab2878..fd60eea7 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -144,8 +144,6 @@ skip "PNAME_LN changed in SPARQL 1.1" when 'bind05.rq', 'bind08.rq', 'syntax-bind-02.rq', 'strbefore02.rq' skip "Equivalent form" - when 'subset-01.rq', 'subset-02.rq', 'set-equals-1.rq', 'subset-03.rq' - pending("TODO Minus") when 'exists03.rq', 'exists04.rq', 'exists05.rq' skip('TODO Exists') when 'syntax-aggregate-02.rq', 'syntax-aggregate-14.rq', 'syntax-aggregate-15.rq' From 1133b8eab918a3e752c4226de6a05ffb98ca93ca Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 10 Jan 2022 12:02:30 -0800 Subject: [PATCH 26/38] Aggregate operators with DISINCT and SEPARATOR. --- lib/sparql/algebra/operator/avg.rb | 4 +++- lib/sparql/algebra/operator/count.rb | 18 +++++++++++++- lib/sparql/algebra/operator/group_concat.rb | 26 ++++++++++++++++++++- lib/sparql/algebra/operator/max.rb | 4 +++- lib/sparql/algebra/operator/min.rb | 4 +++- lib/sparql/algebra/operator/sample.rb | 4 +++- lib/sparql/algebra/operator/sum.rb | 4 +++- spec/suite_spec.rb | 2 -- 8 files changed, 57 insertions(+), 9 deletions(-) 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/count.rb b/lib/sparql/algebra/operator/count.rb index 5a86a271..ef976399 100644 --- a/lib/sparql/algebra/operator/count.rb +++ b/lib/sparql/algebra/operator/count.rb @@ -31,6 +31,20 @@ class Operator # (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 include Aggregate @@ -53,7 +67,9 @@ def apply(enum, **options) # # @return [String] def to_sparql(**options) - "COUNT(#{operands.empty? ? '*' : 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/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/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 d9664dea..48ab4c13 100644 --- a/lib/sparql/algebra/operator/min.rb +++ b/lib/sparql/algebra/operator/min.rb @@ -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/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/sum.rb b/lib/sparql/algebra/operator/sum.rb index 6332a8a9..ed957ae1 100644 --- a/lib/sparql/algebra/operator/sum.rb +++ b/lib/sparql/algebra/operator/sum.rb @@ -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/spec/suite_spec.rb b/spec/suite_spec.rb index fd60eea7..4bae1b38 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -146,8 +146,6 @@ skip "Equivalent form" when 'exists03.rq', 'exists04.rq', 'exists05.rq' skip('TODO Exists') - when 'syntax-aggregate-02.rq', 'syntax-aggregate-14.rq', 'syntax-aggregate-15.rq' - pending("TODO Aggregates") when 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', 'agg-groupconcat-3.rq', 'agg-sample-01.rq', 'sq03.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' From 9a507e4607d7dbbcf1e39b9f14fc2431d6a092df Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 10 Jan 2022 15:44:59 -0800 Subject: [PATCH 27/38] Grouped extensions may use temporary variables inside of operators. --- lib/sparql/algebra/operator/group.rb | 37 ++++++++++++++++++++++++++-- spec/suite_spec.rb | 2 -- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/sparql/algebra/operator/group.rb b/lib/sparql/algebra/operator/group.rb index 9fbf59c5..15222055 100644 --- a/lib/sparql/algebra/operator/group.rb +++ b/lib/sparql/algebra/operator/group.rb @@ -40,6 +40,22 @@ class Operator # (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 @@ -161,8 +177,25 @@ def to_sparql(extensions: {}, filter_ops: [], **options) temp_bindings = operands[1].inject({}) {|memo, (var, op)| memo.merge(var => op)} # Replace extensions from temporary bindings temp_bindings.each do |var, op| - ext_var = extensions.invert.fetch(var) - extensions[ext_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| diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 4bae1b38..bee96f4d 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -150,8 +150,6 @@ 'agg-sample-01.rq', 'sq03.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' pending("TODO SubSelect") - when 'agg-err-01.rq' - pending "TODO key not found" when 'pp06.rq', 'path-ng-01.rq', 'path-ng-02.rq' pending "TODO graph name on property path" when 'pp09.rq', 'pp10.rq', 'path-p2.rq', 'path-p4.rq' From 2b484da3affdaca6e867a15b491927d64abdc2f9 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 11 Jan 2022 12:57:37 -0800 Subject: [PATCH 28/38] Property path grouping and named graphs containing a property path. --- lib/sparql/algebra/operator/alt.rb | 2 +- lib/sparql/algebra/operator/graph.rb | 22 ++++++++++++++++++++-- lib/sparql/algebra/operator/notoneof.rb | 15 ++++++++++++--- lib/sparql/algebra/operator/reverse.rb | 13 ++++++++++++- lib/sparql/algebra/operator/seq.rb | 2 +- lib/sparql/algebra/operator/sequence.rb | 5 ++++- spec/suite_spec.rb | 14 +++----------- 7 files changed, 53 insertions(+), 20 deletions(-) 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/graph.rb b/lib/sparql/algebra/operator/graph.rb index a20d11ec..1f700cfb 100644 --- a/lib/sparql/algebra/operator/graph.rb +++ b/lib/sparql/algebra/operator/graph.rb @@ -37,6 +37,7 @@ class Operator # :x :p :z # GRAPH ?g { :x :b ?a . GRAPH ?g2 { :x :p ?x } } # } + # # @example SSE (syntax-graph-05.rq) # (prefix ((: )) # (join @@ -47,6 +48,21 @@ class Operator # (graph ?g2 # (bgp (triple :x :p ?x))))))) # + # @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 include Query @@ -117,8 +133,10 @@ def rewrite(&block) # Treat this as a top-level, generating SELECT ... WHERE {} # @return [String] def to_sparql(top_level: true, **options) - str = "GRAPH #{operands.first.to_sparql(**options)} " + - operands.last.to_sparql(top_level: false, **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 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/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/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/spec/suite_spec.rb b/spec/suite_spec.rb index bee96f4d..164a8318 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -142,27 +142,19 @@ 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' skip "Equivalent form" - when 'exists03.rq', 'exists04.rq', 'exists05.rq' - skip('TODO Exists') when 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', 'agg-groupconcat-3.rq', - 'agg-sample-01.rq', 'sq03.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', + 'agg-sample-01.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' pending("TODO SubSelect") - when 'pp06.rq', 'path-ng-01.rq', 'path-ng-02.rq' - pending "TODO graph name on property path" - when 'pp09.rq', 'pp10.rq', 'path-p2.rq', 'path-p4.rq' - pending "TODO property path grouping" when 'syntax-bindings-02a.rq', 'syntax-bindings-03a.rq', 'syntax-bindings-05a.rq' pending "TODO top-level values" when 'syn-pname-05.rq', 'syn-pname-06.rq', 'syn-pname-07.rq', 'syn-codepoint-escape-01.rq', '1val1STRING_LITERAL1_with_UTF8_boundaries.rq', '1val1STRING_LITERAL1_with_UTF8_boundaries_escaped.rq' pending "TODO escaping" - when 'syn-pp-in-collection.rq' - pending "TODO runtime error and list representation" - when 'strafter02.rq ' - pending "TODO odd project multple bindings" end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" From f3752cad76b059fd0b67da4120ce8f29e2675c58 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 12 Jan 2022 11:45:04 -0800 Subject: [PATCH 29/38] Serializing some sub-select cases. --- lib/sparql/algebra/operator/filter.rb | 2 +- lib/sparql/algebra/operator/project.rb | 26 ++++++++++++++++++++++---- spec/algebra/to_sparql_spec.rb | 22 ++++++++++++---------- spec/suite_spec.rb | 8 ++++---- 4 files changed, 39 insertions(+), 19 deletions(-) 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/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/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index 5e1220ad..2ae28ea1 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -5,12 +5,12 @@ include SPARQL::Algebra -shared_examples "SXP to SPARQL" do |name, sxp| +shared_examples "SXP to SPARQL" do |name, sxp, **options| it(name) do 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) + expect(sparql_result).to generate(sxp, resolve_iris: false, production: production, validate: true, **options) end end @@ -25,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 @@ -38,8 +39,9 @@ def self.read_examples read_examples.each do |op, examples| describe "Operator #{op}:" do - examples.each do |sxp| - it_behaves_like "SXP to SPARQL", sxp, sxp + 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 diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 164a8318..df64e577 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -144,11 +144,11 @@ 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' + when 'bind05.rq', 'bind08.rq', 'syntax-bind-02.rq', 'strbefore02.rq', + 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', 'sq08.rq' skip "Equivalent form" - when 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', 'agg-groupconcat-3.rq', - 'agg-sample-01.rq', 'sq08.rq', 'sq09.rq', 'sq11.rq', 'sq12.rq', - 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' + when 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', + 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' pending("TODO SubSelect") when 'syntax-bindings-02a.rq', 'syntax-bindings-03a.rq', 'syntax-bindings-05a.rq' pending "TODO top-level values" From 6ddab97d9c86813c40ef882079f1108ba1fe2e1b Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 17 Jan 2022 14:01:22 -0800 Subject: [PATCH 30/38] Fix some escaping use cases in to_sparql with RDF and SXP gem updates. --- lib/sparql/algebra/extensions.rb | 1 - spec/suite_spec.rb | 3 --- 2 files changed, 4 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 4667af8e..9e6084a6 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -286,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 diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index df64e577..21aaf264 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -152,9 +152,6 @@ pending("TODO SubSelect") when 'syntax-bindings-02a.rq', 'syntax-bindings-03a.rq', 'syntax-bindings-05a.rq' pending "TODO top-level values" - when 'syn-pname-05.rq', 'syn-pname-06.rq', 'syn-pname-07.rq', 'syn-codepoint-escape-01.rq', - '1val1STRING_LITERAL1_with_UTF8_boundaries.rq', '1val1STRING_LITERAL1_with_UTF8_boundaries_escaped.rq' - pending "TODO escaping" end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" From fa38ee0e5c425a0562dca528cc8889e1634e9542 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 17 Jan 2022 14:45:15 -0800 Subject: [PATCH 31/38] Serialize VALUES at the top-level if the join is at the top-level. --- lib/sparql/algebra/operator.rb | 6 +++ lib/sparql/algebra/operator/join.rb | 8 ++- lib/sparql/algebra/operator/table.rb | 8 ++- spec/algebra/to_sparql_spec.rb | 81 ++++++++++++++-------------- spec/suite_spec.rb | 19 +++---- 5 files changed, 69 insertions(+), 53 deletions(-) diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index e930ff83..d5672b52 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -353,6 +353,8 @@ def self.arity # @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 @@ -369,6 +371,7 @@ def self.to_sparql(content, order_ops: [], project: %i(*), reduced: false, + values_clause: nil, where_clause: true, **options) str = "" @@ -438,6 +441,9 @@ def self.to_sparql(content, # LimitOffsetClauses str << "OFFSET #{offset}\n" unless offset.nil? str << "LIMIT #{limit}\n" unless limit.nil? + + # Values Clause + str << values_clause.to_sparql(**options) if values_clause str end diff --git a/lib/sparql/algebra/operator/join.rb b/lib/sparql/algebra/operator/join.rb index b223af2b..3b29aab4 100644 --- a/lib/sparql/algebra/operator/join.rb +++ b/lib/sparql/algebra/operator/join.rb @@ -120,6 +120,7 @@ def optimize!(**options) # Filter Operations # @return [String] 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. @@ -127,7 +128,12 @@ def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) str << "\nFILTER (#{op.to_sparql(**options)}) ." end - str << "\n" + operands.last.to_sparql(top_level: false, extensions: {}, **options) + "\n}" + if top_level && operands.last.is_a?(Table) + str << "\n}" + options = options.merge(values_clause: operands.last) + else + str << "\n" + operands.last.to_sparql(top_level: false, extensions: {}, **options) + "\n}" + end top_level ? Operator.to_sparql(str, extensions: extensions, **options) : str end diff --git a/lib/sparql/algebra/operator/table.rb b/lib/sparql/algebra/operator/table.rb index 40af1b03..8644426a 100644 --- a/lib/sparql/algebra/operator/table.rb +++ b/lib/sparql/algebra/operator/table.rb @@ -27,6 +27,12 @@ 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) @@ -99,7 +105,7 @@ def to_sparql(**options) row[1..-1].each do |col| line << "#{col[1].to_sparql(**options)} " end - line = line.chop + #line = line.chop line << ")\n" str << line diff --git a/spec/algebra/to_sparql_spec.rb b/spec/algebra/to_sparql_spec.rb index 2ae28ea1..76b09e3b 100644 --- a/spec/algebra/to_sparql_spec.rb +++ b/spec/algebra/to_sparql_spec.rb @@ -78,11 +78,11 @@ def self.read_examples # 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" - # } + # } + # 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: ) @@ -93,41 +93,42 @@ def self.read_examples (dataset () (distinct (project (?parent ?child ?child_label) - (filter (contains (str ?parent) "terms/ensembl/") - (join - (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"))))))))) + (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/suite_spec.rb b/spec/suite_spec.rb index 21aaf264..dd978afc 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -150,8 +150,6 @@ when 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' pending("TODO SubSelect") - when 'syntax-bindings-02a.rq', 'syntax-bindings-03a.rq', 'syntax-bindings-05a.rq' - pending "TODO top-level values" end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" @@ -166,21 +164,20 @@ 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', 'delete-insert-04.ru' - pending("SubSelect") + pending("TODO SubSelect") when 'delete-insert-04b.ru', 'delete-insert-05b.ru', 'delete-insert-05b.ru' pending "TODO sub-joins" - when 'syntax-update-38.ru' - pending "empty query" - when 'large-request-01.ru' - skip "large request" end - pending("Whitespace in string tokens") if %w( - syntax-update-26.ru syntax-update-27.ru syntax-update-28.ru - syntax-update-36.ru - ).include?(t.entry) t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}\n" sse = SPARQL.parse(t.action.query_string, From c98ecf7614950a91a1e9b50efcc42a4222c531da Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 17 Jan 2022 16:53:16 -0800 Subject: [PATCH 32/38] More sub-select use cases. --- lib/sparql/algebra/operator/join.rb | 2 +- spec/suite_spec.rb | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/sparql/algebra/operator/join.rb b/lib/sparql/algebra/operator/join.rb index 3b29aab4..dab09d5e 100644 --- a/lib/sparql/algebra/operator/join.rb +++ b/lib/sparql/algebra/operator/join.rb @@ -132,7 +132,7 @@ def to_sparql(top_level: true, filter_ops: [], extensions: {}, **options) str << "\n}" options = options.merge(values_clause: operands.last) else - str << "\n" + operands.last.to_sparql(top_level: false, extensions: {}, **options) + "\n}" + 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 diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index dd978afc..d33aad17 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -145,11 +145,12 @@ 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' - skip "Equivalent form" - when 'sq09.rq', 'sq11.rq', 'sq12.rq', 'sq13.rq', 'sq14.rq', + 'agg-groupconcat-1.rq', 'agg-groupconcat-2.rq', + 'sq08.rq', 'sq12.rq', 'sq13.rq', 'syntax-SELECTscope1.rq', 'syntax-SELECTscope3.rq' - pending("TODO SubSelect") + skip "Equivalent form" + when 'sq09.rq', 'sq14.rq' + pending("SubSelect") end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" @@ -171,12 +172,11 @@ 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', - 'delete-insert-04.ru' - pending("TODO SubSelect") - when 'delete-insert-04b.ru', 'delete-insert-05b.ru', 'delete-insert-05b.ru' - pending "TODO sub-joins" + 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" From a43b1222cb2da75c48c1b73858426cd320bf1e41 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 18 Jan 2022 14:56:08 -0800 Subject: [PATCH 33/38] Parser Ruby evaluation error corner-case iin Query clause. --- lib/sparql/grammar/parser11.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sparql/grammar/parser11.rb b/lib/sparql/grammar/parser11.rb index 4a675ab9..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] From 6b37d56b3f2d8caf0c0a997540d14ebb42d1bf2a Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 18 Jan 2022 14:56:47 -0800 Subject: [PATCH 34/38] SPARQL-star re-serialization use cases with embedded triples. --- lib/sparql/algebra/extensions.rb | 30 ++++++++++----------- lib/sparql/algebra/operator.rb | 13 ++++----- lib/sparql/algebra/operator/bgp.rb | 8 ++++++ lib/sparql/algebra/operator/construct.rb | 2 +- lib/sparql/algebra/operator/delete.rb | 4 ++- lib/sparql/algebra/operator/delete_data.rb | 2 +- lib/sparql/algebra/operator/delete_where.rb | 2 +- lib/sparql/algebra/operator/insert.rb | 4 ++- lib/sparql/algebra/operator/insert_data.rb | 2 +- lib/sparql/algebra/operator/table.rb | 6 +++-- spec/suite_spec.rb | 6 ++++- 11 files changed, 49 insertions(+), 30 deletions(-) diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 9e6084a6..0c94d008 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -373,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 ## @@ -439,7 +441,7 @@ def to_sxp_bin # Filter Operations # @return [String] 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 = @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 if top_level SPARQL::Algebra::Operator.to_sparql(str, filter_ops: filter_ops, **options) @@ -452,11 +454,9 @@ def to_sparql(top_level: true, filter_ops: [], **options) # Extensons extensions = options.fetch(:extensions, []) extensions.each do |as, expression| - str << "\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) + str << "\nBIND (" << v << " AS " << as.to_sparql(**options) << ") ." end str = "{#{str}}" unless filter_ops.empty? && extensions.empty? str diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index d5672b52..a210c3e5 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -384,8 +384,11 @@ 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 @@ -399,11 +402,9 @@ def self.to_sparql(content, # 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 # Filter diff --git a/lib/sparql/algebra/operator/bgp.rb b/lib/sparql/algebra/operator/bgp.rb index 5a039666..6d1704f8 100644 --- a/lib/sparql/algebra/operator/bgp.rb +++ b/lib/sparql/algebra/operator/bgp.rb @@ -13,6 +13,14 @@ class Operator # (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/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/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/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/table.rb b/lib/sparql/algebra/operator/table.rb index 8644426a..4594eba8 100644 --- a/lib/sparql/algebra/operator/table.rb +++ b/lib/sparql/algebra/operator/table.rb @@ -103,9 +103,11 @@ def to_sparql(**options) 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 diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index d33aad17..2cb7531b 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -151,6 +151,10 @@ skip "Equivalent form" when 'sq09.rq', 'sq14.rq' pending("SubSelect") + when 'sparql-star-expr-02.rq' + pending("TODO SPARQL-star values no select") + when 'sparql-star-order-by.rq' + pending("TODO SPARQL-star union reversals") end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" @@ -235,7 +239,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 + it_behaves_like "to_sparql", man.attributes['id'], man.label, man.comment, man.entries end end end From caa30769f75f2db473e7cba7c1d128eed7403347 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 19 Jan 2022 12:10:35 -0800 Subject: [PATCH 35/38] Add back `Expression#evaluate` (used by SHACL) --- lib/sparql/algebra/expression.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/sparql/algebra/expression.rb b/lib/sparql/algebra/expression.rb index 9b7e3b82..3aec2313 100644 --- a/lib/sparql/algebra/expression.rb +++ b/lib/sparql/algebra/expression.rb @@ -350,6 +350,22 @@ def optimize!(**options) self end + ## + # Evaluates this expression using the given variable `bindings`. + # + # This is the default implementation, which simply returns `self`. + # Subclasses can override this method in order to implement something + # more useful. + # + # @param [RDF::Query::Solution] bindings + # a query solution containing zero or more variable bindings + # @param [Hash{Symbol => Object}] options ({}) + # options passed from query + # @return [Expression] `self` + def evaluate(bindings, **options) + self + end + ## # Returns the SPARQL S-Expression (SSE) representation of this expression. # From feaa97c97bc985839252c447fa120d7721c31322 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 19 Jan 2022 12:59:49 -0800 Subject: [PATCH 36/38] Fix corner-case where values is the top-level operator. --- lib/sparql/algebra/operator.rb | 2 +- lib/sparql/algebra/operator/table.rb | 6 ++++-- spec/suite_spec.rb | 2 -- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index a210c3e5..7fd4983a 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -444,7 +444,7 @@ def self.to_sparql(content, str << "LIMIT #{limit}\n" unless limit.nil? # Values Clause - str << values_clause.to_sparql(**options) if values_clause + str << values_clause.to_sparql(top_level: false, **options) if values_clause str end diff --git a/lib/sparql/algebra/operator/table.rb b/lib/sparql/algebra/operator/table.rb index 4594eba8..013fea32 100644 --- a/lib/sparql/algebra/operator/table.rb +++ b/lib/sparql/algebra/operator/table.rb @@ -97,8 +97,10 @@ 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) + 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 = '(' @@ -114,7 +116,7 @@ def to_sparql(**options) end str << "}\n" - str + top_level ? Operator.to_sparql(str, **options) : str end end # Table end # Operator diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index 2cb7531b..da9ab187 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -151,8 +151,6 @@ skip "Equivalent form" when 'sq09.rq', 'sq14.rq' pending("SubSelect") - when 'sparql-star-expr-02.rq' - pending("TODO SPARQL-star values no select") when 'sparql-star-order-by.rq' pending("TODO SPARQL-star union reversals") end From 80334394ec41ae170efcd71c17c7f4ca9eecf211 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 19 Jan 2022 13:37:15 -0800 Subject: [PATCH 37/38] Give up on complex sub-select with slice use case round-trip. --- sparql.gemspec | 4 ++-- spec/suite_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/suite_spec.rb b/spec/suite_spec.rb index da9ab187..731c8392 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -152,7 +152,7 @@ when 'sq09.rq', 'sq14.rq' pending("SubSelect") when 'sparql-star-order-by.rq' - pending("TODO SPARQL-star union reversals") + pending("OFFSET/LIMIT in sub-select") end t.logger = RDF::Spec.logger t.logger.debug "Source:\n#{t.action.query_string}" From f74e01f47b6ec71ed3aa66b59333f9ffe6716e01 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 19 Jan 2022 13:40:02 -0800 Subject: [PATCH 38/38] Version 3.2.1. --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 944880fa..e4604e3a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.2.1