Skip to content

Commit

Permalink
Finish 3.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Jan 19, 2022
2 parents bf2fb1e + f74e01f commit cde8a35
Show file tree
Hide file tree
Showing 71 changed files with 1,159 additions and 321 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -22,7 +22,7 @@ jobs:
ruby:
- 2.6
- 2.7
- 3.0
- "3.0"
- 3.1
- ruby-head
- jruby
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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 }"

Expand Down
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
3.2.0
3.2.1
6 changes: 6 additions & 0 deletions examples/function_call.sse
@@ -0,0 +1,6 @@
(prefix ((: <http://example.org/>)
(rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>)
(xsd: <http://www.w3.org/2001/XMLSchema#>))
(project (?a ?v ?boolean)
(extend ((?boolean (xsd:boolean ?v)))
(bgp (triple ?a :p ?v)))))
23 changes: 20 additions & 3 deletions lib/sparql/algebra.rb
Expand Up @@ -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'
Expand Down Expand Up @@ -262,6 +280,7 @@ module SPARQL
# * {SPARQL::Algebra::Operator::Extend}
# * {SPARQL::Algebra::Operator::Filter}
# * {SPARQL::Algebra::Operator::Floor}
# * {SPARQL::Algebra::Operator::FunctionCall}
# * {SPARQL::Algebra::Operator::Graph}
# * {SPARQL::Algebra::Operator::GreaterThan}
# * {SPARQL::Algebra::Operator::GreaterThanOrEqual}
Expand Down Expand Up @@ -339,11 +358,9 @@ module SPARQL
# * {SPARQL::Algebra::Operator::With}
# * {SPARQL::Algebra::Operator::Year}
#
# TODO
# ====
# * Operator#optimize needs to be completed and tested.
#
# @see http://www.w3.org/TR/sparql11-query/#sparqlAlgebra
# @see https://jena.apache.org/documentation/notes/sse.html
module Algebra
include RDF

Expand Down
31 changes: 28 additions & 3 deletions lib/sparql/algebra/expression.rb
Expand Up @@ -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

Expand All @@ -86,6 +88,13 @@ def self.new(sse, **options)
raise ArgumentError, "invalid SPARQL::Algebra::Expression form: #{sse.inspect}" unless sse.is_a?(Array)

operator = Operator.for(sse.first, sse.length - 1)

# If we don't find an operator, and sse.first is an extension IRI, use a function call
if !operator && sse.first.is_a?(RDF::URI) && self.extension?(sse.first)
operator = Operator.for(:function_call, sse.length)
sse.unshift(:function_call)
end

unless operator
return case sse.first
when Array
Expand Down Expand Up @@ -115,11 +124,16 @@ def self.new(sse, **options)
end

debug(options) {"#{operator.inspect}(#{operands.map(&:inspect).join(',')})"}
logger = options[:logger]
options.delete_if {|k, v| [:debug, :logger, :depth, :prefixes, :base_uri, :update, :validate].include?(k) }
begin
operator.new(*operands, **options)
rescue ArgumentError => e
error(options) {"Operator=#{operator.inspect}: #{e}"}
if logger
logger.error("Operator=#{operator.inspect}: #{e}")
else
raise "Operator=#{operator.inspect}: #{e}"
end
end
end

Expand Down Expand Up @@ -163,6 +177,17 @@ def self.extensions
@extensions ||= {}
end

##
# Is an extension function available?
#
# It's either a registered extension, or an XSD casting function
#
# @param [RDF::URI] function
# @return [Boolean]
def self.extension?(function)
function.to_s.start_with?(RDF::XSD.to_s) || self.extensions[function]
end

##
# Invoke an extension function.
#
Expand Down
68 changes: 36 additions & 32 deletions lib/sparql/algebra/extensions.rb
Expand Up @@ -70,28 +70,12 @@ def to_sxp_bin
# Returns a partial SPARQL grammar for this array.
#
# @param [String] delimiter (" ")
# If the first element is an IRI, treat it as an extension function
# @return [String]
def to_sparql(delimiter: " ", **options)
def to_sparql(delimiter: " ", **options)
map {|e| e.to_sparql(**options)}.join(delimiter)
end

##
# Evaluates the array using the given variable `bindings`.
#
# In this case, the Array has two elements, the first of which is
# an XSD datatype, and the second is the expression to be evaluated.
# The result is cast as a literal of the appropriate type
#
# @param [RDF::Query::Solution] bindings
# a query solution containing zero or more variable bindings
# @param [Hash{Symbol => Object}] options ({})
# options passed from query
# @return [RDF::Term]
# @see SPARQL::Algebra::Expression.evaluate
def evaluate(bindings, **options)
SPARQL::Algebra::Expression.extension(*self.map {|o| o.evaluate(bindings, **options)})
end

##
# If `#execute` is invoked, it implies that a non-implemented Algebra operator
# is being invoked
Expand Down Expand Up @@ -302,7 +286,6 @@ def to_sparql(**options)
end
end # RDF::Term


# Override RDF::Queryable to execute against SPARQL::Algebra::Query elements as well as RDF::Query and RDF::Pattern
module RDF::Queryable
alias_method :query_without_sparql, :query
Expand Down Expand Up @@ -390,15 +373,17 @@ def to_sxp(prefixes: nil, base_uri: nil)
# @param [Boolean] as_statement (false) serialize as < ... >, otherwise TRIPLE(...)
# @return [String]
def to_sparql(as_statement: false, **options)
return "TRIPLE(#{to_triple.to_sparql(as_statement: true, **options)})" unless as_statement

to_triple.map do |term|
if term.is_a?(::RDF::Statement)
"<<" + term.to_sparql(**options) + ">>"
else
term.to_sparql(**options)
end
end.join(" ") + " ."
if as_statement
to_triple.map do |term|
if term.is_a?(::RDF::Statement)
"<<" + term.to_sparql(as_statement: true, **options) + ">>"
else
term.to_sparql(**options)
end
end.join(" ")
else
"TRIPLE(#{to_triple.to_sparql(as_statement: true, **options)})"
end
end

##
Expand Down Expand Up @@ -448,15 +433,34 @@ def to_sxp_bin

##
#
# Returns a partial SPARQL grammar for this term.
# Returns a partial SPARQL grammar for this query.
#
# @param [Boolean] top_level (true)
# Treat this as a top-level, generating SELECT ... WHERE {}
# @param [Array<Operator>] filter_ops ([])
# Filter Operations
# @return [String]
def to_sparql(top_level: true, **options)
str = @patterns.map { |e| e.to_sparql(as_statement: true, top_level: false, **options) }.join("\n")
def to_sparql(top_level: true, filter_ops: [], **options)
str = @patterns.map { |e| e.to_sparql(as_statement: true, top_level: false, **options) }.join(". \n")
str = "GRAPH #{graph_name.to_sparql(**options)} {\n#{str}\n}\n" if graph_name
top_level ? SPARQL::Algebra::Operator.to_sparql(str, **options) : str
if top_level
SPARQL::Algebra::Operator.to_sparql(str, filter_ops: filter_ops, **options)
else
# Filters
filter_ops.each do |op|
str << "\nFILTER (#{op.to_sparql(**options)}) ."
end

# Extensons
extensions = options.fetch(:extensions, [])
extensions.each do |as, expression|
v = expression.to_sparql(as_statement: true, **options)
v = "<< #{v} >>" if expression.is_a?(RDF::Statement)
str << "\nBIND (" << v << " AS " << as.to_sparql(**options) << ") ."
end
str = "{#{str}}" unless filter_ops.empty? && extensions.empty?
str
end
end

##
Expand Down

0 comments on commit cde8a35

Please sign in to comment.