Compare commits
No commits in common. "ac33f992220f809ed7263cf6560838e668bd44b4" and "a1163bfc8d6ed564b33f6cb4d2dc77222e009ab8" have entirely different histories.
ac33f99222
...
a1163bfc8d
@ -1,37 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="ImportHostingAssets into local" type="GradleRunConfiguration" factoryName="Gradle">
|
|
||||||
<ExternalSystemSettings>
|
|
||||||
<option name="env">
|
|
||||||
<map>
|
|
||||||
<entry key="HSADMINNG_POSTGRES_ADMIN_PASSWORD" value="password" />
|
|
||||||
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="postgres" />
|
|
||||||
<entry key="HSADMINNG_POSTGRES_JDBC_URL" value="jdbc:postgresql://localhost:5432/postgres" />
|
|
||||||
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="executionName" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="externalSystemIdString" value="GRADLE" />
|
|
||||||
<option name="scriptParameters" value="" />
|
|
||||||
<option name="taskDescriptions">
|
|
||||||
<list />
|
|
||||||
</option>
|
|
||||||
<option name="taskNames">
|
|
||||||
<list>
|
|
||||||
<option value=":importHostingAssets" />
|
|
||||||
<option value="--tests" />
|
|
||||||
<option value=""net.hostsharing.hsadminng.hs.migration.ImportHostingAssets"" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
<option name="vmOptions" />
|
|
||||||
</ExternalSystemSettings>
|
|
||||||
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
|
||||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
|
||||||
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
|
||||||
<extension name="coverage" sample_coverage="false" />
|
|
||||||
</EXTENSION>
|
|
||||||
<DebugAllEnabled>false</DebugAllEnabled>
|
|
||||||
<RunAsTest>true</RunAsTest>
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
@ -33,4 +33,37 @@
|
|||||||
<RunAsTest>true</RunAsTest>
|
<RunAsTest>true</RunAsTest>
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<configuration default="false" name="ImportHostingAssets" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":importHostingAssets" />
|
||||||
|
<option value="--tests" />
|
||||||
|
<option value=""net.hostsharing.hsadminng.hs.migration.ImportHostingAssets"" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="coverage" sample_coverage="false" />
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>true</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
</component>
|
</component>
|
53
sql/examples.sql
Normal file
53
sql/examples.sql
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
-- ========================================================
|
||||||
|
-- First Example Entity with History
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS customer (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"reference" int not null unique, -- 10000-99999
|
||||||
|
"prefix" character(3) unique
|
||||||
|
);
|
||||||
|
|
||||||
|
CALL create_historicization('customer');
|
||||||
|
|
||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
-- Second Example Entity with History
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS package_type (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"name" character varying(8)
|
||||||
|
);
|
||||||
|
|
||||||
|
CALL create_historicization('package_type');
|
||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
-- Third Example Entity with History
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS package (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"name" character varying(5),
|
||||||
|
"customer_id" INTEGER REFERENCES customer(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CALL create_historicization('package');
|
||||||
|
|
||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
-- query historical data
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
ABORT;
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
SET LOCAL hsadminng.currentUser TO 'mih42_customer_aaa';
|
||||||
|
SET LOCAL hsadminng.currentTask TO 'adding customer_aaa';
|
||||||
|
INSERT INTO package (customer_id, name) VALUES (10000, 'aaa00');
|
||||||
|
COMMIT;
|
||||||
|
-- Usage:
|
||||||
|
|
||||||
|
SET hsadminng.timestamp TO '2022-07-12 08:53:27.723315';
|
||||||
|
SET hsadminng.timestamp TO '2022-07-12 11:38:27.723315';
|
||||||
|
SELECT * FROM customer_hv p WHERE prefix = 'aaa';
|
@ -1,174 +0,0 @@
|
|||||||
|
|
||||||
-- ========================================================
|
|
||||||
-- Historization
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
|
|
||||||
drop PROCEDURE if exists tx_create_historical_view;
|
|
||||||
drop procedure if exists tx_create_historicization;
|
|
||||||
drop view if exists hs_hosting_asset_hv;
|
|
||||||
drop table if exists hs_hosting_asset_ex;
|
|
||||||
drop trigger if exists hs_hosting_asset_historicize_tg on hs_hosting_asset;
|
|
||||||
drop procedure if exists tx_historicize_tf;
|
|
||||||
drop type "tx_operation";
|
|
||||||
|
|
||||||
CREATE TYPE "tx_operation" AS ENUM ('INSERT', 'UPDATE', 'DELETE');
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION tx_historicize_tf()
|
|
||||||
RETURNS trigger
|
|
||||||
LANGUAGE plpgsql STRICT AS $$
|
|
||||||
DECLARE
|
|
||||||
currentTask VARCHAR(127);
|
|
||||||
curContextId bigint;
|
|
||||||
"row" RECORD;
|
|
||||||
"alive" BOOLEAN;
|
|
||||||
"sql" varchar;
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
-- determine task
|
|
||||||
currentTask = current_setting('hsadminng.currentTask');
|
|
||||||
assert currentTask IS NOT NULL AND length(currentTask) >= 12,
|
|
||||||
format('hsadminng.currentTask (%s) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask);
|
|
||||||
assert length(currentTask) <= 127,
|
|
||||||
format('hsadminng.currentTask (%s) must not be longer than 127 characters"', currentTask);
|
|
||||||
|
|
||||||
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
|
|
||||||
"row" := NEW;
|
|
||||||
"alive" := TRUE;
|
|
||||||
ELSE -- DELETE or TRUNCATE
|
|
||||||
"row" := OLD;
|
|
||||||
"alive" := FALSE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
curContextId := txid_current()+bigIntHash(currentTask);
|
|
||||||
|
|
||||||
sql := format('INSERT INTO %3$I_ex ' ||
|
|
||||||
' VALUES (DEFAULT, curContextId, %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME);
|
|
||||||
RAISE NOTICE 'sql: %', sql;
|
|
||||||
EXECUTE sql USING "row";
|
|
||||||
|
|
||||||
RETURN "row";
|
|
||||||
END; $$;
|
|
||||||
|
|
||||||
-- ------
|
|
||||||
|
|
||||||
CREATE OR REPLACE PROCEDURE tx_create_historical_view(baseTable varchar)
|
|
||||||
LANGUAGE plpgsql AS $$
|
|
||||||
DECLARE
|
|
||||||
createTriggerSQL varchar;
|
|
||||||
viewName varchar;
|
|
||||||
versionsTable varchar;
|
|
||||||
createViewSQL varchar;
|
|
||||||
baseCols varchar;
|
|
||||||
BEGIN
|
|
||||||
viewName = quote_ident(format('%s_hv', baseTable));
|
|
||||||
versionsTable = quote_ident(format('%s_ex', baseTable));
|
|
||||||
baseCols = (SELECT string_agg(quote_ident(column_name), ', ')
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public' AND table_name = baseTable);
|
|
||||||
|
|
||||||
createViewSQL = format(
|
|
||||||
'CREATE OR REPLACE VIEW %1$s AS' ||
|
|
||||||
'(' ||
|
|
||||||
' SELECT %2$s' ||
|
|
||||||
' FROM %3$s' ||
|
|
||||||
' WHERE alive = TRUE' ||
|
|
||||||
' AND version_id IN' ||
|
|
||||||
' (' ||
|
|
||||||
' SELECT max(vt.version_id) AS history_id' ||
|
|
||||||
' FROM %3$s AS vt' ||
|
|
||||||
' JOIN tx_history as txh ON vt.tx_id = txh.tx_id' ||
|
|
||||||
' WHERE txh.tx_timestamp <= current_setting(''hsadminng.historical_timestamp'')::timestamp' ||
|
|
||||||
' GROUP BY id' ||
|
|
||||||
' )' ||
|
|
||||||
')',
|
|
||||||
viewName, baseCols, versionsTable
|
|
||||||
);
|
|
||||||
RAISE NOTICE 'sql: %', createViewSQL;
|
|
||||||
EXECUTE createViewSQL;
|
|
||||||
|
|
||||||
createTriggerSQL = 'CREATE TRIGGER ' || baseTable || '_historicize_tg' ||
|
|
||||||
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
|
||||||
' FOR EACH ROW EXECUTE PROCEDURE tx_historicize_tf()';
|
|
||||||
RAISE NOTICE 'sql: %', createTriggerSQL;
|
|
||||||
EXECUTE createTriggerSQL;
|
|
||||||
|
|
||||||
END; $$;
|
|
||||||
|
|
||||||
-- ------
|
|
||||||
|
|
||||||
CREATE OR REPLACE PROCEDURE tx_create_historicization(baseTable varchar)
|
|
||||||
LANGUAGE plpgsql AS $$
|
|
||||||
DECLARE
|
|
||||||
createHistTableSql varchar;
|
|
||||||
createTriggerSQL varchar;
|
|
||||||
viewName varchar;
|
|
||||||
versionsTable varchar;
|
|
||||||
createViewSQL varchar;
|
|
||||||
baseCols varchar;
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
-- create the history table
|
|
||||||
createHistTableSql = '' ||
|
|
||||||
'CREATE TABLE ' || baseTable || '_ex (' ||
|
|
||||||
' version_id serial PRIMARY KEY,' ||
|
|
||||||
' context_id bigint NOT NULL REFERENCES tx_context(contextid),' ||
|
|
||||||
' trigger_op tx_operation NOT NULL,' ||
|
|
||||||
' alive boolean not null,' ||
|
|
||||||
-- followed by all columns of the original table
|
|
||||||
' LIKE ' || baseTable ||
|
|
||||||
' EXCLUDING CONSTRAINTS' ||
|
|
||||||
' EXCLUDING STATISTICS' ||
|
|
||||||
')';
|
|
||||||
RAISE NOTICE 'sql: %', createHistTableSql;
|
|
||||||
EXECUTE createHistTableSql;
|
|
||||||
|
|
||||||
-- create the historical view
|
|
||||||
viewName = quote_ident(format('%s_hv', baseTable));
|
|
||||||
versionsTable = quote_ident(format('%s_ex', baseTable));
|
|
||||||
baseCols = (SELECT string_agg(quote_ident(column_name), ', ')
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public' AND table_name = baseTable);
|
|
||||||
|
|
||||||
createViewSQL = format(
|
|
||||||
'CREATE OR REPLACE VIEW %1$s AS' ||
|
|
||||||
'(' ||
|
|
||||||
' SELECT %2$s' ||
|
|
||||||
' FROM %3$s' ||
|
|
||||||
' WHERE alive = TRUE' ||
|
|
||||||
' AND version_id IN' ||
|
|
||||||
' (' ||
|
|
||||||
' SELECT max(vt.version_id) AS history_id' ||
|
|
||||||
' FROM %3$s AS vt' ||
|
|
||||||
' JOIN tx_context as txc ON vt.context_id = txc.contextid' ||
|
|
||||||
' WHERE txc.txtimestamp <= current_setting(''hsadminng.historical_timestamp'')::timestamp' ||
|
|
||||||
' GROUP BY context_id' ||
|
|
||||||
' )' ||
|
|
||||||
')',
|
|
||||||
viewName, baseCols, versionsTable
|
|
||||||
);
|
|
||||||
RAISE NOTICE 'sql: %', createViewSQL;
|
|
||||||
EXECUTE createViewSQL;
|
|
||||||
|
|
||||||
createTriggerSQL = 'CREATE TRIGGER ' || baseTable || '_historicize_tg' ||
|
|
||||||
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
|
||||||
' FOR EACH ROW EXECUTE PROCEDURE tx_historicize_tf()';
|
|
||||||
RAISE NOTICE 'sql: %', createTriggerSQL;
|
|
||||||
EXECUTE createTriggerSQL;
|
|
||||||
|
|
||||||
END; $$;
|
|
||||||
|
|
||||||
|
|
||||||
call tx_create_historicization('hs_hosting_asset');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ROLLBACK;
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
|
|
||||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
|
||||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
|
||||||
update hs_hosting_asset set caption='lug00 a' where identifier='lug00' and type='MANAGED_WEBSPACE';
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
SET hsadminng.historical_timestamp TO '2022-07-12 11:38:27.723315';
|
|
||||||
SELECT * FROM hs_hosting_asset_ex p WHERE identifier = 'lug00';
|
|
@ -1,144 +1,143 @@
|
|||||||
rollback;
|
|
||||||
drop table if exists hs_hosting_asset_versions;
|
|
||||||
drop table if exists hs_hosting_asset_ex;
|
|
||||||
drop view if exists hs_hosting_asset_hv;
|
|
||||||
drop procedure if exists tx_create_historicization;
|
|
||||||
drop view if exists tx_create_historical_view;
|
|
||||||
drop function if exists tx_historicize_tf();
|
|
||||||
drop type if exists tx_operation;
|
|
||||||
|
|
||||||
-- ========================================================
|
-- ========================================================
|
||||||
-- Historization
|
-- Historization
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
create type "tx_operation" as enum ('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE');
|
CREATE TABLE "tx_history" (
|
||||||
|
"tx_id" BIGINT NOT NULL UNIQUE,
|
||||||
|
"tx_timestamp" TIMESTAMP NOT NULL,
|
||||||
|
"user" VARCHAR(64) NOT NULL, -- references postgres user
|
||||||
|
"task" VARCHAR NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
create or replace function tx_historicize_tf()
|
CREATE TYPE "operation" AS ENUM ('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE');
|
||||||
returns trigger
|
|
||||||
language plpgsql
|
-- see https://www.postgresql.org/docs/current/plpgsql-trigger.html
|
||||||
strict as $$
|
|
||||||
declare
|
CREATE OR REPLACE FUNCTION historicize()
|
||||||
currentUser varchar(63);
|
RETURNS trigger
|
||||||
currentTask varchar(127);
|
LANGUAGE plpgsql STRICT AS $$
|
||||||
"row" record;
|
DECLARE
|
||||||
"alive" boolean;
|
currentUser VARCHAR(63);
|
||||||
"sql" varchar;
|
currentTask VARCHAR(127);
|
||||||
begin
|
"row" RECORD;
|
||||||
|
"alive" BOOLEAN;
|
||||||
|
"sql" varchar;
|
||||||
|
BEGIN
|
||||||
-- determine user_id
|
-- determine user_id
|
||||||
begin
|
BEGIN
|
||||||
currentUser := current_setting('hsadminng.currentUser');
|
currentUser := current_setting('hsadminng.currentUser');
|
||||||
exception
|
EXCEPTION WHEN OTHERS THEN
|
||||||
when others then
|
currentUser := NULL;
|
||||||
currentUser := null;
|
END;
|
||||||
end;
|
IF (currentUser IS NULL OR currentUser = '') THEN
|
||||||
if (currentUser is null or currentUser = '') then
|
RAISE EXCEPTION 'hsadminng.currentUser must be defined, please use "SET LOCAL ...;"';
|
||||||
raise exception 'hsadminng.currentUser must be defined, please use "SET LOCAL ...;"';
|
END IF;
|
||||||
end if;
|
RAISE NOTICE 'currentUser: %', currentUser;
|
||||||
raise notice 'currentUser: %', currentUser;
|
|
||||||
|
|
||||||
-- determine task
|
-- determine task
|
||||||
currentTask = current_setting('hsadminng.currentTask');
|
currentTask = current_setting('hsadminng.currentTask');
|
||||||
assert currentTask is not null and length(currentTask) >= 12,
|
assert currentTask IS NOT NULL AND length(currentTask) >= 12,
|
||||||
format('hsadminng.currentTask (%s) must be defined and min 12 characters long, please use "SET LOCAL ...;"',
|
format('hsadminng.currentTask (%s) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask);
|
||||||
currentTask);
|
|
||||||
assert length(currentTask) <= 127,
|
assert length(currentTask) <= 127,
|
||||||
format('hsadminng.currentTask (%s) must not be longer than 127 characters"', currentTask);
|
format('hsadminng.currentTask (%s) must not be longer than 127 characters"', currentTask);
|
||||||
|
|
||||||
if (TG_OP = 'INSERT') or (TG_OP = 'UPDATE') then
|
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
|
||||||
"row" := NEW;
|
"row" := NEW;
|
||||||
"alive" := true;
|
"alive" := TRUE;
|
||||||
else -- DELETE or TRUNCATE
|
ELSE -- DELETE or TRUNCATE
|
||||||
"row" := OLD;
|
"row" := OLD;
|
||||||
"alive" := false;
|
"alive" := FALSE;
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
sql := format('INSERT INTO %3$I_ex VALUES (DEFAULT, txid_current(), %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME);
|
sql := format('INSERT INTO tx_history VALUES (txid_current(), now(), %1L, %2L) ON CONFLICT DO NOTHING', currentUser, currentTask);
|
||||||
raise notice 'sql: %', sql;
|
RAISE NOTICE 'sql: %', sql;
|
||||||
execute sql using "row";
|
EXECUTE sql;
|
||||||
|
sql := format('INSERT INTO %3$I_versions VALUES (DEFAULT, txid_current(), %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME);
|
||||||
|
RAISE NOTICE 'sql: %', sql;
|
||||||
|
EXECUTE sql USING "row";
|
||||||
|
|
||||||
return "row";
|
RETURN "row";
|
||||||
end; $$;
|
END; $$;
|
||||||
|
|
||||||
create or replace procedure tx_create_historical_view(baseTable varchar)
|
CREATE OR REPLACE PROCEDURE create_historical_view(baseTable varchar)
|
||||||
language plpgsql as $$
|
LANGUAGE plpgsql AS $$
|
||||||
declare
|
DECLARE
|
||||||
createTriggerSQL varchar;
|
createTriggerSQL varchar;
|
||||||
viewName varchar;
|
viewName varchar;
|
||||||
exVersionsTable varchar;
|
versionsTable varchar;
|
||||||
createViewSQL varchar;
|
createViewSQL varchar;
|
||||||
baseCols varchar;
|
baseCols varchar;
|
||||||
begin
|
BEGIN
|
||||||
|
|
||||||
viewName = quote_ident(format('%s_hv', baseTable));
|
viewName = quote_ident(format('%s_hv', baseTable));
|
||||||
exVersionsTable = quote_ident(format('%s_ex', baseTable));
|
versionsTable = quote_ident(format('%s_versions', baseTable));
|
||||||
baseCols = (select string_agg(quote_ident(column_name), ', ')
|
baseCols = (SELECT string_agg(quote_ident(column_name), ', ')
|
||||||
from information_schema.columns
|
FROM information_schema.columns
|
||||||
where table_schema = 'public'
|
WHERE table_schema = 'public' AND table_name = baseTable);
|
||||||
and table_name = baseTable);
|
|
||||||
|
|
||||||
createViewSQL = format(
|
createViewSQL = format(
|
||||||
'CREATE OR REPLACE VIEW %1$s AS' ||
|
'CREATE OR REPLACE VIEW %1$s AS' ||
|
||||||
'(' ||
|
'(' ||
|
||||||
' SELECT %2$s' ||
|
' SELECT %2$s' ||
|
||||||
' FROM %3$s' ||
|
' FROM %3$s' ||
|
||||||
' WHERE alive = TRUE' ||
|
' WHERE alive = TRUE' ||
|
||||||
' AND version_id IN' ||
|
' AND version_id IN' ||
|
||||||
' (' ||
|
' (' ||
|
||||||
' SELECT max(ex.version_id) AS history_id' ||
|
' SELECT max(vt.version_id) AS history_id' ||
|
||||||
' FROM %3$s AS ex' ||
|
' FROM %3$s AS vt' ||
|
||||||
' JOIN tx_context as txc ON ex.txid = txc.txid' ||
|
' JOIN tx_history as txh ON vt.tx_id = txh.tx_id' ||
|
||||||
' WHERE txc.txtimestamp <= current_setting(''hsadminng.tx_history_timestamp'')::timestamp' ||
|
' WHERE txh.tx_timestamp <= current_setting(''hsadminng.timestamp'')::timestamp' ||
|
||||||
' OR txc.txid = current_setting(''hsadminng.tx_history_txid'')' ||
|
' GROUP BY id' ||
|
||||||
' GROUP BY uuid' ||
|
' )' ||
|
||||||
' )' ||
|
')',
|
||||||
')',
|
viewName, baseCols, versionsTable
|
||||||
viewName, baseCols, exVersionsTable
|
);
|
||||||
);
|
RAISE NOTICE 'sql: %', createViewSQL;
|
||||||
raise notice 'sql: %', createViewSQL;
|
EXECUTE createViewSQL;
|
||||||
execute createViewSQL;
|
|
||||||
|
|
||||||
createTriggerSQL = 'CREATE TRIGGER ' || baseTable || '_tx_historicize_tf' ||
|
createTriggerSQL = 'CREATE TRIGGER ' || baseTable || '_historicize' ||
|
||||||
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
||||||
' FOR EACH ROW EXECUTE PROCEDURE tx_historicize_tf()';
|
' FOR EACH ROW EXECUTE PROCEDURE historicize()';
|
||||||
raise notice 'sql: %', createTriggerSQL;
|
RAISE NOTICE 'sql: %', createTriggerSQL;
|
||||||
execute createTriggerSQL;
|
EXECUTE createTriggerSQL;
|
||||||
|
|
||||||
end; $$;
|
END; $$;
|
||||||
|
|
||||||
create or replace procedure tx_create_historicization(baseTable varchar)
|
CREATE OR REPLACE PROCEDURE create_historicization(baseTable varchar)
|
||||||
language plpgsql as $$
|
LANGUAGE plpgsql AS $$
|
||||||
declare
|
DECLARE
|
||||||
createHistTableSql varchar;
|
createHistTableSql varchar;
|
||||||
createTriggerSQL varchar;
|
createTriggerSQL varchar;
|
||||||
viewName varchar;
|
viewName varchar;
|
||||||
exVersionsTable varchar;
|
versionsTable varchar;
|
||||||
createViewSQL varchar;
|
createViewSQL varchar;
|
||||||
baseCols varchar;
|
baseCols varchar;
|
||||||
begin
|
BEGIN
|
||||||
|
|
||||||
-- create the history table
|
-- create the history table
|
||||||
createHistTableSql = '' ||
|
createHistTableSql = '' ||
|
||||||
'CREATE TABLE ' || baseTable || '_ex (' ||
|
'CREATE TABLE ' || baseTable || '_versions (' ||
|
||||||
' version_id serial PRIMARY KEY,' ||
|
' version_id serial PRIMARY KEY,' ||
|
||||||
' txid bigint NOT NULL REFERENCES tx_context(txid),' ||
|
' tx_id bigint NOT NULL REFERENCES tx_history(tx_id),' ||
|
||||||
' trigger_op tx_operation NOT NULL,' ||
|
' trigger_op operation NOT NULL,' ||
|
||||||
' alive boolean not null,' ||
|
' alive boolean not null,' ||
|
||||||
' LIKE ' || baseTable ||
|
|
||||||
' EXCLUDING CONSTRAINTS' ||
|
' LIKE ' || baseTable ||
|
||||||
' EXCLUDING STATISTICS' ||
|
' EXCLUDING CONSTRAINTS' ||
|
||||||
')';
|
' EXCLUDING STATISTICS' ||
|
||||||
raise notice 'sql: %', createHistTableSql;
|
')';
|
||||||
execute createHistTableSql;
|
RAISE NOTICE 'sql: %', createHistTableSql;
|
||||||
|
EXECUTE createHistTableSql;
|
||||||
|
|
||||||
-- create the historical view
|
-- create the historical view
|
||||||
viewName = quote_ident(format('%s_hv', baseTable));
|
viewName = quote_ident(format('%s_hv', baseTable));
|
||||||
exVersionsTable = quote_ident(format('%s_ex', baseTable));
|
versionsTable = quote_ident(format('%s_versions', baseTable));
|
||||||
baseCols = (select string_agg(quote_ident(column_name), ', ')
|
baseCols = (SELECT string_agg(quote_ident(column_name), ', ')
|
||||||
from information_schema.columns
|
FROM information_schema.columns
|
||||||
where table_schema = 'public'
|
WHERE table_schema = 'public' AND table_name = baseTable);
|
||||||
and table_name = baseTable);
|
|
||||||
|
|
||||||
createViewSQL = format(
|
createViewSQL = format(
|
||||||
'CREATE OR REPLACE VIEW %1$s AS' ||
|
'CREATE OR REPLACE VIEW %1$s AS' ||
|
||||||
'(' ||
|
'(' ||
|
||||||
' SELECT %2$s' ||
|
' SELECT %2$s' ||
|
||||||
@ -146,52 +145,22 @@ begin
|
|||||||
' WHERE alive = TRUE' ||
|
' WHERE alive = TRUE' ||
|
||||||
' AND version_id IN' ||
|
' AND version_id IN' ||
|
||||||
' (' ||
|
' (' ||
|
||||||
' SELECT max(ex.version_id) AS history_id' ||
|
' SELECT max(vt.version_id) AS history_id' ||
|
||||||
' FROM %3$s AS ex' ||
|
' FROM %3$s AS vt' ||
|
||||||
' JOIN tx_context as txc ON ex.txid = txc.txid' ||
|
' JOIN tx_history as txh ON vt.tx_id = txh.tx_id' ||
|
||||||
' WHERE txc.txtimestamp <= current_setting(''hsadminng.tx_history_timestamp'')::timestamp' ||
|
' WHERE txh.tx_timestamp <= current_setting(''hsadminng.timestamp'')::timestamp' ||
|
||||||
' GROUP BY uuid' ||
|
' GROUP BY id' ||
|
||||||
' )' ||
|
' )' ||
|
||||||
')',
|
')',
|
||||||
viewName, baseCols, exVersionsTable
|
viewName, baseCols, versionsTable
|
||||||
);
|
);
|
||||||
raise notice 'sql: %', createViewSQL;
|
RAISE NOTICE 'sql: %', createViewSQL;
|
||||||
execute createViewSQL;
|
EXECUTE createViewSQL;
|
||||||
|
|
||||||
createTriggerSQL = 'CREATE TRIGGER ' || baseTable || '_tx_historicize_tf' ||
|
createTriggerSQL = 'CREATE TRIGGER ' || baseTable || '_historicize' ||
|
||||||
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
||||||
' FOR EACH ROW EXECUTE PROCEDURE tx_historicize_tf()';
|
' FOR EACH ROW EXECUTE PROCEDURE historicize()';
|
||||||
raise notice 'sql: %', createTriggerSQL;
|
RAISE NOTICE 'sql: %', createTriggerSQL;
|
||||||
execute createTriggerSQL;
|
EXECUTE createTriggerSQL;
|
||||||
|
|
||||||
end; $$;
|
END; $$;
|
||||||
|
|
||||||
--- ==================================================
|
|
||||||
|
|
||||||
call tx_create_historicization('hs_hosting_asset');
|
|
||||||
|
|
||||||
-- and expanded:
|
|
||||||
|
|
||||||
-- ===========================================================================================
|
|
||||||
|
|
||||||
rollback;
|
|
||||||
begin transaction;
|
|
||||||
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
|
|
||||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
|
|
||||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- prod
|
|
||||||
-- 'hs_booking_project#D-1000300-mimdefaultproject:ADMIN'); -- test
|
|
||||||
-- update hs_hosting_asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
|
|
||||||
update hs_hosting_asset set caption='mim00 A ' || now()::text where identifier = 'mim00' and type = 'MANAGED_WEBSPACE'; -- test
|
|
||||||
update hs_hosting_asset set caption='mim00 B ' || now()::text where identifier = 'mim00' and type = 'MANAGED_WEBSPACE'; -- test
|
|
||||||
commit;
|
|
||||||
|
|
||||||
-- single version at point in time
|
|
||||||
-- set hsadminng.tx_history_timestamp to '2024-08-27 07:44:03'; -- UTC
|
|
||||||
set hsadminng.tx_history_timestamp to '2024-08-27 07:44:03'; -- UTC
|
|
||||||
select uuid, version, identifier, caption from hs_hosting_asset_hv p where identifier in ('lug00', 'mim00');
|
|
||||||
|
|
||||||
-- all versions
|
|
||||||
select txc.txtimestamp, txc.currentUser, txc.currentTask, haex.*
|
|
||||||
from hs_hosting_asset_ex haex
|
|
||||||
join tx_context txc on haex.txid=txc.txid
|
|
||||||
where haex.identifier in ('lug00', 'mim00');
|
|
||||||
|
@ -23,10 +23,8 @@ do $$
|
|||||||
*/
|
*/
|
||||||
create table tx_context
|
create table tx_context
|
||||||
(
|
(
|
||||||
-- FIXME: what whas the purpose of such a hash(task+txid)?
|
contextId bigint primary key not null,
|
||||||
-- contextId bigint primary key not null,
|
txId bigint not null,
|
||||||
-- txId bigint not null,
|
|
||||||
txId bigint primary key not null,
|
|
||||||
txTimestamp timestamp not null,
|
txTimestamp timestamp not null,
|
||||||
currentUser varchar(63) not null, -- not the uuid, because users can be deleted
|
currentUser varchar(63) not null, -- not the uuid, because users can be deleted
|
||||||
assumedRoles varchar(1023) not null, -- not the uuids, because roles can be deleted
|
assumedRoles varchar(1023) not null, -- not the uuids, because roles can be deleted
|
||||||
@ -45,8 +43,7 @@ create index on tx_context using brin (txTimestamp);
|
|||||||
*/
|
*/
|
||||||
create table tx_journal
|
create table tx_journal
|
||||||
(
|
(
|
||||||
-- contextId bigint not null references tx_context (contextId), -- FIXME: this ...
|
contextId bigint not null references tx_context (contextId),
|
||||||
txId bigint not null references tx_context (txId), -- FIXME: ... or that?
|
|
||||||
targetTable text not null,
|
targetTable text not null,
|
||||||
targetUuid uuid not null, -- Assumes that all audited tables have a uuid column.
|
targetUuid uuid not null, -- Assumes that all audited tables have a uuid column.
|
||||||
targetOp operation not null,
|
targetOp operation not null,
|
||||||
@ -65,7 +62,7 @@ create index on tx_journal (targetTable, targetUuid);
|
|||||||
create view tx_journal_v as
|
create view tx_journal_v as
|
||||||
select txc.*, txj.targettable, txj.targetop, txj.targetuuid, txj.targetdelta
|
select txc.*, txj.targettable, txj.targetop, txj.targetuuid, txj.targetdelta
|
||||||
from tx_journal txj
|
from tx_journal txj
|
||||||
left join tx_context txc using (txId) -- FIXME: or contextId?
|
left join tx_context txc using (contextid)
|
||||||
order by txc.txtimestamp;
|
order by txc.txtimestamp;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
@ -80,37 +77,31 @@ create or replace function tx_journal_trigger()
|
|||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
curTask text;
|
curTask text;
|
||||||
-- curContextId bigint; FIXME: needed?
|
curContextId bigint;
|
||||||
curTxId bigint;
|
|
||||||
begin
|
begin
|
||||||
curTask := currentTask();
|
curTask := currentTask();
|
||||||
-- curContextId := txid_current()+bigIntHash(curTask); FIXME: needed?
|
curContextId := txid_current()+bigIntHash(curTask);
|
||||||
curTxId := txid_current();
|
|
||||||
|
|
||||||
insert
|
insert
|
||||||
-- FIXME
|
into tx_context (contextId, txId, txTimestamp, currentUser, assumedRoles, currentTask, currentRequest)
|
||||||
-- into tx_context (contextId, txId, txTimestamp, currentUser, assumedRoles, currentTask, currentRequest)
|
values (curContextId, txid_current(), now(),
|
||||||
-- values (curContextId, txid_current(), now(),
|
currentUser(), assumedRoles(), curTask, currentRequest())
|
||||||
-- currentUser(), assumedRoles(), curTask, currentRequest())
|
|
||||||
into tx_context (txId, txTimestamp, currentUser, assumedRoles, currentTask, currentRequest)
|
|
||||||
values ( curTxId, now(),
|
|
||||||
currentUser(), assumedRoles(), curTask, currentRequest())
|
|
||||||
on conflict do nothing;
|
on conflict do nothing;
|
||||||
|
|
||||||
case tg_op
|
case tg_op
|
||||||
when 'INSERT' then insert
|
when 'INSERT' then insert
|
||||||
into tx_journal
|
into tx_journal
|
||||||
values (curTxId, -- FIXME: curContextId?
|
values (curContextId,
|
||||||
tg_table_name, new.uuid, tg_op::operation,
|
tg_table_name, new.uuid, tg_op::operation,
|
||||||
to_jsonb(new));
|
to_jsonb(new));
|
||||||
when 'UPDATE' then insert
|
when 'UPDATE' then insert
|
||||||
into tx_journal
|
into tx_journal
|
||||||
values (curTxId, -- FIXME: curContextId?
|
values (curContextId,
|
||||||
tg_table_name, old.uuid, tg_op::operation,
|
tg_table_name, old.uuid, tg_op::operation,
|
||||||
jsonb_changes_delta(to_jsonb(old), to_jsonb(new)));
|
jsonb_changes_delta(to_jsonb(old), to_jsonb(new)));
|
||||||
when 'DELETE' then insert
|
when 'DELETE' then insert
|
||||||
into tx_journal
|
into tx_journal
|
||||||
values (curTxId, -- FIXME: curContextId?
|
values (curContextId,
|
||||||
tg_table_name, old.uuid, 'DELETE'::operation,
|
tg_table_name, old.uuid, 'DELETE'::operation,
|
||||||
null::jsonb);
|
null::jsonb);
|
||||||
else raise exception 'Trigger op % not supported for %.', tg_op, tg_table_name;
|
else raise exception 'Trigger op % not supported for %.', tg_op, tg_table_name;
|
||||||
|
@ -610,7 +610,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
|
|||||||
deleteTestDataFromHsOfficeTables();
|
deleteTestDataFromHsOfficeTables();
|
||||||
resetHsOfficeSequences();
|
resetHsOfficeSequences();
|
||||||
deleteFromTestTables();
|
deleteFromTestTables();
|
||||||
deleteFromCommonTables();
|
deleteFromRbacTables();
|
||||||
|
|
||||||
jpaAttempt.transacted(() -> {
|
jpaAttempt.transacted(() -> {
|
||||||
context(rbacSuperuser);
|
context(rbacSuperuser);
|
||||||
|
@ -249,7 +249,6 @@ public class CsvDataImport extends ContextBasedTest {
|
|||||||
context(rbacSuperuser);
|
context(rbacSuperuser);
|
||||||
// TODO.perf: could we instead skip creating test-data based on an env var?
|
// TODO.perf: could we instead skip creating test-data based on an env var?
|
||||||
em.createNativeQuery("delete from hs_hosting_asset where true").executeUpdate();
|
em.createNativeQuery("delete from hs_hosting_asset where true").executeUpdate();
|
||||||
// FIXME em.createNativeQuery("delete from hs_hosting_asset_ex where true").executeUpdate();
|
|
||||||
em.createNativeQuery("delete from hs_booking_item where true").executeUpdate();
|
em.createNativeQuery("delete from hs_booking_item where true").executeUpdate();
|
||||||
em.createNativeQuery("delete from hs_booking_project where true").executeUpdate();
|
em.createNativeQuery("delete from hs_booking_project where true").executeUpdate();
|
||||||
em.createNativeQuery("delete from hs_office_coopassetstransaction where true").executeUpdate();
|
em.createNativeQuery("delete from hs_office_coopassetstransaction where true").executeUpdate();
|
||||||
@ -293,7 +292,7 @@ public class CsvDataImport extends ContextBasedTest {
|
|||||||
}).assertSuccessful();
|
}).assertSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void deleteFromCommonTables() {
|
protected void deleteFromRbacTables() {
|
||||||
jpaAttempt.transacted(() -> {
|
jpaAttempt.transacted(() -> {
|
||||||
context(rbacSuperuser);
|
context(rbacSuperuser);
|
||||||
em.createNativeQuery("delete from rbacuser_rv where name not like 'superuser-%'").executeUpdate();
|
em.createNativeQuery("delete from rbacuser_rv where name not like 'superuser-%'").executeUpdate();
|
||||||
|
Loading…
Reference in New Issue
Block a user