README.sql in chrono_model-0.4.0 vs README.sql in chrono_model-0.5.0.beta

- old
+ new

@@ -22,78 +22,124 @@ create table history.countries ( hid serial primary key, valid_from timestamp not null, valid_to timestamp not null default '9999-12-31', - recorded_at timestamp not null default now(), + recorded_at timestamp not null default timezone('UTC', now()), constraint from_before_to check (valid_from < valid_to), constraint overlapping_times exclude using gist ( box( - point( extract( epoch from valid_from), id ), - point( extract( epoch from valid_to - interval '1 millisecond'), id ) + point( date_part( 'epoch', valid_from), id ), + point( date_part( 'epoch', valid_to - interval '1 millisecond'), id ) ) with && ) ) inherits ( temporal.countries ); -- Inherited primary key -create index country_inherit_pkey ON countries ( id ) +create index country_inherit_pkey on history.countries ( id ) --- Snapshot of all entities at a specific point in time -create index country_snapshot on history.countries ( valid_from, valid_to ) +-- Snapshot of data at a specific point in time +create index country_snapshot on history.countries USING gist ( + box( + point( date_part( 'epoch', valid_from ), 0 ), + point( date_part( 'epoch', valid_to ), 0 ) + ) +) --- Snapshot of a single entity at a specific point in time -create index country_instance_snapshot on history.countries ( id, valid_from, valid_to ) +-- Used by the rules queries when UPDATE'ing and DELETE'ing +create index country_valid_from on history.countries ( valid_from ) +create index country_valid_to on history.countries ( valid_from ) +create index country_recorded_at on history.countries ( id, valid_to ) --- History update -create index country_instance_update on history.countries ( id, valid_to ) - -- Single instance whole history -create index country_instance_history on history.countries ( id, recorded_at ) +create index country_instance_history on history.countries ( id, recorded_at ) -- The countries view, what the Rails' application ORM will actually CRUD on, and -- the core of the temporal updates. -- -- SELECT - return only current data -- -create view public.countries as select * from only temporal.countries; +create view public.countries as select *, xmin as __xid from only temporal.countries; -- INSERT - insert data both in the current data table and in the history table. --- Return data from the history table as the RETURNING clause must be the last --- one in the rule. +-- +-- A trigger is required if there is a serial ID column, as rules by +-- design cannot handle the following case: +-- +-- * INSERT INTO ... SELECT: if using currval(), all the rows +-- inserted in the history will have the same identity value; +-- +-- * if using a separate sequence to solve the above case, it may go +-- out of sync with the main one if an INSERT statement fails due +-- to a table constraint (the first one is nextval()'ed but the +-- nextval() on the history one never happens) +-- +-- So, only for this case, we resort to an AFTER INSERT FOR EACH ROW trigger. +-- +-- Ref: GH Issue #4. +-- create rule countries_ins as on insert to public.countries do instead ( - insert into temporal.countries ( name ) values ( new.name ); - insert into history.countries ( id, name, valid_from ) - values ( currval('temporal.countries_id_seq'), new.name, now() ) - returning ( new.name ) + insert into temporal.countries ( name ) values ( new.name ); + returning ( id, new.name, xmin ) ); +create or replace function temporal.countries_ins() returns trigger as $$ + begin + insert into history.countries ( id, name, valid_from ) + values ( currval('temporal.countries_id_seq'), new.name, timezone('utc', now()) ); + return null; + end; +$$ language plpgsql; + +create trigger history_ins after insert on temporal.countries_ins() + for each row execute procedure temporal.countries_ins(); + -- UPDATE - set the last history entry validity to now, save the current data in -- a new history entry and update the current table with the new data. +-- In transactions, create the new history entry only on the first statement, +-- and update the history instead on subsequent ones. -- -create rule countries_upd as on update to countries do instead ( +create rule countries_upd_first as on update to countries +where old.__xid::char(10)::int8 <> (txid_current() & (2^32-1)::int8) +do instead ( update history.countries - set valid_to = now() - where id = old.id and valid_to = '9999-12-31'; + set valid_to = timezone('UTC', now()) + where id = old.id and valid_to = '9999-12-31'; insert into history.countries ( id, name, valid_from ) - values ( old.id, new.name, now() ); + values ( old.id, new.name, timezone('UTC', now()) ); update only temporal.countries - set name = new.name - where id = old.id + set name = new.name + where id = old.id ); +create rule countries_upd_next as on update to countries do instead ( + update history.countries + set name = new.name + where id = old.id and valid_from = timezone('UTC', now()) + update only temporal.countries + set name = new.name + where id = old.id +) + -- DELETE - save the current data in the history and eventually delete the data --- from the current table. +-- from the current table. Special case for records INSERTed and DELETEd in the +-- same transaction - they won't appear at all in history. -- create rule countries_del as on delete to countries do instead ( + delete from history.countries + where id = old.id + and valid_from = timezone('UTC', now()) + and valid_to = '9999-12-31' + update history.countries set valid_to = now() - where id = old.id and valid_to = '9999-12-31'; + where id = old.id and valid_to = '9999-12-31'; delete from only temporal.countries where temporal.countries.id = old.id );