rdf 3.0.11 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
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 " ."