0.0
No commit activity in last 3 years
No release in over 3 years
The time travel gem adds in-table version control to models.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
~> 5.2
 Project Readme

Time Travel

The time travel gem adds in-table version control to your data. It lets you see, correct and update records at any point in time, but preserves the entire history of corrections and updates to your data so you can drill-down and find out exactly what happened when something goes wrong.

How It Works

Lets say that we're a new bank and we're planning to maintain our customers' cash balances in a table called balance.

We first add time travel fields to the model with

> bundle exec rake generate time_travel balance
> bundle exec rake db:migrate

include the Time Travel helper in the balance model

class Balance < ActiveRecord::Base
  include TimeTravel::TimelineHelper
  
  ...

and define the timeline_fields method to return the fields that uniquely identify each timeline in our model(in our case, the cash account id)

  ...
  def self.timeline_fields
    :cash_account_id
  end

Then we start off our operations

Day 1 - 6th Septermber - New Account

The operations team informs us that a new customer created our bank's first account and deposited $500 5 days ago

We record this info with

> timeline=balance.timeline(cash_account_id: 1)
> timeline.create(amount: 500, effective_from: Time.now - 5.days)

After a few minutes, they ping us again and tells us that the customer also submitted an additional $200 two days ago, so we record that as well with

# the new balance is 500+200, which is $700
> timeline.update(amount: 700, effective_from: Time.now - 2.days)

Day 2 - 7th September - Corrections

An operations guy walks in hurriedly and tells us that they are extremely sorry but the amounts deposited were recorded wrong, it was $600 and $300 and not $500 and $200

We cross-check with the team and record the updates

> timeline.update(amount: 600, effective_from: Time.now - 6 days)
> timeline.update(amount: 900, effective_from: Time.now - 3.days)

Day 3 - 8th September - Reconcilliation

On day 3, the customer walks in and tells us that something is wrong with our systems and that the balances were different yesterday and day-before even though he didn't deposit or withdraw any money

So our support team starts with checking the current balance first

To decipher what happened, the team looks at two time ranges in each record. The effective time range(effective_from and effective_till) tells them what period the data was recorded for, while the valid time range(valid_from and valid_till) tells them when the data was recorded. An infinite end date(1-1-3000) tells them that the record is currently effective or currently valid depending on which time range it shows up on.

> timeline.at(Time.now)
{
  "id"=>6,
  "cash_account_id"=>1,
  "amount"=>900,
  "reference_id"=>nil,
  "effective_from"=>"2021-09-04T18:30:00.000Z",
  "effective_till"=>"3000-01-01T00:00:00.000Z",
  "valid_from"=>"2021-09-07T18:30:00.000Z",
  "valid_till"=>"3000-01-01T00:00:00.000Z"
}

The above record tells them that the customer's balance changed to $900 on the 4th of September and is currently effective, and that the amount was recorded on the 7th and is currently valid.

They check if the customer expects the balance to be $900 and he confirms this

Great, atleast the current balance in order, so they start digging in deeper to check what the balance was 2 days ago

> timeline.at(Time.now - 2.days, as_of: Time.now - 2.days)
{
  "id"=>3,
  "cash_account_id"=>1,
  "amount"=>700,
  "reference_id"=>nil,
  "effective_from"=>"2021-09-04T18:30:00.000Z",
  "effective_till"=>"3000-01-01T00:00:00.000Z",
  "valid_from"=>"2021-09-06T18:30:00.000Z",
  "valid_till"=>"2021-09-07T18:30:00.000Z"
}

They realize from this record is that the balance was indeed different two days ago, and it reflected $700. Additionally the valid time range tells them that there was a correction made to the balance a day later since the valid time range ends on the 7th.

They inform the customer that two days ago, he might have seen a balance of $700, and he confirms this.

They then check further and compare balance data recorded two days ago to find out what happened

> timeline.as_of(Time.now - 2.days)
[
  {
    "id"=>2,
    "cash_account_id"=>1,
    "amount"=>500,
    "reference_id"=>nil,
    "effective_from"=>"2021-09-01T18:30:00.000Z",
    "effective_till"=>"2021-09-04T18:30:00.000Z",
    "valid_from"=>"2021-09-06T18:30:00.000Z",
    "valid_till"=>"2021-09-07T18:30:00.000Z"
  },
  {
    "id"=>3,
    "cash_account_id"=>1,
    "amount"=>700,
    "reference_id"=>nil,
    "effective_from"=>"2021-09-04T18:30:00.000Z",
    "effective_till"=>"3000-01-01T00:00:00.000Z",
    "valid_from"=>"2021-09-06T18:30:00.000Z",
    "valid_till"=>"2021-09-07T18:30:00.000Z"
  }
]

> timeline.as_of(Time.now)
[
  {
    "id"=>5,
    "cash_account_id"=>1,
    "amount"=>600,
    "reference_id"=>nil,
    "effective_from"=>"2021-09-01T18:30:00.000Z",
    "effective_till"=>"2021-09-04T18:30:00.000Z",
    "valid_from"=>"2021-09-07T18:30:00.000Z",
    "valid_till"=>"3000-01-01T00:00:00.000Z"
  },
  {
    "id"=>6,
    "cash_account_id"=>1,
    "amount"=>900,
    "reference_id"=>nil,
    "effective_from"=>"2021-09-04T18:30:00.000Z",
    "effective_till"=>"3000-01-01T00:00:00.000Z",
    "valid_from"=>"2021-09-07T18:30:00.000Z",
    "valid_till"=>"3000-01-01T00:00:00.000Z"
  }
]

From the time ranges, they understand that the balance dates were correct and not altered, but the amounts were corrected a day after the amounts were initially recorded.

They inform the customer about exactly what happened with his accounts in the last two days.

The customer, wanting to ensure that everything is right, asks them when the additional $300 was recorded. and they inform the customer that the date of the second deposit is 4 days ago, but the correct amount was updated yesterday.

The customer feels satisfied that all the changes were tracked accurately, thanks us and leaves

Installation

Install the gem with

  gem 'time-travel'

Then run:

bundle install

Usage

Creating a new model that tracks history

To create a new model which will track history, use the time_travel generator to create a scaffold

bundle exec rake generate time_travel <NewModel> <fields>

Then, include TimeTravel::TimelineHelper in your ActiveRecord Model, and define which fields identify a unique timeline in your model for which changes and corrections need to be tracked

In the example below. a CashBalance model has a :cash_account_id field which uniquely identifies the account for which the cash balance needs to be tracked.

class CashBalance < ActiveRecord::Base
  include TimeTravel::TimelineHelper

  def self.timeline_fields
    :cash_account_id
  end
end

Adding history tracking to an existing model

Adding fields for history tracking

To add history tracking to an existing model, you can use the time_travel generator, specifying the name of a model that already exists.

bundle exec rake generate time_travel <ExistingModel>

Migrating existing data

To migrate existing data, you'll need to populate the effective and valid time ranges in each record in your model with a custom script of your own. An easy way to do this for a table that has a single date field is to order the records by the field and chain the dates from subsequent records to create the effective time range. The valid time range can be set to the current date onwards if you don't care about history prior to the migration.

Manipulating data in a Time Travel model

To apply changes to a timeline, first create a timeline object

timeline=balance.timeline(cash_account_id: 1)

Then use the create, update or terminate methods to modify the timeline.

In case you're not sure if you need to create a new timeline or update it, you can always call create_or_update to do the dirty work for you.

You can pass in effective_from and effective_till dates to indicate the period during which you want to create or update records. This is especially useful if you want to correct an older record.

The valid_from and valid_till fields are managed by the gem, based on when records are added or corrected and cannot be modified explicitly on the timeline.

Here are some examples of operations:

# create account with balance of Rs. 500, effective from now onwards
timeline.create({amount: 500})
# create account with balance Rs. 500, effective from 1st of August
timeline.create({amount: 500}, effective_from: Date.parse("01/09/2018").beginning_of_day))
# update account with balance Rs. 1000, effective from now onwards
timeline.update({amount: 1000})
# update account with balance Rs. 1500, effective from 20th of August
timeline.update({amount: 1500}, effective_from: Date.parse("20/09/2018").beginning_of_day)
# correct account balance to Rs. 2000 between 5th and 22nd of August
timeline.update({amount: 2000},
          effective_from: Date.parse("05/09/2018").begining_of_day,
          effective_till: Date.parse("22/09/2018").begining_of_day)
# close account now
timeline.terminate()
# close account, effective from 30th August timeline.terminate(effective_from: Date.parse("30/09/2018"))

Updates can be applied in bulk by supplying attributes in an array and using the bulk_update method

Accessing the timelines

To access the records in the timelines, use the at and as_of methods.

The at method returns a single record at a point in the timelines.

# retrieve a currently valid record, effective 2 days ago
timeline.at(Time.now - 2 days)
# retrieve record which was valid and effective 2 days ago
timeline.at(Time.now - 2.days, as_of: Time.now - 2 days) 

The as_of method returns the entire history of records which were valid on a given date

# the currently valid set of records
timeline.as_of(Time.now)
# the set of records valid two days ago
timeline.as_of(Time.now - 2.days)

Updating data directly

Data on any record can be directly updated by using ActiveRecord methods on your model.

You might want to do this for fields which you don't want to track on the timelines.

Avoid updating the time ranges directly though, unless you really know what you're doing.

We allow updates to the time ranges since you might need to migrate an existing model to support time_travel functionality.

SQL and Native modes

By default time travel applies updates using native ruby logic.

For performance, the sql mode is also available with support for Postgres.

To switch modes, create an initializer as follows

TimeTravel.configure do
  update_mode="sql"
end

and install the postgres plsql function with

rake time_travel:create_postgres_function

Behind the Scenes

The time travel gem uses bi-temporal modelling to track changes and corrections. There's a lot of material online that covers it in-case you want to dig deeper into what makes the gem work.