Subscribe

Automatically Creating Inverse Changesets and When They Don’t Behave as Expected

The Talis Platform uses changesets as a mechanism for updating RDF. As the configuration of the Platform is itself stored as RDF, we also use changesets to modify its configuration. This can be as part of a release or to make requested changes to a customer’s store.

I recently needed to apply a large number of changesets to the Platform configuration. But before applying them, I wanted to create another set of changesets which would, if necessary, reverse all the changes – I wanted to be able to rollback if anything went wrong.

So my changesets looked something like this:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:cs="http://purl.org/vocab/changeset/schema#">
   <cs:ChangeSet rdf:about="http://example.com/changesets#change-1">
    <cs:subjectOfChange rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
    <cs:removal>
      <rdf:Statement>
        <rdf:subject rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
        <rdf:predicate rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty"/>
        <rdf:object rdf:resource="http://api.talis.com/stores/mystore/exampleconfig/old"/>
      </rdf:Statement>
    </cs:removal>
    <cs:addition>
      <rdf:Statement>
        <rdf:subject rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
        <rdf:predicate rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty"/>
        <rdf:object rdf:resource="http://api.talis.com/stores/mystore/exampleconfig/new"/>
      </rdf:Statement>
    </cs:addition>
  </cs:ChangeSet>
</rdf:RDF>

This changeset can be reversed by changing the removals to additions and changing the additions to removals. This is easy to achieve with sed:

for f in changesetdirectory/* ; do
  sed -e 's/cs:addition/TOBEAREMOVAL/' -e 's/cs:removal/TOBEANADDITION/' \
    -e 's/TOBEAREMOVAL/cs:removal/'  -e 's/TOBEANADDITION/cs:additon/' $f > rollback/$f
done

The above script creates an inverse of every changeset in the specified changesetdirectory and places them in the rollback directory. The inverse of the example changeset above is created as below:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:cs="http://purl.org/vocab/changeset/schema#">
   <cs:ChangeSet rdf:about="http://example.com/changesets#change-1">
    <cs:subjectOfChange rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
    <cs:addition>
      <rdf:Statement>
        <rdf:subject rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
        <rdf:predicate rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty"/>
        <rdf:object rdf:resource="http://api.talis.com/stores/mystore/exampleconfig/old"/>
      </rdf:Statement>
    </cs:addition>
    <cs:removal>
      <rdf:Statement>
        <rdf:subject rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
        <rdf:predicate rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty"/>
        <rdf:object rdf:resource="http://api.talis.com/stores/mystore/exampleconfig/new"/>
      </rdf:Statement>
    </cs:removal>
  </cs:ChangeSet>
</rdf:RDF>

So the original changeset removes the triple:

http://api.talis.com/stores/mystore/exampleconfig 

http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty

http://api.talis.com/stores/mystore/exampleconfig/old

and replaces it with:

http://api.talis.com/stores/mystore/exampleconfig 

http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty

http://api.talis.com/stores/mystore/exampleconfig/new

The inverse changeset removes the triple:

http://api.talis.com/stores/mystore/exampleconfig 

http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty

http://api.talis.com/stores/mystore/exampleconfig/new

and replaces the original:

http://api.talis.com/stores/mystore/exampleconfig 

http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty

http://api.talis.com/stores/mystore/exampleconfig/old

Using this technique, I successfully created inverse changesets which, if I had needed to, would have rolled back the changes to the configuration.

However, there is a caveat. The set semantics of a triplestore can be a gotcha.

Suppose the following triple already exists:

http://api.talis.com/stores/mystore/exampleconfig 

http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty

http://api.talis.com/stores/mystore/exampleconfig/alreadyexists

The following changeset could be applied:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:cs="http://purl.org/vocab/changeset/schema#">
   <cs:ChangeSet rdf:about="http://example.com/changesets#change-1">
    <cs:subjectOfChange rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
    <cs:addition>
      <rdf:Statement>
        <rdf:subject rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
        <rdf:predicate rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty"/>
        <rdf:object rdf:resource="http://api.talis.com/stores/mystore/exampleconfig/alreadyexists"/>
      </rdf:Statement>
    </cs:addition>
  </cs:ChangeSet>
</rdf:RDF>

This changeset is accepted but doesn’t actually modify the triples as the triple it adds already existed. Creating an inverse of this changeset gives us:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:cs="http://purl.org/vocab/changeset/schema#">
   <cs:ChangeSet rdf:about="http://example.com/changesets#change-1">
    <cs:subjectOfChange rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
    <cs:removal>
      <rdf:Statement>
        <rdf:subject rdf:resource="http://api.talis.com/stores/mystore/exampleconfig"/>
        <rdf:predicate rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#exampleproperty"/>
        <rdf:object rdf:resource="http://api.talis.com/stores/mystore/exampleconfig/alreadyexists"/>
      </rdf:Statement>
    </cs:removal>
  </cs:ChangeSet>
</rdf:RDF>

However, applying the inverse changeset removes the triple. As the triple existed before applying the first changeset the inverse of the changeset did not have the result we were looking for. It ended up deleting the triple which existed before we started.

So creating inverse changesets in this way can be useful, but only when you know with certainty that any triples added in the original changeset did not already exist.

Batch Changesets ARC Plugin

Platform Release 12 included a very useful new feature: the ability to send more than one changeset in a single POST to your store.

To generate a batch changeset from 2 versions of an RDF graph, you can use an ARC plugin called Talis_ChangeSetBuilderPlugin.

To use it:


	  $args = array(
			'before' => $before, //can be rdf/xml, turtle, or an ARC simpleIndex array
			'after' => $after,  //can be rdf/xml, turtle, or an ARC simpleIndex array
		);
		$cs = ARC2::getComponent('Talis_ChangeSetBuilderPlugin', $args);
		$cs_response = $store->get_metabox()->apply_versioned_changeset($cs); 

The plugin also relies upon the IndexUtils Plugin. The easiest way to get them all set up is to change to your arc directory and do:


svn co http://n2.talis.com/svn/playground/kwijibo/PHP/arc/plugins/trunk/ plugins

Rollbacks in Moriarty

Editing resources in the metabox of Talis Platform stores is done with Changesets. If you choose to use the versioned changesets API, your changesets will be stored as data in the metabox as well.

The great practical benefit of doing this is you can then reverse previous ChangeSets to return a resource to its previous state. You can read about one way to reverse changesets on the wiki. You can also now create rollback changesets from Moriarty with the new Rollback class.

To use it:


define('MORIARTY_ARC_DIR', 'arc/');
require 'moriarty/store.class.php';
require 'moriarty/rollback.class.php';  

//create a store object
$store = new Store('http://api.talis.com/stores/my_store');  

//Instantiate the Rollback class with a sparql service object:
$sparql = $store->get_sparql_service();
$rollback = new Rollback($sparql);  

//Call the to_changeset method, with a changeset's uri as the argument
$HTTP_Response = $rollback->to_changeset('http://api.talis.com/stores/my_store/items/1200302910905#self');  

// the body of the response is the changeset you need to revert back to the
// state of the resource before the changeset that you have given the URI of  

if($HTTP_Response->is_success()){  

//submit changeset  

	$rollbackResponse =  $store->apply_versioned_changeset($HTTP_Response->body);  

	if($rollbackResponse->is_success()){
		//relax!
	}else{
		// throw an error
	}  

}