Ecto IN clauses with tuples

For anyone looking on this, I’ve added correct type support:

  @doc """
  Builds a row constructor IN query for composite keys, also known as tuple IN query.

  It outputs `(id, name) in ((1::bigint, "Company A"::varchar), (2::bigint, "Company B"::varchar))`.

  ## Example

      iex> from(s in schema, where: ^tuple_in(schema, [:id, :name], [[1, "Company A"], [2, "Company B"]]))

  """
  @spec tuple_in(Ecto.Schema.t(), [atom()], [[any()]]) :: Ecto.Query.dynamic_expr()
  def tuple_in(schema, fields, values) when length(fields) > 0 and length(values) > 0 do
    types = Enum.map(fields, &schema.__schema__(:type, &1))

    quoted_fields =
      Enum.map(fields, fn field ->
        quote do
          field(s, ^unquote(field))
        end
      end)

    quoted_values =
      Enum.map(values, fn values ->
        quoted_type_values =
          values
          |> Enum.zip(types)
          |> Enum.map(fn {value, type} ->
            quote do
              type(^unquote(Macro.escape(value)), unquote(type))
            end
          end)

        params = "(#{Enum.map_join(quoted_type_values, ",", fn _ -> "?" end)})"

        quote do
          fragment(unquote(params), unquote_splicing(quoted_type_values))
        end
      end)

    params =
      "(#{Enum.map_join(quoted_fields, ",", fn _ -> "?" end)}) in (#{Enum.map_join(values, ",", fn _ -> "?" end)})"

    {term, _bindings} =
      Code.eval_quoted(
        quote do
          dynamic(
            [s],
            fragment(unquote(params), unquote_splicing(quoted_fields), unquote_splicing(quoted_values))
          )
        end
      )

    term
  end

  def tuple_in(_schema, _fields, _values), do: dynamic(false)
2 Likes