feat: start impliment postgres hooks

This commit is contained in:
2026-04-30 21:36:22 +00:00
parent 70c44f1fa7
commit bf7ee34716
7 changed files with 160 additions and 8 deletions

View File

@@ -0,0 +1 @@
0.1.0

View File

@@ -0,0 +1,257 @@
-- 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"();

View File

@@ -0,0 +1,51 @@
DO $bootstrap$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'hectic' AND t.typname = 'migration_name'
) THEN
CREATE DOMAIN "hectic"."migration_name" AS TEXT
CHECK (VALUE ~ '^[0-9]{14}-.*');
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'hectic' AND t.typname = 'sha256'
) THEN
CREATE DOMAIN "hectic"."sha256" AS CHAR(64)
CHECK (VALUE ~ '^[0-9a-f]{64}$');
END IF;
END
$bootstrap$;
CREATE OR REPLACE FUNCTION "hectic"."sha256_lower"() RETURNS trigger
LANGUAGE plpgsql AS $fn$
BEGIN
NEW."hash" := lower(NEW."hash");
RETURN NEW;
END
$fn$;
CREATE TABLE IF NOT EXISTS "hectic"."migration" (
"id" SERIAL PRIMARY KEY,
"name" "hectic"."migration_name" UNIQUE NOT NULL,
"hash" "hectic"."sha256" UNIQUE NOT NULL,
"applied_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
DO $trg$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'hectic_t_sha256_lower'
AND tgrelid = '"hectic"."migration"'::regclass
AND NOT tgisinternal
) THEN
CREATE TRIGGER "hectic_t_sha256_lower"
BEFORE INSERT OR UPDATE ON "hectic"."migration"
FOR EACH ROW EXECUTE FUNCTION "hectic"."sha256_lower"();
END IF;
END
$trg$;

View File

@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS "hectic"."secret" (
"id" SERIAL PRIMARY KEY,
"key" TEXT UNIQUE NOT NULL,
"value" TEXT NOT NULL
);
CREATE OR REPLACE FUNCTION "hectic"."load_secrets_from_env"(env_content TEXT)
RETURNS void
LANGUAGE plpgsql AS $fn$
DECLARE
line TEXT;
k TEXT;
v TEXT;
BEGIN
TRUNCATE TABLE "hectic"."secret";
FOR line IN
SELECT regexp_split_to_table(env_content, E'\n')
LOOP
line := btrim(line);
IF line = '' OR line LIKE '#%' THEN
CONTINUE;
END IF;
k := split_part(line, '=', 1);
v := substring(line FROM position('=' IN line) + 1);
k := btrim(k);
v := btrim(v);
IF v ~ '^".*"$' OR v ~ '^''.*''$' THEN
v := substring(v FROM 2 FOR char_length(v) - 2);
END IF;
INSERT INTO "hectic"."secret" ("key", "value") VALUES (k, v);
END LOOP;
END
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."get_secret"(k TEXT)
RETURNS TEXT
LANGUAGE plpgsql AS $fn$
BEGIN
RETURN (
SELECT "value"
FROM "hectic"."secret"
WHERE "key" = k
);
END
$fn$;

View File

@@ -0,0 +1,26 @@
CREATE SCHEMA IF NOT EXISTS "hectic";
CREATE TABLE IF NOT EXISTS "hectic"."version" (
"name" TEXT PRIMARY KEY,
"version" TEXT NOT NULL,
"installed_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
DO $check$
DECLARE
existing TEXT;
BEGIN
SELECT "version" INTO existing
FROM "hectic"."version"
WHERE "name" = 'hectic';
IF existing IS NULL THEN
INSERT INTO "hectic"."version" ("name", "version")
VALUES ('hectic', '@HECTIC_VERSION@');
ELSIF existing <> '@HECTIC_VERSION@' THEN
RAISE EXCEPTION
'hectic schema version mismatch: database has %, code expects %',
existing, '@HECTIC_VERSION@';
END IF;
END
$check$;