diff --git a/lib/rdf.rb b/lib/rdf.rb index 780d1bdf..61e20c0a 100644 --- a/lib/rdf.rb +++ b/lib/rdf.rb @@ -7,52 +7,53 @@ module RDF # RDF mixins - autoload :Countable, 'rdf/mixin/countable' - autoload :Durable, 'rdf/mixin/durable' - autoload :Enumerable, 'rdf/mixin/enumerable' - autoload :Indexable, 'rdf/mixin/indexable' - autoload :Mutable, 'rdf/mixin/mutable' - autoload :Queryable, 'rdf/mixin/queryable' - autoload :Readable, 'rdf/mixin/readable' - autoload :TypeCheck, 'rdf/mixin/type_check' - autoload :Writable, 'rdf/mixin/writable' + autoload :Countable, 'rdf/mixin/countable' + autoload :Durable, 'rdf/mixin/durable' + autoload :Enumerable, 'rdf/mixin/enumerable' + autoload :Indexable, 'rdf/mixin/indexable' + autoload :Mutable, 'rdf/mixin/mutable' + autoload :Queryable, 'rdf/mixin/queryable' + autoload :Readable, 'rdf/mixin/readable' + autoload :TypeCheck, 'rdf/mixin/type_check' + autoload :Transactable, 'rdf/mixin/transactable' + autoload :Writable, 'rdf/mixin/writable' # RDF objects - autoload :Graph, 'rdf/model/graph' - autoload :IRI, 'rdf/model/uri' - autoload :Literal, 'rdf/model/literal' - autoload :Node, 'rdf/model/node' - autoload :Resource, 'rdf/model/resource' - autoload :Statement, 'rdf/model/statement' - autoload :URI, 'rdf/model/uri' - autoload :Value, 'rdf/model/value' - autoload :Term, 'rdf/model/term' + autoload :Graph, 'rdf/model/graph' + autoload :IRI, 'rdf/model/uri' + autoload :Literal, 'rdf/model/literal' + autoload :Node, 'rdf/model/node' + autoload :Resource, 'rdf/model/resource' + autoload :Statement, 'rdf/model/statement' + autoload :URI, 'rdf/model/uri' + autoload :Value, 'rdf/model/value' + autoload :Term, 'rdf/model/term' # RDF collections - autoload :List, 'rdf/model/list' + autoload :List, 'rdf/model/list' # RDF serialization - autoload :Format, 'rdf/format' - autoload :Reader, 'rdf/reader' - autoload :ReaderError, 'rdf/reader' - autoload :Writer, 'rdf/writer' - autoload :WriterError, 'rdf/writer' + autoload :Format, 'rdf/format' + autoload :Reader, 'rdf/reader' + autoload :ReaderError, 'rdf/reader' + autoload :Writer, 'rdf/writer' + autoload :WriterError, 'rdf/writer' # RDF serialization formats - autoload :NTriples, 'rdf/ntriples' - autoload :NQuads, 'rdf/nquads' + autoload :NTriples, 'rdf/ntriples' + autoload :NQuads, 'rdf/nquads' # RDF storage - autoload :Changeset, 'rdf/changeset' - autoload :Dataset, 'rdf/model/dataset' - autoload :Repository, 'rdf/repository' - autoload :Transaction, 'rdf/transaction' + autoload :Changeset, 'rdf/changeset' + autoload :Dataset, 'rdf/model/dataset' + autoload :Repository, 'rdf/repository' + autoload :Transaction, 'rdf/transaction' # RDF querying - autoload :Query, 'rdf/query' + autoload :Query, 'rdf/query' # RDF vocabularies - autoload :Vocabulary, 'rdf/vocabulary' + autoload :Vocabulary, 'rdf/vocabulary' autoload :StrictVocabulary, 'rdf/vocabulary' VOCABS = Dir.glob(File.join(File.dirname(__FILE__), 'rdf', 'vocab', '*.rb')).map { |f| File.basename(f)[0...-(File.extname(f).size)].to_sym } rescue [] diff --git a/lib/rdf/mixin/mutable.rb b/lib/rdf/mixin/mutable.rb index d02ba1b0..58478855 100644 --- a/lib/rdf/mixin/mutable.rb +++ b/lib/rdf/mixin/mutable.rb @@ -221,7 +221,7 @@ def apply_changeset(changeset) # @raise [NotImplementederror] when snapshots aren't implemented for the # class def snapshot - raise NotImplementedError, " #{self.class} does not implement snapshots" + raise NotImplementedError, "#{self.class} does not implement snapshots" end ## diff --git a/lib/rdf/mixin/transactable.rb b/lib/rdf/mixin/transactable.rb new file mode 100644 index 00000000..e575ff97 --- /dev/null +++ b/lib/rdf/mixin/transactable.rb @@ -0,0 +1,94 @@ +module RDF + ## + # A transaction application mixin. + # + # Classes that include this module must provide a `#begin_transaction` method + # returning an {RDF::Transaction}. + # + # @example running a read/write transaction with block syntax + # repository = RDF::Repository.new # or other transactable + # + # repository.transaction(mutable: true) do |tx| + # tx.insert [:node, RDF.type, RDF::OWL.Thing] + # # ... + # end + # + # @see RDF::Transaction + # @since 2.0.0 + module Transactable + ## + # Executes the given block in a transaction. + # + # @example running a transaction + # repository.transaction do |tx| + # tx.insert [RDF::URI("http://rubygems.org/gems/rdf"), RDF::RDFS.label, "RDF.rb"] + # end + # + # Raising an error within the transaction block causes automatic rollback. + # + # @param mutable [Boolean] + # allows changes to the transaction, otherwise it is a read-only snapshot of the underlying repository. + # @yield [tx] + # @yieldparam [RDF::Transaction] tx + # @yieldreturn [void] ignored + # @return [self] + # @see RDF::Transaction + # @since 0.3.0 + def transaction(mutable: false, &block) + tx = begin_transaction(mutable: mutable) + begin + case block.arity + when 1 then block.call(tx) + else tx.instance_eval(&block) + end + rescue => error + rollback_transaction(tx) + raise error + end + commit_transaction(tx) + self + end + alias_method :transact, :transaction + + protected + + ## + # Begins a new transaction. + # + # Subclasses implementing transaction-capable storage adapters may wish + # to override this method in order to begin a transaction against the + # underlying storage. + # + # @param mutable [Boolean] Create a mutable or immutable transaction. + # @param graph_name [Boolean] A default graph name for statements inserted + # or deleted (default: nil) + # @return [RDF::Transaction] + def begin_transaction(mutable: false, graph_name: nil) + raise NotImplementedError + end + + ## + # Rolls back the given transaction. + # + # @param [RDF::Transaction] tx + # @return [void] ignored + # @since 0.3.0 + def rollback_transaction(tx) + tx.rollback + end + + ## + # Commits the given transaction. + # + # Subclasses implementing transaction-capable storage adapters may wish + # to override this method in order to commit the given transaction to + # the underlying storage. + # + # @param [RDF::Transaction] tx + # @return [void] ignored + # @since 0.3.0 + def commit_transaction(tx) + tx.execute + end + end +end diff --git a/lib/rdf/model/graph.rb b/lib/rdf/model/graph.rb index a24d0a80..d26b5140 100644 --- a/lib/rdf/model/graph.rb +++ b/lib/rdf/model/graph.rb @@ -35,6 +35,7 @@ class Graph include RDF::Enumerable include RDF::Queryable include RDF::Mutable + include RDF::Transactable ## # Returns the options passed to this graph when it was constructed. @@ -335,10 +336,19 @@ def clear_statements @data.delete(graph_name: graph_name || false) end + ## + # @private + # Opens a transaction over the graph + # @see RDF::Transactable#begin_transaction + def begin_transaction(mutable: false, graph_name: @graph_name) + @data.send(:begin_transaction, mutable: mutable, graph_name: graph_name) + end + protected :query_pattern protected :insert_statement protected :delete_statement protected :clear_statements + protected :begin_transaction ## # @private diff --git a/lib/rdf/repository.rb b/lib/rdf/repository.rb index 2d1f4fb3..ed1c4df6 100644 --- a/lib/rdf/repository.rb +++ b/lib/rdf/repository.rb @@ -43,6 +43,8 @@ module RDF class Repository < Dataset include RDF::Mutable + include RDF::Transactable + DEFAULT_TX_CLASS = RDF::Transaction ## @@ -176,78 +178,16 @@ def isolation_level def snapshot raise NotImplementedError.new("#{self.class}#snapshot") end + + protected - ## - # Executes the given block in a transaction. - # - # @example - # repository.transaction do |tx| - # tx.insert [RDF::URI("http://rubygems.org/gems/rdf"), RDF::RDFS.label, "RDF.rb"] - # end - # - # @param mutable [Boolean] - # allows changes to the transaction, otherwise it is a read-only snapshot of the underlying repository. - # @yield [tx] - # @yieldparam [RDF::Transaction] tx - # @yieldreturn [void] ignored - # @return [self] - # @see RDF::Transaction - # @since 0.3.0 - def transaction(mutable: false, &block) - tx = begin_transaction(mutable: mutable) - begin - case block.arity - when 1 then block.call(tx) - else tx.instance_eval(&block) - end - rescue => error - rollback_transaction(tx) - raise error + ## + # @private + # @see RDF::Transactable#begin_transaction + # @since 0.3.0 + def begin_transaction(mutable: false, graph_name: nil) + @tx_class.new(self, mutable: mutable, graph_name: graph_name) end - commit_transaction(tx) - self - end - alias_method :transact, :transaction - - protected - - ## - # Begins a new transaction. - # - # Subclasses implementing transaction-capable storage adapters may wish - # to override this method in order to begin a transaction against the - # underlying storage. - # - # @param mutable [Boolean] Create a mutable or immutable transaction. - # @return [RDF::Transaction] - # @since 0.3.0 - def begin_transaction(mutable: false) - @tx_class.new(self, mutable: mutable) - end - - ## - # Rolls back the given transaction. - # - # @param [RDF::Transaction] tx - # @return [void] ignored - # @since 0.3.0 - def rollback_transaction(tx) - tx.rollback - end - - ## - # Commits the given transaction. - # - # Subclasses implementing transaction-capable storage adapters may wish - # to override this method in order to commit the given transaction to - # the underlying storage. - # - # @param [RDF::Transaction] tx - # @return [void] ignored - # @since 0.3.0 - def commit_transaction(tx) - tx.execute - end ## # @see RDF::Repository @@ -533,14 +473,24 @@ def initialize(*) def insert_statement(statement) @snapshot = @snapshot.class - .new(data: @snapshot.send(:insert_to, @snapshot.send(:data), statement)) + .new(data: @snapshot.send(:insert_to, + @snapshot.send(:data), + process_statement(statement))) end def delete_statement(statement) @snapshot = @snapshot.class - .new(data: @snapshot.send(:delete_from, @snapshot.send(:data), statement)) + .new(data: @snapshot.send(:delete_from, + @snapshot.send(:data), + process_statement(statement))) end - + + ## + # @see RDF::Dataset#isolation_level + def isolation_level + :serializable + end + def execute raise TransactionError, 'Cannot execute a rolled back transaction. ' \ 'Open a new one instead.' if @rolledback diff --git a/lib/rdf/transaction.rb b/lib/rdf/transaction.rb index a4dc8a8c..14b1d5b9 100644 --- a/lib/rdf/transaction.rb +++ b/lib/rdf/transaction.rb @@ -98,6 +98,14 @@ def self.begin(repository, mutable: false, **options, &block) # @since 2.0.0 attr_reader :repository + ## + # The default graph name to apply to statements inserted or deleted by the + # transaction. + # + # @return [RDF::Resource, nil] + # @since 2.0.0 + attr_reader :graph_name + ## # RDF statement mutations to apply when executed. # @@ -139,12 +147,13 @@ def inserts # Whether this is a read-only or read/write transaction. # @yield [tx] # @yieldparam [RDF::Transaction] tx - def initialize(repository, mutable: false, **options, &block) + def initialize(repository, graph_name: nil, mutable: false, **options, &block) @repository = repository @snapshot = repository.supports?(:snapshots) ? repository.snapshot : repository - @options = options.dup - @mutable = mutable + @options = options.dup + @mutable = mutable + @graph_name = graph_name raise TransactionError, 'Tried to open a mutable transaction on an immutable repository' if @@ -163,7 +172,8 @@ def initialize(repository, mutable: false, **options, &block) ## # @see RDF::Dataset#isolation_level def isolation_level - snapshot.isolation_level + return :repeatable_read if repository.supports?(:snapshots) + :read_committed end ## @@ -247,7 +257,7 @@ def rollback # @return [void] # @see RDF::Writable#insert_statement def insert_statement(statement) - @changes.insert(statement) + @changes.insert(process_statement(statement)) end ## @@ -257,7 +267,7 @@ def insert_statement(statement) # @return [void] # @see RDF::Mutable#delete_statement def delete_statement(statement) - @changes.delete(statement) + @changes.delete(process_statement(statement)) end def query_pattern(*args, &block) @@ -270,6 +280,22 @@ def query_execute(*args, &block) undef_method :load, :update, :clear + private + + ## + # @private Adds the default graph_name to the statement, when one it does + # not already have one. + # + # @param statement [RDF::Statement] + # @return [RDF::Statement] + def process_statement(statement) + if graph_name && statement.graph_name.nil? + statement = statement.dup + statement.graph_name = graph_name + end + statement + end + public ## diff --git a/spec/model_graph_spec.rb b/spec/model_graph_spec.rb index 70739ec6..64c4b3cc 100644 --- a/spec/model_graph_spec.rb +++ b/spec/model_graph_spec.rb @@ -166,6 +166,37 @@ it_behaves_like 'an RDF::Enumerable' end + context "as a transactable" do + require 'rdf/spec/transactable' + + let(:transactable) { RDF::Graph.new } + it_behaves_like 'an RDF::Transactable' + + context 'with graph_name' do + let(:transactable) do + RDF::Graph.new graph_name: name, data: RDF::Repository.new + end + + let(:name) { RDF::URI('g') } + + it_behaves_like 'an RDF::Transactable' + + it 'inserts to graph' do + st = [RDF::URI('s'), RDF::URI('p'), 'o'] + expect { transactable.transaction(mutable: true) { insert(st) } } + .to change { transactable.statements }.to contain_exactly(st) + end + + it 'deletes from graph' do + st = [RDF::URI('s'), RDF::URI('p'), 'o'] + transactable.insert(st) + + expect { transactable.transaction(mutable: true) { delete(st) } } + .to change { transactable.statements }.to be_empty + end + end + end + context "when querying statements" do require 'rdf/spec/queryable' it_behaves_like 'an RDF::Queryable' diff --git a/spec/transaction_spec.rb b/spec/transaction_spec.rb index 74e68ebe..c5b0e5b7 100644 --- a/spec/transaction_spec.rb +++ b/spec/transaction_spec.rb @@ -66,3 +66,11 @@ end end end + +describe RDF::Repository::Implementation::SerializedTransaction do + let(:repository) { RDF::Repository.new } + + # @see lib/rdf/spec/transaction.rb in rdf-spec + it_behaves_like "an RDF::Transaction", + RDF::Repository::Implementation::SerializedTransaction +end