README.sql in chrono_model-0.5.3 vs README.sql in chrono_model-0.8.0
- old
+ new
@@ -7,141 +7,155 @@
create schema history; -- schema containing all history tables
-- Current countries data - nothing special
--
create table temporal.countries (
- id serial primary key,
- name varchar
+ id serial primary key,
+ name varchar,
+ updated_at timestamptz
);
-- Countries historical data.
--
-- Inheritance is used to avoid duplicating the schema from the main table.
-- Please note that columns on the main table cannot be dropped, and other caveats
-- http://www.postgresql.org/docs/9.0/static/ddl-inherit.html#DDL-INHERIT-CAVEATS
--
create table history.countries (
-
hid serial primary key,
- valid_from timestamp not null,
- valid_to timestamp not null default '9999-12-31',
+ validity tsrange,
recorded_at timestamp not null default timezone('UTC', now()),
- constraint from_before_to check (valid_from < valid_to),
+ constraint overlapping_times exclude using gist ( id with =, validity with && )
- constraint overlapping_times exclude using gist (
- box(
- 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 history.countries ( id )
+create index country_inherit_pkey on history.countries ( id );
-- 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 )
- )
-)
+create index country_snapshot on history.countries USING gist ( validity );
--- 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 )
+-- Used by the trigger functions when UPDATE'ing and DELETE'ing
+create index country_lower_validity on history.countries ( lower(validity) )
+create index country_upper_validity on history.countries ( upper(validity) )
+create index country_recorded_at on history.countries ( id, valid_to )
-- Single instance whole history
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.
+-- The countries view, what the Rails' application ORM will actually CRUD on,
+-- and the entry point of the temporal triggers.
--
-- SELECT - return only current data
--
-create view public.countries as select *, xmin as __xid from only temporal.countries;
+create view public.countries as select * from only temporal.countries;
--- INSERT - insert data both in the current data table and in the history table.
+-- INSERT - insert data both in the current data table and in the history one.
--
--- A trigger is required if there is a serial ID column, as rules by
--- design cannot handle the following case:
+create or replace function public.chronomodel_countries_insert() returns trigger as $$
+ begin
+ if new.id is null then
+ new.id = nextval('temporal.countries_id_seq');
+ end if;
+
+ insert into temporal.countries ( id, name, updated_at )
+ values ( new.id, new.name, new.updated_at );
+
+ insert into history.countries (id, name, updated_at, validity )
+ values ( new.id, new.name, new.updated_at, tsrange(timezone('utc', now()), null) );
+
+ return new;
+ end;
+$$ language plpgsql;
+
+create trigger chronomodel_insert
+ instead of insert on public.countries
+ for each row execute procedure public.chronomodel_countries_insert();
+
+-- UPDATE - set the last history entry validity to now, save the current data
+-- in a new history entry and update the temporal table with the new data.
--
--- * INSERT INTO ... SELECT: if using currval(), all the rows
--- inserted in the history will have the same identity value;
+-- If a row in the history with the current ID and current timestamp already
+-- exists, update it with new data. This logic makes possible to "squash"
+-- together changes made in a transaction in a single history row.
--
--- * 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)
+-- If the update doesn't change the data, it is skipped and the trigger
+-- returns NULL.
--
--- So, only for this case, we resort to an AFTER INSERT FOR EACH ROW trigger.
+-- By default, history is not recorded if only the updated_at field
+-- is changed.
--
--- Ref: GH Issue #4.
---
-create rule countries_ins as on insert to public.countries do instead (
+create function chronomodel_countries_update() returns trigger as $$
+ declare _now timestamp;
+ declare _hid integer;
+ declare _old record;
+ declare _new record;
+ begin
+
+ if old is not distinct from new then
+ return null;
+ end if;
- insert into temporal.countries ( name ) values ( new.name );
- returning ( id, new.name, xmin )
-);
+ _old := row(old.name);
+ _new := row(new.name);
-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;
+ if _old is not distinct from new then
+ update only temporal.countries set ( name, updated_at ) = ( new.name, new.updated_at ) where id = old.id
+ return new;
+ end if;
+
+ _now := timezone('utc', now());
+ _hid := null;
+
+ select hid into _hid from history.countries where id = old.id and lower(validity) = _now;
+
+ if _hid is not null then
+ update history.countries set ( name, updated_at ) = ( new.name ) where hid = _hid;
+ else
+ update history.countries set validity = tsrange(lower(validity), _now)
+ where id = old.id and upper_inf(validity);
+
+ insert into history.countries ( id, name, updated_at, validity )
+ values ( old.id, new.name, new.updated_at, tsrange(_now, null) );
+ end if;
+
+ update only temporal.countries set ( name ) = ( new.name ) where id = old.id;
+
+ return new;
end;
$$ language plpgsql;
-create trigger history_ins after insert on temporal.countries_ins()
- for each row execute procedure temporal.countries_ins();
+create trigger chronomodel_update
+ instead of update on temporal.countries
+ for each row execute procedure chronomodel_countries_update();
--- 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.
+-- DELETE - save the current data in the history and eventually delete the
+-- data from the temporal table.
--
-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 = timezone('UTC', now())
- where id = old.id and valid_to = '9999-12-31';
+-- The first DELETE is required to remove history for records INSERTed and
+-- DELETEd in the same transaction.
+--
+create or replace function chronomodel_countries_delete() returns trigger as $$
+ declare _now timestamp;
+ begin
+ _now := timezone('utc', now());
- insert into history.countries ( id, name, valid_from )
- values ( old.id, new.name, timezone('UTC', now()) );
+ delete from history.countries
+ where id = old.id and validity = tsrange(_now, null);
- update only temporal.countries
- 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 history.countries set valid_to = _now
+ where id = old.id and upper_inf(validity);
- update only temporal.countries
- set name = new.name
- where id = old.id
-)
+ delete from only temporal.countries
+ where temporal.id = old.id;
--- DELETE - save the current data in the history and eventually delete the data
--- 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'
+ return old;
+ end;
+$$ language plpgsql;
- update history.countries
- set valid_to = now()
- where id = old.id and valid_to = '9999-12-31';
-
- delete from only temporal.countries
- where temporal.countries.id = old.id
-);
+create trigger chronomodel_delete
+ instead of delete on temporal.countries
+ for each row execute procedure chronomodel_countries_delete();
-- EOF