diff --git a/lib/expr.ex b/lib/expr.ex index 197fcd2..b99eb16 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -3510,57 +3510,72 @@ defmodule AshSql.Expr do value end - element_type = - case type do - {{:array, type}, _} -> type - {{:in, type}, _} -> type - _ -> nil - end + case bindings.sql_behaviour.list_expr(query, value, bindings, embedded?, acc, type) do + {:ok, expr, acc} -> + {expr, acc} - elements = - Enum.map(value, fn list_item -> - if type do - {:expr, %Ash.Query.Function.Type{arguments: [list_item, element_type, []]}} - else - {:expr, list_item} - end - end) - |> Enum.intersperse({:raw, ","}) + {:error, error} -> + raise "Error while building list expression: #{error}" - if is_map? do - do_dynamic_expr( - query, - %Fragment{ - embedded?: embedded?, - arguments: - [ - raw: "array_to_json(ARRAY[" - ] ++ elements ++ [raw: "])"] - }, - bindings, - embedded?, - acc, - type - ) - else - do_dynamic_expr( - query, - %Fragment{ - embedded?: embedded?, - arguments: - [ - raw: "ARRAY[" - ] ++ elements ++ [raw: "]"] - }, - bindings, - embedded?, - acc, - type - ) + :error -> + default_encode_list(query, value, bindings, embedded?, acc, type, is_map?) end end end + # `ARRAY[...]` is Postgres syntax. It stays the default so implementations that do not + # override `list_expr/6` render exactly as they did before. + defp default_encode_list(query, value, bindings, embedded?, acc, type, is_map?) do + element_type = + case type do + {{:array, type}, _} -> type + {{:in, type}, _} -> type + _ -> nil + end + + elements = + Enum.map(value, fn list_item -> + if type do + {:expr, %Ash.Query.Function.Type{arguments: [list_item, element_type, []]}} + else + {:expr, list_item} + end + end) + |> Enum.intersperse({:raw, ","}) + + if is_map? do + do_dynamic_expr( + query, + %Fragment{ + embedded?: embedded?, + arguments: + [ + raw: "array_to_json(ARRAY[" + ] ++ elements ++ [raw: "])"] + }, + bindings, + embedded?, + acc, + type + ) + else + do_dynamic_expr( + query, + %Fragment{ + embedded?: embedded?, + arguments: + [ + raw: "ARRAY[" + ] ++ elements ++ [raw: "]"] + }, + bindings, + embedded?, + acc, + type + ) + end + end + defp list_expr(query, value, bindings, embedded?, acc, type) do if list_requires_encoding?(value) do encode_list(query, value, bindings, embedded?, acc, type) diff --git a/lib/implementation.ex b/lib/implementation.ex index 177c9c6..090ca03 100644 --- a/lib/implementation.ex +++ b/lib/implementation.ex @@ -9,6 +9,20 @@ defmodule AshSql.Implementation do @callback repo(Ash.Resource.t(), :mutate | :read) :: module @callback expr(Ecto.Query.t(), Ash.Expr.t(), map, boolean, AshSql.Expr.ExprInfo.t(), term) :: {:ok, term, AshSql.Expr.ExprInfo.t()} | {:error, term} | :error + + @doc """ + Render a list literal that has to be encoded, i.e. one holding maps, lists or expressions. + + `AshSql.Expr` renders those as `ARRAY[...]` (or `array_to_json(ARRAY[...])`), which is + Postgres syntax. Implementations whose database has no array constructor override this to + render the list themselves. Returning `:error`, which is the default, keeps the `ARRAY[...]` + rendering. + + The value is passed after embedded resources have been dumped, so it is a plain list. + """ + @callback list_expr(Ecto.Query.t(), list(), map, boolean, AshSql.Expr.ExprInfo.t(), term) :: + {:ok, term, AshSql.Expr.ExprInfo.t()} | {:error, term} | :error + @callback simple_join_first_aggregates(Ash.Resource.t()) :: list(atom) @callback parameterized_type( @@ -50,6 +64,7 @@ defmodule AshSql.Implementation do def strpos_function, do: "strpos" def expr(_, _, _, _, _, _), do: :error + def list_expr(_, _, _, _, _, _), do: :error def simple_join_first_aggregates(_), do: [] def list_aggregate(_), do: nil def multicolumn_distinct?, do: true @@ -88,6 +103,7 @@ defmodule AshSql.Implementation do defoverridable array_overlap_operator?: 0, equals_any?: 0, expr: 6, + list_expr: 6, ilike?: 0, strpos_function: 0, require_ash_functions_for_or_and_and?: 0,