diff --git a/lib/default.nix b/lib/default.nix index 0de7a96..4ff2f6b 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -100,14 +100,37 @@ in { # -- Cargo.toml -- cargoToml = src: (builtins.fromTOML (builtins.readFile "${src}/Cargo.toml")); - # SQL bundle bootstrapping `hectic.created_at` / `hectic.updated_at` inheritance enforcement. - # Consumers can either: - # * read the SQL string for inline pipelines: `self.lib.hecticInheritance.sql` - # * reference the source path: `self.lib.hecticInheritance.path` - # * use the per-system package: `pkgs.hectic.hectic-inheritance` (provides - # `$out/share/hectic/hectic-inheritance.sql`) + # Consolidated SQL bundles for the `hectic` schema. Single source of truth + # for everything that creates objects in the `hectic` namespace, used by + # migrator (init-time), db-tool (postgres-init), and pkgs.hectic.postgres-secrets. + # + # The whole hectic system shares one `versionString`; `hectic-version.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 - path = ../package/db-tool/sql/hectic-inheritance.sql; + path = ./hook/sql/hectic-inheritance.sql; in { inherit path; sql = builtins.readFile path; diff --git a/lib/hook/sql/HECTIC_VERSION b/lib/hook/sql/HECTIC_VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/lib/hook/sql/HECTIC_VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/package/db-tool/sql/hectic-inheritance.sql b/lib/hook/sql/hectic-inheritance.sql similarity index 100% rename from package/db-tool/sql/hectic-inheritance.sql rename to lib/hook/sql/hectic-inheritance.sql diff --git a/lib/hook/sql/hectic-migration.sql b/lib/hook/sql/hectic-migration.sql new file mode 100644 index 0000000..0547918 --- /dev/null +++ b/lib/hook/sql/hectic-migration.sql @@ -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$; diff --git a/lib/hook/sql/hectic-secret.sql b/lib/hook/sql/hectic-secret.sql new file mode 100644 index 0000000..a173951 --- /dev/null +++ b/lib/hook/sql/hectic-secret.sql @@ -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$; diff --git a/lib/hook/sql/hectic-version.sql b/lib/hook/sql/hectic-version.sql new file mode 100644 index 0000000..3c74372 --- /dev/null +++ b/lib/hook/sql/hectic-version.sql @@ -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$; diff --git a/package/db-tool/default.nix b/package/db-tool/default.nix index 94aab4e..84c2ad3 100644 --- a/package/db-tool/default.nix +++ b/package/db-tool/default.nix @@ -2,7 +2,7 @@ let shell = "${dash}/bin/dash"; - hecticInheritanceSqlPath = ./sql/hectic-inheritance.sql; + hecticInheritanceSqlPath = ../../lib/hook/sql/hectic-inheritance.sql; hecticInheritance = runCommand "hectic-inheritance" { } '' mkdir -p "$out/share/hectic"