lib/endo/adapters/postgres.ex

defmodule Endo.Adapters.Postgres do
  @moduledoc """
  Adapter module implementing the ability for Endo to reflect upon any
  Postgres-based Ecto Repo.

  See `Endo` documentation for list of features.

  In future, parts of `Endo`'s top level documentation may be moved here, but as
  this is the only supported adapter at the time of writing, this isn't the case.
  """

  @behaviour Endo.Adapter

  alias Endo.Adapters.Postgres.Column
  alias Endo.Adapters.Postgres.Index
  alias Endo.Adapters.Postgres.PgClass
  alias Endo.Adapters.Postgres.PgIndex
  alias Endo.Adapters.Postgres.Table
  alias Endo.Adapters.Postgres.TableConstraint

  @spec list_tables(repo :: module(), opts :: Keyword.t()) :: [Table.t()]
  def list_tables(repo, opts \\ []) when is_atom(repo) do
    preloads = [:columns, table_constraints: :constraint_column_usage]

    preload_indexes = fn %Table{table_name: name} = table ->
      indexes = PgClass.query(collate_indexes: true, relname: name)
      %Table{table | indexes: repo.all(indexes)}
    end

    opts
    |> Table.query()
    |> repo.all()
    |> Task.async_stream(&(&1 |> repo.preload(preloads) |> preload_indexes.()))
    |> Enum.map(fn {:ok, %Table{} = table} -> table end)
  end

  @spec to_endo(Table.t()) :: Endo.Table.t()
  @spec to_endo(TableConstraint.t()) :: Endo.Association.t()
  @spec to_endo(Column.t()) :: Endo.Column.t()
  @spec to_endo(Index.t()) :: Endo.Index.t()

  def to_endo(%Table{} = table) do
    %Endo.Table{
      adapter: __MODULE__,
      name: table.table_name,
      columns: Enum.map(table.columns, &to_endo/1),
      indexes: Enum.map(table.indexes, &to_endo/1),
      associations:
        table.table_constraints
        |> Enum.filter(&(&1.constraint_type == "FOREIGN KEY"))
        |> Enum.map(&to_endo/1)
    }
  end

  def to_endo(%Column{} = column) do
    %Endo.Column{
      adapter: __MODULE__,
      name: column.column_name,
      type: column.data_type
    }
  end

  def to_endo(%TableConstraint{} = constraint) do
    %Endo.Association{
      adapter: __MODULE__,
      name: constraint.constraint_name,
      type: constraint.constraint_column_usage.table_name
    }
  end

  def to_endo(%Index{} = index) do
    metadata = index.pg_index || %PgIndex{}

    %Endo.Index{
      adapter: __MODULE__,
      name: index.name,
      columns: index.columns,
      is_primary: metadata.indisprimary,
      is_unique: metadata.indisunique
    }
  end
end