lib/mix/tasks/pgflow.setup.ex

defmodule Mix.Tasks.Pgflow.Setup do
  @shortdoc "Generates one Ecto migration that installs pgflow's core schema + helper functions"

  @moduledoc """
  Generates an Ecto migration in the consumer app that installs the pgflow
  schema by calling `PgFlow.Migration.up/0` and `PgFlow.HelpersMigration.up/0`
  (and optionally `PgFlowDashboard.Migration.up/0`) in the correct order.

  This mirrors `mix tango.setup` and `mix good_analytics.setup`. The output
  is a single wrapper migration, not per-statement migrations — SQL is
  vendored inside pgflow.

  ## Usage

      mix pgflow.setup
      mix pgflow.setup --repo MyApp.OtherRepo
      mix pgflow.setup --no-helpers
      mix pgflow.setup --dashboard

  ## Options

    * `--repo` - Ecto repo module to install against. Defaults to the first
      entry in `config :my_app, ecto_repos: [...]`.

    * `--no-helpers` - Skip `PgFlow.HelpersMigration` (Elixir-binding SQL
      helpers: worker registration, flow input/output queries). Default:
      helpers are installed.

    * `--dashboard` - Also install `PgFlowDashboard.Migration` (dashboard
      views). Default: skipped. Add this if you use the PgFlow LiveView
      dashboard.

  ## Prerequisites

  Before `mix ecto.migrate` picks up the generated migration, the consumer
  must have:

    * pgmq installed (run `mix pgflow.gen.pgmq_migration` to generate a
      migration that installs pgmq via SQL-only method — required unless
      your Postgres already provides pgmq, e.g. Supabase).
    * `pg_cron` extension registered (`CREATE EXTENSION pg_cron` in an
      earlier migration).
    * `citext`, `pg_trgm`, `pgcrypto` extensions registered.

  The typical migration order is:

    1. `install_extensions` (citext, pg_trgm, pgcrypto, pg_cron)
    2. `install_pgmq` (from `mix pgflow.gen.pgmq_migration`)
    3. `setup_pgflow` (this task)
  """

  use Mix.Task

  alias Mix.Tasks.Pgflow.Helpers

  @impl Mix.Task
  def run(args) do
    {opts, _, _} =
      OptionParser.parse(args,
        switches: [repo: :string, helpers: :boolean, dashboard: :boolean]
      )

    repo = Helpers.resolve_repo(opts[:repo])
    helpers? = Keyword.get(opts, :helpers, true)
    dashboard? = Keyword.get(opts, :dashboard, false)

    migrations_dir = Path.join(Helpers.priv_path(repo), "migrations")
    File.mkdir_p!(migrations_dir)

    timestamp = timestamp()
    filename = "#{timestamp}_setup_pgflow.exs"
    full_path = Path.join(migrations_dir, filename)

    File.write!(full_path, migration_content(repo, helpers?, dashboard?))

    Mix.shell().info("""
    Created migration: #{full_path}

    Run `mix ecto.migrate` to apply.

    Ensure pgmq is installed before this migration runs (via
    `mix pgflow.gen.pgmq_migration`) unless your Postgres already ships pgmq.
    """)
  end

  defp timestamp do
    {{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()

    :io_lib.format("~4..0B~2..0B~2..0B~2..0B~2..0B~2..0B", [y, m, d, hh, mm, ss])
    |> IO.iodata_to_binary()
  end

  defp migration_content(repo, helpers?, dashboard?) do
    up_steps =
      [
        "PgFlow.Migration.up()",
        if(helpers?, do: "PgFlow.HelpersMigration.up()"),
        if(dashboard?, do: "PgFlowDashboard.Migration.up()")
      ]
      |> Enum.reject(&is_nil/1)

    down_steps =
      [
        if(dashboard?, do: "PgFlowDashboard.Migration.down()"),
        if(helpers?, do: "PgFlow.HelpersMigration.down()"),
        "PgFlow.Migration.down()"
      ]
      |> Enum.reject(&is_nil/1)

    up_body = up_steps |> Enum.map_join("\n    ", & &1)
    down_body = down_steps |> Enum.map_join("\n    ", & &1)

    gen_command =
      "mix pgflow.setup" <>
        if(helpers?, do: "", else: " --no-helpers") <>
        if(dashboard?, do: " --dashboard", else: "")

    sources = installed_sources_note(helpers?, dashboard?)

    """
    defmodule #{inspect(repo)}.Migrations.SetupPgflow do
      @moduledoc \"\"\"
      Installs pgflow's core schema + Elixir helper functions.

      Generated by: `#{gen_command}`

      SQL sources (vendored inside pgflow):
      #{sources}

      `PgFlow.Migration.up/0` runs via EctoEvolver with statement-level
      splitting — the V01 core bundle contains 138 statements and
      Postgrex rejects multi-statement queries (error `42601`).
      `PgFlow.HelpersMigration.up/0` adds Elixir-binding SQL functions
      (`flow_exists`, `register_worker`, `get_flow_input`, ...) that the
      library's query layer calls via RPC.

      Idempotent: rerun is a no-op via EctoEvolver's tracking comment on
      `pgflow.pgflow_version`.

      Prerequisites (applied as earlier migrations):
        1. Postgres extensions — `mix pgflow.gen.postgres_extensions_migration`
        2. pgmq schema + functions — `mix pgflow.gen.pgmq_migration` (or a
           native `CREATE EXTENSION pgmq` on Supabase/atlas-postgres-pgflow)
      \"\"\"
      use Ecto.Migration

      def up do
        #{up_body}
      end

      def down do
        #{down_body}
      end
    end
    """
  end

  defp installed_sources_note(helpers?, dashboard?) do
    [
      {true,
       "  - priv/pgflow_core/sql/versions/v01/v01_up.sql       (core: flows, runs, steps, workers, step_tasks, step_states, deps)"},
      {helpers?,
       "  - priv/pgflow_helpers/sql/versions/v01/v01_up.sql    (Elixir-binding RPC helpers)"},
      {dashboard?,
       "  - priv/pgflow_dashboard/sql/versions/v01/v01_up.sql  (LiveView dashboard views + functions)"}
    ]
    |> Enum.filter(fn {include?, _} -> include? end)
    |> Enum.map_join("\n  ", fn {_, line} -> line end)
  end
end