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.
- checksums.yaml +4 -4
- data/{README → README.md} +9 -44
- data/VERSION +1 -1
- data/bin/rdf +1 -1
- data/lib/rdf.rb +40 -49
- data/lib/rdf/changeset.rb +161 -0
- data/lib/rdf/cli.rb +195 -33
- data/lib/rdf/cli/vocab-loader.rb +13 -3
- data/lib/rdf/format.rb +44 -26
- data/lib/rdf/mixin/enumerable.rb +133 -97
- data/lib/rdf/mixin/enumerator.rb +8 -0
- data/lib/rdf/mixin/indexable.rb +1 -1
- data/lib/rdf/mixin/mutable.rb +101 -22
- data/lib/rdf/mixin/queryable.rb +21 -32
- data/lib/rdf/mixin/transactable.rb +94 -0
- data/lib/rdf/mixin/writable.rb +12 -3
- data/lib/rdf/model/dataset.rb +48 -0
- data/lib/rdf/model/graph.rb +73 -43
- data/lib/rdf/model/list.rb +61 -33
- data/lib/rdf/model/literal.rb +20 -19
- data/lib/rdf/model/literal/double.rb +20 -4
- data/lib/rdf/model/literal/numeric.rb +15 -13
- data/lib/rdf/model/node.rb +15 -16
- data/lib/rdf/model/statement.rb +1 -43
- data/lib/rdf/model/term.rb +10 -8
- data/lib/rdf/model/uri.rb +35 -34
- data/lib/rdf/model/value.rb +1 -1
- data/lib/rdf/nquads.rb +2 -11
- data/lib/rdf/ntriples.rb +1 -1
- data/lib/rdf/ntriples/reader.rb +33 -46
- data/lib/rdf/ntriples/writer.rb +42 -5
- data/lib/rdf/query.rb +6 -40
- data/lib/rdf/query/pattern.rb +4 -17
- data/lib/rdf/query/solutions.rb +6 -6
- data/lib/rdf/reader.rb +65 -14
- data/lib/rdf/repository.rb +365 -229
- data/lib/rdf/transaction.rb +211 -84
- data/lib/rdf/util.rb +1 -0
- data/lib/rdf/util/cache.rb +5 -5
- data/lib/rdf/util/file.rb +12 -9
- data/lib/rdf/util/logger.rb +272 -0
- data/lib/rdf/version.rb +2 -2
- data/lib/rdf/vocab/owl.rb +82 -77
- data/lib/rdf/vocab/rdfs.rb +22 -17
- data/lib/rdf/vocab/xsd.rb +5 -0
- data/lib/rdf/vocabulary.rb +50 -56
- data/lib/rdf/writer.rb +104 -52
- metadata +45 -90
- data/lib/rdf/mixin/inferable.rb +0 -5
- data/lib/rdf/vocab/cc.rb +0 -128
- data/lib/rdf/vocab/cert.rb +0 -245
- data/lib/rdf/vocab/dc.rb +0 -948
- data/lib/rdf/vocab/dc11.rb +0 -167
- data/lib/rdf/vocab/dcat.rb +0 -214
- data/lib/rdf/vocab/doap.rb +0 -337
- data/lib/rdf/vocab/exif.rb +0 -941
- data/lib/rdf/vocab/foaf.rb +0 -614
- data/lib/rdf/vocab/geo.rb +0 -157
- data/lib/rdf/vocab/gr.rb +0 -1501
- data/lib/rdf/vocab/ht.rb +0 -236
- data/lib/rdf/vocab/ical.rb +0 -528
- data/lib/rdf/vocab/ma.rb +0 -513
- data/lib/rdf/vocab/mo.rb +0 -2412
- data/lib/rdf/vocab/og.rb +0 -222
- data/lib/rdf/vocab/ogc.rb +0 -58
- data/lib/rdf/vocab/prov.rb +0 -1550
- data/lib/rdf/vocab/rsa.rb +0 -72
- data/lib/rdf/vocab/rss.rb +0 -66
- data/lib/rdf/vocab/schema.rb +0 -10569
- data/lib/rdf/vocab/sioc.rb +0 -669
- data/lib/rdf/vocab/skos.rb +0 -238
- data/lib/rdf/vocab/skosxl.rb +0 -57
- data/lib/rdf/vocab/v.rb +0 -383
- data/lib/rdf/vocab/vcard.rb +0 -841
- data/lib/rdf/vocab/vmd.rb +0 -383
- data/lib/rdf/vocab/void.rb +0 -186
- data/lib/rdf/vocab/vs.rb +0 -28
- data/lib/rdf/vocab/wdrs.rb +0 -134
- data/lib/rdf/vocab/wot.rb +0 -167
- data/lib/rdf/vocab/xhtml.rb +0 -8
- data/lib/rdf/vocab/xhv.rb +0 -505
data/lib/rdf/query/solutions.rb
CHANGED
@@ -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 [
|
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 [
|
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 [
|
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 [
|
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 [
|
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 [
|
242
|
+
# @return [self]
|
243
243
|
# @raise [ArgumentError] if `length` is negative
|
244
244
|
def limit(length)
|
245
245
|
length = length.to_i
|
data/lib/rdf/reader.rb
CHANGED
@@ -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,
|
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
|
-
|
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-
|
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-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/rdf/repository.rb
CHANGED
@@ -40,12 +40,55 @@ module RDF
|
|
40
40
|
# @example Deleting all statements from a repository
|
41
41
|
# repository.clear!
|
42
42
|
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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>]
|
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(
|
121
|
+
def self.load(urls, options = {}, &block)
|
79
122
|
self.new(options) do |repository|
|
80
|
-
Array(
|
81
|
-
repository.load(
|
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
|
97
|
-
# @
|
98
|
-
# @
|
99
|
-
# @option options [Boolean]
|
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]
|
103
|
-
#
|
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(
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
170
|
+
# Returns `true` if this respository supports the given `feature`.
|
128
171
|
#
|
129
|
-
#
|
130
|
-
|
131
|
-
|
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
|
-
#
|
136
|
-
#
|
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
|
-
# @
|
139
|
-
def
|
140
|
-
|
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
|
-
|
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
|
-
#
|
181
|
-
#
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
#
|
195
|
-
|
196
|
-
|
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
|
-
#
|
209
|
-
#
|
210
|
-
#
|
211
|
-
#
|
212
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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::
|
259
|
-
def
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
# @
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
-
# @
|
318
|
-
|
319
|
-
|
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
|
-
#
|
331
|
-
#
|
332
|
-
# @
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
359
|
-
#
|
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#
|
362
|
-
def query_pattern(pattern, &block)
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
{
|
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
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
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
|