3.5 KiB · text History b868907
defmodule GitGud.Repo.Migrations.AddPackageFormats do
  use Ecto.Migration

  @moduledoc """
  Phase 0 of the private package registries plan: the substrate the
  PyPI/Hex/Cargo/apt adapters all sit on.

  Three changes:

    * `packages.format` — which registry protocol serves this package.
      Everything that exists today came from zot, so it backfills to
      `oci`. Uniqueness moves from `(repository_id, kind, name)` to
      `(repository_id, format, normalized_name)`, which is what every
      adapter actually looks up by.

    * `packages.normalized_name` — the form the ecosystem canonicalises
      to (PEP 503 for PyPI, lowercase for Cargo, verbatim for Hex/OCI).
      Stored rather than computed so lookups hit an index.

    * `package_files` — one row per uploaded artifact. OCI keeps its
      bytes in zot, but every other format hands us a file (a wheel, a
      `.tar`, a `.crate`, a `.deb`) that lives in `GitGud.Storage`.
      A version may have several (sdist + wheels, per-arch debs).
  """

  def up do
    alter table(:packages) do
      add :format, :string, null: false, default: "oci", size: 20
      add :normalized_name, :string, size: 200
    end

    execute "UPDATE packages SET format = 'oci', normalized_name = lower(name)"

    alter table(:packages) do
      modify :normalized_name, :string, null: false, size: 200
    end

    drop unique_index(:packages, [:repository_id, :kind, :name])
    create unique_index(:packages, [:repository_id, :format, :normalized_name])
    create index(:packages, [:format, :normalized_name])

    # Non-OCI versions are identified by the artifact's own checksum,
    # which lives on `package_files` — there is no single manifest
    # digest to require here.
    alter table(:package_versions) do
      modify :digest, :string, null: true
      add :metadata, :map
      add :yanked, :boolean, null: false, default: false
      add :yanked_reason, :string
    end

    create table(:package_files) do
      add :package_version_id, references(:package_versions, on_delete: :delete_all), null: false
      add :filename, :string, null: false, size: 300
      add :content_type, :string
      add :size, :bigint, null: false, default: 0
      # Lowercase hex. SHA-256 is the floor for every format we serve —
      # apt clients are required to reject MD5Sum/SHA1 for security.
      add :sha256, :string, null: false, size: 64
      # Only populated where a format's index demands it (apt's optional
      # MD5Sum field, Hex's inner tarball checksum).
      add :md5, :string, size: 32
      add :storage_bucket, :string, null: false
      add :storage_key, :string, null: false
      # Per-format extras: requires-python and the PEP 592 yank reason,
      # a crate's dependency table, a .deb's control stanza.
      add :metadata, :map
      add :uploader_id, references(:users, on_delete: :nilify_all)

      timestamps(type: :utc_datetime)
    end

    create unique_index(:package_files, [:package_version_id, :filename])
    create index(:package_files, [:sha256])
  end

  def down do
    drop table(:package_files)

    alter table(:package_versions) do
      remove :yanked_reason
      remove :yanked
      remove :metadata
      modify :digest, :string, null: false
    end

    drop index(:packages, [:format, :normalized_name])
    drop unique_index(:packages, [:repository_id, :format, :normalized_name])
    create unique_index(:packages, [:repository_id, :kind, :name])

    alter table(:packages) do
      remove :normalized_name
      remove :format
    end
  end
end