rdf 3.1.0 → 3.1.5

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.
@@ -77,14 +77,15 @@ module RDF; class Query
77
77
  ##
78
78
  # Returns the normalization of the specified `hash_pattern`.
79
79
  #
80
- # @param [Hash{Symbol => Object}] hash_pattern (Hash.new)
81
- # the query pattern as a hash.
82
- # @param [Hash{Symbol => Object}] options (Hash.new)
83
- # any additional normalization options.
84
- # @option options [String] :anonymous_subject_format ("__%s__")
85
- # the string format for anonymous subjects.
86
- # @return [Hash{Symbol => Object}]
87
- # the resulting query pattern as a normalized hash.
80
+ # @overload normalize!(hash_pattern, **options)
81
+ # @param [Hash{Symbol => Object}] hash_pattern (Hash.new)
82
+ # the query pattern as a hash.
83
+ # @param [Hash{Symbol => Object}] **options
84
+ # any additional normalization options.
85
+ # @option options [String] :anonymous_subject_format ("__%s__")
86
+ # the string format for anonymous subjects.
87
+ # @return [Hash{Symbol => Object}]
88
+ # the resulting query pattern as a normalized hash.
88
89
  def normalize!(*args)
89
90
  hash_pattern = args.shift
90
91
  options = args.shift || {}
@@ -50,10 +50,12 @@ module RDF; class Query
50
50
 
51
51
  # Estmate cost positionally, with variables being least expensive as objects, then predicates, then subjects, then graph_names.
52
52
  # XXX does not consider bound variables, which would need to be dynamically calculated.
53
- @cost = (@object.nil? || @object.is_a?(Variable) ? 1 : 0) +
54
- (@predicate.nil? || @predicate.is_a?(Variable) ? 2 : 0) +
55
- (@subject.nil? || @subject.is_a?(Variable) ? 4 : 0) +
56
- (@graph_name.is_a?(Variable) ? 8 : 0)
53
+ @cost = (@object.nil? || @object.is_a?(Variable) ? 8 : 0) +
54
+ (@predicate.nil? || @predicate.is_a?(Variable) ? 4 : 0) +
55
+ (@subject.nil? || @subject.is_a?(Variable) ? 2 : 0) +
56
+ (@graph_name.is_a?(Variable) ? 1 : 0) +
57
+ (@object.is_a?(Pattern) ? (@object.cost * 4) : 0) +
58
+ (@subject.is_a?(Pattern) ? (@subject.cost * 2) : 0)
57
59
  super
58
60
  end
59
61
 
@@ -84,10 +86,10 @@ module RDF; class Query
84
86
  # @return [Boolean] `true` or `false`
85
87
  # @since 0.3.0
86
88
  def has_variables?
87
- subject.is_a?(Variable) ||
88
- predicate.is_a?(Variable) ||
89
- object.is_a?(Variable) ||
90
- graph_name.is_a?(Variable)
89
+ subject && subject.variable? ||
90
+ predicate && predicate.variable? ||
91
+ object && object.variable? ||
92
+ graph_name && graph_name.variable?
91
93
  end
92
94
  alias_method :variables?, :has_variables?
93
95
 
@@ -117,13 +119,33 @@ module RDF; class Query
117
119
  false
118
120
  end
119
121
 
122
+ ##
123
+ # Checks pattern equality against a statement, considering nesting.
124
+ #
125
+ # * A pattern which has a pattern as a subject or an object, matches
126
+ # a statement having a statement as a subject or an object using {#eql?}.
127
+ #
128
+ # @param [Statement] other
129
+ # @return [Boolean]
130
+ #
131
+ # @see RDF::URI#==
132
+ # @see RDF::Node#==
133
+ # @see RDF::Literal#==
134
+ # @see RDF::Query::Variable#==
135
+ def eql?(other)
136
+ return false unless other.is_a?(Statement) && (self.graph_name || false) == (other.graph_name || false)
137
+
138
+ predicate == other.predicate &&
139
+ (subject.is_a?(Pattern) ? subject.eql?(other.subject) : subject == other.subject) &&
140
+ (object.is_a?(Pattern) ? object.eql?(other.object) : object == other.object)
141
+ end
142
+
120
143
  ##
121
144
  # Executes this query pattern on the given `queryable` object.
122
145
  #
123
146
  # Values are matched using using Queryable#query_pattern.
124
147
  #
125
- # If the optional `bindings` are given, variables will be substituted with their values
126
- # when executing the query.
148
+ # If the optional `bindings` are given, variables will be substituted with their values when executing the query.
127
149
  #
128
150
  # To match triples only in the default graph, set graph_name to `false`.
129
151
  #
@@ -198,6 +220,8 @@ module RDF; class Query
198
220
  solution[predicate.to_sym] = statement.predicate if predicate.is_a?(Variable)
199
221
  solution[object.to_sym] = statement.object if object.is_a?(Variable)
200
222
  solution[graph_name.to_sym] = statement.graph_name if graph_name.is_a?(Variable)
223
+ solution.merge!(subject.solution(statement.subject)) if subject.is_a?(Pattern)
224
+ solution.merge!(object.solution(statement.object)) if object.is_a?(Pattern)
201
225
  end
202
226
  end
203
227
 
@@ -229,7 +253,8 @@ module RDF; class Query
229
253
  # @return [Integer] (0..3)
230
254
  def variable_count
231
255
  [subject, predicate, object, graph_name].inject(0) do |memo, term|
232
- memo += (term.is_a?(Variable) ? 1 : 0)
256
+ memo += (term.is_a?(Variable) ? 1 :
257
+ (term.is_a?(Pattern) ? term.variable_count : 0))
233
258
  end
234
259
  end
235
260
  alias_method :cardinality, :variable_count
@@ -243,7 +268,7 @@ module RDF; class Query
243
268
  # @return [Hash{Symbol => Variable}]
244
269
  def variables
245
270
  [subject, predicate, object, graph_name].inject({}) do |memo, term|
246
- term.is_a?(Variable) ? memo.merge(term.variables) : memo
271
+ term && term.variable? ? memo.merge(term.variables) : memo
247
272
  end
248
273
  end
249
274
 
@@ -254,8 +279,10 @@ module RDF; class Query
254
279
  # @return [self]
255
280
  def bind(solution)
256
281
  self.to_quad.each_with_index do |term, index|
257
- if term && term.variable? && solution[term]
282
+ if term.is_a?(Variable) && solution[term]
258
283
  self[index] = solution[term]
284
+ elsif term.is_a?(Pattern)
285
+ term.bind(solution)
259
286
  end
260
287
  end
261
288
  self
@@ -283,9 +310,9 @@ module RDF; class Query
283
310
  # @return [Hash{Symbol => RDF::Term}]
284
311
  def bindings
285
312
  bindings = {}
286
- bindings.merge!(subject.bindings) if subject.is_a?(Variable)
313
+ bindings.merge!(subject.bindings) if subject && subject.variable?
287
314
  bindings.merge!(predicate.bindings) if predicate.is_a?(Variable)
288
- bindings.merge!(object.bindings) if object.is_a?(Variable)
315
+ bindings.merge!(object.bindings) if object && object.variable?
289
316
  bindings.merge!(graph_name.bindings) if graph_name.is_a?(Variable)
290
317
  bindings
291
318
  end
@@ -330,7 +357,13 @@ module RDF; class Query
330
357
  StringIO.open do |buffer| # FIXME in RDF::Statement
331
358
  buffer << 'OPTIONAL ' if optional?
332
359
  buffer << [subject, predicate, object].map do |r|
333
- r.is_a?(RDF::Query::Variable) ? r.to_s : RDF::NTriples.serialize(r)
360
+ if r.is_a?(RDF::Query::Variable)
361
+ r.to_s
362
+ elsif r.is_a?(RDF::Query::Pattern)
363
+ "<<#{r.to_s[0..-3]}>>"
364
+ else
365
+ RDF::NTriples.serialize(r)
366
+ end
334
367
  end.join(" ")
335
368
  buffer << case graph_name
336
369
  when nil, false then " ."
@@ -195,12 +195,31 @@ class RDF::Query
195
195
  # Merges the bindings from the given `other` query solution into this
196
196
  # one, overwriting any existing ones having the same name.
197
197
  #
198
+ # ## RDFStar (RDF*)
199
+ #
200
+ # If merging a binding for a statement to a pattern,
201
+ # merge their embedded solutions.
202
+ #
198
203
  # @param [RDF::Query::Solution, #to_h] other
199
204
  # another query solution or hash bindings
200
205
  # @return [void] self
201
206
  # @since 0.3.0
202
207
  def merge!(other)
203
- @bindings.merge!(other.to_h)
208
+ @bindings.merge!(other.to_h) do |key, v1, v2|
209
+ # Don't merge a pattern over a statement
210
+ # This happens because JOIN does a reverse merge,
211
+ # and a pattern is set in v2.
212
+ v2.is_a?(Pattern) ? v1 : v2
213
+ end
214
+ # Merge bindings from patterns
215
+ embedded_solutions = []
216
+ @bindings.each do |k, v|
217
+ if v.is_a?(Pattern) && other[k].is_a?(RDF::Statement)
218
+ embedded_solutions << v.solution(other[k])
219
+ end
220
+ end
221
+ # Merge embedded solutions
222
+ embedded_solutions.each {|soln| merge!(soln)}
204
223
  self
205
224
  end
206
225
 
@@ -66,13 +66,15 @@ module RDF; class Query
66
66
  #
67
67
  # @return [Array<Symbol>]
68
68
  def variable_names
69
- variables = self.inject({}) do |result, solution|
70
- solution.each_name do |name|
71
- result[name] ||= true
69
+ @variable_names ||= begin
70
+ variables = self.inject({}) do |result, solution|
71
+ solution.each_name do |name|
72
+ result[name] ||= true
73
+ end
74
+ result
72
75
  end
73
- result
76
+ variables.keys
74
77
  end
75
- variables.keys
76
78
  end
77
79
 
78
80
  ##
@@ -136,6 +138,7 @@ module RDF; class Query
136
138
  # @yieldreturn [Boolean]
137
139
  # @return [self]
138
140
  def filter(criteria = {})
141
+ @variable_names = nil
139
142
  if block_given?
140
143
  self.reject! do |solution|
141
144
  !yield(solution.is_a?(Solution) ? solution : Solution.new(solution))
@@ -223,6 +226,13 @@ module RDF; class Query
223
226
  solution.bindings.delete_if { |k, v| !variables.include?(k.to_sym) }
224
227
  end
225
228
  end
229
+
230
+ # Make sure variable_names are ordered by projected variables
231
+ projected_vars, vars = variables.map(&:to_sym), variable_names
232
+ vars = variable_names
233
+
234
+ # Maintain projected order, and add any non-projected variables
235
+ @variable_names = (projected_vars & vars) + (vars - projected_vars)
226
236
  self
227
237
  end
228
238
  alias_method :select, :project
@@ -157,12 +157,24 @@ class RDF::Query
157
157
  ##
158
158
  # Rebinds this variable to the given `value`.
159
159
  #
160
- # @param [RDF::Term] value
161
- # @return [RDF::Term] the previous value, if any.
160
+ # @overload bind(value)
161
+ # @param [RDF::Query::Solution] value
162
+ # @return [self] the bound variable
163
+ #
164
+ # @overload bind(value)
165
+ # @param [RDF::Term] value
166
+ # @return [RDF::Term] the previous value, if any.
162
167
  def bind(value)
163
- old_value = self.value
164
- self.value = value
165
- old_value
168
+ if value.is_a?(RDF::Query::Solution)
169
+ self.value = value.to_h.fetch(name, self.value)
170
+ self
171
+ else
172
+ warn "[DEPRECATION] RDF::Query::Variable#bind should be used with a solution, not a term.\n" +
173
+ "Called from #{Gem.location_of_caller.join(':')}"
174
+ old_value = self.value
175
+ self.value = value
176
+ old_value
177
+ end
166
178
  end
167
179
  alias_method :bind!, :bind
168
180
 
@@ -121,6 +121,12 @@ module RDF
121
121
  # @return [Array<RDF::CLI::Option>]
122
122
  def self.options
123
123
  [
124
+ RDF::CLI::Option.new(
125
+ symbol: :base_uri,
126
+ control: :url,
127
+ datatype: RDF::URI,
128
+ on: ["--uri URI"],
129
+ description: "Base URI of input file, defaults to the filename.") {|arg| RDF::URI(arg)},
124
130
  RDF::CLI::Option.new(
125
131
  symbol: :canonicalize,
126
132
  datatype: TrueClass,
@@ -153,11 +159,11 @@ module RDF
153
159
  end
154
160
  end,
155
161
  RDF::CLI::Option.new(
156
- symbol: :base_uri,
157
- control: :url,
158
- datatype: RDF::URI,
159
- on: ["--uri URI"],
160
- description: "Base URI of input file, defaults to the filename.") {|arg| RDF::URI(arg)},
162
+ symbol: :rdfstar,
163
+ control: :select,
164
+ datatype: [:PG, :SA],
165
+ on: ["--rdf-star MODE"],
166
+ description: "Parse RDF*, either in Property Graph mode (PG) or Separate Assertions mode (SA).") {|arg| arg.to_sym},
161
167
  RDF::CLI::Option.new(
162
168
  symbol: :validate,
163
169
  datatype: TrueClass,
@@ -261,42 +267,48 @@ module RDF
261
267
  #
262
268
  # @param [IO, File, String] input
263
269
  # the input stream to read
264
- # @param [Encoding] encoding (Encoding::UTF_8)
265
- # the encoding of the input stream
266
- # @param [Boolean] validate (false)
267
- # whether to validate the parsed statements and values
270
+ # @param [#to_s] base_uri (nil)
271
+ # the base URI to use when resolving relative URIs (not supported by
272
+ # all readers)
268
273
  # @param [Boolean] canonicalize (false)
269
274
  # whether to canonicalize parsed literals
275
+ # @param [Encoding] encoding (Encoding::UTF_8)
276
+ # the encoding of the input stream
270
277
  # @param [Boolean] intern (true)
271
278
  # whether to intern all parsed URIs
279
+ # @param [:PG, :SA] rdfstar (nil)
280
+ # support parsing RDF* statement resources.
281
+ # If `:PG`, referenced statements are also emitted.
282
+ # If `:SA`, referenced statements are not emitted.
272
283
  # @param [Hash] prefixes (Hash.new)
273
284
  # the prefix mappings to use (not supported by all readers)
274
- # @param [#to_s] base_uri (nil)
275
- # the base URI to use when resolving relative URIs (not supported by
276
- # all readers)
277
285
  # @param [Hash{Symbol => Object}] options
278
286
  # any additional options
287
+ # @param [Boolean] validate (false)
288
+ # whether to validate the parsed statements and values
279
289
  # @yield [reader] `self`
280
290
  # @yieldparam [RDF::Reader] reader
281
291
  # @yieldreturn [void] ignored
282
292
  def initialize(input = $stdin,
283
- encoding: Encoding::UTF_8,
284
- validate: false,
293
+ base_uri: nil,
285
294
  canonicalize: false,
295
+ encoding: Encoding::UTF_8,
286
296
  intern: true,
287
297
  prefixes: Hash.new,
288
- base_uri: nil,
298
+ rdfstar: nil,
299
+ validate: false,
289
300
  **options,
290
301
  &block)
291
302
 
292
303
  base_uri ||= input.base_uri if input.respond_to?(:base_uri)
293
304
  @options = options.merge({
294
- encoding: encoding,
295
- validate: validate,
305
+ base_uri: base_uri,
296
306
  canonicalize: canonicalize,
307
+ encoding: encoding,
297
308
  intern: intern,
298
309
  prefixes: prefixes,
299
- base_uri: base_uri
310
+ rdfstar: rdfstar,
311
+ validate: validate
300
312
  })
301
313
 
302
314
  @input = case input
@@ -389,6 +401,9 @@ module RDF
389
401
  # Statements are yielded in the order that they are read from the input
390
402
  # stream.
391
403
  #
404
+ # If the `rdfstar` option is `:PG` and triples include
405
+ # embedded statements, they are also enumerated.
406
+ #
392
407
  # @overload each_statement
393
408
  # @yield [statement]
394
409
  # each statement
@@ -405,7 +420,11 @@ module RDF
405
420
  def each_statement(&block)
406
421
  if block_given?
407
422
  begin
408
- loop { block.call(read_statement) }
423
+ loop do
424
+ st = read_statement
425
+ block.call(st)
426
+ each_pg_statement(st, &block) if options[:rdfstar] == :PG
427
+ end
409
428
  rescue EOFError
410
429
  rewind rescue nil
411
430
  end
@@ -422,6 +441,9 @@ module RDF
422
441
  # Triples are yielded in the order that they are read from the input
423
442
  # stream.
424
443
  #
444
+ # If the `rdfstar` option is `:PG` and triples include
445
+ # embedded statements, they are also enumerated.
446
+ #
425
447
  # @overload each_triple
426
448
  # @yield [subject, predicate, object]
427
449
  # each triple
@@ -439,7 +461,14 @@ module RDF
439
461
  def each_triple(&block)
440
462
  if block_given?
441
463
  begin
442
- loop { block.call(*read_triple) }
464
+ loop do
465
+ triple = read_triple
466
+ block.call(*triple)
467
+ if options[:rdfstar] == :PG
468
+ block.call(*triple[0].to_a) if triple[0].is_a?(Statement)
469
+ block.call(*triple[2].to_a) if triple[2].is_a?(Statement)
470
+ end
471
+ end
443
472
  rescue EOFError
444
473
  rewind rescue nil
445
474
  end
@@ -550,6 +579,22 @@ module RDF
550
579
  log_error("Expected object (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
551
580
  end
552
581
 
582
+ ##
583
+ # Recursively emit embedded statements in Property Graph mode
584
+ #
585
+ # @param [RDF::Statement] statement
586
+ def each_pg_statement(statement, &block)
587
+ if statement.subject.is_a?(Statement)
588
+ block.call(statement.subject)
589
+ each_pg_statement(statement.subject, &block)
590
+ end
591
+
592
+ if statement.object.is_a?(Statement)
593
+ block.call(statement.object)
594
+ each_pg_statement(statement.object, &block)
595
+ end
596
+ end
597
+
553
598
  public
554
599
  ##
555
600
  # Returns the encoding of the input stream.
@@ -182,6 +182,7 @@ module RDF
182
182
  when :validity then @options.fetch(:with_validity, true)
183
183
  when :literal_equality then true
184
184
  when :atomic_write then false
185
+ when :rdfstar then false
185
186
  when :snapshots then false
186
187
  else false
187
188
  end
@@ -266,6 +267,7 @@ module RDF
266
267
  when :validity then @options.fetch(:with_validity, true)
267
268
  when :literal_equality then true
268
269
  when :atomic_write then true
270
+ when :rdfstar then true
269
271
  when :snapshots then true
270
272
  else false
271
273
  end
@@ -340,7 +342,14 @@ module RDF
340
342
  # @see Mutable#apply_changeset
341
343
  def apply_changeset(changeset)
342
344
  data = @data
343
- changeset.deletes.each { |del| data = delete_from(data, del) }
345
+ changeset.deletes.each do |del|
346
+ if del.constant?
347
+ data = delete_from(data, del)
348
+ else
349
+ # we need this condition to handle wildcard statements
350
+ query_pattern(del) { |stmt| data = delete_from(data, stmt) }
351
+ end
352
+ end
344
353
  changeset.inserts.each { |ins| data = insert_to(data, ins) }
345
354
  @data = data
346
355
  end
@@ -388,20 +397,25 @@ module RDF
388
397
  graph_name.eql?(c)
389
398
 
390
399
  ss = if subject.nil? || subject.is_a?(RDF::Query::Variable)
391
- ss
392
- elsif ss.has_key?(subject)
393
- { subject => ss[subject] }
394
- else
395
- []
396
- end
400
+ ss
401
+ elsif subject.is_a?(RDF::Query::Pattern)
402
+ # Match subjects which are statements matching this pattern
403
+ ss.keys.select {|s| s.statement? && subject.eql?(s)}.inject({}) do |memo, st|
404
+ memo.merge(st => ss[st])
405
+ end
406
+ elsif ss.has_key?(subject)
407
+ { subject => ss[subject] }
408
+ else
409
+ []
410
+ end
397
411
  ss.each do |s, ps|
398
412
  ps = if predicate.nil? || predicate.is_a?(RDF::Query::Variable)
399
- ps
400
- elsif ps.has_key?(predicate)
401
- { predicate => ps[predicate] }
402
- else
403
- []
404
- end
413
+ ps
414
+ elsif ps.has_key?(predicate)
415
+ { predicate => ps[predicate] }
416
+ else
417
+ []
418
+ end
405
419
  ps.each do |p, os|
406
420
  os.each do |o, object_options|
407
421
  next unless object.nil? || object.eql?(o)