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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04020d0f2d52ea9619dc58bf25c0660f62469e492cbf32893345cea0f888bc06
4
- data.tar.gz: f6f982cfcc78222576f741b77bd4665e58f922641906ee8d66ca370063713752
3
+ metadata.gz: d98eb5dea7ada1bdeb9335b93088f3a9fcdd3b6556e6665fc87e097f683f8160
4
+ data.tar.gz: 3ea06346654920eb84518c646ac28eac52ff09ee5f758bf38303ccf528160c0f
5
5
  SHA512:
6
- metadata.gz: 427b55415d048501faad82f0355b74d40e21732e7e3ed61f44cb6f0db5446a58666989c341276db0afdd093b5b77a0bc335aa52de0620a18fe9b1d59cf32201d
7
- data.tar.gz: 775d4e5037ab9d3affaacf71ee7723f6337da9959ce6acf7c5dd24e424706ff681f446a7087ee7af81046cc9b53f9385b38e5e2bc6529164311fa33108fe4742
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-star][].
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 1.2
77
- The gem supports some of the extensions proposed by the [SPARQL 1.2 Community Group](https://github.com/w3c/sparql-12). In particular, the following extensions are now implemented:
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-12/blob/main/SEP/SEP-0002/sep-0002.md)
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-12/blob/main/SEP/SEP-0003/sep-0003.md)
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
- ### SPARQLStar (SPARQL-star)
113
+ ### SPARQL 1.2
111
114
 
112
- The gem supports [SPARQL-star][] where patterns may include sub-patterns recursively, for a kind of Reification.
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 [RDF-star][].
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
- [RDF-star]: https://w3c.github.io/rdf-star/rdf-star-cg-spec.html
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/sparql11-query/#sparqlGrammar
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
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
- Rack::Server.start(app: app, Port: options.fetch(:port, 9292))
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
- puts " --dataset: File containing RDF graph or dataset"
79
- puts " --debug: Display detailed debug output"
80
- puts " --execute,-e: Use option argument as the SPARQL input if no query-file given"
81
- puts " --format: Output format for results (json, xml, csv, tsv, html, sparql, sse, or another RDF format)"
82
- puts " --port,-p Port on which to run server; defaults to 9292"
83
- puts " --sse: Query input is in SSE format"
84
- puts " --update: Process query as a SPARQL Update"
85
- puts " --verbose: Display details of processing"
86
- puts " --help,-?: This message"
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
- cmd, input = ARGV.shift, nil
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
@@ -40,7 +40,7 @@ module SPARQL; module Algebra
40
40
  end
41
41
 
42
42
  ##
43
- # @param [Enumerable<Array<RDF::Term>>] enum
43
+ # @param [Enumerable<Array<RDF::Term>] enum
44
44
  # Enumerable yielding evaluated operands
45
45
  # @return [RDF::Term]
46
46
  # @abstract
@@ -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(quoted: true) if sse.first == :qtriple
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 : (quoted? ? :qtriple : :triple)),
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
- quoted? ? ('<<' + str + '>>') : str
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 ((: <http://example.com/ns#>))
22
- # (bgp (triple (qtriple :a :b :c) :p1 :o1)))
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
- RDF::Literal(enum.flatten.map(&:to_s).join(separator.to_s))
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!(**options)
106
- ops = operands.map {|o| o.optimize(**options) }.select {|o| o.respond_to?(:empty?) && !o.empty?}
107
- @operands = ops
108
- self
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!(**options)
140
- return self
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
- # ops now is one or two executable operators
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
- when 1
151
- expr ? Filter.new(expr, ops.first) : ops.first
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(ops[0], ops[1], expr) : LeftJoin.new(ops[0], ops[1])
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) }.select {|o| o.respond_to?(:empty?) && !o.empty?}
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
- raise NotImplementedError, "unsupported regular expression flag: /s" if flags.include?(?s) # FIXME
61
- options |= Regexp::MULTILINE if flags.include?(?m)
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
 
@@ -31,7 +31,7 @@ module SPARQL; module Algebra
31
31
  # SELECT * { } VALUES () { }
32
32
  #
33
33
  # @example SSE (empty query no values)
34
- # (join (bgp) (table empty))
34
+ # (join (bgp) (table (vars)))
35
35
  #
36
36
  # [61] InlineData ::= 'VALUES' DataBlock
37
37
  #
@@ -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) }.select {|o| o.respond_to?(:empty?) && !o.empty?}
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
  ##
@@ -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