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

@@ -100,14 +100,37 @@ in {
# -- Cargo.toml -- # -- Cargo.toml --
cargoToml = src: (builtins.fromTOML (builtins.readFile "${src}/Cargo.toml")); cargoToml = src: (builtins.fromTOML (builtins.readFile "${src}/Cargo.toml"));
# SQL bundle bootstrapping `hectic.created_at` / `hectic.updated_at` inheritance enforcement. # Consolidated SQL bundles for the `hectic` schema. Single source of truth
# Consumers can either: # for everything that creates objects in the `hectic` namespace, used by
# * read the SQL string for inline pipelines: `self.lib.hecticInheritance.sql` # migrator (init-time), db-tool (postgres-init), and pkgs.hectic.postgres-secrets.
# * reference the source path: `self.lib.hecticInheritance.path` #
# * use the per-system package: `pkgs.hectic.hectic-inheritance` (provides # The whole hectic system shares one `versionString`; `hectic-version.sql`
# `$out/share/hectic/hectic-inheritance.sql`) # registers (`'hectic'`, versionString) into `hectic.version` and raises an
# exception on mismatch. Per-hook version rows are intentionally absent.
#
# Each entry exposes:
# * .sql — file contents as a string, with @HECTIC_VERSION@ substituted
# * .path — Nix store path (only on entries that need no substitution)
hectic = let
versionString = lib.fileContents ./hook/sql/HECTIC_VERSION;
static = path: { inherit path; sql = builtins.readFile path; };
templated = path: {
sql = builtins.replaceStrings
[ "@HECTIC_VERSION@" ]
[ versionString ]
(builtins.readFile path);
};
in {
inherit versionString;
version = templated ./hook/sql/hectic-version.sql;
secret = static ./hook/sql/hectic-secret.sql;
migration = static ./hook/sql/hectic-migration.sql;
inheritance = static ./hook/sql/hectic-inheritance.sql;
};
# Back-compat alias. Prefer `self.lib.hectic.inheritance`.
hecticInheritance = let hecticInheritance = let
path = ../package/db-tool/sql/hectic-inheritance.sql; path = ./hook/sql/hectic-inheritance.sql;
in { in {
inherit path; inherit path;
sql = builtins.readFile path; sql = builtins.readFile path;

View File

@@ -0,0 +1 @@
0.1.0

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$;

View File

@@ -2,7 +2,7 @@
let let
shell = "${dash}/bin/dash"; shell = "${dash}/bin/dash";
hecticInheritanceSqlPath = ./sql/hectic-inheritance.sql; hecticInheritanceSqlPath = ../../lib/hook/sql/hectic-inheritance.sql;
hecticInheritance = runCommand "hectic-inheritance" { } '' hecticInheritance = runCommand "hectic-inheritance" { } ''
mkdir -p "$out/share/hectic" mkdir -p "$out/share/hectic"