fuelen

fuelen OP

Hi all!

Just want to share a small code snippet which allows writing CASE expressions using macro which is similar to cond.
Here is an example of usage:

import Ecto.Extension.Query.API
import Ecto.Query

from users in "users",
  select: %{
    email: users.email,
    role_label:
      cond_ do
        users.role == "director" -> "DIRECTOR!"
        users.inserted_at < ago(6, "month") -> "OLD!"
        true -> type(users.role, :string)
      end
  }

generated SQL:

SELECT u0."email", CASE WHEN u0."role" = 'director' THEN 'DIRECTOR!' WHEN u0."inserted_at" < $1::timestamp + (-6::decimal::numeric * interval '1 month') THEN 'OLD!' WHEN TRUE THEN u0."role"::varchar END FROM "users" AS u0

and implementation:

defmodule Ecto.Extension.Query.API do
  defmacro cond_(do: block) do
    bindings =
      block
      |> Enum.reduce([], fn
        {:->, _, [[clause], branch]}, acc ->
          [branch, clause | acc]
      end)
      |> Enum.reverse()

    bindings_number = length(bindings)

    sql =
      IO.iodata_to_binary([
        "CASE",
        List.duplicate(" WHEN ? THEN ?", div(bindings_number, 2)),
        " END"
      ])

    quote do
      fragment(unquote(sql), unquote_splicing(bindings))
    end
  end
end

I hope someone will find this useful :slight_smile:

First 3 of 3 Posts Switch mode

hauleth

hauleth

Super nice. Once upon a time I was thinking about doing exactly the same magic.

Small suggestion for improvement - define from macro in your module and then traverse the query to replace all cond with your implementation. This should result with clearer API :wink:

axelson

axelson

Scenic Core Team

This is a neat improvement to ecto’s sql!

But the downside to this is that (if I’m following correctly) this improvement would no longer be composable with other ecto DSL improvements since only one person can define from

fuelen

fuelen OP

Well, this is my first attempt to implement @hauleth idea. Some kind of composability is still possible

defmodule CustomEctoFrom do
  defmacro __using__(map) do
    quote do
      require Ecto.Query

      defmacro from(expr, kw) do
        kw = CustomEctoFrom.traverse(kw, unquote(map))

        quote do
          Ecto.Query.from(unquote(expr), unquote(kw))
        end
      end
    end
  end

  def traverse({function, args}, map) when is_atom(function) do
    {function, traverse(args, map)}
  end

  def traverse({function, meta, args}, map) do
    function =
      case Map.fetch(map, function) do
        {:ok, aliased_to} -> aliased_to
        :error -> function
      end

    {traverse(function, map), meta, traverse(args, map)}
  end

  def traverse(kw, map) when is_list(kw) do
    for elem <- kw, do: traverse(elem, map)
  end

  def traverse(term, _map), do: term
end

defmodule OtherModuleWithExtensions do
  defmacro concat(a, b) do
    quote do
      fragment("? || ?", unquote(a), unquote(b))
    end
  end
end

defmodule Test do
  import Ecto.Extension.Query.API
  import OtherModuleWithExtensions
  use CustomEctoFrom, %{cond: :cond_, <>: :concat}

  def test do
    from users in "users",
      select: %{
        full_name: users.first_name <> " " <> users.last_name,
        email: users.email,
        role_label:
          cond do
            users.role == "director" -> "DIRECTOR!"
            users.inserted_at < ago(6, "month") -> "OLD!"
            true -> type(users.role, :string)
          end,
        id: users.id
      }
  end
end
— All posts loaded —

Where Next? Top

Trending in Guides/Tuts Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New

We're in Beta

About us Mission Statement