if Code.ensure_loaded?(Ecto.Query.API) do defmodule QuackDB.Ecto.Lambda do @moduledoc false @binary_operators %{ +: "+", -: "-", *: "*", /: "/", ==: "=", !=: "!=", >: ">", <: "<", >=: ">=", <=: "<=", and: "AND", or: "OR" } @supported_expression_message "Supported expressions are literals, pinned values, lambda variables, arithmetic, comparisons, boolean operators, rem/2, is_nil/1, and case_when/1" @type option :: {:function, atom() | String.t()} | {:arities, [non_neg_integer()]} @spec to_sql!(Macro.t(), [option()]) :: {String.t(), [Macro.t()]} def to_sql!(lambda_ast, options) do function = Keyword.fetch!(options, :function) arities = Keyword.fetch!(options, :arities) case lambda_ast do {:fn, _meta, [{:->, _arrow_meta, [args, body]}]} when is_list(args) -> validate_arity!(function, args, arities) vars = lambda_vars!(function, args) var_names = Enum.map(vars, &Atom.to_string/1) {body_sql, params} = expr(body, vars, function) {["lambda ", Enum.join(var_names, ", "), " : ", body_sql] |> IO.iodata_to_binary(), params} _other -> raise ArgumentError, "expected #{function} lambda as `fn x -> expr end`, got: #{Macro.to_string(lambda_ast)}" end end defp validate_arity!(function, args, arities) do arity = length(args) unless arity in arities do expected = arities |> Enum.map_join(" or ", &Integer.to_string/1) raise ArgumentError, "unsupported #{function} lambda arity #{arity}; expected #{expected} parameters" end end defp lambda_vars!(function, args) do Enum.map(args, fn {name, _meta, context} when is_atom(name) and is_atom(context) -> if QuackDB.Identifier.valid?(name) do name else raise ArgumentError, "invalid DuckDB lambda parameter `#{name}` in #{function}; parameters must be simple variables" end other -> raise ArgumentError, "invalid DuckDB lambda parameter in #{function}: #{Macro.to_string(other)}" end) end defp expr({op, _meta, [left, right]}, vars, function) when is_map_key(@binary_operators, op) do {left_sql, left_params} = expr(left, vars, function) {right_sql, right_params} = expr(right, vars, function) {["(", left_sql, " ", Map.fetch!(@binary_operators, op), " ", right_sql, ")"], left_params ++ right_params} end defp expr({:-, _meta, [value]}, vars, function) do {value_sql, params} = expr(value, vars, function) {["(-", value_sql, ")"], params} end defp expr({:not, _meta, [{:is_nil, _nil_meta, [value]}]}, vars, function) do {value_sql, params} = expr(value, vars, function) {["(", value_sql, " IS NOT NULL)"], params} end defp expr({:not, _meta, [value]}, vars, function) do {value_sql, params} = expr(value, vars, function) {["(NOT ", value_sql, ")"], params} end defp expr({:is_nil, _meta, [value]}, vars, function) do {value_sql, params} = expr(value, vars, function) {["(", value_sql, " IS NULL)"], params} end defp expr({:rem, _meta, [left, right]}, vars, function) do {left_sql, left_params} = expr(left, vars, function) {right_sql, right_params} = expr(right, vars, function) {["(", left_sql, " % ", right_sql, ")"], left_params ++ right_params} end defp expr({:case_when, _meta, [[do: clauses]]}, vars, function) do {when_clauses, else_expression} = QuackDB.Ecto.Conditionals.__split_clauses__!(clauses) {when_sql, when_param_groups} = Enum.map_reduce(when_clauses, [], fn {condition, expression}, param_groups -> {condition_sql, condition_params} = expr(condition, vars, function) {expression_sql, expression_params} = expr(expression, vars, function) {["WHEN ", condition_sql, " THEN ", expression_sql, " "], [expression_params, condition_params | param_groups]} end) {else_sql, else_params} = expr(else_expression, vars, function) when_params = when_param_groups |> Enum.reverse() |> List.flatten() {["CASE ", when_sql, "ELSE ", else_sql, " END"], when_params ++ else_params} end defp expr({:^, _meta, [_value]} = pinned, _vars, _function), do: {"?", [pinned]} defp expr({name, _meta, context}, vars, function) when is_atom(name) and is_atom(context) do if name in vars do {Atom.to_string(name), []} else raise ArgumentError, "unknown DuckDB lambda variable `#{name}` in #{function}; use a lambda parameter or pin external values with ^#{name}" end end defp expr(nil, _vars, _function), do: {"NULL", []} defp expr(true, _vars, _function), do: {"TRUE", []} defp expr(false, _vars, _function), do: {"FALSE", []} defp expr(value, _vars, _function) when is_integer(value), do: {Integer.to_string(value), []} defp expr(value, _vars, _function) when is_float(value) do {value |> QuackDB.SQL.literal!() |> IO.iodata_to_binary(), []} end defp expr(value, _vars, _function) when is_binary(value) do {value |> QuackDB.SQL.literal!() |> IO.iodata_to_binary(), []} end defp expr(other, _vars, function) do raise ArgumentError, "unsupported DuckDB lambda expression in #{function}: #{Macro.to_string(other)}. #{@supported_expression_message}" end end end