Use Case: Keeping clients coupled to the current version

Abstract

DataClass enables compiler feedback on the coupling between a client and a version. It also provides a mechanism to ensure that clients maintain strong coupling to whatever the current version of a class of databases is.

Body

In addition to generating a complete interface to which clients can couple, DataClass also provides a layer of indirection called "lenses." Each lens is essentially an alias for a version. Clients coupled to a lens behave exactly as if they were coupled to the underlying version with the only difference being that the class of databases controls which version the client is coupled to rather than the client.

One such lens is the current version lens. Using the current keyword, a class of databases can identify one and only one version as the current version. Clients coupled to the current lens of a class of database will fail to compile if someone makes a breaking change to a class of databases.

Consider the following database:

You can click on keywords and concepts in blue.
handnamespace HexagonSoftware.Examples
{
  database BeamReadings
  {
    types int as integer, number ("numeric") as real, dt as datetime, str as string;
    
    version 1.0
    {
      design
      {
        public table Device
        {
          public column ID with DataType = type(int);
          public column CodeName with DataType = type(str(24));
        }
        
        public table Readings
        {
          public column ID with DataType = type(int);
          public column DeviceID ("DID") with DataType = type(int);
          public column TimeStamp ("TS") with DataType = type(dt);
          public column Input ("I") with DataType = type(number(12, 6));
          public column Output("O") with DataType = type(number(12, 6));
        }
      }
    }
    
    // The current keyword points the current lens at this version
    current version 1.1 : 1.0
    {
      design
      {
        public table PowerSources
        {
          public column ID with DataType = type(int);
          public column InputContribution with DataType = type(number(12, 6));
        }
      }
    }
  }
}

Database.dbc

Furthermore, imagine the follow client code exists:

namespace HexagonSoftware.Examples
{
  public class Client
  {
    protected readonly Database.Proxy proxy;
    
    protected Client(IDbConnection conn)
    {
      // cannot establish a proxy against the wrong version of a DB
      proxy = BeamReadings.Design.Current.Proxy.GetInstance(conn);
    }
    
    public static Client GetInstance(IDbConnection conn)
    {
      return new Client(conn);
    }
    
    public string GetBeamReport(int deviceId)
    {
      var readings = proxy.Readings;
      var result = new StringWriter();
      
      foreach(var row in
        ExecuteSql("SELECT * FROM " + readings + " WHERE " + readings.DeviceID + " = ?", deviceId))
      {
        var timeStamp = row[readings.TimeStamp];
        var input = (decimal)row[readings.Input];
        var output = (decimal)row[readings.Output];
        
        result.WriteLine(@"{0}: {1}:{2} ({3})", timeStamp, input, output, output/input);
      }
      
      return result.ToString();
    }
  }
}
        

Client.cs

Because this client is coupled to the design of the current version, it will compile and execute against a database of that version. If we add a new version that makes a non-breaking change to design and mark that as the current version this client can be recompiled and will start executing against that new version. On the other hand, if we make a breaking change, when we try to recompile this client to talk to the new version of the BeamReadings database, we will get errors.

As an example, imagine the following slight change to the BeamReadings database in which readings are aggregated together via a Burns entity...

You can click on keywords and concepts in blue.
handnamespace HexagonSoftware.Examples
{
  database BeamReadings
  {
    version 1.0 { /* SNIP */ }
    version 1.1 { /* SNIP */ }
    current version 2.0 : 1.1
    {
      design
      {
        public table Burns
        {
          public column ID with DataType = type(int);
          // this field is copied from the old Readings DeviceID column.
          public column DeviceID : version(1.1).Readings.DeviceID;
        }
        
        public table Readings : base.Readings
        {
          // The device ID column was moved to Burns
          removed DeviceID;
          public column BurnID with DataType = type(int);
        }
      }
    }
  }
}

Database.dbc

In the case of this change, Client.cs would fail to compile because the following line depends on the Readings table having a DeviceID column:

ExecuteSql("SELECT * FROM " + readings + " WHERE " + readings.DeviceID + " = ?", deviceId))

Clients that were directly coupled to specific version, however, would remain unaffected. Those clients would still be able to compile and operate against databases of those versions.

Upon seeing this error the maintainer of Client.cs can modify that class to correctly couple to the new design...

namespace HexagonSoftware.Examples
{
  public class Client
  {
    protected readonly Database.Proxy proxy;
    
    protected Client(IDbConnection conn)
    {
      // cannot establish a proxy against the wrong version of a DB
      proxy = BeamReadings.Design.Current.Proxy.GetInstance(conn);
    }
    
    public static Client GetInstance(IDbConnection conn)
    {
      return new Client(conn);
    }
    
    public string GetBeamReport(int deviceId)
    {
      var readings = proxy.Readings;
      var burns = proxy.Burns;
      var result = new StringWriter();
      
      foreach(var row in ExecuteSql(GetQuery(), deviceId))
      {
        var timeStamp = row[readings.TimeStamp];
        var input = (decimal)row[readings.Input];
        var output = (decimal)row[readings.Output];
        
        result.WriteLine(@"{0}: {1}:{2} ({3})", timeStamp, input, output, output/input);
      }
      
      return result.ToString();
    }
    
    private string GetQuery()
    {
      return string.Format(
        @"SELECT * FROM {0} INNER JOIN {1} ON {0}.{2} = {1}.{3} WHERE {1}.{3} = ?",
        proxy.Readings,
        proxy.Burns,
        proxy.Readings.BurnID,
        proxy.Burns.ID);
    }
  }
}
        

Related Use Cases

Related Concepts

Related Keywords

Other Actions

documentation | all examples | use cases | concepts | keywords