priv/repo/migrations/20260816220000_create_sarah_blueprints.exs

main at 58e6347eeb72 · 5 KB

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

  def up do
    create table(:sarah_blueprint_revisions, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :revision, :string, null: false
      add :sequence, :bigint, null: false
      add :parent_revision, :string
      add :digest, :string, null: false
      add :status, :string, null: false, default: "admitted"
      add :compatibility_min, :integer, null: false
      add :compatibility_max, :integer, null: false
      add :author, :string, null: false
      add :reason, :text, null: false
      add :receipt, :map, null: false, default: %{}
      timestamps(type: :utc_datetime_usec, updated_at: false)
    end

    create unique_index(:sarah_blueprint_revisions, [:revision])
    create unique_index(:sarah_blueprint_revisions, [:sequence])

    create constraint(:sarah_blueprint_revisions, :sarah_blueprint_revision_digest_check,
             check: "digest ~ '^[0-9a-f]{64}$'"
           )

    create constraint(:sarah_blueprint_revisions, :sarah_blueprint_revision_status_check,
             check: "status IN ('admitted')"
           )

    create constraint(:sarah_blueprint_revisions, :sarah_blueprint_revision_compatibility_check,
             check: "compatibility_min >= 1 AND compatibility_max >= compatibility_min"
           )

    create constraint(:sarah_blueprint_revisions, :sarah_blueprint_revision_metadata_check,
             check:
               "octet_length(revision) BETWEEN 1 AND 100 AND " <>
                 "octet_length(author) BETWEEN 1 AND 200 AND " <>
                 "octet_length(reason) BETWEEN 1 AND 2000 AND " <>
                 "jsonb_typeof(receipt) = 'object' AND octet_length(receipt::text) <= 8192"
           )

    create table(:sarah_blueprint_facts, primary_key: false) do
      add :id, :binary_id, primary_key: true

      add :revision_id,
          references(:sarah_blueprint_revisions, type: :binary_id, on_delete: :restrict),
          null: false

      add :fact_id, :string, null: false
      add :section, :string, null: false
      add :value_type, :string, null: false
      add :typed_value, :map, null: false
      add :source_type, :string, null: false
      add :source_ref, :string, null: false
      add :source_status, :string, null: false
      add :source_observed_at, :utc_datetime_usec, null: false
      add :source_digest, :string, null: false
      add :introduced_revision, :string, null: false
      add :retired_revision, :string
      add :compatibility_min, :integer, null: false
      add :compatibility_max, :integer, null: false
      add :capability_ref, :string
      add :promise_ref, :string
      add :author, :string, null: false
      add :reason, :text, null: false
      add :receipt, :map, null: false, default: %{}
      timestamps(type: :utc_datetime_usec, updated_at: false)
    end

    create unique_index(:sarah_blueprint_facts, [:revision_id, :fact_id])
    create index(:sarah_blueprint_facts, [:fact_id, :introduced_revision])

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_section_check,
             check:
               "section IN ('identity', 'voice', 'vocabulary', 'roles', 'product_truths', 'rules', 'examples')"
           )

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_value_type_check,
             check: "value_type IN ('text', 'terms', 'role', 'example')"
           )

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_source_type_check,
             check:
               "source_type IN ('repository_document', 'release_artifact', 'persona_source', 'founder_direction')"
           )

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_source_status_check,
             check: "source_status IN ('admitted', 'binding', 'historical_evidence')"
           )

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_digest_check,
             check: "source_digest ~ '^[0-9a-f]{64}$'"
           )

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_compatibility_check,
             check: "compatibility_min >= 1 AND compatibility_max >= compatibility_min"
           )

    create constraint(:sarah_blueprint_facts, :sarah_blueprint_fact_bounds_check,
             check:
               "octet_length(fact_id) BETWEEN 1 AND 160 AND " <>
                 "octet_length(source_ref) BETWEEN 1 AND 1000 AND " <>
                 "octet_length(author) BETWEEN 1 AND 200 AND " <>
                 "octet_length(reason) BETWEEN 1 AND 2000 AND " <>
                 "jsonb_typeof(typed_value) = 'object' AND octet_length(typed_value::text) <= 8192 AND " <>
                 "jsonb_typeof(receipt) = 'object' AND octet_length(receipt::text) <= 8192"
           )

    execute("""
    CREATE FUNCTION reject_sarah_blueprint_mutation()
    RETURNS trigger AS $$
    BEGIN
      RAISE EXCEPTION 'admitted Sarah Blueprint records are immutable; append a revision';
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("""
    CREATE TRIGGER protect_sarah_blueprint_revisions
    BEFORE UPDATE OR DELETE ON sarah_blueprint_revisions
    FOR EACH ROW EXECUTE FUNCTION reject_sarah_blueprint_mutation();
    """)

    execute("""
    CREATE TRIGGER protect_sarah_blueprint_facts
    BEFORE UPDATE OR DELETE ON sarah_blueprint_facts
    FOR EACH ROW EXECUTE FUNCTION reject_sarah_blueprint_mutation();
    """)
  end

  def down do
    drop table(:sarah_blueprint_facts)
    drop table(:sarah_blueprint_revisions)
    execute("DROP FUNCTION reject_sarah_blueprint_mutation()")
  end
end