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
@@ -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