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.
Reminds me: what do you think about these proposed extentions to the changeset mechanism?
A possible solution: make the RDF store to generate and return you a “rollback changeset” which contains updates needed to reverse the changeset you are giving it. Since the RDF store knows exactly what triples will be changed, it can give you a correct rollback changeset.