priv/repo/migrations/20260816234500_create_module_lifecycle_receipts.exs

58e6347eeb72 · 2 KB

defmodule Sarah.Repo.Migrations.CreateModuleLifecycleReceipts do
  use Ecto.Migration

  def up do
    create table(:module_lifecycle_receipts, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :module_id, :string, null: false
      add :module_version, :integer, null: false
      add :generation, :integer, null: false
      add :action, :string, null: false
      add :from_state, :string, null: false
      add :to_state, :string, null: false
      add :base_artifact_digest, :string, null: false
      add :resulting_artifact_digest, :string, null: false
      add :base_registry_digest, :string, null: false
      add :resulting_registry_digest, :string, null: false
      add :actor_id, :string, null: false
      add :auth_method, :string, null: false
      add :approval_receipt_ref, :string, null: false
      add :reason, :text, null: false
      add :predecessor, :map
      add :deprecation, :map
      add :dependent_refs, {:array, :string}, null: false, default: []
      timestamps(type: :utc_datetime_usec, updated_at: false)
    end

    create unique_index(:module_lifecycle_receipts, [
             :module_id,
             :module_version,
             :generation
           ])

    create unique_index(:module_lifecycle_receipts, [:approval_receipt_ref])

    create constraint(:module_lifecycle_receipts, :module_lifecycle_state_check,
             check:
               "action IN ('stage','admit','deprecate','disable','revoke','rollback') AND from_state IN ('staged','admitted','deprecated','disabled','revoked') AND to_state IN ('staged','admitted','deprecated','disabled','revoked')"
           )

    create constraint(:module_lifecycle_receipts, :module_lifecycle_digest_check,
             check:
               "base_artifact_digest ~ '^[0-9a-f]{64}$' AND resulting_artifact_digest ~ '^[0-9a-f]{64}$' AND base_registry_digest ~ '^[0-9a-f]{64}$' AND resulting_registry_digest ~ '^[0-9a-f]{64}$'"
           )

    execute("""
    CREATE FUNCTION reject_module_lifecycle_receipt_mutation()
    RETURNS trigger AS $$
    BEGIN
      RAISE EXCEPTION 'module lifecycle receipts are append-only';
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("""
    CREATE TRIGGER module_lifecycle_receipts_append_only
    BEFORE UPDATE OR DELETE ON module_lifecycle_receipts
    FOR EACH ROW EXECUTE FUNCTION reject_module_lifecycle_receipt_mutation();
    """)
  end

  def down do
    execute(
      "DROP TRIGGER IF EXISTS module_lifecycle_receipts_append_only ON module_lifecycle_receipts"
    )

    execute("DROP FUNCTION IF EXISTS reject_module_lifecycle_receipt_mutation()")
    drop table(:module_lifecycle_receipts)
  end
end