Linell

Linell

Dynamic Query Generation

I’m trying to build a query builder that takes as input a bunch of JSON and parses it out
into a valid Ecto query. I’ve got most of the actual parsing working, but I can’t quite grok
the process of actually turning a “rule” from the JSON into a query.

Essentially what I’m doing now is this:

  def json_to_query(json) do
    query = from students in "students",
      select: students.id,
      distinct: true,
      limit: 10

    parse(query, json)
  end

  defp parse(source_query, node) do
    res = node
      |> Map.get("rules")
      |> Enum.reduce(source_query, fn(r, q) -> parse_node(q, r) end) 

    res
  end

  defp parse_node(source_query, node) do
    if (is_top_level_node(node)), do: parse(source_query, node), else: sql_from_rule(source_query, node)
  end

  def sql_from_rule(source_query, rule) do
    field    = Map.get(rule, "field")
    value    = Map.get(rule, "values")   |> List.first |> Map.get("value") |> get_value
    operator = Map.get(rule, "operator") |> get_operator

    # IEx.pry
    source_query
      |> join_clause(field)
      |> where_clause(field, value, operator)
  end

  defp join_clause(query, field) do
    table = String.split(field, ".") |> List.first

    case table do
      "students" -> query
      _          -> query |> join(:inner, [s], t in ^table, s.id == t.student_id)
    end
  end

  defp where_clause(query, field, value, operator) do
    column = String.split(field, ".") |> List.last

    # The obvious problem here is that it's going to end up generating the initial
    # query just fine, and the joins are fine as far as I can tell, but this
    # is not referencing the correct table when it generates the where. Instead of 
    # `where grades.term == "T1"` I end up with `where "grades.term" == "T1"` which is
    # just comparing the strings.
    query
      |> where(^column == ^value)
  end

I’ve found a few related forum posts, but can’t quite figure out how to make the code work
for my situation. Here’s an example of a rule that I’ll be parsing:

            {  
               "rules":[  
                  {  
                     "id":"9a9b899a-0123-4456-b89a-b15f9237a0de",
                     "field":"students.gifted",
                     "type":"boolean",
                     "input":"boolean",
                     "operator":"equal",
                     "values":[  
                        {  
                           "type":"boolean",
                           "value":true
                        }
                     ]
                  },
                  {  
                     "rules":[  
                        {  
                           "id":"aa98baaa-89ab-4cde-b012-315f9238650b",
                           "field":"grades.term",
                           "type":"select",
                           "input":"select",
                           "operator":"select_equals",
                           "values":[  
                              {  
                                 "type":"select",
                                 "value":"T1"
                              }
                           ]
                        },
                        {  
                           "id":"a9bb8a8b-4567-489a-bcde-f15f92389859",
                           "field":"students.school_id",
                           "type":"select",
                           "input":"select",
                           "operator":"select_any_in",
                           "values":[  
                              {  
                                 "type":"multiselect",
                                 "value":[  
                                    "234siteid",
                                    "456siteid"
                                 ]
                              }
                           ]
                        }
                     ],
                     "condition":"AND"
                  }

The above should generate something like:

select
  id,
  students.gifted,
  grades.term,
  students.school_id
from
  students s
    join grades g on s.id == g.student_id
where
  s.gifted == true and
  (grades.term == "T1" and s.school_id in ("234siteid", "456siteid"))

I’m looking into dynamic queries but I
don’t quite understand how I could use them in this context. I’d appreciate any help.

Most Liked

OvermindDL1

OvermindDL1

Congrats! It looks like you hit the same problems that I do with Ecto (which would be solved by named tables). :slight_smile:

I have a very bad but workable solution (that no one posted a better one) at: Ecto Query Patterns - Named joins ^.^;

Linell

Linell

Using Ecto’s field I’m able to get to the point of doing something like this:

    column = String.split(field, ".") |> List.last |> String.to_atom

    query
      |> where([s, ..., c], field(c, ^column) == ^value)

The problem is that the table inside of c isn’t always correct.

Where Next?

Popular in Questions Top

aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
Kagamiiiii
Student & New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

We're in Beta

About us Mission Statement