rdf 0.2.3 → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -45,17 +45,24 @@ class RDF::Query
45
45
  class Variable
46
46
  include RDF::Value
47
47
 
48
- # @return [Symbol] The variable's name.
48
+ ##
49
+ # The variable's name.
50
+ #
51
+ # @return [Symbol]
49
52
  attr_accessor :name
50
-
51
53
  alias_method :to_sym, :name
52
54
 
53
- # @return [Value] The variable's value.
55
+ ##
56
+ # The variable's value.
57
+ #
58
+ # @return [RDF::Value]
54
59
  attr_accessor :value
55
60
 
56
61
  ##
57
- # @param [Symbol] name
58
- # @param [Value] value
62
+ # @param [Symbol, #to_sym] name
63
+ # the variable name
64
+ # @param [RDF::Value] value
65
+ # an optional variable value
59
66
  def initialize(name = nil, value = nil)
60
67
  @name = (name || "g#{__id__.to_i.abs}").to_sym
61
68
  @value = value
@@ -98,50 +105,67 @@ class RDF::Query
98
105
  ##
99
106
  # Rebinds this variable to the given `value`.
100
107
  #
101
- # @param [Value] value
102
- # @return [Value] The previous value, if any.
108
+ # @param [RDF::Value] value
109
+ # @return [RDF::Value] the previous value, if any.
103
110
  def bind(value)
104
111
  old_value = self.value
105
112
  self.value = value
106
113
  old_value
107
114
  end
108
-
109
115
  alias_method :bind!, :bind
110
116
 
111
117
  ##
112
118
  # Unbinds this variable, discarding any currently bound value.
113
119
  #
114
- # @return [Value] The previous value, if any.
120
+ # @return [RDF::Value] the previous value, if any.
115
121
  def unbind
116
122
  old_value = self.value
117
123
  self.value = nil
118
124
  old_value
119
125
  end
120
-
121
126
  alias_method :unbind!, :unbind
122
127
 
123
128
  ##
124
129
  # Returns this variable as `Hash`.
125
130
  #
126
- # @return [Hash{Symbol => Variable}]
131
+ # @return [Hash{Symbol => RDF::Query::Variable}]
127
132
  def variables
128
- { name => self }
133
+ {name => self}
129
134
  end
130
-
131
135
  alias_method :to_hash, :variables
132
136
 
133
137
  ##
134
138
  # Returns this variable's bindings (if any) as a `Hash`.
135
139
  #
136
- # @return [Hash{Symbol => Value}]
140
+ # @return [Hash{Symbol => RDF::Value}]
137
141
  def bindings
138
- unbound? ? {} : { name => value }
142
+ unbound? ? {} : {name => value}
143
+ end
144
+
145
+ ##
146
+ # Returns a hash code for this variable.
147
+ #
148
+ # @return [Fixnum]
149
+ # @since 0.3.0
150
+ def hash
151
+ @name.hash
152
+ end
153
+
154
+ ##
155
+ # Returns `true` if this variable is equivalent to a given `other`
156
+ # variable.
157
+ #
158
+ # @param [Object] other
159
+ # @return [Boolean] `true` or `false`
160
+ # @since 0.3.0
161
+ def eql?(other)
162
+ other.is_a?(RDF::Query::Variable) && @name.eql?(other.name)
139
163
  end
140
164
 
141
165
  ##
142
166
  # Compares this variable with the given value.
143
167
  #
144
- # @param [Value] other
168
+ # @param [RDF::Value] other
145
169
  # @return [Boolean]
146
170
  def ===(other)
147
171
  if unbound?
@@ -158,5 +182,5 @@ class RDF::Query
158
182
  def to_s
159
183
  unbound? ? "?#{name}" : "?#{name}=#{value}"
160
184
  end
161
- end
162
- end
185
+ end # Variable
186
+ end # RDF::Query
data/lib/rdf/query.rb CHANGED
@@ -1,224 +1,274 @@
1
1
  module RDF
2
2
  ##
3
- # An RDF basic graph pattern query.
3
+ # An RDF basic graph pattern (BGP) query.
4
4
  #
5
- # @example Filtering solutions using a hash
6
- # query.filter(:author => RDF::URI.new("http://ar.to/#self"))
7
- # query.filter(:author => "Arto Bendiken")
8
- # query.filter(:author => [RDF::URI.new("http://ar.to/#self"), "Arto Bendiken"])
9
- # query.filter(:updated => RDF::Literal.new(Date.today))
5
+ # @example Constructing a basic graph pattern query (1)
6
+ # query = RDF::Query.new do
7
+ # pattern [:person, RDF.type, FOAF.Person]
8
+ # pattern [:person, FOAF.name, :name]
9
+ # pattern [:person, FOAF.mbox, :email]
10
+ # end
10
11
  #
11
- # @example Filtering solutions using a block
12
- # query.filter { |solution| solution.author.literal? }
13
- # query.filter { |solution| solution.title =~ /^SPARQL/ }
14
- # query.filter { |solution| solution.price < 30.5 }
15
- # query.filter { |solution| solution.bound?(:date) }
16
- # query.filter { |solution| solution.age.datatype == RDF::XSD.integer }
17
- # query.filter { |solution| solution.name.language == :es }
12
+ # @example Constructing a basic graph pattern query (2)
13
+ # query = RDF::Query.new({
14
+ # :person => {
15
+ # RDF.type => FOAF.Person,
16
+ # FOAF.name => :name,
17
+ # FOAF.mbox => :email,
18
+ # }
19
+ # })
18
20
  #
19
- # @example Reordering solutions based on a variable
20
- # query.order_by(:updated)
21
- # query.order_by(:updated, :created)
21
+ # @example Executing a basic graph pattern query
22
+ # graph = RDF::Graph.load('etc/doap.nt')
23
+ # query.execute(graph).each do |solution|
24
+ # solution.inspect
25
+ # end
22
26
  #
23
- # @example Selecting particular variables only
24
- # query.select(:title)
25
- # query.select(:title, :description)
27
+ # @example Constructing and executing a query in one go (1)
28
+ # solutions = RDF::Query.execute(graph) do
29
+ # pattern [:person, RDF.type, FOAF.Person]
30
+ # end
26
31
  #
27
- # @example Eliminating duplicate solutions
28
- # query.distinct!
29
- #
30
- # @example Limiting the number of solutions
31
- # query.offset(25).limit(10)
32
- #
33
- # @example Counting the number of solutions
34
- # query.count
35
- #
36
- # @example Iterating over all found solutions
37
- # query.each_solution { |solution| puts solution.inspect }
32
+ # @example Constructing and executing a query in one go (2)
33
+ # solutions = RDF::Query.execute(graph, {
34
+ # :person => {
35
+ # RDF.type => FOAF.Person,
36
+ # }
37
+ # })
38
38
  #
39
+ # @since 0.3.0
39
40
  class Query
40
- autoload :Pattern, 'rdf/query/pattern'
41
- autoload :Solution, 'rdf/query/solution'
42
- autoload :Variable, 'rdf/query/variable'
41
+ autoload :Pattern, 'rdf/query/pattern'
42
+ autoload :Solution, 'rdf/query/solution'
43
+ autoload :Solutions, 'rdf/query/solutions'
44
+ autoload :Variable, 'rdf/query/variable'
43
45
 
44
- include ::Enumerable
46
+ ##
47
+ # Executes a query on the given `queryable` graph or repository.
48
+ #
49
+ # @param [RDF::Queryable] queryable
50
+ # the graph or repository to query
51
+ # @param [Hash{Object => Object}] patterns
52
+ # optional hash patterns to initialize the query with
53
+ # @param [Hash{Symbol => Object}] options
54
+ # any additional keyword options (see {RDF::Query#initialize})
55
+ # @yield [query]
56
+ # @yieldparam [RDF::Query] query
57
+ # @yieldreturn [void] ignored
58
+ # @return [RDF::Query::Solutions]
59
+ # the resulting solution sequence
60
+ # @see RDF::Query#execute
61
+ def self.execute(queryable, patterns = nil, options = {}, &block)
62
+ self.new(patterns, options, &block).execute(queryable, options)
63
+ end
45
64
 
65
+ ##
66
+ # The variables used in this query.
67
+ #
46
68
  # @return [Hash{Symbol => RDF::Query::Variable}]
47
69
  attr_reader :variables
48
70
 
71
+ ##
72
+ # The patterns that constitute this query.
73
+ #
49
74
  # @return [Array<RDF::Query::Pattern>]
50
75
  attr_reader :patterns
51
76
 
52
- # @return [Array<Hash{Symbol => RDF::Value}>] An unordered sequence of query solutions.
53
- attr_accessor :solutions
77
+ ##
78
+ # The solution sequence for this query.
79
+ #
80
+ # @return [RDF::Query::Solutions]
81
+ attr_reader :solutions
54
82
 
83
+ ##
84
+ # Any additional options for this query.
85
+ #
55
86
  # @return [Hash]
56
87
  attr_reader :options
57
88
 
58
89
  ##
59
- # @param [Hash{Symbol => Object}] options
60
- # @yield [query]
61
- # @yieldparam [Query]
62
- def initialize(options = {}, &block)
90
+ # Initializes a new basic graph pattern query.
91
+ #
92
+ # @overload initialize(patterns = [], options = {})
93
+ # @param [Array<RDF::Query::Pattern>] patterns
94
+ # ...
95
+ # @param [Hash{Symbol => Object}] options
96
+ # any additional keyword options
97
+ # @option options [RDF::Query::Solutions] :solutions (Solutions.new)
98
+ # @yield [query]
99
+ # @yieldparam [RDF::Query] query
100
+ # @yieldreturn [void] ignored
101
+ #
102
+ # @overload initialize(patterns, options = {})
103
+ # @param [Hash{Object => Object}] patterns
104
+ # ...
105
+ # @param [Hash{Symbol => Object}] options
106
+ # any additional keyword options
107
+ # @option options [RDF::Query::Solutions] :solutions (Solutions.new)
108
+ # @yield [query]
109
+ # @yieldparam [RDF::Query] query
110
+ # @yieldreturn [void] ignored
111
+ def initialize(patterns = nil, options = {}, &block)
63
112
  @options = options.dup
64
- @variables = @options.delete(:variables) || {}
65
- @patterns = @options.delete(:patterns) || []
66
- @solutions = @options.delete(:solutions) || []
113
+ @variables = {}
114
+ @solutions = @options.delete(:solutions) || Solutions.new
115
+
116
+ @patterns = case patterns
117
+ when Hash then compile_hash_patterns(patterns.dup)
118
+ when Array then patterns
119
+ else []
120
+ end
67
121
 
68
122
  if block_given?
69
123
  case block.arity
70
- when 1 then block.call(self)
71
- else instance_eval(&block)
124
+ when 0 then instance_eval(&block)
125
+ else block.call(self)
72
126
  end
73
127
  end
74
128
  end
75
129
 
76
130
  ##
77
- # Enumerates over each query solution.
131
+ # Appends the given query `pattern` to this query.
78
132
  #
79
- # @yield [solution]
80
- # @yieldparam [Solution] solution
81
- # @return [Enumerator]
82
- def each_solution(&block)
83
- unless block_given?
84
- enum_for(:each_solution)
85
- else
86
- solutions.each do |solution|
87
- block.call(solution.is_a?(Solution) ? solution : Solution.new(solution))
88
- end
89
- end
133
+ # @param [RDF::Query::Pattern] pattern
134
+ # a triple query pattern
135
+ # @return [void] self
136
+ def <<(pattern)
137
+ @patterns << Pattern.from(pattern)
138
+ self
90
139
  end
91
140
 
92
- alias_method :each, :each_solution
93
-
94
141
  ##
95
- # Returns the number of query solutions.
142
+ # Appends the given query `pattern` to this query.
96
143
  #
97
- # @return [Integer]
98
- def count
99
- solutions.size
144
+ # @param [RDF::Query::Pattern] pattern
145
+ # a triple query pattern
146
+ # @param [Hash{Symbol => Object}] options
147
+ # any additional keyword options
148
+ # @option options [Boolean] :optional (false)
149
+ # whether this is an optional pattern
150
+ # @return [void] self
151
+ def pattern(pattern, options = {})
152
+ @patterns << Pattern.from(pattern, options)
153
+ self
100
154
  end
101
155
 
102
- alias_method :size, :count
103
-
104
156
  ##
105
- # Filters the solution sequence by the given criteria.
157
+ # Executes this query on the given `queryable` graph or repository.
106
158
  #
107
- # @param [Hash{Symbol => Object}] criteria
108
- # @yield [solution]
109
- # @yieldparam [Solution] solution
110
- # @yieldreturn [Boolean]
111
- # @return [Query]
112
- def filter(criteria = {}, &block)
113
- if block_given?
114
- solutions.reject! do |solution|
115
- !block.call(solution.is_a?(Solution) ? solution : Solution.new(solution))
116
- end
117
- else
118
- solutions.reject! do |solution|
119
- solution = solution.is_a?(Solution) ? solution : Solution.new(solution)
120
- results = criteria.map do |name, value|
121
- solution[name] == value
122
- end
123
- !results.all?
124
- end
125
- end
126
- self
127
- end
159
+ # @param [RDF::Queryable] queryable
160
+ # the graph or repository to query
161
+ # @param [Hash{Symbol => Object}] options
162
+ # any additional keyword options
163
+ # @return [RDF::Query::Solutions]
164
+ # the resulting solution sequence
165
+ # @see http://www.holygoat.co.uk/blog/entry/2005-10-25-1
166
+ def execute(queryable, options = {})
167
+ @solutions = Solutions.new
168
+ @failed = false
169
+ @patterns.each do |pattern|
170
+ case pattern.variable_count
171
+ when 0 # no variables
172
+ if pattern.execute(queryable).empty?
173
+ # return an empty solution sequence:
174
+ @solutions.clear
175
+ @failed = true
176
+ break
177
+ end
128
178
 
129
- alias_method :filter!, :filter
179
+ when 3 # only variables
180
+ pattern.execute(queryable) do |statement|
181
+ @solutions << pattern.solution(statement)
182
+ end
130
183
 
131
- ##
132
- # Reorders the solution sequence based on `variables`.
133
- #
134
- # @param [Array<Symbol>] variables
135
- # @return [Query]
136
- def order(*variables)
137
- if variables.empty?
138
- raise ArgumentError.new("wrong number of arguments (0 for 1)")
139
- else
140
- # TODO: support for descending sort, e.g. order(:s => :asc, :p => :desc)
141
- variables.map!(&:to_sym)
142
- solutions.sort! do |a, b|
143
- a = variables.map { |variable| a[variable].to_s }
144
- b = variables.map { |variable| b[variable].to_s }
145
- a <=> b
146
- end
147
- end
148
- self
149
- end
184
+ else case # 1 or 2 variables
150
185
 
151
- alias_method :order_by, :order
186
+ when !@solutions.have_variables?(pattern.variables.values)
187
+ if @solutions.empty?
188
+ pattern.execute(queryable) do |statement|
189
+ @solutions << pattern.solution(statement)
190
+ end
191
+ else # union
192
+ old_solutions, @solutions = @solutions, Solutions.new
193
+ old_solutions.each do |solution|
194
+ pattern.execute(queryable) do |statement|
195
+ @solutions << solution.merge(pattern.solution(statement))
196
+ end
197
+ end
198
+ end
152
199
 
153
- ##
154
- # Restricts the the solution sequence to the given `variables` only.
155
- #
156
- # @param [Array<Symbol>] variables
157
- # @return [Query]
158
- def project(*variables)
159
- unless variables.empty?
160
- variables.map!(&:to_sym)
161
- solutions.each do |bindings|
162
- bindings.delete_if { |k, v| !variables.include?(k) } # FIXME
200
+ else # intersection
201
+ @solutions.each_with_index do |solution, index|
202
+ failed = true
203
+ pattern.execute(queryable, solution) do |statement|
204
+ failed = false
205
+ solution.merge!(pattern.solution(statement))
206
+ end
207
+ @solutions[index] = nil if failed && !pattern.optional?
208
+ end
209
+ @solutions.compact! # remove `nil` entries
210
+ end
163
211
  end
164
212
  end
165
- self
213
+ @solutions
166
214
  end
167
215
 
168
- alias_method :select, :project
169
-
170
216
  ##
171
- # Ensures solutions in the solution sequence are unique.
217
+ # Returns `true` if this query did not match when last executed.
172
218
  #
173
- # @return [Query]
174
- def distinct
175
- solutions.uniq!
176
- self
219
+ # When the solution sequence is empty, this method can be used to
220
+ # determine whether the query failed to match or not.
221
+ #
222
+ # @return [Boolean]
223
+ # @see #matched?
224
+ def failed?
225
+ @failed
177
226
  end
178
227
 
179
- alias_method :distinct!, :distinct
180
- alias_method :reduced, :distinct
181
- alias_method :reduced!, :distinct
182
-
183
228
  ##
184
- # Limits the solution sequence to bindings starting from the `start`
185
- # offset in the overall solution sequence.
229
+ # Returns `true` if this query matched when last executed.
230
+ #
231
+ # When the solution sequence is empty, this method can be used to
232
+ # determine whether the query matched successfully or not.
186
233
  #
187
- # @param [Integer, #to_i] start
188
- # @return [Query]
189
- def offset(start)
190
- slice(start, solutions.size - start.to_i)
234
+ # @return [Boolean]
235
+ # @see #failed?
236
+ def matched?
237
+ !@failed
191
238
  end
192
239
 
193
- alias_method :offset!, :offset
194
-
195
240
  ##
196
- # Limits the number of solutions to `length`.
241
+ # Enumerates over each matching query solution.
197
242
  #
198
- # @param [Integer, #to_i] length
199
- # @return [Query]
200
- def limit(length)
201
- slice(0, length)
243
+ # @yield [solution]
244
+ # @yieldparam [RDF::Query::Solution] solution
245
+ # @return [Enumerator]
246
+ def each_solution(&block)
247
+ @solutions.each(&block)
202
248
  end
249
+ alias_method :each, :each_solution
203
250
 
204
- alias_method :limit!, :limit
251
+ protected
205
252
 
206
253
  ##
207
- # Limits the solution sequence to `length` bindings starting from the
208
- # `start` offset in the overall solution sequence.
209
- #
210
- # @param [Integer, #to_i] start
211
- # @param [Integer, #to_i] length
212
- # @return [Query]
213
- def slice(start, length)
214
- if (start = start.to_i) < solutions.size
215
- solutions.slice!(start, length.to_i)
216
- else
217
- solutions = []
254
+ # @private
255
+ def compile_hash_patterns(hash_patterns)
256
+ patterns = []
257
+ hash_patterns.each do |s, pos|
258
+ raise ArgumentError, "invalid hash pattern: #{hash_patterns.inspect}" unless pos.is_a?(Hash)
259
+ pos.each do |p, os|
260
+ case os
261
+ when Hash
262
+ patterns += os.keys.map { |o| [s, p, o] }
263
+ patterns += compile_hash_patterns(os)
264
+ when Array
265
+ patterns += os.map { |o| [s, p, o] }
266
+ else
267
+ patterns << [s, p, os]
268
+ end
269
+ end
218
270
  end
219
- self
271
+ patterns.map { |pattern| Pattern.from(pattern) }
220
272
  end
221
-
222
- alias_method :slice!, :slice
223
- end
224
- end
273
+ end # Query
274
+ end # RDF