How to design a data-version-controll DQL Schema

We are wondering how to design a DQL-Data-Schema, which is able to store versioned data.

We must be able to each write a Query…

  • which only returns the just the current values
  • which returns the values of a specific Version (Timetravel)
  • which returns all of the history values and the current values. So I can see how the value has changed over time. (like a git-history-view)

We must version following:

  • Scalar Types (integer, string, float, etc.)
  • List of Scalars
  • List of Relationships

Every version must contain the Information:

  • User (String) Who did it?
  • Date (DateTime) When did it happen?
  • Message (String) Why did it happen?

What we also want =>

  • 1 Mutation == 1 Version (which could contain several changes) like a git-commit which contains several changes

For easier Understanding, we can have a look at following dql example schema (which has no versioning right now):

Type Train { 
  Horsepower: int 
  OutsideColor: String 
  Wagon: [uid]      """ a list of wagons connected to the Train """
} 
 
Type Wagon { 
  CurrentLoad: int 
  MaxLoad: int 
}

We not only want to version the properties (like Horsepower) but also the Relationship between Train and Wagon. As you can imagine a Train is connected to different Wagons over Time. We like to save which wagon was connected to which train over time. We want to be able to query which train had which wagons with which properties at a specific time/version.

Please give us a suggestion how to solve that versioning most efficient with the DQL Schema and DQL Query Language?

Many different ways you could do this each having pros and cons based upon your exact use case.

Here is one way. I wrote this up with a GraphQL Schema as it makes it simpler to see IMO. DQL schema would be generated from this GraphQL schema with type and interface dotted predicates.

interface Train {
  id: ID
  Horsepower: Int
  OutsideColor: String
  Wagons: [WagonWithHistory]
}

interface History {
  byUser: User!
  changedOn: DateTime!
  reason: String!
}

type TrainHistory implements Train & History {
  ref: Train! @hasInverse(field: "History")
}

type TrainWithHistory implements Train {
  History: [TrainHistory] @hasInverse(field: "ref")
}

interface Wagon {
  id: ID
  CurrentLoad: Int
  MaxLoad: Int
}

type WagonHistory implements Wagon & History {
  ref: Wagon! @hasInverse(field: "History")
}

type WagonWithHistory implements Wagon {
  History: [WagonHistory] @hasInverse(field: "ref")
}

type User {
  id: ID
  username: String! @id
}
query getTrain($id: ID) {
  getTrainWithHistory(id: $id) {
    id
    Horsepower
    OutsideColor
    Wagons {
      id
      CurrentLoad
      MaxLoad
    }
  }
}

This depends if you know exactly what version ID you want, of if you only know the version timestamp you want.

query getTrainBefore($id: ID, $before: DateTime, $wagonHistory: Boolean = true) {
  getTrainWithHistory(id: $id) {
    id
    History(filter: { changedOn: { le: $before } } order: { desc: changedOn } first: 1) {
      # could have no History
      byUser {
        username
      }
      changedOn
      reason
      Horsepower
      OutsideColor
      Wagons {
        id
        CurrentLoad @skip(if: $wagonHistory)
        MaxLoad @skip(if: $wagonHistory)
        History(filter: { changedOn: { le: $before } } order: { desc: changedOn } first: 1) @include(if: $wagonHistory) {
          # could have no History
          byUser {
            username
          }
          changedOn
          reason
          CurrentLoad
          MaxLoad
        }
      }
    }
  }
}
fragment HistoryDetails on History {
  byUser {
    username
  }
  changedOn
  reason
}
fragment WagonDetails on WagonWithHistory {
  id
  CurrentLoad
  MaxLoad
  History(order: { desc: changedOn }) {
    ...HistoryDetails
    CurrentLoad
    MaxLoad
  }
}
fragment TrainDetails on {
  Horsepower
  OutsideColor
  Wagons {
    ...WagonDetails
  }
}
query getTrainBefore($id: ID) {
  getTrainWithHistory(id: $id) {
    id
    ...TrainDetails
    History(order: { desc: changedOn }) {
      ...HistoryDetails
      ...TrainDetails
    }
  }
}

Working with this schema doing mutations gets tricky, but not impossible. With DQL you can do variable blocks and upserts to get the current version, create a node copying the current version, and then updating what you want to the new version. That is more than I wish to code out here as a proof of concept example, but it is all possible :100:

This is all feasible with the schema suggested above.