DEV Community

calvinsadewa
calvinsadewa

Posted on

Hacking Ecto: Constructing Ecto Query from Raw SQL

Hi all, here's sharing "hack" tips for constructing Ecto.Query from Raw SQL. You would want to do this in case you want all good feature of Ecto.Query but want to bypass safeguard put in place.

A bit of introduction

Ecto is really cool database layer in Elixir, for SQL it let you define SQL query inside Elixir in readable and concise way, and execute it with automatic mapping to defined data structure.
Alt Text
example of SQL query and execution in Ecto

Ecto.Query is main module and data structure for defining a query in Ecto. Ecto.Query is composable and dynamically programmable, which means that we can extend a lot and reuse query functionality.

Alt Text
we define with_search_keyword query filtering function and use it as part of bill_of_ladings function

Constructing Ecto from Raw Query

By default, from, usual construction method of Ecto.Query doesn't support complicated SQL. from support only Ecto.Schema data structure and table name in string, like:

from(t in "transporter",
  where: t.name == "STAR TRANSPORT COMPANY")
Enter fullscreen mode Exit fullscreen mode

However, you can specify Raw SQL in join, via fragment, which usually used in case you have complicated join query.

from(t in "transporter",
  join: i in fragment("SELECT * FROM invoice WHERE invoice.transporter_id = ?", t.id),
  where: i.date == ^date
)
Enter fullscreen mode Exit fullscreen mode

We can leverage this Raw SQL join to get Raw SQL as base object of Ecto.Query.

Define dummy query

We need a dummy query, need to has exactly one result. as example we can query from "constants" table and limit it to 1

dummy_query = from x in "truck_types", as: :dummy, select: x.ksuid, limit: 1
Enter fullscreen mode Exit fullscreen mode

Join dummy query with raw query

By using subquery, we can reuse the dummy query to be used as join component, which we use then to join with raw data

aggregate_subquery = 
  from(d in subquery(dummy_query))
    |> join(:inner, [], raw in fragment(@raw_sql), on: true, as: :raw)
    |> select([raw: raw], %{
      id: raw.id,
      name: raw.name
    })

Enter fullscreen mode Exit fullscreen mode

Example of @raw_sql would be below. @raw_sql need to be constant string.

SELECT id, name FROM transporter
UNION ALL
SELECT id, name FROM shipper
Enter fullscreen mode Exit fullscreen mode

Wrap joined query in subquery (again)

By wrapping the joined query again in subquery, we will have a query which is just like Ecto.Query but with custom SQL

query = from(q in subquery(aggregate_subquery))
Enter fullscreen mode Exit fullscreen mode

Now you can use query as in usual Ecto way

Like in below, which search from transporter and shipper who has name "ANDI"

from(q in query,
  where: name == "ANDI"
)
Enter fullscreen mode Exit fullscreen mode

Why would you want to hack it?

Ecto has really flexible and expressive way to write SQL, why then you would want to resort to such hacking as writing Raw SQL?

For me, the case come when i need to retrofit an existing SQL query, ~500 line - full with Postgres specific SQL feature, with existing functionality which is already built with Ecto.Query. Rewriting it in Ecto would need to decipher such 500 line of SQL, and would be full of fragment for safety hatch of writing Raw SQL. I reason that doing it all in Raw SQL would be easier to comprehend, which then led to this hack.

Top comments (0)