Skip to main content

lib/selecto_components/execution/ctes.ex

defmodule SelectoComponents.Execution.CTEs do
  @moduledoc """
  CTE synchronization and application helpers for execution planning.
  """

  alias SelectoComponents.Form.ColumnCatalog

  @field_param_sections ~w(selected order_by group_by aggregate x_axis y_axis series)
  @view_state_field_keys [:selected, :order_by, :group_by, :aggregate, :x_axis, :y_axis, :series]

  def sync_view_config(view_config, %Selecto{} = selecto) when is_map(view_config) do
    derived_names = derived_cte_names_from_view_config(view_config, selecto)
    existing_ctes = get_map_value(view_config, :ctes, [])
    synced_ctes = build_cte_entries(derived_names, existing_ctes)

    Map.put(view_config, :ctes, synced_ctes)
  end

  def sync_view_config(view_config, _selecto), do: view_config

  def apply_for_params(selecto, params) when is_map(params) do
    explicit_names =
      params
      |> ctes_from_params([])
      |> Enum.map(&cte_entry_name/1)
      |> Enum.reject(&is_nil/1)

    derived_names = derived_cte_names_from_params(params, selecto)

    Enum.reduce(explicit_names ++ derived_names, selecto, fn
      name, acc when is_binary(name) and name != "" ->
        if name in ColumnCatalog.available_cte_names(acc) and not cte_already_applied?(acc, name) do
          Selecto.with_cte(acc, name)
        else
          acc
        end

      _name, acc ->
        acc
    end)
  end

  def apply_for_params(selecto, _params), do: selecto

  defp ctes_from_params(params, default) when is_map(params) do
    case Map.get(params, "ctes") do
      section when is_map(section) ->
        section
        |> Enum.sort_by(fn {_uuid, value} -> sort_index(value) end)
        |> Enum.map(fn {uuid, value} ->
          cte_uuid = get_map_value(value, :uuid, uuid)
          name = get_map_value(value, :name)

          {cte_uuid, name, Map.drop(stringify_map_keys(value), ["uuid", "name", "index"])}
        end)
        |> Enum.reject(fn {_uuid, name, _config} -> is_nil(name) or to_string(name) == "" end)

      _ ->
        default
    end
  end

  defp derived_cte_names_from_params(params, %Selecto{} = selecto) when is_map(params) do
    field_ids = field_ids_from_params(params) ++ filter_ids_from_params(params)
    ColumnCatalog.required_cte_names_for_fields(selecto, field_ids)
  end

  defp derived_cte_names_from_params(_params, _selecto), do: []

  defp derived_cte_names_from_view_config(view_config, %Selecto{} = selecto)
       when is_map(view_config) do
    field_ids =
      field_ids_from_view_config(view_config) ++ filter_ids_from_view_config(view_config)

    ColumnCatalog.required_cte_names_for_fields(selecto, field_ids)
  end

  defp derived_cte_names_from_view_config(_view_config, _selecto), do: []

  defp field_ids_from_params(params) when is_map(params) do
    @field_param_sections
    |> Enum.flat_map(fn section ->
      params
      |> Map.get(section, %{})
      |> list_field_ids_from_param_section()
    end)
  end

  defp list_field_ids_from_param_section(section) when is_map(section) do
    section
    |> Map.values()
    |> Enum.map(&get_map_value(&1, :field))
    |> Enum.reject(&is_nil/1)
  end

  defp list_field_ids_from_param_section(_section), do: []

  defp filter_ids_from_params(params) when is_map(params) do
    params
    |> Map.get("filters", %{})
    |> Map.values()
    |> Enum.map(&get_map_value(&1, :filter))
    |> Enum.reject(&is_nil/1)
  end

  defp field_ids_from_view_config(view_config) when is_map(view_config) do
    view_config
    |> get_map_value(:views, %{})
    |> Map.values()
    |> Enum.flat_map(&field_ids_from_view_state/1)
  end

  defp field_ids_from_view_state(view_state) when is_map(view_state) do
    @view_state_field_keys
    |> Enum.flat_map(fn key ->
      view_state
      |> get_map_value(key, [])
      |> list_field_ids_from_items()
    end)
  end

  defp field_ids_from_view_state(_view_state), do: []

  defp list_field_ids_from_items(items) when is_list(items) do
    items
    |> Enum.map(fn
      {_uuid, field, _config} -> field
      [_, field, _config] -> field
      _other -> nil
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp list_field_ids_from_items(_items), do: []

  defp filter_ids_from_view_config(view_config) when is_map(view_config) do
    view_config
    |> get_map_value(:filters, [])
    |> Enum.map(fn
      {_uuid, _section, filter_value} -> get_map_value(filter_value, :filter)
      [_, _, filter_value] -> get_map_value(filter_value, :filter)
      _other -> nil
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp build_cte_entries(names, existing_ctes) when is_list(names) do
    existing_by_name =
      Map.new(existing_ctes, fn entry ->
        case normalize_cte_entry(entry) do
          {uuid, name, config} -> {name, {uuid, name, config}}
          nil -> {nil, nil}
        end
      end)

    names
    |> Enum.uniq()
    |> Enum.map(fn name ->
      Map.get(existing_by_name, name, {"auto-cte-#{name}", name, %{}})
    end)
  end

  defp build_cte_entries(_names, _existing_ctes), do: []

  defp normalize_cte_entry({uuid, name, config}),
    do: {to_string(uuid), to_string(name), config || %{}}

  defp normalize_cte_entry([uuid, name, config]),
    do: {to_string(uuid), to_string(name), config || %{}}

  defp normalize_cte_entry(_entry), do: nil

  defp cte_entry_name({_, name, _}) when is_binary(name), do: name
  defp cte_entry_name([_, name, _]) when is_binary(name), do: name
  defp cte_entry_name(_entry), do: nil

  defp cte_already_applied?(%Selecto{} = selecto, name) do
    selecto
    |> get_in([Access.key(:set, %{}), Access.key(:ctes, [])])
    |> Enum.any?(fn spec ->
      spec_name =
        Map.get(spec, :name) ||
          Map.get(spec, :as) ||
          Map.get(spec, "name") ||
          Map.get(spec, "as")

      to_string(spec_name || "") == name
    end)
  end

  defp cte_already_applied?(_selecto, _name), do: false

  defp stringify_map_keys(map) when is_map(map) do
    Map.new(map, fn {key, value} -> {to_string(key), value} end)
  end

  defp stringify_map_keys(_value), do: %{}

  defp sort_index(value) when is_map(value) do
    case Map.get(value, "index") do
      idx when is_binary(idx) ->
        case Integer.parse(idx) do
          {num, ""} -> num
          _ -> 0
        end

      idx when is_integer(idx) ->
        idx

      _ ->
        0
    end
  end

  defp sort_index(_value), do: 0

  defp get_map_value(map, key, default \\ nil)

  defp get_map_value(map, key, default) when is_map(map) do
    Map.get(map, key, Map.get(map, to_string(key), default))
  end

  defp get_map_value(_map, _key, default), do: default
end