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)






















