rdf 1.99.1 → 2.0.0.beta1

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/{README → README.md} +9 -44
  3. data/VERSION +1 -1
  4. data/bin/rdf +1 -1
  5. data/lib/rdf.rb +40 -49
  6. data/lib/rdf/changeset.rb +161 -0
  7. data/lib/rdf/cli.rb +195 -33
  8. data/lib/rdf/cli/vocab-loader.rb +13 -3
  9. data/lib/rdf/format.rb +44 -26
  10. data/lib/rdf/mixin/enumerable.rb +133 -97
  11. data/lib/rdf/mixin/enumerator.rb +8 -0
  12. data/lib/rdf/mixin/indexable.rb +1 -1
  13. data/lib/rdf/mixin/mutable.rb +101 -22
  14. data/lib/rdf/mixin/queryable.rb +21 -32
  15. data/lib/rdf/mixin/transactable.rb +94 -0
  16. data/lib/rdf/mixin/writable.rb +12 -3
  17. data/lib/rdf/model/dataset.rb +48 -0
  18. data/lib/rdf/model/graph.rb +73 -43
  19. data/lib/rdf/model/list.rb +61 -33
  20. data/lib/rdf/model/literal.rb +20 -19
  21. data/lib/rdf/model/literal/double.rb +20 -4
  22. data/lib/rdf/model/literal/numeric.rb +15 -13
  23. data/lib/rdf/model/node.rb +15 -16
  24. data/lib/rdf/model/statement.rb +1 -43
  25. data/lib/rdf/model/term.rb +10 -8
  26. data/lib/rdf/model/uri.rb +35 -34
  27. data/lib/rdf/model/value.rb +1 -1
  28. data/lib/rdf/nquads.rb +2 -11
  29. data/lib/rdf/ntriples.rb +1 -1
  30. data/lib/rdf/ntriples/reader.rb +33 -46
  31. data/lib/rdf/ntriples/writer.rb +42 -5
  32. data/lib/rdf/query.rb +6 -40
  33. data/lib/rdf/query/pattern.rb +4 -17
  34. data/lib/rdf/query/solutions.rb +6 -6
  35. data/lib/rdf/reader.rb +65 -14
  36. data/lib/rdf/repository.rb +365 -229
  37. data/lib/rdf/transaction.rb +211 -84
  38. data/lib/rdf/util.rb +1 -0
  39. data/lib/rdf/util/cache.rb +5 -5
  40. data/lib/rdf/util/file.rb +12 -9
  41. data/lib/rdf/util/logger.rb +272 -0
  42. data/lib/rdf/version.rb +2 -2
  43. data/lib/rdf/vocab/owl.rb +82 -77
  44. data/lib/rdf/vocab/rdfs.rb +22 -17
  45. data/lib/rdf/vocab/xsd.rb +5 -0
  46. data/lib/rdf/vocabulary.rb +50 -56
  47. data/lib/rdf/writer.rb +104 -52
  48. metadata +45 -90
  49. data/lib/rdf/mixin/inferable.rb +0 -5
  50. data/lib/rdf/vocab/cc.rb +0 -128
  51. data/lib/rdf/vocab/cert.rb +0 -245
  52. data/lib/rdf/vocab/dc.rb +0 -948
  53. data/lib/rdf/vocab/dc11.rb +0 -167
  54. data/lib/rdf/vocab/dcat.rb +0 -214
  55. data/lib/rdf/vocab/doap.rb +0 -337
  56. data/lib/rdf/vocab/exif.rb +0 -941
  57. data/lib/rdf/vocab/foaf.rb +0 -614
  58. data/lib/rdf/vocab/geo.rb +0 -157
  59. data/lib/rdf/vocab/gr.rb +0 -1501
  60. data/lib/rdf/vocab/ht.rb +0 -236
  61. data/lib/rdf/vocab/ical.rb +0 -528
  62. data/lib/rdf/vocab/ma.rb +0 -513
  63. data/lib/rdf/vocab/mo.rb +0 -2412
  64. data/lib/rdf/vocab/og.rb +0 -222
  65. data/lib/rdf/vocab/ogc.rb +0 -58
  66. data/lib/rdf/vocab/prov.rb +0 -1550
  67. data/lib/rdf/vocab/rsa.rb +0 -72
  68. data/lib/rdf/vocab/rss.rb +0 -66
  69. data/lib/rdf/vocab/schema.rb +0 -10569
  70. data/lib/rdf/vocab/sioc.rb +0 -669
  71. data/lib/rdf/vocab/skos.rb +0 -238
  72. data/lib/rdf/vocab/skosxl.rb +0 -57
  73. data/lib/rdf/vocab/v.rb +0 -383
  74. data/lib/rdf/vocab/vcard.rb +0 -841
  75. data/lib/rdf/vocab/vmd.rb +0 -383
  76. data/lib/rdf/vocab/void.rb +0 -186
  77. data/lib/rdf/vocab/vs.rb +0 -28
  78. data/lib/rdf/vocab/wdrs.rb +0 -134
  79. data/lib/rdf/vocab/wot.rb +0 -167
  80. data/lib/rdf/vocab/xhtml.rb +0 -8
  81. data/lib/rdf/vocab/xhv.rb +0 -505
@@ -112,7 +112,7 @@ module RDF; class Query
112
112
  # @yield [solution]
113
113
  # @yieldparam [RDF::Query::Solution] solution
114
114
  # @yieldreturn [Boolean]
115
- # @return [void] `self`
115
+ # @return [self]
116
116
  def filter(criteria = {})
117
117
  if block_given?
118
118
  self.reject! do |solution|
@@ -164,7 +164,7 @@ module RDF; class Query
164
164
  # @yieldparam [RDF::Query::Solution] q
165
165
  # @yieldparam [RDF::Query::Solution] b
166
166
  # @yieldreturn [Integer] -1, 0, or 1 depending on value of comparator
167
- # @return [void] `self`
167
+ # @return [self]
168
168
  def order(*variables)
169
169
  if variables.empty? && !block_given?
170
170
  raise ArgumentError, "wrong number of arguments (0 for 1)"
@@ -191,7 +191,7 @@ module RDF; class Query
191
191
  # Restricts this solution sequence to the given `variables` only.
192
192
  #
193
193
  # @param [Array<Symbol, #to_sym>] variables
194
- # @return [void] `self`
194
+ # @return [self]
195
195
  def project(*variables)
196
196
  if variables.empty?
197
197
  raise ArgumentError, "wrong number of arguments (0 for 1)"
@@ -208,7 +208,7 @@ module RDF; class Query
208
208
  ##
209
209
  # Ensures that the solutions in this solution sequence are unique.
210
210
  #
211
- # @return [void] `self`
211
+ # @return [self]
212
212
  def distinct
213
213
  self.uniq!
214
214
  self
@@ -223,7 +223,7 @@ module RDF; class Query
223
223
  #
224
224
  # @param [Integer, #to_i] start
225
225
  # zero or a positive or negative integer
226
- # @return [void] `self`
226
+ # @return [self]
227
227
  def offset(start)
228
228
  case start = start.to_i
229
229
  when 0 then nil
@@ -239,7 +239,7 @@ module RDF; class Query
239
239
  #
240
240
  # @param [Integer, #to_i] length
241
241
  # zero or a positive integer
242
- # @return [void] `self`
242
+ # @return [self]
243
243
  # @raise [ArgumentError] if `length` is negative
244
244
  def limit(length)
245
245
  length = length.to_i
@@ -39,6 +39,7 @@ module RDF
39
39
  class Reader
40
40
  extend ::Enumerable
41
41
  extend RDF::Util::Aliasing::LateBound
42
+ include RDF::Util::Logger
42
43
  include RDF::Readable
43
44
  include RDF::Enumerable # @since 0.3.0
44
45
 
@@ -108,6 +109,52 @@ module RDF
108
109
  end
109
110
  end
110
111
 
112
+ ##
113
+ # Options suitable for automatic Reader provisioning.
114
+ # @return [Array<RDF::CLI::Option>]
115
+ def self.options
116
+ [
117
+ RDF::CLI::Option.new(
118
+ symbol: :canonicalize,
119
+ datatype: TrueClass,
120
+ on: ["--canonicalize"],
121
+ description: "Canonicalize input/output.") {true},
122
+ RDF::CLI::Option.new(
123
+ symbol: :encoding,
124
+ datatype: Encoding,
125
+ on: ["--encoding ENCODING"],
126
+ description: "The encoding of the input stream.") {|arg| Encoding.find arg},
127
+ RDF::CLI::Option.new(
128
+ symbol: :intern,
129
+ datatype: TrueClass,
130
+ on: ["--intern"],
131
+ description: "Intern all parsed URIs.") {true},
132
+ RDF::CLI::Option.new(
133
+ symbol: :prefixes,
134
+ datatype: Hash,
135
+ multiple: true,
136
+ on: ["--prefixes PREFIX,PREFIX"],
137
+ description: "A comma-separated list of prefix:uri pairs.") do |arg|
138
+ arg.split(',').inject({}) do |memo, pfxuri|
139
+ pfx,uri = pfxuri.split(':', 2)
140
+ memo.merge(pfx.to_sym => RDF::URI(uri))
141
+ end
142
+ end,
143
+ RDF::CLI::Option.new(
144
+ symbol: :base_uri,
145
+ datatype: RDF::URI,
146
+ on: ["--uri URI"],
147
+ description: "Base URI of input file, defaults to the filename.") {|arg| RDF::URI(arg)},
148
+ RDF::CLI::Option.new(
149
+ symbol: :validate,
150
+ datatype: TrueClass,
151
+ on: ["--validate"],
152
+ description: "Validate input file.") {true},
153
+ ]
154
+ end
155
+
156
+ # Returns a hash of options appropriate for use with this reader
157
+
111
158
  class << self
112
159
  alias_method :format_class, :format
113
160
  end
@@ -125,20 +172,21 @@ module RDF
125
172
  # end
126
173
  #
127
174
  # @param [String, #to_s] filename
175
+ # @param [Symbol] format
128
176
  # @param [Hash{Symbol => Object}] options
129
177
  # any additional options (see {RDF::Util::File.open_file}, {RDF::Reader#initialize} and {RDF::Format.for})
130
- # @option options [Symbol] :format (:ntriples)
131
178
  # @yield [reader]
132
179
  # @yieldparam [RDF::Reader] reader
133
180
  # @yieldreturn [void] ignored
134
181
  # @raise [RDF::FormatError] if no reader found for the specified format
135
- def self.open(filename, options = {}, &block)
182
+ def self.open(filename, format: nil, **options, &block)
136
183
  Util::File.open_file(filename, options) do |file|
137
184
  format_options = options.dup
138
185
  format_options[:content_type] ||= file.content_type if file.respond_to?(:content_type)
139
186
  format_options[:file_name] ||= filename
140
187
  options[:encoding] ||= file.encoding if file.respond_to?(:encoding)
141
- reader = self.for(format_options[:format] || format_options) do
188
+ options[:filename] ||= filename
189
+ reader = self.for(format || format_options) do
142
190
  # Return a sample from the input file
143
191
  sample = file.read(1000)
144
192
  file.rewind
@@ -350,7 +398,7 @@ module RDF
350
398
  #
351
399
  # @return [void]
352
400
  # @since 0.2.3
353
- # @see http://ruby-doc.org/core-1.9/classes/IO.html#M001692
401
+ # @see http://ruby-doc.org/core-2.2.2/IO.html#method-i-rewind
354
402
  def rewind
355
403
  @input.rewind
356
404
  end
@@ -364,7 +412,7 @@ module RDF
364
412
  #
365
413
  # @return [void]
366
414
  # @since 0.2.2
367
- # @see http://ruby-doc.org/core-1.9/classes/IO.html#M001699
415
+ # @see http://ruby-doc.org/core-2.2.2/IO.html#method-i-close
368
416
  def close
369
417
  @input.close unless @input.closed?
370
418
  end
@@ -380,16 +428,22 @@ module RDF
380
428
  ##
381
429
  # @return [Boolean]
382
430
  #
383
- # @note this parses the full input.
431
+ # @note this parses the full input and is valid only in the reader block.
384
432
  # Use `Reader.new(input, validate: true)` if you intend to capture the
385
433
  # result.
386
434
  #
435
+ # @example Parsing RDF statements from a file
436
+ # RDF::NTriples::Reader.new("!!invalid input??") do |reader|
437
+ # reader.valid? # => false
438
+ # end
439
+ #
387
440
  # @see RDF::Value#validate! for Literal & URI validation relevant to
388
441
  # error handling.
389
442
  # @see Enumerable#valid?
390
443
  def valid?
391
- super
392
- rescue ArgumentError, RDF::ReaderError
444
+ super && !log_statistics[:error]
445
+ rescue ArgumentError, RDF::ReaderError => e
446
+ log_error(e.message)
393
447
  false
394
448
  end
395
449
 
@@ -421,8 +475,7 @@ module RDF
421
475
  # @return [void]
422
476
  # @raise [RDF::ReaderError]
423
477
  def fail_subject
424
- raise RDF::ReaderError.new("ERROR [line #{lineno}] Expected subject (found: #{current_line.inspect})",
425
- lineno: lineno)
478
+ log_error("Expected subject (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
426
479
  end
427
480
 
428
481
  ##
@@ -431,8 +484,7 @@ module RDF
431
484
  # @return [void]
432
485
  # @raise [RDF::ReaderError]
433
486
  def fail_predicate
434
- raise RDF::ReaderError.new("ERROR [line #{lineno}] Expected predicate (found: #{current_line.inspect})",
435
- lineno: lineno)
487
+ log_error("Expected predicate (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
436
488
  end
437
489
 
438
490
  ##
@@ -441,8 +493,7 @@ module RDF
441
493
  # @return [void]
442
494
  # @raise [RDF::ReaderError]
443
495
  def fail_object
444
- raise RDF::ReaderError.new("ERROR [line #{lineno}] Expected object (found: #{current_line.inspect})",
445
- lineno: lineno)
496
+ log_error("Expected object (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
446
497
  end
447
498
 
448
499
  public
@@ -40,12 +40,55 @@ module RDF
40
40
  # @example Deleting all statements from a repository
41
41
  # repository.clear!
42
42
  #
43
- class Repository
44
- include RDF::Countable
45
- include RDF::Enumerable
46
- include RDF::Queryable
43
+ # Repositories support transactions with a variety of ACID semantics:
44
+ #
45
+ # Atomicity is indicated by `#supports?(:atomic_write)`. When atomicity is
46
+ # supported, writes through `#transaction`, `#apply_changeset` and
47
+ # `#delete_insert` are applied atomically.
48
+ #
49
+ # Consistency should be guaranteed, in general. Repositories that don't
50
+ # support consistency, or that have specialized definitions of consistency
51
+ # above those declared by the RDF data model, should advertise this fact in
52
+ # their documentation.
53
+ #
54
+ # Isolation may be supported at various levels, indicated by
55
+ # `#isolation_level`:
56
+ # - `:read_uncommitted`: Inserts & deletes in an uncommitted transaction
57
+ # scope may be visible to other transactions (or via `#each`, etc...)
58
+ # - `:read_committed`: Inserts & deletes may be visible to other
59
+ # transactions once committed
60
+ # - `:repeatable_read`: Phantom reads may be possible
61
+ # - `:snapshot`: A transaction reads a consistent snapshot of the data.
62
+ # Write skew anomalies may occur (for various definitions of consistency)
63
+ # - `:serializable`: A transaction reads a consistent snapshot of the data.
64
+ # When two or more transactions attempt conflicting writes, only one of
65
+ # them may succeed.
66
+ #
67
+ # Durability is noted via `RDF::Durable` support and `#durable?`
68
+ # /`#nondurable?`.
69
+ #
70
+ # @example Transational read from a repository
71
+ # repository.transaction do |tx|
72
+ # tx.has_statement?(statement)
73
+ # tx.query([:s, :p, :o])
74
+ # end
75
+ #
76
+ # @example Transational read/write of a repository
77
+ # repository.transaction(mutable: true) do |tx|
78
+ # tx.insert(*statements)
79
+ # tx.insert(statement)
80
+ # tx.insert([subject, predicate, object])
81
+ # tx.delete(*statements)
82
+ # tx.delete(statement)
83
+ # tx.delete([subject, predicate, object])
84
+ # end
85
+ #
86
+ class Repository < Dataset
47
87
  include RDF::Mutable
48
- include RDF::Durable
88
+
89
+ include RDF::Transactable
90
+
91
+ DEFAULT_TX_CLASS = RDF::Transaction
49
92
 
50
93
  ##
51
94
  # Returns the options passed to this repository when it was constructed.
@@ -69,16 +112,16 @@ module RDF
69
112
  ##
70
113
  # Loads one or more RDF files into a new transient in-memory repository.
71
114
  #
72
- # @param [String, Array<String>] filenames
115
+ # @param [String, Array<String>] urls
73
116
  # @param [Hash{Symbol => Object}] options
74
117
  # Options from {RDF::Repository#initialize} and {RDF::Mutable#load}
75
118
  # @yield [repository]
76
119
  # @yieldparam [Repository]
77
120
  # @return [void]
78
- def self.load(filenames, options = {}, &block)
121
+ def self.load(urls, options = {}, &block)
79
122
  self.new(options) do |repository|
80
- Array(filenames).each do |filename|
81
- repository.load(filename, options)
123
+ Array(urls).each do |url|
124
+ repository.load(url, options)
82
125
  end
83
126
 
84
127
  if block_given?
@@ -93,28 +136,28 @@ module RDF
93
136
  ##
94
137
  # Initializes this repository instance.
95
138
  #
96
- # @param [Hash{Symbol => Object}] options
97
- # @option options [URI, #to_s] :uri (nil)
98
- # @option options [String, #to_s] :title (nil)
99
- # @option options [Boolean] :with_graph_name (true)
139
+ # @param [URI, #to_s] uri (nil)
140
+ # @param [String, #to_s] title (nil)
141
+ # @param [Hash{Symbol => Object}] options
142
+ # @option options [Boolean] :with_graph_name (true)
100
143
  # Indicates that the repository supports named graphs, otherwise,
101
144
  # only the default graph is supported.
102
- # @option options [Boolean] :with_context (true)
103
- # Alias for :with_graph_name. :with_context is deprecated in RDF.rb 2.0.
145
+ # @option options [Boolean] :with_validity (true)
146
+ # Indicates that the repository supports named validation.
147
+ # @option options [Boolean] :transaction_class (DEFAULT_TX_CLASS)
148
+ # Specifies the RDF::Transaction implementation to use in this Repository.
104
149
  # @yield [repository]
105
150
  # @yieldparam [Repository] repository
106
- def initialize(options = {}, &block)
107
- if options[:with_context]
108
- warn "[DEPRECATION] the :contexts option to Repository#initialize is deprecated in RDF.rb 2.0, use :graph_name instead. Called from #{Gem.location_of_caller.join(':')}"
109
- options[:graph_name] ||= options.delete(:with_context)
110
- end
111
- @options = {with_graph_name: true}.merge(options)
112
- @uri = @options.delete(:uri)
113
- @title = @options.delete(:title)
151
+ def initialize(uri: nil, title: nil, **options, &block)
152
+ @options = {with_graph_name: true, with_validity: true}.merge(options)
153
+ @uri = uri
154
+ @title = title
114
155
 
115
156
  # Provide a default in-memory implementation:
116
157
  send(:extend, Implementation) if self.class.equal?(RDF::Repository)
117
158
 
159
+ @tx_class ||= @options.delete(:transaction_class) { DEFAULT_TX_CLASS }
160
+
118
161
  if block_given?
119
162
  case block.arity
120
163
  when 1 then block.call(self)
@@ -124,109 +167,90 @@ module RDF
124
167
  end
125
168
 
126
169
  ##
127
- # Returns a developer-friendly representation of this object.
170
+ # Returns `true` if this respository supports the given `feature`.
128
171
  #
129
- # @return [String]
130
- def inspect
131
- sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, uri.to_s)
172
+ # Supported features include those from {RDF::Enumerable#supports?} along
173
+ # with the following:
174
+ # * `:atomic_write` supports atomic write in transaction scopes
175
+ # * `:snapshots` supports creation of immutable snapshots of current
176
+ # contents via #snapshot.
177
+ # @see RDF::Enumerable#supports?
178
+ def supports?(feature)
179
+ case feature.to_sym
180
+ when :graph_name then @options[:with_graph_name]
181
+ when :inference then false # forward-chaining inference
182
+ when :validity then @options.fetch(:with_validity, true)
183
+ when :atomic_write then false
184
+ when :snapshots then false
185
+ else false
186
+ end
132
187
  end
133
188
 
134
189
  ##
135
- # Outputs a developer-friendly representation of this object to
136
- # `stderr`.
190
+ # Performs a set of deletes and inserts as a combined operation within a
191
+ # transaction. The Repository's transaction semantics apply to updates made
192
+ # through this method.
137
193
  #
138
- # @return [void]
139
- def inspect!
140
- each_statement { |statement| statement.inspect! }
141
- nil
142
- end
194
+ # @see RDF::Mutable#delete_insert
195
+ def delete_insert(deletes, inserts)
196
+ return super unless supports?(:atomic_write)
143
197
 
144
- ##
145
- # Executes the given block in a transaction.
146
- #
147
- # @example
148
- # repository.transaction do |tx|
149
- # tx.insert [RDF::URI("http://rubygems.org/gems/rdf"), RDF::RDFS.label, "RDF.rb"]
150
- # end
151
- #
152
- # @param [RDF::Resource] graph_name
153
- # Context on which to run the transaction, use `false` for the default
154
- # graph_name and `nil` the entire Repository
155
- # @yield [tx]
156
- # @yieldparam [RDF::Transaction] tx
157
- # @yieldreturn [void] ignored
158
- # @return [self]
159
- # @see RDF::Transaction
160
- # @since 0.3.0
161
- def transaction(graph_name = nil, &block)
162
- tx = begin_transaction(graph_name)
163
- begin
164
- case block.arity
165
- when 1 then block.call(tx)
166
- else tx.instance_eval(&block)
167
- end
168
- rescue => error
169
- rollback_transaction(tx)
170
- raise error
198
+ transaction(mutable: true) do
199
+ deletes.respond_to?(:each_statement) ? delete(deletes) : delete(*deletes)
200
+ inserts.respond_to?(:each_statement) ? insert(inserts) : insert(*inserts)
171
201
  end
172
- commit_transaction(tx)
173
- self
174
202
  end
175
- alias_method :transact, :transaction
176
-
177
- protected
178
203
 
179
204
  ##
180
- # Begins a new transaction.
181
- #
182
- # Subclasses implementing transaction-capable storage adapters may wish
183
- # to override this method in order to begin a transaction against the
184
- # underlying storage.
185
- #
186
- # @param [RDF::Resource] graph_name
187
- # @return [RDF::Transaction]
188
- # @since 0.3.0
189
- def begin_transaction(graph_name)
190
- RDF::Transaction.new(graph: graph_name)
205
+ # @private
206
+ # @see RDF::Enumerable#project_graph
207
+ def project_graph(graph_name, &block)
208
+ RDF::Graph.new(graph_name: graph_name, data: self).
209
+ project_graph(graph_name, &block)
191
210
  end
192
211
 
193
212
  ##
194
- # Rolls back the given transaction.
195
- #
196
- # Subclasses implementing transaction-capable storage adapters may wish
197
- # to override this method in order to roll back the given transaction in
198
- # the underlying storage.
199
- #
200
- # @param [RDF::Transaction] tx
201
- # @return [void] ignored
202
- # @since 0.3.0
203
- def rollback_transaction(tx)
204
- # nothing to do
213
+ # @see RDF::Dataset#isolation_level
214
+ def isolation_level
215
+ supports?(:snapshot) ? :repeatable_read : super
205
216
  end
206
217
 
207
218
  ##
208
- # Commits the given transaction.
209
- #
210
- # Subclasses implementing transaction-capable storage adapters may wish
211
- # to override this method in order to commit the given transaction to
212
- # the underlying storage.
213
- #
214
- # @param [RDF::Transaction] tx
215
- # @return [void] ignored
216
- # @since 0.3.0
217
- def commit_transaction(tx)
218
- tx.execute(self)
219
+ # A queryable snapshot of the repository for isolated reads.
220
+ #
221
+ # @return [Dataset] an immutable Dataset containing a current snapshot of
222
+ # the Repository contents.
223
+ def snapshot
224
+ raise NotImplementedError.new("#{self.class}#snapshot")
219
225
  end
226
+
227
+ protected
228
+
229
+ ##
230
+ # @private
231
+ # @see RDF::Transactable#begin_transaction
232
+ # @since 0.3.0
233
+ def begin_transaction(mutable: false, graph_name: nil)
234
+ @tx_class.new(self, mutable: mutable, graph_name: graph_name)
235
+ end
220
236
 
221
237
  ##
238
+ # A default Repository implementation supporting atomic writes and
239
+ # serializable transactions.
240
+ #
222
241
  # @see RDF::Repository
223
242
  module Implementation
243
+ require 'hamster'
224
244
  DEFAULT_GRAPH = false
225
245
 
226
246
  ##
227
247
  # @private
228
248
  def self.extend_object(obj)
229
- obj.instance_variable_set(:@data, obj.options.delete(:data) || {})
249
+ obj.instance_variable_set(:@data, obj.options.delete(:data) ||
250
+ Hamster::Hash.new)
251
+ obj.instance_variable_set(:@tx_class,
252
+ obj.options.delete(:transaction_class) ||
253
+ SerializedTransaction)
230
254
  super
231
255
  end
232
256
 
@@ -235,49 +259,66 @@ module RDF
235
259
  # @see RDF::Enumerable#supports?
236
260
  def supports?(feature)
237
261
  case feature.to_sym
238
- #statement named graphs
239
- when :context
240
- warn "[DEPRECATION] the :context feature is deprecated in RDF.rb 2.0; use :graph_name instead. Called from #{Gem.location_of_caller.join(':')}"
241
- @options[:with_context] || @options[:with_graph_name]
242
- when :graph_name then @options[:with_graph_name]
243
- when :inference then false # forward-chaining inference
244
- when :validity then @options.fetch(:with_validity, true)
245
- else false
262
+ when :graph_name then @options[:with_graph_name]
263
+ when :inference then false # forward-chaining inference
264
+ when :validity then @options.fetch(:with_validity, true)
265
+ when :atomic_write then true
266
+ when :snapshots then true
267
+ else false
246
268
  end
247
269
  end
248
270
 
271
+ ##
272
+ # @private
273
+ # @see RDF::Countable#count
274
+ def count
275
+ count = 0
276
+ @data.each do |_, ss|
277
+ ss.each do |_, ps|
278
+ ps.each { |_, os| count += os.size }
279
+ end
280
+ end
281
+ count
282
+ end
283
+
249
284
  ##
250
285
  # @private
251
286
  # @see RDF::Durable#durable?
252
287
  def durable?
253
288
  false
254
289
  end
290
+
291
+ ##
292
+ # @private
293
+ # @see RDF::Enumerable#has_graph?
294
+ def has_graph?(graph)
295
+ @data.has_key?(graph)
296
+ end
255
297
 
256
298
  ##
257
299
  # @private
258
- # @see RDF::Countable#count
259
- def count
260
- count = 0
261
- @data.each do |g, ss|
262
- ss.each do |s, ps|
263
- ps.each do |p, os|
264
- count += os.size
265
- end
300
+ # @see RDF::Enumerable#each_graph
301
+ def graph_names(options = nil, &block)
302
+ @data.keys.reject { |g| g == DEFAULT_GRAPH }.to_a
303
+ end
304
+
305
+ ##
306
+ # @private
307
+ # @see RDF::Enumerable#each_graph
308
+ def each_graph(&block)
309
+ if block_given?
310
+ @data.each_key do |gn|
311
+ yield RDF::Graph.new(graph_name: (gn == DEFAULT_GRAPH ? nil : gn), data: self)
266
312
  end
267
313
  end
268
- count
314
+ enum_graph
269
315
  end
270
316
 
271
317
  ##
272
318
  # @private
273
319
  # @see RDF::Enumerable#has_statement?
274
320
  def has_statement?(statement)
275
- s, p, o, g = statement.to_quad
276
- g ||= DEFAULT_GRAPH
277
- @data.has_key?(g) &&
278
- @data[g].has_key?(s) &&
279
- @data[g][s].has_key?(p) &&
280
- @data[g][s][p].include?(o)
321
+ has_statement_in?(@data, statement)
281
322
  end
282
323
 
283
324
  ##
@@ -285,16 +326,11 @@ module RDF
285
326
  # @see RDF::Enumerable#each_statement
286
327
  def each_statement(&block)
287
328
  if block_given?
288
- # Note that to iterate in a more consistent fashion despite
289
- # possible concurrent mutations to `@data`, we use `#dup` to make
290
- # shallow copies of the nested hashes before beginning the
291
- # iteration over their keys and values.
292
- @data.dup.each do |g, ss|
293
- ss.dup.each do |s, ps|
294
- ps.dup.each do |p, os|
295
- os.dup.each do |o|
296
- # FIXME: yield has better performance, but broken in MRI 2.2: See https://bugs.ruby-lang.org/issues/11451.
297
- block.call(RDF::Statement.new(s, p, o, graph_name: g.equal?(DEFAULT_GRAPH) ? nil : g))
329
+ @data.each do |g, ss|
330
+ ss.each do |s, ps|
331
+ ps.each do |p, os|
332
+ os.each do |o|
333
+ yield RDF::Statement.new(s, p, o, graph_name: g.equal?(DEFAULT_GRAPH) ? nil : g)
298
334
  end
299
335
  end
300
336
  end
@@ -305,92 +341,81 @@ module RDF
305
341
  alias_method :each, :each_statement
306
342
 
307
343
  ##
308
- # @private
309
- # @see RDF::Enumerable#has_context?
310
- # @deprecated Use {#has_graph?} instead.
311
- def has_context?(value)
312
- warn "[DEPRECATION] Repository#has_context? is deprecated in RDF.rb 2.0, use Repository#has_graph? instead. Called from #{Gem.location_of_caller.join(':')}"
313
- has_graph?(value)
344
+ # @see Mutable#apply_changeset
345
+ def apply_changeset(changeset)
346
+ data = @data
347
+ changeset.deletes.each { |del| data = delete_from(data, del) }
348
+ changeset.inserts.each { |ins| data = insert_to(data, ins) }
349
+ @data = data
314
350
  end
315
351
 
316
352
  ##
317
- # @private
318
- # @see RDF::Enumerable#has_graph?
319
- def has_graph?(value)
320
- @data.keys.include?(value)
321
- end
322
- ##
323
- # @private
324
- # @see RDF::Enumerable#each_graph
325
- def graph_names(options = nil, &block)
326
- @data.keys.reject {|g| g == DEFAULT_GRAPH}
353
+ # @see RDF::Dataset#isolation_level
354
+ def isolation_level
355
+ :serializable
327
356
  end
328
357
 
329
358
  ##
330
- # @private
331
- # @see RDF::Enumerable#each_context
332
- # @deprecated Use {#each_graph} instead.
333
- def each_context(&block)
334
- warn "[DEPRECATION] Repository#each_context is deprecated in RDF.rb 2.0, use Repository#each_graph instead. Called from #{Gem.location_of_caller.join(':')}"
335
- if block_given?
336
- contexts = @data.keys
337
- contexts.delete(DEFAULT_GRAPH)
338
- contexts.each(&block)
339
- end
340
- enum_context
359
+ # A readable & queryable snapshot of the repository for isolated reads.
360
+ #
361
+ # @return [Dataset] an immutable Dataset containing a current snapshot of
362
+ # the Repository contents.
363
+ #
364
+ # @see Mutable#snapshot
365
+ def snapshot
366
+ self.class.new(data: @data).freeze
341
367
  end
342
368
 
343
- ##
344
- # @private
345
- # @see RDF::Enumerable#each_graph
346
- def each_graph(&block)
347
- if block_given?
348
- @data.each_key do |gn|
349
- yield RDF::Graph.new(gn == DEFAULT_GRAPH ? nil : gn, data: self)
350
- end
351
- end
352
- enum_graph
353
- end
354
-
355
- protected
369
+ protected
356
370
 
357
371
  ##
358
- # Match elements with eql?, not ==
359
- # Context of `false` matches default graph. Unbound variable matches non-false graph name
372
+ # Match elements with `eql?`, not `==`
373
+ #
374
+ # `graph_name` of `false` matches default graph. Unbound variable matches
375
+ # non-false graph name
376
+ #
360
377
  # @private
361
- # @see RDF::Queryable#query
362
- def query_pattern(pattern, &block)
363
- graph_name = pattern.graph_name
364
- subject = pattern.subject
365
- predicate = pattern.predicate
366
- object = pattern.object
367
-
368
- cs = @data.has_key?(graph_name) ? {graph_name => @data[graph_name]} : @data.dup
369
- cs.each do |c, ss|
370
- next unless graph_name.nil? || graph_name == false && !c || graph_name.eql?(c)
371
- ss = if ss.has_key?(subject)
372
- { subject => ss[subject] }
373
- elsif subject.nil? || subject.is_a?(RDF::Query::Variable)
374
- ss.dup
375
- else
376
- []
377
- end
378
- ss.each do |s, ps|
379
- ps = if ps.has_key?(predicate)
380
- { predicate => ps[predicate] }
381
- elsif predicate.nil? || predicate.is_a?(RDF::Query::Variable)
382
- ps.dup
378
+ # @see RDF::Queryable#query_pattern
379
+ def query_pattern(pattern, options = {}, &block)
380
+ snapshot = @data
381
+ if block_given?
382
+ graph_name = pattern.graph_name
383
+ subject = pattern.subject
384
+ predicate = pattern.predicate
385
+ object = pattern.object
386
+
387
+ cs = snapshot.has_key?(graph_name) ? { graph_name => snapshot[graph_name] } : snapshot
388
+
389
+ cs.each do |c, ss|
390
+ next unless graph_name.nil? ||
391
+ graph_name == false && !c ||
392
+ graph_name.eql?(c)
393
+
394
+ ss = if subject.nil? || subject.is_a?(RDF::Query::Variable)
395
+ ss
396
+ elsif ss.has_key?(subject)
397
+ { subject => ss[subject] }
383
398
  else
384
399
  []
385
400
  end
386
- ps.each do |p, os|
387
- os = os.dup # TODO: is this really needed?
388
- os.each do |o|
389
- next unless object.nil? || object.eql?(o)
390
- block.call(RDF::Statement.new(s, p, o, graph_name: c.equal?(DEFAULT_GRAPH) ? nil : c))
401
+ ss.each do |s, ps|
402
+ ps = if predicate.nil? || predicate.is_a?(RDF::Query::Variable)
403
+ ps
404
+ elsif ps.has_key?(predicate)
405
+ { predicate => ps[predicate] }
406
+ else
407
+ []
408
+ end
409
+ ps.each do |p, os|
410
+ os.each do |o|
411
+ next unless object.nil? || object.eql?(o)
412
+ yield RDF::Statement.new(s, p, o, graph_name: c.equal?(DEFAULT_GRAPH) ? nil : c)
413
+ end
391
414
  end
392
415
  end
393
416
  end
417
+ else
418
+ enum_for(:query_pattern, pattern, options)
394
419
  end
395
420
  end
396
421
 
@@ -398,47 +423,158 @@ module RDF
398
423
  # @private
399
424
  # @see RDF::Mutable#insert
400
425
  def insert_statement(statement)
401
- raise ArgumentError, "Statement #{statement.inspect} is incomplete" if statement.incomplete?
402
- unless has_statement?(statement)
403
- s, p, o, c = statement.to_quad
404
- c = DEFAULT_GRAPH unless supports?(:graph_name)
405
- c ||= DEFAULT_GRAPH
406
- @data[c] ||= {}
407
- @data[c][s] ||= {}
408
- @data[c][s][p] ||= []
409
- @data[c][s][p] << o
410
- end
426
+ @data = insert_to(@data, statement)
411
427
  end
412
428
 
413
429
  ##
414
430
  # @private
415
431
  # @see RDF::Mutable#delete
416
432
  def delete_statement(statement)
417
- if has_statement?(statement)
418
- s, p, o, c = statement.to_quad
419
- c = DEFAULT_GRAPH unless supports?(:graph_name)
420
- c ||= DEFAULT_GRAPH
421
- @data[c][s][p].delete(o)
422
- @data[c][s].delete(p) if @data[c][s][p].empty?
423
- @data[c].delete(s) if @data[c][s].empty?
424
- @data.delete(c) if @data[c].empty?
425
- end
433
+ @data = delete_from(@data, statement)
426
434
  end
427
435
 
428
436
  ##
429
437
  # @private
430
438
  # @see RDF::Mutable#clear
431
439
  def clear_statements
432
- @data.clear
440
+ @data = @data.clear
441
+ end
442
+
443
+ ##
444
+ # @private
445
+ # @return [Hamster::Hash]
446
+ def data
447
+ @data
448
+ end
449
+
450
+ ##
451
+ # @private
452
+ # @return [Hamster::Hash]
453
+ def data=(hash)
454
+ @data = hash
455
+ end
456
+
457
+ private
458
+
459
+ ##
460
+ # @private
461
+ # @see #has_statement
462
+ def has_statement_in?(data, statement)
463
+ s, p, o, g = statement.to_quad
464
+ g ||= DEFAULT_GRAPH
465
+
466
+ data.has_key?(g) &&
467
+ data[g].has_key?(s) &&
468
+ data[g][s].has_key?(p) &&
469
+ data[g][s][p].include?(o)
433
470
  end
434
471
 
435
- protected :query_pattern
436
- protected :insert_statement
437
- protected :delete_statement
438
- protected :clear_statements
472
+ ##
473
+ # @private
474
+ # @return [Hamster::Hash] a new, updated hamster hash
475
+ def insert_to(data, statement)
476
+ raise ArgumentError, "Statement #{statement.inspect} is incomplete" if statement.incomplete?
477
+
478
+ unless has_statement_in?(data, statement)
479
+ s, p, o, c = statement.to_quad
480
+ c ||= DEFAULT_GRAPH
481
+
482
+ return data.put(c) do |subs|
483
+ subs = (subs || Hamster::Hash.new).put(s) do |preds|
484
+ preds = (preds || Hamster::Hash.new).put(p) do |objs|
485
+ (objs || Hamster::Set.new).add(o)
486
+ end
487
+ end
488
+ end
489
+ end
490
+ data
491
+ end
492
+
493
+ ##
494
+ # @private
495
+ # @return [Hamster::Hash] a new, updated hamster hash
496
+ def delete_from(data, statement)
497
+ if has_statement_in?(data, statement)
498
+ s, p, o, g = statement.to_quad
499
+ g = DEFAULT_GRAPH unless supports?(:graph_name)
500
+ g ||= DEFAULT_GRAPH
501
+
502
+ os = data[g][s][p].delete(o)
503
+ ps = os.empty? ? data[g][s].delete(p) : data[g][s].put(p, os)
504
+ ss = ps.empty? ? data[g].delete(s) : data[g].put(s, ps)
505
+ return ss.empty? ? data.delete(g) : data.put(g, ss)
506
+ end
507
+ data
508
+ end
509
+
510
+ ##
511
+ # A transaction for the Hamster-based `RDF::Repository::Implementation`
512
+ # with full serializability.
513
+ #
514
+ # @todo refactor me!
515
+ # @see RDF::Transaction
516
+ class SerializedTransaction < Transaction
517
+ ##
518
+ # @see Transaction#initialize
519
+ def initialize(*)
520
+ super
521
+ @base_snapshot = @snapshot
522
+ end
523
+
524
+ ##
525
+ # Inserts the statement to the transaction's working snapshot.
526
+ #
527
+ # @see Transaction#insert_statement
528
+ def insert_statement(statement)
529
+ @snapshot = @snapshot.class
530
+ .new(data: @snapshot.send(:insert_to,
531
+ @snapshot.send(:data),
532
+ process_statement(statement)))
533
+ end
534
+
535
+ ##
536
+ # Deletes the statement from the transaction's working snapshot.
537
+ #
538
+ # @see Transaction#insert_statement
539
+ def delete_statement(statement)
540
+ @snapshot = @snapshot.class
541
+ .new(data: @snapshot.send(:delete_from,
542
+ @snapshot.send(:data),
543
+ process_statement(statement)))
544
+ end
545
+
546
+ ##
547
+ # @see RDF::Dataset#isolation_level
548
+ def isolation_level
549
+ :serializable
550
+ end
551
+
552
+ ##
553
+ # Replaces repository data with the transaction's snapshot in a safely
554
+ # serializable fashion.
555
+ #
556
+ # @note this transaction uses a pessimistic merge strategy which
557
+ # fails the transaction if any data has changed in the repository
558
+ # since transaction start time. However, the specific guarantee is
559
+ # softer: multiple concurrent conflicting transactions will not
560
+ # succeed. We may choose to implement a less pessimistic merge
561
+ # strategy as a non-breaking change.
562
+ #
563
+ # @raise [TransactionError] when the transaction can't be merged.
564
+ # @see Transaction#execute
565
+ def execute
566
+ raise TransactionError, 'Cannot execute a rolled back transaction. ' \
567
+ 'Open a new one instead.' if @rolledback
568
+
569
+ # `Hamster::Hash#==` will use a cheap `#equal?` check first, but fall
570
+ # back on a full Ruby Hash comparison if required.
571
+ raise TransactionError, 'Error merging transaction. Repository' \
572
+ 'has changed during transaction time.' unless
573
+ repository.send(:data) == @base_snapshot.send(:data)
574
+
575
+ repository.send(:data=, @snapshot.send(:data))
576
+ end
577
+ end
439
578
  end # Implementation
440
579
  end # Repository
441
-
442
- # RDF::Dataset is a synonym for RDF::Repository
443
- Dataset = Repository
444
580
  end # RDF