lib/bylaw/ecto/query/boundedness.ex

defmodule Bylaw.Ecto.Query.Boundedness do
  @moduledoc false

  @typep filter_type :: :empty | :restricting | :unrestricting

  @spec root_where_bounded?(term()) :: boolean()
  def root_where_bounded?(%{wheres: wheres}) when is_list(wheres) do
    wheres
    |> Enum.reduce(nil, &combine_where/2)
    |> bounded_filter?()
  end

  def root_where_bounded?(_query), do: false

  @spec combine_where(term(), filter_type() | nil) :: filter_type()
  defp combine_where(%{expr: expr} = where, nil) do
    filter_type(expr, where_params(where))
  end

  defp combine_where(%{expr: expr, op: :or} = where, filter) do
    or_filter(filter, filter_type(expr, where_params(where)))
  end

  defp combine_where(%{expr: expr} = where, filter) do
    and_filter(filter, filter_type(expr, where_params(where)))
  end

  defp combine_where(_where, nil), do: :restricting
  defp combine_where(_where, filter), do: and_filter(filter, :restricting)

  @spec where_params(map()) :: list()
  defp where_params(%{params: params}) when is_list(params), do: params
  defp where_params(_where), do: []

  @spec filter_type(term(), list()) :: filter_type()
  defp filter_type(true, _params), do: :unrestricting
  defp filter_type(false, _params), do: :empty

  defp filter_type({:^, _meta, [index]}, params) when is_integer(index) do
    case Enum.fetch(params, index) do
      {:ok, {value, _type}} -> filter_type(value, [])
      {:ok, value} -> filter_type(value, [])
      :error -> :restricting
    end
  end

  defp filter_type(%Ecto.Query.Tagged{value: value}, _params), do: filter_type(value, [])
  defp filter_type({:type, _meta, [expr, _type]}, params), do: filter_type(expr, params)

  defp filter_type({:and, _meta, [left, right]}, params) do
    left
    |> filter_type(params)
    |> and_filter(filter_type(right, params))
  end

  defp filter_type({:or, _meta, [left, right]}, params) do
    left
    |> filter_type(params)
    |> or_filter(filter_type(right, params))
  end

  defp filter_type({:not, _meta, [expr]}, params) do
    expr
    |> filter_type(params)
    |> negate_filter()
  end

  defp filter_type(_expr, _params), do: :restricting

  @spec and_filter(filter_type(), filter_type()) :: filter_type()
  defp and_filter(:empty, _right), do: :empty
  defp and_filter(_left, :empty), do: :empty
  defp and_filter(:unrestricting, :unrestricting), do: :unrestricting
  defp and_filter(_left, _right), do: :restricting

  @spec or_filter(filter_type(), filter_type()) :: filter_type()
  defp or_filter(:unrestricting, _right), do: :unrestricting
  defp or_filter(_left, :unrestricting), do: :unrestricting
  defp or_filter(:empty, :empty), do: :empty
  defp or_filter(_left, _right), do: :restricting

  @spec negate_filter(filter_type()) :: filter_type()
  defp negate_filter(:unrestricting), do: :empty
  defp negate_filter(:empty), do: :unrestricting
  defp negate_filter(:restricting), do: :restricting

  @spec bounded_filter?(filter_type() | nil) :: boolean()
  defp bounded_filter?(nil), do: false
  defp bounded_filter?(:unrestricting), do: false
  defp bounded_filter?(_filter), do: true
end