rdf 3.0.11 → 3.1.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +1 -1
  3. data/README.md +127 -95
  4. data/UNLICENSE +1 -1
  5. data/VERSION +1 -1
  6. data/etc/doap.nt +79 -85
  7. data/lib/rdf.rb +35 -23
  8. data/lib/rdf/changeset.rb +80 -19
  9. data/lib/rdf/cli.rb +7 -7
  10. data/lib/rdf/format.rb +17 -10
  11. data/lib/rdf/mixin/enumerable.rb +4 -3
  12. data/lib/rdf/mixin/mutable.rb +5 -15
  13. data/lib/rdf/mixin/queryable.rb +12 -4
  14. data/lib/rdf/mixin/transactable.rb +2 -2
  15. data/lib/rdf/mixin/writable.rb +9 -14
  16. data/lib/rdf/model/dataset.rb +1 -1
  17. data/lib/rdf/model/graph.rb +7 -4
  18. data/lib/rdf/model/list.rb +5 -5
  19. data/lib/rdf/model/literal.rb +3 -3
  20. data/lib/rdf/model/statement.rb +32 -9
  21. data/lib/rdf/model/uri.rb +53 -32
  22. data/lib/rdf/nquads.rb +6 -6
  23. data/lib/rdf/ntriples.rb +7 -5
  24. data/lib/rdf/ntriples/reader.rb +29 -7
  25. data/lib/rdf/ntriples/writer.rb +10 -1
  26. data/lib/rdf/query.rb +27 -35
  27. data/lib/rdf/query/hash_pattern_normalizer.rb +14 -12
  28. data/lib/rdf/query/pattern.rb +51 -18
  29. data/lib/rdf/query/solution.rb +20 -1
  30. data/lib/rdf/query/solutions.rb +15 -5
  31. data/lib/rdf/query/variable.rb +17 -5
  32. data/lib/rdf/reader.rb +76 -25
  33. data/lib/rdf/repository.rb +32 -18
  34. data/lib/rdf/transaction.rb +1 -1
  35. data/lib/rdf/util.rb +6 -5
  36. data/lib/rdf/util/cache.rb +2 -2
  37. data/lib/rdf/util/coercions.rb +60 -0
  38. data/lib/rdf/util/file.rb +20 -10
  39. data/lib/rdf/util/logger.rb +6 -6
  40. data/lib/rdf/util/uuid.rb +4 -4
  41. data/lib/rdf/vocab/owl.rb +401 -86
  42. data/lib/rdf/vocab/rdfs.rb +81 -18
  43. data/lib/rdf/vocab/rdfv.rb +147 -1
  44. data/lib/rdf/vocab/writer.rb +41 -3
  45. data/lib/rdf/vocab/xsd.rb +203 -2
  46. data/lib/rdf/vocabulary.rb +73 -15
  47. data/lib/rdf/writer.rb +33 -11
  48. metadata +34 -28
@@ -69,9 +69,9 @@ module RDF
69
69
 
70
70
  begin
71
71
  unless blank? || read_comment
72
- subject = read_uriref || read_node || fail_subject
72
+ subject = read_uriref || read_node || read_rdfstar || fail_subject
73
73
  predicate = read_uriref(intern: true) || fail_predicate
74
- object = read_uriref || read_node || read_literal || fail_object
74
+ object = read_uriref || read_node || read_literal || read_rdfstar || fail_object
75
75
  graph_name = read_uriref || read_node
76
76
  if validate? && !read_eos
77
77
  log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
@@ -96,7 +96,7 @@ module RDF
96
96
  # @param [RDF::Term] object
97
97
  # @return [void]
98
98
  def write_quad(subject, predicate, object, graph_name)
99
- puts format_quad(subject, predicate, object, graph_name, @options)
99
+ puts format_quad(subject, predicate, object, graph_name, **@options)
100
100
  end
101
101
 
102
102
  ##
@@ -107,7 +107,7 @@ module RDF
107
107
  # @return [String]
108
108
  # @since 0.4.0
109
109
  def format_statement(statement, **options)
110
- format_quad(*statement.to_quad, options)
110
+ format_quad(*statement.to_quad, **options)
111
111
  end
112
112
 
113
113
  ##
@@ -120,8 +120,8 @@ module RDF
120
120
  # @param [Hash{Symbol => Object}] options = ({})
121
121
  # @return [String]
122
122
  def format_quad(subject, predicate, object, graph_name, **options)
123
- s = "%s %s %s " % [subject, predicate, object].map { |value| format_term(value, options) }
124
- s += format_term(graph_name, options) + " " if graph_name
123
+ s = "%s %s %s " % [subject, predicate, object].map { |value| format_term(value, **options) }
124
+ s += format_term(graph_name, **options) + " " if graph_name
125
125
  s + "."
126
126
  end
127
127
  end # Writer
@@ -13,17 +13,19 @@ module RDF
13
13
  #
14
14
  # An example of an RDF statement in N-Triples format:
15
15
  #
16
- # <http://rubygems.org/gems/rdf> <http://purl.org/dc/terms/title> "rdf" .
16
+ # <https://rubygems.org/gems/rdf> <http://purl.org/dc/terms/title> "rdf" .
17
17
  #
18
- # Installation
19
- # ------------
18
+ # ## RDFStar (RDF*)
19
+ #
20
+ # Supports statements as resources using `<<s p o>>`.
21
+ #
22
+ # ## Installation
20
23
  #
21
24
  # This is the only RDF serialization format that is directly supported by
22
25
  # RDF.rb. Support for other formats is available in the form of add-on
23
26
  # gems, e.g. 'rdf-xml' or 'rdf-json'.
24
27
  #
25
- # Documentation
26
- # -------------
28
+ # ## Documentation
27
29
  #
28
30
  # * {RDF::NTriples::Format}
29
31
  # * {RDF::NTriples::Reader}
@@ -25,6 +25,10 @@ module RDF::NTriples
25
25
  # end
26
26
  # end
27
27
  #
28
+ # ** RDFStar (RDF*)
29
+ #
30
+ # Supports statements as resources using `<<s p o>>`.
31
+ #
28
32
  # @see http://www.w3.org/TR/rdf-testcases/#ntriples
29
33
  # @see http://www.w3.org/TR/n-triples/
30
34
  class Reader < RDF::Reader
@@ -70,6 +74,10 @@ module RDF::NTriples
70
74
  # 22
71
75
  STRING_LITERAL_QUOTE = /"((?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*)"/.freeze
72
76
 
77
+ # RDF*
78
+ ST_START = /^<</.freeze
79
+ ST_END = /^\s*>>/.freeze
80
+
73
81
  # @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar
74
82
  COMMENT = /^#\s*(.*)$/.freeze
75
83
  NODEID = /^#{BLANK_NODE_LABEL}/.freeze
@@ -96,7 +104,7 @@ module RDF::NTriples
96
104
  def self.unserialize(input, **options)
97
105
  case input
98
106
  when nil then nil
99
- else self.new(input, {logger: []}.merge(options)).read_value
107
+ else self.new(input, logger: [], **options).read_value
100
108
  end
101
109
  end
102
110
 
@@ -104,20 +112,20 @@ module RDF::NTriples
104
112
  # (see unserialize)
105
113
  # @return [RDF::Resource]
106
114
  def self.parse_subject(input, **options)
107
- parse_uri(input, options) || parse_node(input, options)
115
+ parse_uri(input, **options) || parse_node(input, **options)
108
116
  end
109
117
 
110
118
  ##
111
119
  # (see unserialize)
112
120
  # @return [RDF::URI]
113
- def self.parse_predicate(input, *options)
121
+ def self.parse_predicate(input, **options)
114
122
  parse_uri(input, intern: true)
115
123
  end
116
124
 
117
125
  ##
118
126
  # (see unserialize)
119
127
  def self.parse_object(input, **options)
120
- parse_uri(input, options) || parse_node(input, options) || parse_literal(input, options)
128
+ parse_uri(input, **options) || parse_node(input, **options) || parse_literal(input, **options)
121
129
  end
122
130
 
123
131
  ##
@@ -202,7 +210,7 @@ module RDF::NTriples
202
210
  begin
203
211
  read_statement
204
212
  rescue RDF::ReaderError
205
- value = read_uriref || read_node || read_literal
213
+ value = read_uriref || read_node || read_literal || read_rdfstar
206
214
  log_recover
207
215
  value
208
216
  end
@@ -218,9 +226,9 @@ module RDF::NTriples
218
226
 
219
227
  begin
220
228
  unless blank? || read_comment
221
- subject = read_uriref || read_node || fail_subject
229
+ subject = read_uriref || read_node || read_rdfstar || fail_subject
222
230
  predicate = read_uriref(intern: true) || fail_predicate
223
- object = read_uriref || read_node || read_literal || fail_object
231
+ object = read_uriref || read_node || read_literal || read_rdfstar || fail_object
224
232
 
225
233
  if validate? && !read_eos
226
234
  log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
@@ -234,6 +242,20 @@ module RDF::NTriples
234
242
  end
235
243
  end
236
244
 
245
+ ##
246
+ # @return [RDF::Statement]
247
+ def read_rdfstar
248
+ if @options[:rdfstar] && match(ST_START)
249
+ subject = read_uriref || read_node || read_rdfstar || fail_subject
250
+ predicate = read_uriref(intern: true) || fail_predicate
251
+ object = read_uriref || read_node || read_literal || read_rdfstar || fail_object
252
+ if !match(ST_END)
253
+ log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
254
+ end
255
+ RDF::Statement.new(subject, predicate, object)
256
+ end
257
+ end
258
+
237
259
  ##
238
260
  # @return [Boolean]
239
261
  # @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar (comment)
@@ -208,7 +208,7 @@ module RDF::NTriples
208
208
  # @param [RDF::Term] object
209
209
  # @return [void]
210
210
  def write_triple(subject, predicate, object)
211
- puts format_triple(subject, predicate, object, @options)
211
+ puts format_triple(subject, predicate, object, **@options)
212
212
  end
213
213
 
214
214
  ##
@@ -221,6 +221,15 @@ module RDF::NTriples
221
221
  format_triple(*statement.to_triple, **options)
222
222
  end
223
223
 
224
+ ##
225
+ # Returns the N-Triples representation of an RDF* reified statement.
226
+ #
227
+ # @param [RDF::Statement] statement
228
+ # @param [Hash{Symbol => Object}] options ({})
229
+ # @return [String]
230
+ def format_rdfstar(statement, **options)
231
+ "<<%s %s %s>>" % statement.to_a.map { |value| format_term(value, **options) }
232
+ end
224
233
  ##
225
234
  # Returns the N-Triples representation of a triple.
226
235
  #
@@ -90,7 +90,7 @@ module RDF
90
90
  # the resulting solution sequence
91
91
  # @see RDF::Query#execute
92
92
  def self.execute(queryable, patterns = {}, options = {}, &block)
93
- self.new(patterns, options, &block).execute(queryable, options)
93
+ self.new(patterns, **options, &block).execute(queryable, **options)
94
94
  end
95
95
 
96
96
  ##
@@ -134,6 +134,12 @@ module RDF
134
134
  # @return [Hash]
135
135
  attr_reader :options
136
136
 
137
+ ##
138
+ # Scope the query to named graphs matching value
139
+ #
140
+ # @return [RDF::Resource, RDF::Query::Variable, false] graph_name
141
+ attr_accessor :graph_name
142
+
137
143
  ##
138
144
  # Initializes a new basic graph pattern query.
139
145
  #
@@ -180,6 +186,7 @@ module RDF
180
186
  @options = options.dup
181
187
  @solutions = Query::Solutions(solutions)
182
188
  graph_name = name if graph_name.nil?
189
+ @graph_name = graph_name
183
190
 
184
191
  patterns << @options if patterns.empty?
185
192
 
@@ -189,8 +196,6 @@ module RDF
189
196
  else patterns
190
197
  end
191
198
 
192
- self.graph_name = graph_name
193
-
194
199
  if block_given?
195
200
  case block.arity
196
201
  when 1 then block.call(self)
@@ -223,7 +228,7 @@ module RDF
223
228
  # whether this is an optional pattern
224
229
  # @return [void] self
225
230
  def pattern(pattern, **options)
226
- @patterns << Pattern.from(pattern, options)
231
+ @patterns << Pattern.from(pattern, **options)
227
232
  self
228
233
  end
229
234
 
@@ -235,7 +240,7 @@ module RDF
235
240
  # @return [RDF::Query] a copy of `self`
236
241
  # @since 0.3.0
237
242
  def optimize(**options)
238
- self.dup.optimize!(options)
243
+ self.dup.optimize!(**options)
239
244
  end
240
245
 
241
246
  ##
@@ -284,6 +289,8 @@ module RDF
284
289
  # any additional keyword options
285
290
  # @option options [Hash{Symbol => RDF::Term}] bindings
286
291
  # optional variable bindings to use
292
+ # @option options [Boolean] :optimize
293
+ # Optimize query before execution.
287
294
  # @option options [RDF::Query::Solutions] solutions
288
295
  # optional initial solutions for chained queries
289
296
  # @yield [solution]
@@ -294,9 +301,7 @@ module RDF
294
301
  # the resulting solution sequence
295
302
  # @see http://www.holygoat.co.uk/blog/entry/2005-10-25-1
296
303
  # @see http://www.w3.org/TR/sparql11-query/#emptyGroupPattern
297
- def execute(queryable, solutions: Solution.new, graph_name: nil, name: nil, **options, &block)
298
- options = {bindings: {}}.merge(options)
299
-
304
+ def execute(queryable, bindings: {}, solutions: Solution.new, graph_name: nil, name: nil, **options, &block)
300
305
  # Use provided solutions to allow for query chaining
301
306
  # Otherwise, a quick empty solution simplifies the logic below; no special case for
302
307
  # the first pattern
@@ -308,17 +313,17 @@ module RDF
308
313
  return @solutions
309
314
  end
310
315
 
316
+ self.optimize! if options[:optimize]
311
317
  patterns = @patterns
312
318
  graph_name = name if graph_name.nil?
313
- graph_name = self.graph_name if graph_name.nil?
314
- options[:graph_name] = graph_name unless graph_name.nil?
319
+ @graph_name = graph_name unless graph_name.nil?
315
320
 
316
321
  # Add graph_name to pattern, if necessary
317
- unless graph_name.nil?
322
+ unless @graph_name.nil?
318
323
  if patterns.empty?
319
- patterns = [Pattern.new(nil, nil, nil, graph_name: graph_name)]
324
+ patterns = [Pattern.new(nil, nil, nil, graph_name: @graph_name)]
320
325
  else
321
- apply_graph_name(graph_name)
326
+ apply_graph_name(@graph_name)
322
327
  end
323
328
  end
324
329
 
@@ -326,15 +331,15 @@ module RDF
326
331
 
327
332
  old_solutions, @solutions = @solutions, Query::Solutions()
328
333
 
329
- options[:bindings].each_key do |variable|
334
+ bindings.each_key do |variable|
330
335
  if pattern.variables.include?(variable)
331
336
  unbound_solutions, old_solutions = old_solutions, Query::Solutions()
332
- options[:bindings][variable].each do |binding|
337
+ bindings[variable].each do |binding|
333
338
  unbound_solutions.each do |solution|
334
339
  old_solutions << solution.merge(variable => binding)
335
340
  end
336
341
  end
337
- options[:bindings].delete(variable)
342
+ bindings.delete(variable)
338
343
  end
339
344
  end
340
345
 
@@ -407,38 +412,26 @@ module RDF
407
412
  # Is this query scoped to a named graph?
408
413
  # @return [Boolean]
409
414
  def named?
410
- !!options[:graph_name]
415
+ !!graph_name
411
416
  end
412
417
 
413
418
  # Is this query scoped to the default graph?
414
419
  # @return [Boolean]
415
420
  def default?
416
- options[:graph_name] == false
421
+ graph_name == false
417
422
  end
418
423
 
419
424
  # Is this query unscoped? This indicates that it can return results from
420
425
  # either a named graph or the default graph.
421
426
  # @return [Boolean]
422
427
  def unnamed?
423
- options[:graph_name].nil?
424
- end
425
-
426
- # Scope the query to named graphs matching value
427
- # @param [RDF::IRI, RDF::Query::Variable] value
428
- # @return [RDF::IRI, RDF::Query::Variable]
429
- def graph_name=(value)
430
- options[:graph_name] = value
431
- end
432
-
433
- # Scope of this query, if any
434
- # @return [RDF::IRI, RDF::Query::Variable]
435
- def graph_name
436
- options[:graph_name]
428
+ graph_name.nil?
437
429
  end
438
430
 
439
431
  # Apply the graph name specified (or configured) to all patterns that have no graph name
440
432
  # @param [RDF::IRI, RDF::Query::Variable] graph_name (self.graph_name)
441
- def apply_graph_name(graph_name = options[:graph_name])
433
+ def apply_graph_name(graph_name = nil)
434
+ graph_name ||= self.graph_name
442
435
  patterns.each {|pattern| pattern.graph_name = graph_name if pattern.graph_name.nil?} unless graph_name.nil?
443
436
  end
444
437
 
@@ -515,8 +508,7 @@ module RDF
515
508
  # @return [RDF::Query]
516
509
  def dup
517
510
  patterns = @patterns.map {|p| p.dup}
518
- patterns << @options.merge(solutions: @solutions.dup)
519
- Query.new(*patterns)
511
+ Query.new(patterns, graph_name: graph_name, solutions: @solutions.dup, **options)
520
512
  end
521
513
 
522
514
  ##
@@ -77,21 +77,23 @@ 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.
88
- def normalize!(hash_pattern = {}, options = {})
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.
89
+ def normalize!(*args)
90
+ hash_pattern = args.shift
91
+ options = args.shift || {}
92
+ anonymous_subject_format = options.fetch(:anonymous_subject_format, '__%s__')
89
93
  raise ArgumentError, "invalid hash pattern: #{hash_pattern.inspect}" unless hash_pattern.is_a?(Hash)
90
94
 
91
95
  counter = RDF::Query::HashPatternNormalizer::Counter.new
92
96
 
93
- anonymous_subject_format = (options[:anonymous_subject_format] || '__%s__').to_s
94
-
95
97
  hash_pattern.inject({}) { |acc, pair|
96
98
  subject, predicate_to_object = pair
97
99
 
@@ -184,7 +186,7 @@ module RDF; class Query
184
186
  # the query pattern as a hash.
185
187
  # @return [Hash{Symbol => Object}]
186
188
  # the resulting query pattern as a normalized hash.
187
- def normalize!(**hash_pattern)
189
+ def normalize!(hash_pattern)
188
190
  self.class.normalize!(hash_pattern, @options)
189
191
  end
190
192
  end # RDF::Query::HashPatternNormalizer
@@ -17,7 +17,7 @@ module RDF; class Query
17
17
  end
18
18
 
19
19
  ##
20
- # @overload initialize(**options)
20
+ # @overload initialize(options = {})
21
21
  # @param [Hash{Symbol => Object}] options
22
22
  # @option options [Variable, Resource, Symbol, nil] :subject (nil)
23
23
  # @option options [Variable, URI, Symbol, nil] :predicate (nil)
@@ -26,7 +26,7 @@ module RDF; class Query
26
26
  # A graph_name of nil matches any graph, a graph_name of false, matches only the default graph.
27
27
  # @option options [Boolean] :optional (false)
28
28
  #
29
- # @overload initialize(subject, predicate, object, **options)
29
+ # @overload initialize(subject, predicate, object, options = {})
30
30
  # @param [Variable, Resource, Symbol, nil] subject
31
31
  # @param [Variable, URI, Symbol, nil] predicate
32
32
  # @param [Variable, Termm, Symbol, nil] object
@@ -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 " ."