Use Case: Tamper-proofing database classes

Abstract

The history of a database is of paramount importance. Each version of a database class serves as a foundation for its successor. It is critical that we not change the definition of a version that has been deployed to production and DataClass has features built in that defend against this.

Learning Objectives

Body

DataClass builds databases in increments. Each of these increments is called a version and is associated with its own construction logic. It is expected that you will not change the construction logic of a class once it's been committed to production. However, in the unlikely event that you do, DataClass has you covered with its tamper-resistance features.

Consider the following class of database:

You can click on keywords and concepts in blue.
handdatabase MyDB
{
  version 1.0 : initialized
  {
    construction
    {
      step sql
      {
CREATE TABLE SomeTable(
  A CHAR(2),
  B CHAR(3));
      }
    }
  }
}

Imagine that was committed to production. That is: you built a production database that has started to capture information from clients. After that, someone who doesn't know that databases should be developed in increments changes the definition of the current version as follows:

You can click on keywords and concepts in blue.
handdatabase MyDB
{
  version 1.0 : initialized
  {
    construction
    {
      step sql
      {
CREATE TABLE SomeTable(
  [ID] INT IDENTITY(1, 1) PRIMARY KEY,
  A CHAR(2),
  B CHAR(3));
      }
    }
  }
}

In development and testing - where we typically destroy and create databases anew - there is no indication that a problem exists. Every time a new database gets created, it has the new table including an identity.

The problem occurs when someone tries to extend the database class so that a production database can be built. The extended class looks like this:

You can click on keywords and concepts in blue.
handdatabase MyDB
{
  version 1.0 : initialized
  {
    construction
    {
      step sql
      {
CREATE TABLE SomeTable(
  [ID] INT IDENTITY(1, 1) PRIMARY KEY,
  A CHAR(2),
  B CHAR(3));
      }
    }
  }
  
  version 2.0 : 1.0
  {
    construction
    {
      step sql
      {
-- some things
CREATE TABLE SomeReferringTable(
  [ID] INT IDENTITY(1, 1) PRIMARY KEY, 
  SomeId INT FOREIGN KEY REFERENCES
    SomeTable([ID]));
    
-- and some other things
      }
    }
  }
}

Here again, there is no indication of a problem in the testing and development environments. Everything works perfectly. However, in production, we'll find that SomeTable doesn't have everything it needs: the ID column is missing.

To protect you from the potentially disastrous condition in which a production database is upgraded partway from version 1.0 to version 2.0, DataClass defends against this condition.

Each version has a hash code generated based on the contents of its construction steps. The slightest change in one step of a version has a nearly 100% chance (99.99999998%) of altering that hash code. When a database class is asked to build a database up to a particular version the first thing it does is perform an audit of the transitions that have already been executed. If the path deviates in any way from the expectation, the build is aborted before any change is made to the database.

The conditions that can trigger an audit failure are as follows:

  1. An expected version was missing in between two
  2. An unexpected version was present
  3. An expected, present version has a different hash code

That is the last line of defense that DataClass provides. If an audit fails, the exact problem will be reported and no changes will be made to the database. You can then go back into the revision history for your database class and figure out what transition steps were actually committed to production.

DataClass also provides your first line of defense as well. It does this by allowing you to pin the hash code in place at compile time. Going back to the original database class definition: if part of the release process was to fix the hash code at compile time, then the person who initially made the invalid change would have gotten an error.

Let's look at how we fix a hash code in place. We start by trying to pin it with a bogus hash code as with the following example:

You can click on keywords and concepts in blue.
handdatabase MyDB
{
  version 1.0 : initialized
  {
    hash 0;
    construction
    {
      step sql
      {
CREATE TABLE SomeTable(
  A CHAR(2), 
  B CHAR(3));
      }
    }
  }
}

An attempt to compile that class of database will generate the following error:

C:\UseCaseExamples\PinningHashcode.dbc(5,10): error : Expected hash '-1275977213'

As with all pinning activities, we take the failure and use it to change the assertion.

You can click on keywords and concepts in blue.
handdatabase MyDB
{
  version 1.0 : initialized
  {
    hash -1275977213;
    construction
    {
      step sql
      {
CREATE TABLE SomeTable(
  A CHAR(2),
  B CHAR(3));
      }
    }
  }
}

This causes the compile to succeed. Later, when whoever made the erroneous change attempted to compile a the database class, they would have seen that they broke the hash code for that class. Of course, nothing other than training can prevent someone from just changing the hash code again. However, this compilation error does serve as a signal that something is wrong and a well educated team will use it to avoid ever even trying to apply an invalid database class in production.

Related Use Cases

Related Concepts

Related Keywords

Other Actions

documentation | all examples | use cases | concepts | keywords