dm-is-temporal

DataMapper plugin implementing temporal patterns on DataMapper models.

These patterns are based on research by Martin Fowler, Richard Snodgrass and others. For more information follow these links:

Examples

So lets assume you have a simple class. The plugin will automatically create some auxillary tables when you auto_migrate.

require 'rubygems'
require 'dm-core'
require 'dm-migrations'
require 'dm-is-temporal'

DataMapper.setup(:default, "sqlite3::memory:")

class MyModel
  include DataMapper::Resource

  property :id, Serial
  property :name, String

  is_temporal do
    property :foo, Integer
    property :bar, String
  end
end

DataMapper.auto_migrate!

You can create, modify and access it as normal:

m = MyModel.create(:name => 'start', :foo => 42)

m.bar = 'hello'
m.foo              #= 42
m.name             #=> 'start'

Or you can access it at different times (future or past):

old = DateTime.parse('-4712-01-01T00:00:00+00:00')
now = DateTime.now

m.at(old).foo         #=> nil
m.at(now).foo         #=> 42

But it really gets interesting when you modify it at different DateTimes

oldish = DateTime.parse('-4712-01-01T00:00:00+00:00')
nowish = DateTime.parse('2011-03-01T00:00:00+00:00')
future = DateTime.parse('4712-01-01T00:00:00+00:00')

m.at(oldish).foo = 1

m.at(oldish).foo       #=> 1
m.at(nowish).foo       #=> 42
m.at(future).foo       #=> 42

m.at(nowish).foo = 1024

m.at(oldish).foo        #=> 1
m.at(nowish).foo        #=> 1024
m.at(future).foo        #=> 1024

m.at(future).foo = 3

m.at(oldish).foo        #=> 1
m.at(nowish).foo        #=> 1024
m.at(future).foo        #=> 3

Remember that properties outside of the is_temporal block are not versioned. But you can read and write them though the at(time) method if you want:

m.at(now).name = "finished"

m.at(old).name        #=> 'finished'
m.at(now).name        #=> 'finished'

If you try to set a value at the same time as one you already set, it will overwrite the previous value (like non-temporal models). In future versions of dm-is-temporal you will be able to configure if this works or causes an error.

m.at(nowish).foo = 11
m.at(nowish).foo         #=> 11

m.at(nowish).foo = 22
m.at(nowish).foo         #=> 22

You can also update several properties with the same time in a block (I use v for "version" here)

m.at(nowish) do |v|
  v.foo = 42
  v.bar = "cat"
end

m.at(nowish).foo           #=> 42
m.at(nowish).bar           #=> 'cat'

How it works

Temporal patterns differ from versioning patterns (such as dm-is-versioned) in that every version of the temporal properties is a peer (even the most recent). Accessing temporal properties without the at(time) method is just a convinience for at(DateTime.now). Furthermore, a reference to a temporal object doesn't have to change when the state of the temporal object changes. This is due to the continuity table (my_models) illustrated below.

In addition, you have the ability inject versions at previous time-states (modifying history).

When you use the is_temporal form, the plugin will dynamically create a temporal version table. In the example above, these two tables would be created:

# db.my_models table
---------------------------------------------
| id | name                                 |
---------------------------------------------
| 1  | 'start'                              |


# db.my_model_temporal_versions table
-----------------------------------------------------------------------
| id | foo          | bar            | updated_at     | my_model_id   |
-----------------------------------------------------------------------
| 1  | '42'         | null           | DateTime       | 1             |
| 2  | '1024'       | null           | DateTime       | 1             |
| 3  | '1024'       | 'hello'        | DateTime       | 1             |

Thanks

Thanks to the dm-is-versioned folks! I based a lot of my infrastructure on that project.

TODO

  • MyClass.update (update all records for a model) doesn't work
  • Bi-directional Temporal Associations
  • Temporal Property pattern (i.e. multiple independent temporal properties per class)
  • Bi-temporality
  • Add a config flag that enables an error to be raised when attempting to rewrite existing versions

Copyright © 2011 Joe Kutner. Released under the MIT License.

See LICENSE for details.