258 lines
9.1 KiB
PL/PgSQL
258 lines
9.1 KiB
PL/PgSQL
-- hectic.created_at / hectic.updated_at / hectic.immutable inheritance machinery.
|
|
--
|
|
-- Provides:
|
|
-- * schema hectic
|
|
-- * tables hectic.created_at(created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())
|
|
-- hectic.updated_at(updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW())
|
|
-- hectic.immutable() -- pure marker
|
|
-- * function hectic.set_updated_at() -- BEFORE UPDATE row trigger function
|
|
-- * function hectic.block_immutable_dml()
|
|
-- BEFORE INSERT/UPDATE/DELETE/TRUNCATE row+statement trigger function;
|
|
-- allows DML iff current_setting('hectic.migration_mode', true) = 'on'.
|
|
-- * GUC hectic.inheritance_extra_excluded_schemas
|
|
-- (text, comma-separated list of schemas the enforcement trigger skips)
|
|
-- * GUC hectic.migration_mode
|
|
-- (text, 'on' enables DML on tables inheriting hectic.immutable.
|
|
-- Intended use: SET LOCAL inside a migration transaction.)
|
|
-- * event trigger hectic_enforce_created_at_inheritance
|
|
-- RAISE EXCEPTION on CREATE TABLE that does not inherit hectic.created_at
|
|
-- * event trigger hectic_attach_updated_at_trigger
|
|
-- auto-attaches BEFORE UPDATE row trigger calling hectic.set_updated_at()
|
|
-- on any new table that inherits hectic.updated_at and lacks one.
|
|
-- * event trigger hectic_attach_immutable_triggers
|
|
-- auto-attaches BEFORE INSERT/UPDATE/DELETE FOR EACH ROW and BEFORE
|
|
-- TRUNCATE FOR EACH STATEMENT triggers calling hectic.block_immutable_dml()
|
|
-- on any new table that inherits hectic.immutable and lacks them.
|
|
--
|
|
-- Idempotent: safe to run on an already-bootstrapped database.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS "hectic";
|
|
|
|
CREATE TABLE IF NOT EXISTS "hectic"."created_at" (
|
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS "hectic"."updated_at" (
|
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS "hectic"."immutable" ();
|
|
|
|
DO $bootstrap$
|
|
BEGIN
|
|
PERFORM set_config('hectic.inheritance_extra_excluded_schemas',
|
|
current_setting('hectic.inheritance_extra_excluded_schemas', true),
|
|
false);
|
|
EXCEPTION WHEN undefined_object THEN
|
|
PERFORM set_config('hectic.inheritance_extra_excluded_schemas', '', false);
|
|
END
|
|
$bootstrap$;
|
|
|
|
DO $bootstrap_mm$
|
|
BEGIN
|
|
PERFORM set_config('hectic.migration_mode',
|
|
current_setting('hectic.migration_mode', true),
|
|
false);
|
|
EXCEPTION WHEN undefined_object THEN
|
|
PERFORM set_config('hectic.migration_mode', '', false);
|
|
END
|
|
$bootstrap_mm$;
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."set_updated_at"() RETURNS trigger
|
|
LANGUAGE plpgsql AS $fn$
|
|
BEGIN
|
|
NEW."updated_at" := NOW();
|
|
RETURN NEW;
|
|
END
|
|
$fn$;
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."block_immutable_dml"() RETURNS trigger
|
|
LANGUAGE plpgsql AS $fn$
|
|
DECLARE
|
|
mm text;
|
|
BEGIN
|
|
BEGIN
|
|
mm := current_setting('hectic.migration_mode', true);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
mm := '';
|
|
END;
|
|
IF mm = 'on' THEN
|
|
IF TG_LEVEL = 'STATEMENT' THEN RETURN NULL; END IF;
|
|
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
|
RETURN NEW;
|
|
END IF;
|
|
RAISE EXCEPTION
|
|
'hectic: table %.% inherits hectic.immutable; % blocked outside migration_mode',
|
|
quote_ident(TG_TABLE_SCHEMA), quote_ident(TG_TABLE_NAME), TG_OP
|
|
USING HINT = 'wrap the statement in a migration transaction with '
|
|
|| 'SET LOCAL hectic.migration_mode = ''on''';
|
|
END
|
|
$fn$;
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."_is_excluded_schema"(p_schema text) RETURNS boolean
|
|
LANGUAGE plpgsql STABLE AS $fn$
|
|
DECLARE
|
|
extra text;
|
|
s text;
|
|
BEGIN
|
|
IF p_schema = 'hectic'
|
|
OR p_schema = 'information_schema'
|
|
OR p_schema LIKE 'pg\_%' ESCAPE '\'
|
|
THEN
|
|
RETURN true;
|
|
END IF;
|
|
BEGIN
|
|
extra := current_setting('hectic.inheritance_extra_excluded_schemas', true);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
extra := '';
|
|
END;
|
|
IF extra IS NULL OR extra = '' THEN
|
|
RETURN false;
|
|
END IF;
|
|
FOREACH s IN ARRAY string_to_array(extra, ',') LOOP
|
|
IF btrim(s) = p_schema THEN
|
|
RETURN true;
|
|
END IF;
|
|
END LOOP;
|
|
RETURN false;
|
|
END
|
|
$fn$;
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."_table_inherits"(p_oid oid, p_parent regclass) RETURNS boolean
|
|
LANGUAGE sql STABLE AS $fn$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_inherits
|
|
WHERE inhrelid = p_oid AND inhparent = p_parent
|
|
);
|
|
$fn$;
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."enforce_created_at_inheritance"() RETURNS event_trigger
|
|
LANGUAGE plpgsql AS $fn$
|
|
DECLARE
|
|
obj record;
|
|
rel pg_class;
|
|
schema_name text;
|
|
parent_oid oid;
|
|
BEGIN
|
|
parent_oid := 'hectic.created_at'::regclass;
|
|
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
|
|
LOOP
|
|
SELECT * INTO rel FROM pg_class WHERE oid = obj.objid;
|
|
IF NOT FOUND THEN CONTINUE; END IF;
|
|
IF rel.relpersistence = 't' THEN CONTINUE; END IF;
|
|
IF rel.relispartition THEN CONTINUE; END IF;
|
|
SELECT nspname INTO schema_name FROM pg_namespace WHERE oid = rel.relnamespace;
|
|
IF "hectic"."_is_excluded_schema"(schema_name) THEN CONTINUE; END IF;
|
|
IF NOT "hectic"."_table_inherits"(rel.oid, parent_oid) THEN
|
|
RAISE EXCEPTION
|
|
'hectic: table %.% must INHERITS (hectic.created_at)',
|
|
quote_ident(schema_name), quote_ident(rel.relname)
|
|
USING HINT = 'add INHERITS ("hectic"."created_at") to the CREATE TABLE statement, '
|
|
|| 'or add the schema to hectic.inheritance_extra_excluded_schemas';
|
|
END IF;
|
|
END LOOP;
|
|
END
|
|
$fn$;
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."attach_updated_at_trigger"() RETURNS event_trigger
|
|
LANGUAGE plpgsql AS $fn$
|
|
DECLARE
|
|
obj record;
|
|
rel pg_class;
|
|
schema_name text;
|
|
parent_oid oid;
|
|
trigger_name text;
|
|
has_trigger boolean;
|
|
BEGIN
|
|
parent_oid := 'hectic.updated_at'::regclass;
|
|
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
|
|
LOOP
|
|
SELECT * INTO rel FROM pg_class WHERE oid = obj.objid;
|
|
IF NOT FOUND THEN CONTINUE; END IF;
|
|
IF rel.relpersistence = 't' THEN CONTINUE; END IF;
|
|
IF rel.relispartition THEN CONTINUE; END IF;
|
|
SELECT nspname INTO schema_name FROM pg_namespace WHERE oid = rel.relnamespace;
|
|
IF schema_name = 'hectic' THEN CONTINUE; END IF;
|
|
IF NOT "hectic"."_table_inherits"(rel.oid, parent_oid) THEN CONTINUE; END IF;
|
|
trigger_name := 'hectic_set_updated_at';
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_trigger
|
|
WHERE tgrelid = rel.oid AND tgname = trigger_name AND NOT tgisinternal
|
|
) INTO has_trigger;
|
|
IF has_trigger THEN CONTINUE; END IF;
|
|
EXECUTE format(
|
|
'CREATE TRIGGER %I BEFORE UPDATE ON %I.%I FOR EACH ROW EXECUTE FUNCTION "hectic"."set_updated_at"()',
|
|
trigger_name, schema_name, rel.relname
|
|
);
|
|
END LOOP;
|
|
END
|
|
$fn$;
|
|
|
|
DROP EVENT TRIGGER IF EXISTS "hectic_enforce_created_at_inheritance";
|
|
CREATE EVENT TRIGGER "hectic_enforce_created_at_inheritance"
|
|
ON ddl_command_end
|
|
WHEN TAG IN ('CREATE TABLE')
|
|
EXECUTE FUNCTION "hectic"."enforce_created_at_inheritance"();
|
|
|
|
DROP EVENT TRIGGER IF EXISTS "hectic_attach_updated_at_trigger";
|
|
CREATE EVENT TRIGGER "hectic_attach_updated_at_trigger"
|
|
ON ddl_command_end
|
|
WHEN TAG IN ('CREATE TABLE')
|
|
EXECUTE FUNCTION "hectic"."attach_updated_at_trigger"();
|
|
|
|
CREATE OR REPLACE FUNCTION "hectic"."attach_immutable_triggers"() RETURNS event_trigger
|
|
LANGUAGE plpgsql AS $fn$
|
|
DECLARE
|
|
obj record;
|
|
rel pg_class;
|
|
schema_name text;
|
|
parent_oid oid;
|
|
has_row boolean;
|
|
has_trunc boolean;
|
|
BEGIN
|
|
parent_oid := 'hectic.immutable'::regclass;
|
|
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
|
|
LOOP
|
|
SELECT * INTO rel FROM pg_class WHERE oid = obj.objid;
|
|
IF NOT FOUND THEN CONTINUE; END IF;
|
|
IF rel.relpersistence = 't' THEN CONTINUE; END IF;
|
|
IF rel.relispartition THEN CONTINUE; END IF;
|
|
SELECT nspname INTO schema_name FROM pg_namespace WHERE oid = rel.relnamespace;
|
|
IF schema_name = 'hectic' THEN CONTINUE; END IF;
|
|
IF NOT "hectic"."_table_inherits"(rel.oid, parent_oid) THEN CONTINUE; END IF;
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_trigger
|
|
WHERE tgrelid = rel.oid
|
|
AND tgname = 'hectic_block_immutable_dml'
|
|
AND NOT tgisinternal
|
|
) INTO has_row;
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_trigger
|
|
WHERE tgrelid = rel.oid
|
|
AND tgname = 'hectic_block_immutable_truncate'
|
|
AND NOT tgisinternal
|
|
) INTO has_trunc;
|
|
IF NOT has_row THEN
|
|
EXECUTE format(
|
|
'CREATE TRIGGER %I BEFORE INSERT OR UPDATE OR DELETE ON %I.%I '
|
|
|| 'FOR EACH ROW EXECUTE FUNCTION "hectic"."block_immutable_dml"()',
|
|
'hectic_block_immutable_dml', schema_name, rel.relname
|
|
);
|
|
END IF;
|
|
IF NOT has_trunc THEN
|
|
EXECUTE format(
|
|
'CREATE TRIGGER %I BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON %I.%I '
|
|
|| 'FOR EACH STATEMENT EXECUTE FUNCTION "hectic"."block_immutable_dml"()',
|
|
'hectic_block_immutable_truncate', schema_name, rel.relname
|
|
);
|
|
END IF;
|
|
END LOOP;
|
|
END
|
|
$fn$;
|
|
|
|
DROP EVENT TRIGGER IF EXISTS "hectic_attach_immutable_triggers";
|
|
CREATE EVENT TRIGGER "hectic_attach_immutable_triggers"
|
|
ON ddl_command_end
|
|
WHEN TAG IN ('CREATE TABLE')
|
|
EXECUTE FUNCTION "hectic"."attach_immutable_triggers"();
|