class RDF::Transaction

An RDF transaction.

Transactions provide an ACID scope for queries and mutations.

Repository implementations may provide support for transactional updates by providing an atomic implementation of {Mutable#apply_changeset} and responding to ‘#supports?(:atomic_write)` with `true`.

We carefully distinguish between read-only and read/write transactions, in order to enable repository implementations to take out the appropriate locks for concurrency control. Transactions are read-only by default; mutability must be explicitly requested on construction in order to obtain a read/write transaction.

Individual repositories may make their own sets of guarantees within the transaction’s scope. In case repository implementations should be unable to provide full ACID guarantees for transactions, that must be clearly indicated in their documentation. If update atomicity is not provided, ‘#supports?(:atomic_write)` must respond `false`.

@example Executing a read-only transaction

repository = RDF::Repository.new

RDF::Transaction.begin(repository) do |tx|
  tx.query({predicate: RDF::Vocab::DOAP.developer}) do |statement|
    puts statement.inspect
  end
end

@example Executing a read/write transaction

repository = RDF::Repository.new

RDF::Transaction.begin(repository, mutable: true) do |tx|
  subject = RDF::URI("http://example.org/article")
  tx.delete [subject, RDF::RDFS.label, "Old title"]
  tx.insert [subject, RDF::RDFS.label, "New title"]
end

The base class provides an atomic write implementation depending on {RDF::Changeset} and using {Changeset#apply}. Custom {Repository} classes can implement a minimial write-atomic transactions by overriding {#apply_changeset}.

Reads within a transaction run against the live repository by default ({#isolation_level} is ‘:read_committed`). Repositories may provide support for snapshots by implementing {Repository#snapshot} and responding `true` to `#supports?(:snapshots)`. In this case, the transaction will use the {RDF::Dataset} returned by {#snapshot} for reads (`:repeatable_read`).

For datastores that support transactions natively, implementation of a custom {Transaction} subclass is recommended. The {Repository} is responsible for specifying snapshot support and isolation level as appropriate. Note that repositories may provide the snapshot isolation level without implementing {#snapshot}.

@example A repository with a custom transaction class

class MyRepository < RDF::Repository
  DEFAULT_TX_CLASS = MyTransaction
  # ...
  # custom repository logic
  # ...
end

@see RDF::Changeset @see RDF::Mutable#apply_changeset @since 0.3.0

Attributes

changes[R]

RDF statement mutations to apply when executed.

@return [RDF::Changeset] @since 2.0.0

graph_name[R]

The default graph name to apply to statements inserted or deleted by the transaction.

@return [RDF::Resource, nil] @since 2.0.0

options[R]

Any additional options for this transaction.

@return [Hash{Symbol => Object}]

repository[R]

The repository being operated upon.

@return [RDF::Repository] @since 2.0.0

Public Class Methods

begin(repository, mutable: false, **options, &block) click to toggle source

Executes a transaction against the given RDF repository.

@param [RDF::Repository] repository @param [Boolean] mutable (false)

Whether this is a read-only or read/write transaction.

@param [Hash{Symbol => Object}] options @yield [tx] @yieldparam [RDF::Transaction] tx @return [void]

# File lib/rdf/transaction.rb, line 90
def self.begin(repository, mutable: false, **options, &block)
  self.new(repository, mutable: mutable, **options, &block)
end
new(repository, graph_name: nil, mutable: false, **options, &block) click to toggle source

Initializes this transaction.

@param [Hash{Symbol => Object}] options @param [Boolean] mutable (false)

Whether this is a read-only or read/write transaction.

@yield [tx] @yieldparam [RDF::Transaction] tx

# File lib/rdf/transaction.rb, line 130
def initialize(repository, graph_name: nil, mutable: false, **options, &block)
  @repository = repository
  @snapshot = 
    repository.supports?(:snapshots) ? repository.snapshot : repository
  @options    = options.dup
  @mutable    = mutable
  @graph_name = graph_name

  raise TransactionError, 
        'Tried to open a mutable transaction on an immutable repository' if
    @mutable && !@repository.mutable?

  @changes = RDF::Changeset.new
  
  if block_given?
    case block.arity
      when 1 then block.call(self)
      else self.instance_eval(&block)
    end
  end
end

Public Instance Methods

each(*args, &block) click to toggle source

@see RDF::Enumerable#each

# File lib/rdf/transaction.rb, line 76
def each(*args, &block)
  read_target.each(*args, &block)
end
execute() click to toggle source

Executes the transaction

@return [Boolean] ‘true` if the changes are successfully applied. @raise [TransactionError] if the transaction can’t be applied

# File lib/rdf/transaction.rb, line 248
def execute
  raise TransactionError, 'Cannot execute a rolled back transaction. ' \
                          'Open a new one instead.' if instance_variable_defined?(:@rolledback) && @rolledback
  @changes.apply(@repository)
end
has_statement?(*args)
Alias for: statement?
inspect() click to toggle source

Returns a developer-friendly representation of this transaction.

@return [String]

# File lib/rdf/transaction.rb, line 229
def inspect
  sprintf("#<%s:%#0x(changes: -%d/+%d)>", self.class.name,
    self.__id__, self.changes.deletes.count, self.changes.inserts.count)
end
inspect!() click to toggle source

Outputs a developer-friendly representation of this transaction to ‘stderr`.

@return [void]

# File lib/rdf/transaction.rb, line 239
def inspect!
  $stderr.puts(inspect)
end
isolation_level() click to toggle source

@see RDF::Dataset#isolation_level

# File lib/rdf/transaction.rb, line 154
def isolation_level
  return :repeatable_read if repository.supports?(:snapshots)
  :read_committed
end
mutable?() click to toggle source

Returns ‘true` if this is a read/write transaction, `false` otherwise.

@return [Boolean] @see RDF::Writable#mutable?

# File lib/rdf/transaction.rb, line 196
def mutable?
  @mutable
end
mutated?() click to toggle source

Indicates whether the transaction includes changes relative to the target repository’s state at transaction start time.

The response is guaranteed to be ‘true` if executing the transaction against the original repository state would cause a change. It may also return `true` in cases where the repository would not change (e.g. because the transaction would insert statements already present).

@note ‘Transaction` implementers may choose to `NotImplementedError`

if the transaction implementation cannot be implemented efficiently.

@return [Boolean] true if the transaction has mutated (insert/delete)

since transaction start time

@raise [NotImplementedError] if a mutation check is not implemented

# File lib/rdf/transaction.rb, line 175
def mutated?
  return !changes.empty? if self.class == Transaction

  raise NotImplementedError, 
        '#mutated? is not implemented for #{self.class}'
end
readable?() click to toggle source

Returns ‘true` to indicate that this transaction is readable.

@return [Boolean] @see RDF::Readable#readable?

# File lib/rdf/transaction.rb, line 205
def readable?
  true
end
rollback() click to toggle source

Rolls back the transaction

@note: the base class simply replaces its current ‘Changeset` with a

fresh one. Other implementations may need to explictly rollback 
at the supporting datastore.

@note: clients should not rely on using same transaction instance after

rollback.

@return [Boolean] ‘true` if the changes are successfully applied.

# File lib/rdf/transaction.rb, line 265
def rollback
  @changes = RDF::Changeset.new
  @rolledback = true
end
statement?(*args) click to toggle source

@overload statement?

Returns `false` indicating this is not an RDF::Statemenet.
@return [Boolean]
@see RDF::Value#statement?

@overload statement?(statement)

@see    RDF::Enumerable#statement?
# File lib/rdf/transaction.rb, line 216
def statement?(*args)
  case args.length
  when 0 then false
  when 1 then read_target.statement?(*args)
  else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
  end
end
Also aliased as: has_statement?
writable?() click to toggle source

Returns ‘true` if this is a read/write transaction, `false` otherwise.

@return [Boolean] @see RDF::Writable#writable?

# File lib/rdf/transaction.rb, line 187
def writable?
  @mutable
end

Protected Instance Methods

delete_statement(statement) click to toggle source

Appends an RDF statement to the sequence to delete when executed.

@param [RDF::Statement] statement @return [void] @see RDF::Mutable#delete_statement

# File lib/rdf/transaction.rb, line 288
def delete_statement(statement)
  @changes.delete(process_statement(statement))
end
insert_statement(statement) click to toggle source

Appends an RDF statement to the sequence to insert when executed.

@param [RDF::Statement] statement @return [void] @see RDF::Writable#insert_statement

# File lib/rdf/transaction.rb, line 278
def insert_statement(statement)
  @changes.insert(process_statement(statement))
end
query_execute(*args, &block) click to toggle source
# File lib/rdf/transaction.rb, line 296
def query_execute(*args, &block)
  read_target.send(:query_execute, *args, &block)
end
query_pattern(*args, &block) click to toggle source
# File lib/rdf/transaction.rb, line 292
def query_pattern(*args, &block)
  read_target.send(:query_pattern, *args, &block)
end

Private Instance Methods

process_statement(statement) click to toggle source

@private Adds the default graph_name to the statement, when one it does

not already have one.

@param statement [RDF::Statement] @return [RDF::Statement]

# File lib/rdf/transaction.rb, line 310
def process_statement(statement)
  if graph_name
    statement = statement.dup
    statement.graph_name = graph_name
  end
  statement
end
read_target() click to toggle source
# File lib/rdf/transaction.rb, line 318
def read_target
  return @snapshot if graph_name.nil?
  return @snapshot.project_graph(nil) if graph_name == false
  @snapshot.project_graph(graph_name)
end