sparql 3.3.1 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +13 -15
- data/VERSION +1 -1
- data/bin/sparql +33 -24
- data/lib/sparql/algebra/aggregate.rb +1 -1
- data/lib/sparql/algebra/expression.rb +2 -2
- data/lib/sparql/algebra/extensions.rb +29 -2
- data/lib/sparql/algebra/operator/bgp.rb +7 -3
- data/lib/sparql/algebra/operator/group_concat.rb +5 -1
- data/lib/sparql/algebra/operator/join.rb +11 -5
- data/lib/sparql/algebra/operator/left_join.rb +13 -12
- data/lib/sparql/algebra/operator/minus.rb +1 -1
- data/lib/sparql/algebra/operator/path.rb +38 -0
- data/lib/sparql/algebra/operator/regex.rb +41 -3
- data/lib/sparql/algebra/operator/table.rb +1 -1
- data/lib/sparql/algebra/operator/union.rb +1 -1
- data/lib/sparql/algebra/operator.rb +8 -0
- data/lib/sparql/algebra.rb +5 -1
- data/lib/sparql/grammar/meta.rb +568 -51294
- data/lib/sparql/grammar/meta11.rb +51297 -0
- data/lib/sparql/grammar/parser.rb +3115 -0
- data/lib/sparql/grammar/parser11.rb +3 -3
- data/lib/sparql/grammar/{terminals11.rb → terminals.rb} +11 -11
- data/lib/sparql/grammar.rb +33 -26
- data/lib/sparql/server.rb +15 -6
- data/lib/sparql.rb +6 -1
- metadata +105 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d98eb5dea7ada1bdeb9335b93088f3a9fcdd3b6556e6665fc87e097f683f8160
|
4
|
+
data.tar.gz: 3ea06346654920eb84518c646ac28eac52ff09ee5f758bf38303ccf528160c0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58ff08062bc059a8858b324365f4ae36b064da2d6f1f6f133735b29d905c576c474679eb34244d787afe3c40b2496bf12835ab7c1cabe9886ebd4709ad8c2725
|
7
|
+
data.tar.gz: 62c32c0d911b28760ac545720575a1ddd9493b38eae10bbe6fc678df9b0abf06c2da17ee4fa0801f0cc0423115081254304fde091ec7e337dfeefd45c33ce72f
|
data/README.md
CHANGED
@@ -27,7 +27,7 @@ An implementation of [SPARQL][] for [RDF.rb][].
|
|
27
27
|
* Implementation Report: {file:etc/earl.html EARL}
|
28
28
|
* Compatible with Ruby >= 3.0.
|
29
29
|
* Supports Unicode query strings both on all versions of Ruby.
|
30
|
-
* Provisional support for [SPARQL
|
30
|
+
* Provisional support for [SPARQL 1.2][].
|
31
31
|
|
32
32
|
## Description
|
33
33
|
|
@@ -68,17 +68,20 @@ Not supported:
|
|
68
68
|
* [Entailment Regimes][SPARQL 1.1 Entailment Regimes], and
|
69
69
|
* [Graph Store HTTP Protocol][SPARQL 1.1 Graph Store HTTP Protocol] but the closely related [Linked Data Platform][] implemented in [rdf-ldp](https://github.com/ruby-rdf/rdf-ldp) supports these use cases.
|
70
70
|
|
71
|
+
### Optimizations
|
72
|
+
Generally, optimizing a query can lead to improved performance, sometimes dramatically (e.g., `?s rdf:rest*/rdf:first ?o`). Optimization can be done when parsing a query using the `:optimize` option, or the `optimize` method on a parsed query.
|
73
|
+
|
71
74
|
### Updates for RDF 1.1
|
72
75
|
Starting with version 1.1.2, the SPARQL gem uses the 1.1 version of the [RDF.rb][], which adheres to [RDF 1.1 Concepts](https://www.w3.org/TR/rdf11-concepts/) rather than [RDF 1.0](https://www.w3.org/TR/rdf-concepts/). The main difference is that there is now no difference between a _Simple Literal_ (a literal with no datatype or language) and a Literal with datatype _xsd:string_; this causes some minor differences in the way in which queries are understood, and when expecting different results.
|
73
76
|
|
74
77
|
Additionally, queries now take a block, or return an `Enumerator`; this is in keeping with much of the behavior of [RDF.rb][] methods, including `Queryable#query`, and with version 1.1 or [RDF.rb][], Query#execute. As a consequence, all queries which used to be of the form `query.execute(repository)` may equally be called as `repository.query(query)`. Previously, results were returned as a concrete class implementing `RDF::Queryable` or `RDF::Query::Solutions`, these are now `Enumerators`.
|
75
78
|
|
76
|
-
### SPARQL
|
77
|
-
The gem supports some of the extensions proposed by the [SPARQL
|
79
|
+
### SPARQL Dev
|
80
|
+
The gem supports some of the extensions proposed by the [SPARQL Dev Community Group](https://github.com/w3c/sparql-dev). In particular, the following extensions are now implemented:
|
78
81
|
|
79
|
-
* [SEP-0002: better support for Durations, Dates, and Times](https://github.com/w3c/sparql-
|
82
|
+
* [SEP-0002: better support for Durations, Dates, and Times](https://github.com/w3c/sparql-dev/blob/main/SEP/SEP-0002/sep-0002.md)
|
80
83
|
* This includes full support for `xsd:date`, `xsd:time`, `xsd:duration`, `xsd:dayTimeDuration`, and `xsd:yearMonthDuration` along with associated XPath/XQuery functions including a new `ADJUST` builtin. (**Note: This feature is subject to change or elimination as the standards process progresses.**)
|
81
|
-
* [SEP-0003: Property paths with a min/max hop](https://github.com/w3c/sparql-
|
84
|
+
* [SEP-0003: Property paths with a min/max hop](https://github.com/w3c/sparql-dev/blob/main/SEP/SEP-0003/sep-0003.md)
|
82
85
|
* This includes support for non-counting path forms such as `rdf:rest{1,3}` to match the union of paths `rdf:rest`, `rdf:rest/rdf:rest`, and `rdf:rest/rdf:rest/rdf:rest`. (**Note: This feature is subject to change or elimination as the standards process progresses.**)
|
83
86
|
|
84
87
|
### SPARQL Extension Functions
|
@@ -107,9 +110,9 @@ See {SPARQL::Algebra::Expression.register_extension} for details.
|
|
107
110
|
|
108
111
|
A call to execute a parsed query can include pre-bound variables, which cause queries to be executed with matching variables bound as defined. Variable pre-binding can be done using a Hash structure, or a Query Solution. See [Query with Binding example](#query-with-binding) and {SPARQL::Algebra::Query#execute}.
|
109
112
|
|
110
|
-
###
|
113
|
+
### SPARQL 1.2
|
111
114
|
|
112
|
-
The gem supports [SPARQL
|
115
|
+
The gem supports [SPARQL 1.2][] where patterns may include sub-patterns recursively, for a kind of Reification.
|
113
116
|
|
114
117
|
For example, the following Turtle* file uses a statement as the subject of another statement:
|
115
118
|
|
@@ -170,7 +173,7 @@ As well as a `CONSTRUCT`:
|
|
170
173
|
<<?bob foaf:age ?age>> ?b ?c .
|
171
174
|
}
|
172
175
|
|
173
|
-
Note that results can be serialized only when the format supports [
|
176
|
+
Note that results can be serialized only when the format supports [SPARQL 1,2][].
|
174
177
|
|
175
178
|
#### SPARQL results
|
176
179
|
|
@@ -462,8 +465,6 @@ see <https://unlicense.org/> or the accompanying {file:UNLICENSE}.
|
|
462
465
|
|
463
466
|
A copy of the [SPARQL EBNF][] and derived parser files are included in the repository, which are not covered under the UNLICENSE. These files are covered via the [W3C Document License](https://www.w3.org/Consortium/Legal/2002/copyright-documents-20021231).
|
464
467
|
|
465
|
-
A copy of the [SPARQL 1.0 tests][] and [SPARQL 1.1 tests][] are also included in the repository, which are not covered under the UNLICENSE; see the references for test copyright information.
|
466
|
-
|
467
468
|
[Ruby]: https://ruby-lang.org/
|
468
469
|
[RDF]: https://www.w3.org/RDF/
|
469
470
|
[RDF::DO]: https://rubygems.org/gems/rdf-do
|
@@ -474,20 +475,17 @@ A copy of the [SPARQL 1.0 tests][] and [SPARQL 1.1 tests][] are also included in
|
|
474
475
|
[PDD]: https://unlicense.org/#unlicensing-contributions
|
475
476
|
[SPARQL]: https://en.wikipedia.org/wiki/SPARQL
|
476
477
|
[SPARQL 1.0]: https://www.w3.org/TR/sparql11-query/
|
477
|
-
[SPARQL 1.0 tests]:https://www.w3.org/2001/sw/DataAccess/tests/
|
478
|
-
[SPARQL 1.1 tests]: https://www.w3.org/2009/sparql/docs/tests/
|
479
478
|
[SSE]: https://jena.apache.org/documentation/notes/sse.html
|
480
479
|
[SXP]: https://dryruby.github.io/sxp
|
481
480
|
[grammar]: https://www.w3.org/TR/sparql11-query/#grammar
|
482
481
|
[RDF 1.1]: https://www.w3.org/TR/rdf11-concepts
|
483
482
|
[RDF.rb]: https://ruby-rdf.github.io/rdf
|
484
|
-
[
|
485
|
-
[SPARQL-star]: https://w3c.github.io/rdf-star/rdf-star-cg-spec.html#sparql-query-language
|
483
|
+
[SPARQL 1.2]: https://www.w3.org/TR/sparql12-query
|
486
484
|
[Linked Data]: https://rubygems.org/gems/linkeddata
|
487
485
|
[SPARQL doc]: https://ruby-rdf.github.io/sparql/frames
|
488
486
|
[SPARQL XML]: https://www.w3.org/TR/rdf-sparql-XMLres/
|
489
487
|
[SPARQL JSON]: https://www.w3.org/TR/rdf-sparql-json-res/
|
490
|
-
[SPARQL EBNF]: https://www.w3.org/TR/
|
488
|
+
[SPARQL EBNF]: https://www.w3.org/TR/sparql12-query/#sparqlGrammar
|
491
489
|
|
492
490
|
[SSD]: https://www.w3.org/TR/sparql11-service-description/
|
493
491
|
[Rack]: https://rack.github.io
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.3.
|
1
|
+
3.3.2
|
data/bin/sparql
CHANGED
@@ -3,6 +3,8 @@ require 'rubygems'
|
|
3
3
|
$:.unshift("../../lib", __FILE__)
|
4
4
|
require 'logger'
|
5
5
|
require 'sparql'
|
6
|
+
require 'rack'
|
7
|
+
require 'rackup'
|
6
8
|
begin
|
7
9
|
require 'linkeddata'
|
8
10
|
rescue LoadError
|
@@ -45,6 +47,8 @@ def run(input, **options)
|
|
45
47
|
SPARQL::Grammar.parse(input, **options)
|
46
48
|
end
|
47
49
|
|
50
|
+
query = query.optimize if options[:optimize]
|
51
|
+
|
48
52
|
puts ("\nSSE:\n" + query.to_sse) if options[:debug]
|
49
53
|
|
50
54
|
if options[:parse_only]
|
@@ -64,42 +68,46 @@ end
|
|
64
68
|
|
65
69
|
def server(options)
|
66
70
|
app = SPARQL::Server.application(**options)
|
67
|
-
|
71
|
+
Rackup::Server.start(app: app, Port: options.fetch(:port, 9292))
|
68
72
|
rescue LoadError
|
69
|
-
$stderr.puts "Running SPARQL server requires Rack and Sinatra to be in environment: #{$!.message}"
|
73
|
+
$stderr.puts "Running SPARQL server requires Rack, Rackup, and Sinatra to be in environment: #{$!.message}"
|
70
74
|
end
|
71
75
|
|
76
|
+
cmd, input = ARGV.shift, nil
|
77
|
+
|
78
|
+
OPT_ARGS = [
|
79
|
+
["--dataset", GetoptLong::REQUIRED_ARGUMENT, "File containing RDF graph or dataset"],
|
80
|
+
["--debug", GetoptLong::NO_ARGUMENT, "Debugging output"],
|
81
|
+
["--execute", "-e", GetoptLong::REQUIRED_ARGUMENT, "Run against source in argument"],
|
82
|
+
["--format", GetoptLong::REQUIRED_ARGUMENT, "Output format for results (json, xml, csv, tsv, html, sparql, sse, or another RDF format)"],
|
83
|
+
["--help", "-?", GetoptLong::NO_ARGUMENT, "print this message"],
|
84
|
+
["--optimize", GetoptLong::NO_ARGUMENT, "Perform query optimizations"],
|
85
|
+
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT, "Port on which to run server; defaults to 9292"],
|
86
|
+
["--sse", GetoptLong::NO_ARGUMENT, "Query input is in SSE format"],
|
87
|
+
["--update", GetoptLong::NO_ARGUMENT, "Process query as a SPARQL Update"],
|
88
|
+
["--verbose", GetoptLong::NO_ARGUMENT, "Verbose output"],
|
89
|
+
]
|
90
|
+
|
72
91
|
def usage
|
73
92
|
puts "Usage: #{File.basename($0)} execute [options] query-file Execute a query against the specified dataset"
|
74
93
|
puts " #{File.basename($0)} parse [options] query-file Parse a query into SPARQL S-Expressions (SSE)"
|
75
94
|
puts " #{File.basename($0)} query [options] end-point query-file Run the query against a remote end-point"
|
76
95
|
puts " #{File.basename($0)} server [options] dataset-file Start a server initialized from the specified dataset"
|
77
96
|
puts "Options:"
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
97
|
+
width = OPT_ARGS.map do |o|
|
98
|
+
l = o.first.length
|
99
|
+
l += o[1].length + 2 if o[1].is_a?(String)
|
100
|
+
l
|
101
|
+
end.max
|
102
|
+
OPT_ARGS.each do |o|
|
103
|
+
s = " %-*s " % [width, (o[1].is_a?(String) ? "#{o[0,2].join(', ')}" : o[0])]
|
104
|
+
s += o.last
|
105
|
+
puts s
|
106
|
+
end
|
87
107
|
exit(0)
|
88
108
|
end
|
89
109
|
|
90
|
-
|
91
|
-
|
92
|
-
opts = GetoptLong.new(
|
93
|
-
["--dataset", GetoptLong::REQUIRED_ARGUMENT],
|
94
|
-
["--debug", GetoptLong::NO_ARGUMENT],
|
95
|
-
["--execute", "-e", GetoptLong::REQUIRED_ARGUMENT],
|
96
|
-
["--format", GetoptLong::REQUIRED_ARGUMENT],
|
97
|
-
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
|
98
|
-
["--sse", GetoptLong::NO_ARGUMENT],
|
99
|
-
["--update", GetoptLong::NO_ARGUMENT],
|
100
|
-
["--verbose", GetoptLong::NO_ARGUMENT],
|
101
|
-
["--help", "-?", GetoptLong::NO_ARGUMENT]
|
102
|
-
)
|
110
|
+
opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]})
|
103
111
|
|
104
112
|
logger = Logger.new(STDERR)
|
105
113
|
logger.level = Logger::WARN
|
@@ -116,6 +124,7 @@ opts.each do |opt, arg|
|
|
116
124
|
when '--debug' then options[:debug] = true ; logger.level = Logger::DEBUG
|
117
125
|
when '--execute' then input = arg
|
118
126
|
when '--format' then options[:format] = arg.to_sym
|
127
|
+
when '--optimize' then options[:optimize] = true
|
119
128
|
when '--port' then options[:port] = arg.to_i
|
120
129
|
when '--sse' then options[:sse] = true
|
121
130
|
when '--update' then options[:update] = true
|
@@ -142,7 +142,7 @@ module SPARQL; module Algebra
|
|
142
142
|
begin
|
143
143
|
# Due to confusion over (triple) and special-case for (qtriple)
|
144
144
|
if operator == RDF::Query::Pattern
|
145
|
-
options = options.merge(
|
145
|
+
options = options.merge(tripleTerm: true) if sse.first == :qtriple
|
146
146
|
elsif operator == Operator::Triple && PATTERN_PARENTS.include?(parent_operator)
|
147
147
|
operator = RDF::Query::Pattern
|
148
148
|
end
|
@@ -428,7 +428,7 @@ module SPARQL; module Algebra
|
|
428
428
|
# @return [SPARQL::Algebra::Expression] `self`
|
429
429
|
# @raise [ArgumentError] if the value is invalid
|
430
430
|
def validate!
|
431
|
-
raise ArgumentError if invalid?
|
431
|
+
raise ArgumentError, "#{self.inspect} is invalid" if invalid?
|
432
432
|
self
|
433
433
|
end
|
434
434
|
alias_method :validate, :validate!
|
@@ -355,7 +355,7 @@ class RDF::Statement
|
|
355
355
|
# Transform Statement Pattern into an SXP
|
356
356
|
# @return [Array]
|
357
357
|
def to_sxp_bin
|
358
|
-
[ (has_graph? ? :quad : (
|
358
|
+
[ (has_graph? ? :quad : (tripleTerm? ? :qtriple : :triple)),
|
359
359
|
(:inferred if inferred?),
|
360
360
|
subject,
|
361
361
|
predicate,
|
@@ -381,7 +381,7 @@ class RDF::Statement
|
|
381
381
|
# @return [String]
|
382
382
|
def to_sparql(**options)
|
383
383
|
str = to_triple.map {|term| term.to_sparql(**options)}.join(" ")
|
384
|
-
|
384
|
+
tripleTerm? ? ('<<(' + str + ')>>') : str
|
385
385
|
end
|
386
386
|
|
387
387
|
##
|
@@ -429,6 +429,23 @@ class RDF::Query
|
|
429
429
|
end
|
430
430
|
end
|
431
431
|
|
432
|
+
# Two queries can be merged if they share the same graph_name
|
433
|
+
#
|
434
|
+
# @param [RDF::Query] other
|
435
|
+
# @return [Boolean]
|
436
|
+
def mergable?(other)
|
437
|
+
other.is_a?(RDF::Query) && self.graph_name == other.graph_name
|
438
|
+
end
|
439
|
+
|
440
|
+
# Two queries are merged by
|
441
|
+
#
|
442
|
+
# @param [RDF::Query] other
|
443
|
+
# @return [RDF::Query]
|
444
|
+
def merge(other)
|
445
|
+
raise ArgumentError, "Can't merge with #{other.class}" unless mergable?(other)
|
446
|
+
self.dup.tap {|q| q.instance_variable_set(:@patterns, q.patterns + other.patterns)}
|
447
|
+
end
|
448
|
+
|
432
449
|
##
|
433
450
|
#
|
434
451
|
# Returns a partial SPARQL grammar for this query.
|
@@ -552,6 +569,16 @@ class RDF::Query::Pattern
|
|
552
569
|
#
|
553
570
|
# @return [Boolean] `true`
|
554
571
|
def executable?; true; end
|
572
|
+
|
573
|
+
##
|
574
|
+
# Returns an S-Expression (SXP) representation
|
575
|
+
#
|
576
|
+
# @param [Hash{Symbol => RDF::URI}] prefixes (nil)
|
577
|
+
# @param [RDF::URI] base_uri (nil)
|
578
|
+
# @return [String]
|
579
|
+
def to_sxp(prefixes: nil, base_uri: nil)
|
580
|
+
to_sxp_bin.to_sxp(prefixes: prefixes, base_uri: base_uri)
|
581
|
+
end
|
555
582
|
end
|
556
583
|
|
557
584
|
##
|
@@ -15,11 +15,15 @@ module SPARQL; module Algebra
|
|
15
15
|
#
|
16
16
|
# @example SPARQL Grammar (sparql-star)
|
17
17
|
# PREFIX : <http://example.com/ns#>
|
18
|
-
# SELECT * {<< :a :b :c >> :p1 :o1.}
|
18
|
+
# SELECT * {<< :a :b :c ~ :r >> :p1 :o1.}
|
19
19
|
#
|
20
20
|
# @example SSE (sparql-star)
|
21
|
-
# (prefix
|
22
|
-
# (
|
21
|
+
# (prefix
|
22
|
+
# ((: <http://example.com/ns#>))
|
23
|
+
# (bgp
|
24
|
+
# (triple :r <http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies>
|
25
|
+
# (qtriple :a :b :c))
|
26
|
+
# (triple :r :p1 :o1)))
|
23
27
|
#
|
24
28
|
# @see https://www.w3.org/TR/sparql11-query/#sparqlAlgebra
|
25
29
|
class BGP < Operator
|
@@ -5,6 +5,8 @@ module SPARQL; module Algebra
|
|
5
5
|
#
|
6
6
|
# GroupConcat is a set function which performs a string concatenation across the values of an expression with a group. The order of the strings is not specified. The separator character used in the concatenation may be given with the scalar argument SEPARATOR.
|
7
7
|
#
|
8
|
+
# If all operands are language-tagged strings with the same language (and direction), the result shares the language (and direction).
|
9
|
+
#
|
8
10
|
# [127] Aggregate::= ... | 'GROUP_CONCAT' '(' 'DISTINCT'? Expression ( ';' 'SEPARATOR' '=' String )? ')'
|
9
11
|
#
|
10
12
|
# @example SPARQL Grammar
|
@@ -72,7 +74,9 @@ module SPARQL; module Algebra
|
|
72
74
|
# @return [RDF::Term] An arbitrary term
|
73
75
|
# @raise [TypeError] If enum is empty
|
74
76
|
def apply(enum, separator, **options)
|
75
|
-
|
77
|
+
op1_lang = enum.first.language
|
78
|
+
lang = op1_lang if op1_lang && enum.all? {|v| v.language == op1_lang}
|
79
|
+
RDF::Literal(enum.flatten.map(&:to_s).join(separator.to_s), language: lang)
|
76
80
|
end
|
77
81
|
|
78
82
|
##
|
@@ -101,11 +101,17 @@ module SPARQL; module Algebra
|
|
101
101
|
#
|
102
102
|
# @return [Join, RDF::Query] `self`
|
103
103
|
# @return [self]
|
104
|
-
# @see SPARQL::Algebra::Expression#optimize
|
105
|
-
def optimize
|
106
|
-
ops = operands.map {|o| o.optimize(**options) }.
|
107
|
-
|
108
|
-
|
104
|
+
# @see SPARQL::Algebra::Expression#optimize
|
105
|
+
def optimize(**options)
|
106
|
+
ops = operands.map {|o| o.optimize(**options) }.reject {|o| o.respond_to?(:empty?) && o.empty?}
|
107
|
+
case ops.length
|
108
|
+
when 0
|
109
|
+
SPARQL::Algebra::Expression[:bgp]
|
110
|
+
when 1
|
111
|
+
ops.first
|
112
|
+
else
|
113
|
+
self.class.new(ops)
|
114
|
+
end
|
109
115
|
end
|
110
116
|
|
111
117
|
##
|
@@ -136,21 +136,22 @@ module SPARQL; module Algebra
|
|
136
136
|
# @return [Object] a copy of `self`
|
137
137
|
# @see SPARQL::Algebra::Expression#optimize
|
138
138
|
# FIXME
|
139
|
-
def optimize
|
140
|
-
|
141
|
-
ops = operands.map {|o| o.optimize(**options) }.select {|o| o.respond_to?(:empty?) && !o.empty?}
|
142
|
-
expr = ops.pop unless ops.last.executable?
|
139
|
+
def optimize(**options)
|
140
|
+
lhs, rhs, expr = operands.map {|o| o.optimize(**options) }
|
143
141
|
expr = nil if expr.respond_to?(:true?) && expr.true?
|
144
|
-
|
145
|
-
|
146
|
-
# expr is a filter expression, which may have been optimized to 'true'
|
147
|
-
case ops.length
|
148
|
-
when 0
|
142
|
+
|
143
|
+
if lhs.empty? && rhs.empty?
|
149
144
|
RDF::Query.new # Empty query, expr doesn't matter
|
150
|
-
|
151
|
-
|
145
|
+
elsif rhs.empty?
|
146
|
+
# Expression doesn't matter, just use the first operand
|
147
|
+
lhs
|
148
|
+
elsif lhs.empty?
|
149
|
+
# Result is the filter of the second operand if there is an expression
|
150
|
+
# FIXME: doesn't seem to work
|
151
|
+
#expr ? Filter.new(expr, rhs) : rhs
|
152
|
+
self.dup
|
152
153
|
else
|
153
|
-
expr ? LeftJoin.new(
|
154
|
+
expr ? LeftJoin.new(rhs, lhs, expr) : LeftJoin.new(lhs, rhs)
|
154
155
|
end
|
155
156
|
end
|
156
157
|
|
@@ -91,7 +91,7 @@ module SPARQL; module Algebra
|
|
91
91
|
# @return [self]
|
92
92
|
# @see SPARQL::Algebra::Expression#optimize!
|
93
93
|
def optimize!(**options)
|
94
|
-
ops = operands.map {|o| o.optimize(**options) }.
|
94
|
+
ops = operands.map {|o| o.optimize(**options) }.reject {|o| o.respond_to?(:empty?) && o.empty?}
|
95
95
|
@operands = ops
|
96
96
|
self
|
97
97
|
end
|
@@ -3,6 +3,8 @@ module SPARQL; module Algebra
|
|
3
3
|
##
|
4
4
|
# The SPARQL Property Path `path` operator.
|
5
5
|
#
|
6
|
+
# The second element represents a set of predicates which ar associated with the first (subject) and last (object) operands.
|
7
|
+
#
|
6
8
|
# [88] Path ::= PathAlternative
|
7
9
|
#
|
8
10
|
# @example SPARQL Grammar
|
@@ -71,6 +73,42 @@ module SPARQL; module Algebra
|
|
71
73
|
str = operands.to_sparql(top_level: false, **options) + " ."
|
72
74
|
top_level ? Operator.to_sparql(str, **options) : str
|
73
75
|
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Special cases for optimizing a path based on its operands.
|
79
|
+
#
|
80
|
+
# @param [Hash{Symbol => Object}] options
|
81
|
+
# any additional options for optimization
|
82
|
+
# @return [SPARQL::Algebra::Operator]
|
83
|
+
# May returnn a different operator
|
84
|
+
# @see RDF::Query#optimize!
|
85
|
+
def optimize(**options)
|
86
|
+
op = super
|
87
|
+
while true
|
88
|
+
decon = op.to_sxp_bin
|
89
|
+
op = case decon
|
90
|
+
# Reverse
|
91
|
+
in [:path, subject, [:reverse, path], object]
|
92
|
+
Path.new(object, path, subject)
|
93
|
+
# Path* (seq (seq p0 (path* p1)) p2)
|
94
|
+
in [:path, subject, [:seq, [:seq, p0, [:'path*', p1]], p2], object]
|
95
|
+
pp1 = Variable.new(nil, distinguished: false)
|
96
|
+
pp2 = Variable.new(nil, distinguished: false)
|
97
|
+
pp3 = Variable.new(nil, distinguished: false)
|
98
|
+
# Bind variables used in Path*
|
99
|
+
bgp = BGP.new(
|
100
|
+
Triple.new(pp2, p2, subject),
|
101
|
+
Triple.new(object, p1, pp3))
|
102
|
+
# New path with pre-bound variables
|
103
|
+
path = Path.new(pp3, PathStar.new(p2), pp2)
|
104
|
+
Sequence.new(bgp, path)
|
105
|
+
else
|
106
|
+
# No matching patterns
|
107
|
+
break
|
108
|
+
end
|
109
|
+
end
|
110
|
+
op
|
111
|
+
end
|
74
112
|
end # Path
|
75
113
|
end # Operator
|
76
114
|
end; end # SPARQL::Algebra
|
@@ -56,11 +56,49 @@ module SPARQL; module Algebra
|
|
56
56
|
flags = flags.to_s
|
57
57
|
# TODO: validate flag syntax
|
58
58
|
|
59
|
+
# 's' mode in XPath is like ruby MUTLILINE
|
60
|
+
# 'm' mode in XPath is like ruby /^$/ vs /\A\z/
|
61
|
+
unless flags.include?(?m)
|
62
|
+
pattern = '\A' + pattern[1..-1] if pattern.start_with?('^')
|
63
|
+
pattern = pattern[0..-2] + '\z' if pattern.end_with?('$')
|
64
|
+
end
|
65
|
+
|
59
66
|
options = 0
|
60
|
-
|
61
|
-
|
67
|
+
if flags.include?('x')
|
68
|
+
flags = flags.sub('x', '')
|
69
|
+
# If present, whitespace characters (#x9, #xA, #xD and #x20) in the regular expression are removed prior to matching with one exception: whitespace characters within character class expressions (charClassExpr) are not removed. This flag can be used, for example, to break up long regular expressions into readable lines.
|
70
|
+
# Scan pattern entering a state when scanning `[` that does nto remove whitespace and exit that state when scanning `]`.
|
71
|
+
in_charclass = false
|
72
|
+
pattern = pattern.chars.map do |c|
|
73
|
+
case c
|
74
|
+
when '['
|
75
|
+
in_charclass = true
|
76
|
+
c
|
77
|
+
when ']'
|
78
|
+
in_charclass = false
|
79
|
+
c
|
80
|
+
else
|
81
|
+
c.match?(/\s/) && !in_charclass ? '' : c
|
82
|
+
end
|
83
|
+
end.join('')
|
84
|
+
end
|
85
|
+
|
86
|
+
if flags.include?('q')
|
87
|
+
flags = flags.sub('x', '')
|
88
|
+
# if present, all characters in the regular expression are treated as representing themselves, not as metacharacters. In effect, every character that would normally have a special meaning in a regular expression is implicitly escaped by preceding it with a backslash.
|
89
|
+
# Simply replace every character with an escaped version of that character
|
90
|
+
pattern = pattern.chars.map do |c|
|
91
|
+
case c
|
92
|
+
when '.', '?', '*', '^', '$', '+', '(', ')', '[', ']', '{', '}'
|
93
|
+
"\\#{c}"
|
94
|
+
else
|
95
|
+
c
|
96
|
+
end
|
97
|
+
end.join("")
|
98
|
+
end
|
99
|
+
|
100
|
+
options |= Regexp::MULTILINE if flags.include?(?s) # dot-all mode
|
62
101
|
options |= Regexp::IGNORECASE if flags.include?(?i)
|
63
|
-
options |= Regexp::EXTENDED if flags.include?(?x)
|
64
102
|
RDF::Literal(Regexp.new(pattern, options) === text)
|
65
103
|
end
|
66
104
|
|
@@ -69,7 +69,7 @@ module SPARQL; module Algebra
|
|
69
69
|
# @return [self]
|
70
70
|
# @see SPARQL::Algebra::Expression#optimize!
|
71
71
|
def optimize!(**options)
|
72
|
-
ops = operands.map {|o| o.optimize(**options) }.
|
72
|
+
ops = operands.map {|o| o.optimize(**options) }.reject {|o| o.respond_to?(:empty?) && o.empty?}
|
73
73
|
@operands = ops
|
74
74
|
self
|
75
75
|
end
|
@@ -812,6 +812,14 @@ module SPARQL; module Algebra
|
|
812
812
|
operands.inject({}) {|hash, o| o.respond_to?(:variables) ? hash.merge(o.variables) : hash}
|
813
813
|
end
|
814
814
|
|
815
|
+
# In generall, two operands cannot be merged
|
816
|
+
#
|
817
|
+
# @param [RDF::Query] other
|
818
|
+
# @return [Boolean]
|
819
|
+
def mergable?(other)
|
820
|
+
false
|
821
|
+
end
|
822
|
+
|
815
823
|
protected
|
816
824
|
|
817
825
|
##
|
data/lib/sparql/algebra.rb
CHANGED
@@ -402,9 +402,13 @@ module SPARQL
|
|
402
402
|
# a SPARQL S-Expression (SSE) string
|
403
403
|
# @param [Hash{Symbol => Object}] options
|
404
404
|
# any additional options (see {Operator#initialize})
|
405
|
+
# @option options [Boolean] :optimize (false)
|
406
|
+
# Run query optimizer after parsing.
|
405
407
|
# @return [SPARQL::Algebra::Operator]
|
406
408
|
def parse(sse, **options)
|
407
|
-
Expression.parse(sse, **options)
|
409
|
+
query = Expression.parse(sse, **options)
|
410
|
+
query = query.optimize if options[:optimize]
|
411
|
+
query
|
408
412
|
end
|
409
413
|
module_function :parse
|
410
414
|
|