Skip to content

Instantly share code, notes, and snippets.

@Inviz
Forked from anonymous/resource.sql
Last active July 4, 2016 15:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Inviz/03c59a47ce2146a81dc6f48ff7685267 to your computer and use it in GitHub Desktop.
Save Inviz/03c59a47ce2146a81dc6f48ff7685267 to your computer and use it in GitHub Desktop.
-- Example of a postgre-driven immutable versioned and validated models
DROP TABLE orders CASCADE;
CREATE TABLE orders(
id serial, -- Serial ID
root_id integer, -- ID of a first version
parent_id integer, -- ID of a previous version
version integer, -- Version number
errors jsonb, -- Results of validation
created_at TIMESTAMP WITH TIME ZONE, -- Initial creation time
updated_at TIMESTAMP WITH TIME ZONE, -- Last time of update
deleted_at TIMESTAMP WITH TIME ZONE, -- Datestamp of deletion (inherited)
email varchar(255) -- GENERATED: column types
);
-- Scope: history of changes
CREATE OR REPLACE
VIEW orders_versions AS
SELECT * from orders WHERE errors is null ORDER BY root_id, version DESC;
-- Scope: last non-deleted versions
CREATE OR REPLACE
VIEW orders_current AS
SELECT DISTINCT ON (root_id) * from orders_versions WHERE deleted_at is null;
-- find last version of an row
DROP FUNCTION order_head(integer, integer, boolean);
CREATE OR REPLACE FUNCTION order_head(integer, boolean DEFAULT true, integer DEFAULT 2147483646) RETURNS integer
AS 'SELECT version from orders WHERE root_id = $1 and version < $3 and
case when $2 then errors is null else true end
ORDER BY version DESC'
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
-- Inserts new version of a row
CREATE OR REPLACE FUNCTION
create_order() returns trigger language plpgsql AS $$ begin
new.errors = '{}';
-- GENERATED: column validations
IF NOT new.email ~ '^[^@]@.+\..+$' THEN
SELECT jsonb_set(new.errors, '{email}', '"Email is incorrect"') into new.errors;
END IF;
-- fill created_at, start with 0 version and return errors
return (
new.id,
coalesce(new.root_id, new.id), -- inherit root_id or set to self
new.parent_id,
coalesce(new.version, 0), -- start with 0 version unless given
CASE WHEN new.errors::text != '{}' THEN -- pass validation errors if any
new.errors
END,
coalesce(new.created_at, now()), -- inherit or set creation timestamp
new.updated_at, -- inherit modification timestamp
new.deleted_at, -- inherit deletion timestamp
new.email); -- GENERATED: column names
end $$;
CREATE TRIGGER create_order
BEFORE INSERT ON orders
FOR EACH ROW EXECUTE PROCEDURE create_order();
-- Turns update into insert and bumps version
CREATE OR REPLACE FUNCTION
update_order() returns trigger language plpgsql AS $$ begin
INSERT INTO orders(
root_id, parent_id, version,
created_at, updated_at, deleted_at,
email) -- GENERATED: column names
VALUES (
new.root_id, -- inherit root_id
CASE WHEN new.parent_id is null and old.parent_id is not null THEN
old.parent_id
ELSE
old.version
END,
order_head(old.root_id, false) + 1, -- bump version to max + 1
old.created_at, -- inherit creation timestamp
now(), -- update modification timestamp
new.deleted_at, -- inherit deletion timestamp
new.email); -- GENERATED: column names
return null; -- keep row immutable
end $$;
CREATE TRIGGER update_order
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE PROCEDURE update_order();
-- Turns DELETEs into INSERTS with deleted_at timestamp
CREATE OR REPLACE FUNCTION
delete_order() returns trigger language plpgsql AS $$
declare
prev integer := order_head(old.root_id, true, old.parent_id + 1);
begin
-- if deleting actual version or there is no valid previous version, set deleted_at timestamp
if prev is null or ((order_head(old.root_id) = old.version and old.deleted_at is null) and old.parent_id = old.version - 1) THEN
EXECUTE 'UPDATE ' || TG_TABLE_NAME || ' SET deleted_at = now() WHERE id = $1' USING OLD.id;
-- otherwise clone preceeding version without deletion flag and make it current
ELSE
UPDATE orders SET deleted_at=null,parent_id=null
WHERE root_id = old.root_id and version=prev;
END if;
return null; -- dont delete original row
end;
$$;
CREATE TRIGGER delete_order
BEFORE DELETE ON orders
FOR EACH ROW EXECUTE PROCEDURE delete_order();
-- Mark HEAD of an order as deleted (dont need to know version number)
CREATE OR REPLACE function
delete_current_order() returns trigger
language plpgsql
AS $$
begin
DELETE from orders WHERE id=old.id;
return null;
end;
$$;
CREATE TRIGGER delete_current_order
INSTEAD OF DELETE ON orders_current
FOR EACH ROW EXECUTE PROCEDURE delete_current_order();
-- version 0 invalid -- insert
INSERT into orders(email) VALUES('a.com');
-- version 1 valid -- update invalid to valid
UPDATE orders SET email='a@b.com' WHERE email='a.com';
-- version 2 invalid -- update valid to invalid
UPDATE orders SET email='bbb' WHERE email='a@b.com';
-- version 3 valid -- update invalid to valid
UPDATE orders SET email='a@c4.com' WHERE email='bbb';
-- version 4 deleted -- soft delete head
DELETE from orders_current;
-- version 5 valid -- undelete
DELETE from orders WHERE version=4;
-- version 6 valid -- update email
UPDATE orders SET email=email ||'p' WHERE version=5;
-- version 7 valid -- roll back to 3d version again
DELETE from orders WHERE version=4;
-- version 8 valid -- roll back further to 1st, skip invalid 3nd
DELETE from orders_current;
-- version 9 deleted -- no more valid version to roll back to, mark as deleted
DELETE from orders_current;
-- version 10 valid -- undelete again
DELETE from orders WHERE version=(SELECT max(version) from orders);
INSERT into orders(email) VALUES('a@c.com');
INSERT into orders(email) VALUES('d.com');
SELECT * from orders_current;
-- 2 current valid orders
-- id | root_id | parent_id | version | created_at | updated_at | deleted_at | email | errors
-- ---+---------+-----------+---------+------------+------------+------------+-----------+ --------------------------------
-- 11 | 1 | 0 | 10 | 25:48.4728 | 25:48.4808 | | a@b.com |
-- 12 | 12 | | 0 | 25:48.4815 | | | a@c.com |
SELECT * from orders ORDER BY id;
-- 10 versions + 1 + 1
-- id | root_id | parent_id | version | created_at | updated_at | deleted_at | email | errors
-- ---+---------+-----------+---------+------------+------------+------------+-----------+ --------------------------------
-- 1 | 1 | | 0 | 25:48.4728 | | | a.com | {"email": "Email is incorrect"}
-- 2 | 1 | 0 | 1 | 25:48.4728 | 25:48.4738 | | a@b.com |
-- 3 | 1 | 1 | 2 | 25:48.4728 | 25:48.4752 | | bbb | {"email": "Email is incorrect"}
-- 4 | 1 | 2 | 3 | 25:48.4728 | 25:48.4761 | | a@c4.com |
-- 5 | 1 | 3 | 4 | 25:48.4728 | 25:48.4765 | 25:48.4765 | a@c4.com |
-- 6 | 1 | 2 | 5 | 25:48.4728 | 25:48.4780 | | a@c4.com |
-- 7 | 1 | 5 | 6 | 25:48.4728 | 25:48.4787 | | a@c4.comp |
-- 8 | 1 | 2 | 7 | 25:48.4728 | 25:48.4791 | | a@c4.com |
-- 9 | 1 | 0 | 8 | 25:48.4728 | 25:48.4796 | | a@b.com |
-- 10 | 1 | 8 | 9 | 25:48.4728 | 25:48.4802 | 25:48.4802 | a@b.com |
-- 11 | 1 | 0 | 10 | 25:48.4728 | 25:48.4808 | | a@b.com |
-- 12 | 12 | | 0 | 25:48.4815 | | | a@c.com |
-- 13 | 13 | | 0 | 25:48.4817 | | | d.com | {"email": "Email is incorrect"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment