-
-
Save Inviz/03c59a47ce2146a81dc6f48ff7685267 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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='[email protected]' WHERE email='a.com'; | |
-- version 2 invalid -- update valid to invalid | |
UPDATE orders SET email='bbb' WHERE email='[email protected]'; | |
-- version 3 valid -- update invalid to valid | |
UPDATE orders SET email='[email protected]' 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('[email protected]'); | |
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 | | [email protected] | | |
-- 12 | 12 | | 0 | 25:48.4815 | | | [email protected] | | |
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 | | [email protected] | | |
-- 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 | | [email protected] | | |
-- 5 | 1 | 3 | 4 | 25:48.4728 | 25:48.4765 | 25:48.4765 | [email protected] | | |
-- 6 | 1 | 2 | 5 | 25:48.4728 | 25:48.4780 | | [email protected] | | |
-- 7 | 1 | 5 | 6 | 25:48.4728 | 25:48.4787 | | [email protected] | | |
-- 8 | 1 | 2 | 7 | 25:48.4728 | 25:48.4791 | | [email protected] | | |
-- 9 | 1 | 0 | 8 | 25:48.4728 | 25:48.4796 | | [email protected] | | |
-- 10 | 1 | 8 | 9 | 25:48.4728 | 25:48.4802 | 25:48.4802 | [email protected] | | |
-- 11 | 1 | 0 | 10 | 25:48.4728 | 25:48.4808 | | [email protected] | | |
-- 12 | 12 | | 0 | 25:48.4815 | | | [email protected] | | |
-- 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