rdf 0.2.3 → 0.3.0.pre

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 (44) hide show
  1. data/AUTHORS +1 -0
  2. data/{CONTRIBUTORS → CREDITS} +3 -1
  3. data/README +17 -8
  4. data/VERSION +1 -1
  5. data/etc/doap.nt +28 -10
  6. data/lib/rdf/format.rb +55 -47
  7. data/lib/rdf/mixin/countable.rb +3 -6
  8. data/lib/rdf/mixin/enumerable.rb +69 -69
  9. data/lib/rdf/mixin/indexable.rb +26 -0
  10. data/lib/rdf/mixin/mutable.rb +2 -2
  11. data/lib/rdf/mixin/queryable.rb +50 -12
  12. data/lib/rdf/mixin/writable.rb +8 -19
  13. data/lib/rdf/model/literal/boolean.rb +42 -6
  14. data/lib/rdf/model/literal/date.rb +17 -5
  15. data/lib/rdf/model/literal/datetime.rb +18 -6
  16. data/lib/rdf/model/literal/decimal.rb +32 -5
  17. data/lib/rdf/model/literal/double.rb +32 -5
  18. data/lib/rdf/model/literal/integer.rb +16 -5
  19. data/lib/rdf/model/literal/time.rb +6 -6
  20. data/lib/rdf/model/literal/token.rb +5 -5
  21. data/lib/rdf/model/literal/xml.rb +5 -5
  22. data/lib/rdf/model/literal.rb +24 -11
  23. data/lib/rdf/model/node.rb +14 -13
  24. data/lib/rdf/model/uri.rb +315 -42
  25. data/lib/rdf/model/value.rb +1 -1
  26. data/lib/rdf/ntriples/reader.rb +23 -15
  27. data/lib/rdf/ntriples/writer.rb +1 -1
  28. data/lib/rdf/query/pattern.rb +131 -15
  29. data/lib/rdf/query/solution.rb +94 -29
  30. data/lib/rdf/query/solutions.rb +202 -0
  31. data/lib/rdf/query/variable.rb +42 -18
  32. data/lib/rdf/query.rb +210 -160
  33. data/lib/rdf/reader.rb +300 -112
  34. data/lib/rdf/repository.rb +88 -6
  35. data/lib/rdf/transaction.rb +161 -0
  36. data/lib/rdf/util/cache.rb +5 -0
  37. data/lib/rdf/util/file.rb +31 -0
  38. data/lib/rdf/util/uuid.rb +36 -0
  39. data/lib/rdf/util.rb +2 -0
  40. data/lib/rdf/version.rb +3 -3
  41. data/lib/rdf/vocab.rb +43 -35
  42. data/lib/rdf/writer.rb +105 -50
  43. data/lib/rdf.rb +29 -27
  44. metadata +26 -17
@@ -5,12 +5,12 @@ module RDF; class Query
5
5
  ##
6
6
  # @private
7
7
  # @since 0.2.2
8
- def self.from(pattern)
8
+ def self.from(pattern, options = {})
9
9
  case pattern
10
10
  when Pattern then pattern
11
- when Statement then self.new(pattern.to_hash)
12
- when Hash then self.new(pattern)
13
- when Array then self.new(*pattern)
11
+ when Statement then self.new(options.merge(pattern.to_hash))
12
+ when Hash then self.new(options.merge(pattern))
13
+ when Array then self.new(pattern[0], pattern[1], pattern[2], options.merge(:context => pattern[3]))
14
14
  else raise ArgumentError.new("expected RDF::Query::Pattern, RDF::Statement, Hash, or Array, but got #{pattern.inspect}")
15
15
  end
16
16
  end
@@ -49,36 +49,151 @@ module RDF; class Query
49
49
  end
50
50
 
51
51
  ##
52
- # @param [Graph, Repository] graph
52
+ # Returns `true` if this is an optional pattern.
53
+ #
54
+ # @example
55
+ # Pattern.new(:s, :p, :o).optional? #=> false
56
+ # Pattern.new(:s, :p, :o, :optional => true).optional? #=> true
57
+ #
58
+ # @return [Boolean]
59
+ # @since 0.3.0
60
+ def optional?
61
+ !!options[:optional]
62
+ end
63
+
64
+ ##
65
+ # Executes this query pattern on the given `queryable` object.
66
+ #
67
+ # By default any variable terms in this pattern will be treated as `nil`
68
+ # wildcards when executing the query. If the optional `bindings` are
69
+ # given, variables will be substituted with their values when executing
70
+ # the query.
71
+ #
72
+ # @example
73
+ # Pattern.new(:s, :p, :o).execute(RDF::Repository.load('data.nt'))
74
+ #
75
+ # @param [RDF::Queryable] queryable
76
+ # the graph or repository to query
77
+ # @param [Hash{Symbol => RDF::Value}] bindings
78
+ # optional variable bindings to use
79
+ # @yield [statement]
80
+ # each matching statement
81
+ # @yieldparam [RDF::Statement] statement
82
+ # an RDF statement matching this pattern
53
83
  # @return [Enumerator]
54
- def execute(graph, &block)
55
- graph.query(self) # FIXME
84
+ # an enumerator yielding matching statements
85
+ # @see RDF::Queryable#query
86
+ def execute(queryable, bindings = {}, &block)
87
+ variables = self.variables
88
+
89
+ # Does this pattern contain any variables?
90
+ if variables.empty?
91
+ # With no variables to worry about, we will let the repository
92
+ # implementation yield matching statements directly:
93
+ queryable.query(self, &block)
94
+
95
+ # Yes, this pattern uses at least one variable...
96
+ else
97
+ query = {
98
+ :subject => subject && subject.variable? ? bindings[subject.to_sym] : subject,
99
+ :predicate => predicate && predicate.variable? ? bindings[predicate.to_sym] : predicate,
100
+ :object => object && object.variable? ? bindings[object.to_sym] : object,
101
+ # TODO: context handling?
102
+ }
103
+
104
+ # Do all the variable terms refer to distinct variables?
105
+ if variable_count == variables.size
106
+ # If so, we can just let the repository implementation handle
107
+ # everything and yield matching statements directly:
108
+ queryable.query(query, &block)
109
+
110
+ # No, some terms actually refer to the same variable...
111
+ else
112
+ # Figure out which terms refer to the same variable:
113
+ terms = variables.each_key.find do |name|
114
+ terms = variable_terms(name)
115
+ break terms if terms.size > 1
116
+ end
117
+ queryable.query(query) do |statement|
118
+ # Only yield those matching statements where the variable
119
+ # constraint is also satisfied:
120
+ # FIXME: `Array#uniq` uses `#eql?` and `#hash`, not `#==`
121
+ if matches = terms.map { |term| statement.send(term) }.uniq.size.equal?(1)
122
+ block.call(statement)
123
+ end
124
+ end
125
+ end
126
+ end
56
127
  end
57
128
 
58
129
  ##
59
- # Returns `true` if this pattern contains variables.
130
+ # Returns a query solution constructed by binding any variables in this
131
+ # pattern with the corresponding terms in the given `statement`.
60
132
  #
61
- # @return [Boolean]
133
+ # @example
134
+ # pattern.solution(statement)
135
+ #
136
+ # @param [RDF::Statement] statement
137
+ # an RDF statement to bind terms from
138
+ # @return [RDF::Query::Solution]
139
+ def solution(statement)
140
+ RDF::Query::Solution.new do |solution|
141
+ solution[subject.to_sym] = statement.subject if subject.variable?
142
+ solution[predicate.to_sym] = statement.predicate if predicate.variable?
143
+ solution[object.to_sym] = statement.object if object.variable?
144
+ end
145
+ end
146
+
147
+ ##
148
+ # Returns `true` if this pattern contains any variables.
149
+ #
150
+ # @return [Boolean] `true` or `false`
62
151
  def variables?
63
152
  subject.is_a?(Variable) ||
64
153
  predicate.is_a?(Variable) ||
65
154
  object.is_a?(Variable)
66
155
  end
67
156
 
157
+ ##
158
+ # Returns the variable terms in this pattern.
159
+ #
160
+ # @example
161
+ # Pattern.new(RDF::Node.new, :p, 123).variable_terms #=> [:predicate]
162
+ #
163
+ # @param [Symbol, #to_sym] name
164
+ # an optional variable name
165
+ # @return [Array<Symbol>]
166
+ # @since 0.3.0
167
+ def variable_terms(name = nil)
168
+ terms = []
169
+ terms << :subject if subject.is_a?(Variable) && (!name || name.eql?(subject.name))
170
+ terms << :predicate if predicate.is_a?(Variable) && (!name || name.eql?(predicate.name))
171
+ terms << :object if object.is_a?(Variable) && (!name || name.eql?(object.name))
172
+ terms
173
+ end
174
+
68
175
  ##
69
176
  # Returns the number of variables in this pattern.
70
177
  #
178
+ # Note: this does not count distinct variables, and will therefore e.g.
179
+ # return 3 even if two terms are actually the same variable.
180
+ #
71
181
  # @return [Integer] (0..3)
72
182
  def variable_count
73
- variables.size
183
+ count = 0
184
+ count += 1 if subject.is_a?(Variable)
185
+ count += 1 if predicate.is_a?(Variable)
186
+ count += 1 if object.is_a?(Variable)
187
+ count
74
188
  end
75
-
76
189
  alias_method :cardinality, :variable_count
77
190
  alias_method :arity, :variable_count
78
191
 
79
192
  ##
80
193
  # Returns all variables in this pattern.
81
194
  #
195
+ # Note: this returns a hash containing distinct variables only.
196
+ #
82
197
  # @return [Hash{Symbol => Variable}]
83
198
  def variables
84
199
  variables = {}
@@ -91,7 +206,7 @@ module RDF; class Query
91
206
  ##
92
207
  # Returns `true` if this pattern contains bindings.
93
208
  #
94
- # @return [Boolean]
209
+ # @return [Boolean] `true` or `false`
95
210
  def bindings?
96
211
  !bindings.empty?
97
212
  end
@@ -149,16 +264,17 @@ module RDF; class Query
149
264
  end
150
265
 
151
266
  ##
152
- # Returns the string representation of this pattern.
267
+ # Returns a string representation of this pattern.
153
268
  #
154
269
  # @return [String]
155
270
  def to_s
156
271
  StringIO.open do |buffer| # FIXME in RDF::Statement
272
+ buffer << 'OPTIONAL ' if optional?
157
273
  buffer << (subject.is_a?(Variable) ? subject.to_s : "<#{subject}>") << ' '
158
274
  buffer << (predicate.is_a?(Variable) ? predicate.to_s : "<#{predicate}>") << ' '
159
275
  buffer << (object.is_a?(Variable) ? object.to_s : "<#{object}>") << ' .'
160
276
  buffer.string
161
277
  end
162
278
  end
163
- end # class Pattern
164
- end; end # module RDF class Query
279
+ end # Pattern
280
+ end; end # RDF::Query
@@ -22,50 +22,76 @@ class RDF::Query
22
22
  #
23
23
  class Solution
24
24
  # Undefine all superfluous instance methods:
25
- undef_method(*(instance_methods.map(&:to_sym) - [:__id__, :__send__, :__class__, :__eval__, :object_id, :instance_eval, :inspect, :class, :is_a?]))
25
+ undef_method(*(instance_methods.map(&:to_sym) - [:__id__, :__send__, :__class__, :__eval__,
26
+ :object_id, :dup, :instance_eval, :inspect, :to_s,
27
+ :class, :is_a?, :respond_to?, :respond_to_missing?]))
26
28
 
27
29
  include Enumerable
28
30
 
29
31
  ##
30
- # @param [Hash{Symbol => Value}] bindings
31
- def initialize(bindings = {})
32
+ # Initializes the query solution.
33
+ #
34
+ # @param [Hash{Symbol => RDF::Value}] bindings
35
+ # @yield [solution]
36
+ def initialize(bindings = {}, &block)
32
37
  @bindings = bindings.to_hash
38
+
39
+ if block_given?
40
+ case block.arity
41
+ when 1 then block.call(self)
42
+ else instance_eval(&block)
43
+ end
44
+ end
33
45
  end
34
46
 
47
+ # @private
48
+ attr_reader :bindings
49
+
35
50
  ##
36
51
  # Enumerates over every variable binding in this solution.
37
52
  #
38
53
  # @yield [name, value]
39
- # @yieldparam [Symbol, Value]
54
+ # @yieldparam [Symbol] name
55
+ # @yieldparam [RDF::Value] value
40
56
  # @return [Enumerator]
41
57
  def each_binding(&block)
42
58
  @bindings.each(&block)
43
59
  end
44
-
45
60
  alias_method :each, :each_binding
46
61
 
47
62
  ##
48
63
  # Enumerates over every variable name in this solution.
49
64
  #
50
65
  # @yield [name]
51
- # @yieldparam [Symbol]
66
+ # @yieldparam [Symbol] name
52
67
  # @return [Enumerator]
53
68
  def each_name(&block)
54
69
  @bindings.each_key(&block)
55
70
  end
56
-
57
71
  alias_method :each_key, :each_name
58
72
 
59
73
  ##
60
74
  # Enumerates over every variable value in this solution.
61
75
  #
62
76
  # @yield [value]
63
- # @yieldparam [Value]
77
+ # @yieldparam [RDF::Value] value
64
78
  # @return [Enumerator]
65
79
  def each_value(&block)
66
80
  @bindings.each_value(&block)
67
81
  end
68
82
 
83
+ ##
84
+ # Returns `true` if this solution contains bindings for any of the given
85
+ # `variables`.
86
+ #
87
+ # @param [Array<Symbol, #to_sym>] variables
88
+ # an array of variables to check
89
+ # @return [Boolean] `true` or `false`
90
+ # @since 0.3.0
91
+ def has_variables?(variables)
92
+ variables.any? { |variable| bound?(variable) }
93
+ end
94
+
69
95
  ##
70
96
  # Enumerates over every variable in this solution.
71
97
  #
@@ -81,8 +107,9 @@ class RDF::Query
81
107
  ##
82
108
  # Returns `true` if the variable `name` is bound in this solution.
83
109
  #
84
- # @param [Symbol] name
85
- # @return [Boolean]
110
+ # @param [Symbol, #to_sym] name
111
+ # the variable name
112
+ # @return [Boolean] `true` or `false`
86
113
  def bound?(name)
87
114
  !unbound?(name)
88
115
  end
@@ -90,8 +117,9 @@ class RDF::Query
90
117
  ##
91
118
  # Returns `true` if the variable `name` is unbound in this solution.
92
119
  #
93
- # @param [Symbol] name
94
- # @return [Boolean]
120
+ # @param [Symbol, #to_sym] name
121
+ # the variable name
122
+ # @return [Boolean] `true` or `false`
95
123
  def unbound?(name)
96
124
  @bindings[name.to_sym].nil?
97
125
  end
@@ -99,20 +127,58 @@ class RDF::Query
99
127
  ##
100
128
  # Returns the value of the variable `name`.
101
129
  #
102
- # @param [Symbol] name
103
- # @return [Value]
130
+ # @param [Symbol, #to_sym] name
131
+ # the variable name
132
+ # @return [RDF::Value]
104
133
  def [](name)
105
134
  @bindings[name.to_sym]
106
135
  end
107
136
 
108
137
  ##
109
- # @return [Array<Array(Symbol, Value)>}
138
+ # Binds or rebinds the variable `name` to the given `value`.
139
+ #
140
+ # @param [Symbol, #to_sym] name
141
+ # the variable name
142
+ # @param [RDF::Value] value
143
+ # @return [RDF::Value]
144
+ # @since 0.3.0
145
+ def []=(name, value)
146
+ @bindings[name.to_sym] = value
147
+ end
148
+
149
+ ##
150
+ # Merges the bindings from the given `other` query solution into this
151
+ # one, overwriting any existing ones having the same name.
152
+ #
153
+ # @param [RDF::Query::Solution, #to_hash] other
154
+ # another query solution or hash bindings
155
+ # @return [void] self
156
+ # @since 0.3.0
157
+ def merge!(other)
158
+ @bindings.merge!(other.to_hash)
159
+ self
160
+ end
161
+
162
+ ##
163
+ # Merges the bindings from the given `other` query solution with a copy
164
+ # of this one.
165
+ #
166
+ # @param [RDF::Query::Solution, #to_hash] other
167
+ # another query solution or hash bindings
168
+ # @return [RDF::Query::Solution]
169
+ # @since 0.3.0
170
+ def merge(other)
171
+ self.class.new(@bindings.dup).merge!(other)
172
+ end
173
+
174
+ ##
175
+ # @return [Array<Array(Symbol, RDF::Value)>}
110
176
  def to_a
111
177
  @bindings.to_a
112
178
  end
113
179
 
114
180
  ##
115
- # @return [Hash{Symbol => Value}}
181
+ # @return [Hash{Symbol => RDF::Value}}
116
182
  def to_hash
117
183
  @bindings.dup
118
184
  end
@@ -123,18 +189,17 @@ class RDF::Query
123
189
  sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, @bindings.inspect)
124
190
  end
125
191
 
126
- protected
192
+ protected
127
193
 
128
- ##
129
- # @param [Symbol] name
130
- # @return [Value]
131
- def method_missing(name, *args, &block)
132
- if args.empty? && @bindings.has_key?(name.to_sym)
133
- @bindings[name.to_sym]
134
- else
135
- super
136
- end
194
+ ##
195
+ # @param [Symbol] name
196
+ # @return [RDF::Value]
197
+ def method_missing(name, *args, &block)
198
+ if args.empty? && @bindings.has_key?(name.to_sym)
199
+ @bindings[name.to_sym]
200
+ else
201
+ super # raises NoMethodError
137
202
  end
138
-
139
- end
140
- end
203
+ end
204
+ end # Solution
205
+ end # RDF::Query
@@ -0,0 +1,202 @@
1
+ module RDF; class Query
2
+ ##
3
+ # An RDF basic graph pattern (BGP) query solution sequence.
4
+ #
5
+ # @example Filtering solutions using a hash
6
+ # solutions.filter(:author => RDF::URI("http://ar.to/#self"))
7
+ # solutions.filter(:author => "Arto Bendiken")
8
+ # solutions.filter(:author => [RDF::URI("http://ar.to/#self"), "Arto Bendiken"])
9
+ # solutions.filter(:updated => RDF::Literal(Date.today))
10
+ #
11
+ # @example Filtering solutions using a block
12
+ # solutions.filter { |solution| solution.author.literal? }
13
+ # solutions.filter { |solution| solution.title =~ /^SPARQL/ }
14
+ # solutions.filter { |solution| solution.price < 30.5 }
15
+ # solutions.filter { |solution| solution.bound?(:date) }
16
+ # solutions.filter { |solution| solution.age.datatype == RDF::XSD.integer }
17
+ # solutions.filter { |solution| solution.name.language == :es }
18
+ #
19
+ # @example Reordering solutions based on a variable
20
+ # solutions.order_by(:updated)
21
+ # solutions.order_by(:updated, :created)
22
+ #
23
+ # @example Selecting particular variables only
24
+ # solutions.select(:title)
25
+ # solutions.select(:title, :description)
26
+ #
27
+ # @example Eliminating duplicate solutions
28
+ # solutions.distinct
29
+ #
30
+ # @example Limiting the number of solutions
31
+ # solutions.offset(20).limit(10)
32
+ #
33
+ # @example Counting the number of matching solutions
34
+ # solutions.count
35
+ # solutions.count { |solution| solution.price < 30.5 }
36
+ #
37
+ # @example Iterating over all found solutions
38
+ # solutions.each { |solution| puts solution.inspect }
39
+ #
40
+ # @since 0.3.0
41
+ class Solutions < Array
42
+ alias_method :each_solution, :each
43
+
44
+ ##
45
+ # Returns the number of matching query solutions.
46
+ #
47
+ # @overload count
48
+ # @return [Integer]
49
+ #
50
+ # @overload count { |solution| ... }
51
+ # @yield [solution]
52
+ # @yieldparam [RDF::Query::Solution] solution
53
+ # @yieldreturn [Boolean]
54
+ # @return [Integer]
55
+ #
56
+ # @return [Integer]
57
+ def count(&block)
58
+ super
59
+ end
60
+
61
+ ##
62
+ # Filters this solution sequence by the given `criteria`.
63
+ #
64
+ # @param [Hash{Symbol => Object}] criteria
65
+ # @yield [solution]
66
+ # @yieldparam [RDF::Query::Solution] solution
67
+ # @yieldreturn [Boolean]
68
+ # @return [void] `self`
69
+ def filter(criteria = {}, &block)
70
+ if block_given?
71
+ self.reject! do |solution|
72
+ !block.call(solution.is_a?(Solution) ? solution : Solution.new(solution))
73
+ end
74
+ else
75
+ self.reject! do |solution|
76
+ solution = solution.is_a?(Solution) ? solution : Solution.new(solution)
77
+ results = criteria.map do |name, value|
78
+ solution[name] == value
79
+ end
80
+ !results.all?
81
+ end
82
+ end
83
+ self
84
+ end
85
+ alias_method :filter!, :filter
86
+
87
+ ##
88
+ # Reorders this solution sequence by the given `variables`.
89
+ #
90
+ # @param [Array<Symbol, #to_sym>] variables
91
+ # @return [void] `self`
92
+ def order(*variables)
93
+ if variables.empty?
94
+ raise ArgumentError, "wrong number of arguments (0 for 1)"
95
+ else
96
+ # TODO: support for descending sort, e.g. `order(:s => :asc, :p => :desc)`
97
+ variables.map!(&:to_sym)
98
+ self.sort! do |a, b|
99
+ a = variables.map { |variable| a[variable].to_s } # FIXME
100
+ b = variables.map { |variable| b[variable].to_s } # FIXME
101
+ a <=> b
102
+ end
103
+ end
104
+ self
105
+ end
106
+ alias_method :order_by, :order
107
+
108
+ ##
109
+ # Restricts this solution sequence to the given `variables` only.
110
+ #
111
+ # @param [Array<Symbol, #to_sym>] variables
112
+ # @return [void] `self`
113
+ def project(*variables)
114
+ if variables.empty?
115
+ raise ArgumentError, "wrong number of arguments (0 for 1)"
116
+ else
117
+ variables.map!(&:to_sym)
118
+ self.each do |solution|
119
+ solution.bindings.delete_if { |k, v| !variables.include?(k.to_sym) }
120
+ end
121
+ end
122
+ self
123
+ end
124
+ alias_method :select, :project
125
+
126
+ ##
127
+ # Ensures that the solutions in this solution sequence are unique.
128
+ #
129
+ # @return [void] `self`
130
+ def distinct
131
+ self.uniq!
132
+ self
133
+ end
134
+ alias_method :distinct!, :distinct
135
+ alias_method :reduced, :distinct
136
+ alias_method :reduced!, :distinct
137
+
138
+ ##
139
+ # Limits this solution sequence to bindings starting from the `start`
140
+ # offset in the overall solution sequence.
141
+ #
142
+ # @param [Integer, #to_i] start
143
+ # zero or a positive or negative integer
144
+ # @return [void] `self`
145
+ def offset(start)
146
+ case start = start.to_i
147
+ when 0 then nil
148
+ else self.slice!(0...start)
149
+ end
150
+ self
151
+ end
152
+ alias_method :offset!, :offset
153
+
154
+ ##
155
+ # Limits the number of solutions in this solution sequence to a maximum
156
+ # of `length`.
157
+ #
158
+ # @param [Integer, #to_i] length
159
+ # zero or a positive integer
160
+ # @return [void] `self`
161
+ # @raise [ArgumentError] if `length` is negative
162
+ def limit(length)
163
+ length = length.to_i
164
+ raise ArgumentError, "expected zero or a positive integer, got #{length}" if length < 0
165
+ case length
166
+ when 0 then self.clear
167
+ else self.slice!(length..-1) if length < self.size
168
+ end
169
+ self
170
+ end
171
+ alias_method :limit!, :limit
172
+
173
+ ##
174
+ # Returns an array of the distinct variable names used in this solution
175
+ # sequence.
176
+ #
177
+ # @return [Array<Symbol>]
178
+ def variable_names
179
+ variables = self.inject({}) do |result, solution|
180
+ solution.each_name do |name|
181
+ result[name] ||= true
182
+ end
183
+ result
184
+ end
185
+ variables.keys
186
+ end
187
+
188
+ ##
189
+ # Returns `true` if this solution sequence contains bindings for any of
190
+ # the given `variables`.
191
+ #
192
+ # @param [Array<Symbol, #to_sym>] variables
193
+ # an array of variables to check
194
+ # @return [Boolean] `true` or `false`
195
+ # @see RDF::Query::Solution#has_variables?
196
+ # @see RDF::Query#execute
197
+ def have_variables?(variables)
198
+ self.any? { |solution| solution.has_variables?(variables) }
199
+ end
200
+ alias_method :has_variables?, :have_variables?
201
+ end # Solutions
202
+ end; end # RDF::Query