How to perform Dynamic Schema Query

I am trying the following query but getting error (using case in where)

join: a in AppliedOpening,
        on: a.job_opening_id == pipeline.job_opening_id,
        where: case result do
                  "drop" ->
                    a.state != "attempted" and a.state != "unattempted"
                  "unattempted" ->
                    a.state == "unattempted"
                    _ ->
                    a.state == "attempted"
               end,

I want the where: dynamic. Your help will be appreciated.

I do not think its possible that way, but when using function syntax, it might be possible using composition capabilities, something like this:

query = from …, join: …, on: …
query = case result do
  "drop" -> where(query, [a], a.state != "attempted" and a.state != "unattempted")
  "unattempted" -> where(query, [a], a.state == "unattempted")
  _ -> where(query, [a], a.state == "attempted")
end
1 Like

Undefine a error. How to pass a in AppliedOpening

I think you need the full list of bindings

# This query has two bindings: [pipeline,a]
query = from(pipeline in ...,
  join: a in AppliedOpening,
  on: a.job_opening_id == pipeline.job_opening_id
)

# Instead of [pipeline, a] we can use [_,a] because we aren't using "pipeline" here
# but the order of bindings needs to be preserved.
query = case result do
  "drop" -> where(query, [_, a], a.state != "attempted" and a.state != "unattempted")
  "unattempted" -> where(query, [_, a], a.state == "unattempted")
  _ -> where(query, [_, a], a.state == "attempted")
end
1 Like

Elixir doesn’t know what is a in 2nd query command. So, how I pass a in AppliedOpening or tell a is AppliedOpening.

How many tables are in the query? Each table must appear in the binding list for the where even if we only use a.

Show the full query.

1 Like
    query = from(pipeline in CandidatePipeline,
        join: ap in assoc(pipeline, :applicant),
        where:
          pipeline.job_opening_id == ^job_id and pipeline.job_stage_id == ^stage_id and
            pipeline.status == ^status,
        preload: [applicant: ap],
        join: a in AppliedOpening,
        on: a.job_opening_id == pipeline.job_opening_id and a.applicant_id == pipeline.applicant_id,
        join: applicant in Applicant,
        on: applicant.id == pipeline.applicant_id,
        join: r in Result,
        on: r.applied_opening_id == a.id,
        where: r.state == ^result,
        order_by: [desc_nulls_last: r.percent, asc: applicant.name]
      )

From what I can tell your full binding list is:

[pipeline, ap, a, applicant, r]

if you only need a

[_, _, a, _, _]
query = case result do
  "drop" -> where(query, [_, _, a, _, _], a.state != "attempted" and a.state != "unattempted")
  "unattempted" -> where(query, [_, _, a, _, _], a.state == "unattempted")
  _ -> where(query, [_, _, a, _, _], a.state == "attempted")
end
1 Like

Getting following error.
invalid use of _. "_" represents a value to be ignored.

Where is the period “.” after the underscore “_” coming from?

I’ve only used “_,”, i.e. a comma after the underscore.

1 Like

Just Copy paste your code. Error in screenshot

As before - those screen shots are not helpful because they lack context.

In future copy all the information and paste it in between code guards

```text
Paste error information here
```

Error

CompileError
lib/apollo/candidate_pipeline/model/candidate_pipeline.ex:224: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions

… and the code in the neighbourhood of the lines indicated …

Keyword queries are used to dynamically compose pieces of the query together. The idea for your case is to use a case expression compose the correct where part. However keyword queries need a binding list.

An example of converting a query expression to a keyword query:

  def query_expression() do
    # as a query expression
    from(r in Artist,
      left_join: a in Album,
      on: r.id == a.artist_id,
      left_join: t in Track,
      on: a.id == t.album_id,
      order_by: [asc: r.id, asc: a.id, asc: t.index],
      select: %{
        artist_id: r.id,
        name: r.name,
        album_id: a.id,
        album_title: a.title,
        track_id: t.id,
        title: t.title
      }
    )
  end

  def query_keyword() do
    # as a keyword query (without pipelining)
    # the query is being **composed** step by step
    # Note how the binding list changes with each step
    # [r]
    # [r,a]
    # [r,a,t]

    # Being first "Artist" becomes the first in the binding list
    # [r] so "r" refers to "Artist"
    # [x] so "x" refers to "Artist"
    #
    query = Artist

    # Album becomes the second binding
    # [x,y] "x" is "Artist"", "y" is "Album"
    #
    query = join(query, :left, [r], a in Album, on: r.id == a.artist_id)

    # Track becomes the third binding
    # [a,b,c] "a" is "Artist", "b" is "Album", "c" is "Track"
    #
    query = join(query, :left, [r, a], t in Track, on: a.id == t.album_id)
    query = order_by(query, [r, a, t], asc: r.id, asc: a.id, asc: t.index)

    select(query, [r, a, t], %{
      artist_id: r.id,
      name: r.name,
      album_id: a.id,
      album_title: a.title,
      track_id: t.id,
      title: t.title
    })
  end

  def query_expression_composed() do
    # composing a query expressions also requires a binding list
    query = from(Artist)
    query = from([r] in query, left_join: a in Album, on: r.id == a.artist_id)
    query = from([r, a] in query, left_join: t in Track, on: a.id == t.album_id)
    query = from([r, a, t] in query, order_by: [asc: r.id, asc: a.id, asc: t.index])

    from([r, a, t] in query,
      select: %{
        artist_id: r.id,
        name: r.name,
        album_id: a.id,
        album_title: a.title,
        track_id: t.id,
        title: t.title
      }
    )
  end

  def query_kw_named() do
    # keyword query with named bindings
    # when using named bindings
    # the order or number of elements in the
    # binding list no longer matters

    # [x] "x" is "Artist" (positional binding)
    # [artist: y] "y" is "Artist" (named binding)
    #
    query = from(r in Artist, as: :artist)

    # [x,y] "x" is "Artist", "y" is "Album" (positional binding)
    # [album: x, artist: y] "x" is "Album", "y" is "Artist" (named binding)
    #
    query = join(query, :left, [artist: r], a in Album, as: :album, on: r.id == a.artist_id)

    # [x,y,z] "x" is "Artist", "y" is "Album", "z" is "Track" (positional binding)
    # [track: x, album: y, artist: z] "x" is "Track", "y" is "Album", "z" is "Artist" (named binding)
    #
    query =
      join(query, :left, [album: a, artist: r], t in Track, as: :track, on: a.id == t.album_id)

    query = order_by(query, [track: t, album: a, artist: r], asc: r.id, asc: a.id, asc: t.index)

    # Note that here we only need to list the bindings that we need
    # [_, _, t] "t" is "Track" (positional binding). We don't need "Artist" and "Track" so there is an "_" for each.
    # [track: x] "x" is "Track" (named binding). We don't need to list any binding we don't need.
    #
    select(query, [track: t], %{
      id: t.id,
      index: t.index,
      title: t.title
    })
  end

  def query_exp_named() do
    # compositional query expression
    # with named bindings
    query = from(r in Artist, as: :artist)
    query = from([artist: r] in query, left_join: a in Album, as: :album, on: r.id == a.artist_id)

    query =
      from([album: a, artist: r] in query,
        left_join: t in Track,
        as: :track,
        on: a.id == t.album_id
      )

    query =
      from([track: t, album: a, artist: r] in query,
        order_by: [asc: r.id, asc: a.id, asc: t.index]
      )

    from([track: t] in query,
      select: %{
        id: t.id,
        index: t.index,
        title: t.title
      }
    )
  end

Why not simply use named bindings?

3 Likes

Could you give example related to my case?

query = from binding_a in "table_a", as: :base, join: binding_b in "table_b", on: a.id = b.table_id, as: :joined
 
query = case result do
  "drop" -> where(query, [joined: a], a.state != "attempted" and a.state != "unattempted")
  "unattempted" -> where(query, [joined: a], a.state == "unattempted")
  _ -> where(query, [joined: a], a.state == "attempted")
end
  • I didn’t know about named bindings
  • I think the fundamental issue here is that you can get quite far with monolithic query expressions without ever needing to understand the binding system. From that perspective the entire Composition section of the documentation is important and relevant here.

The query expression in the OP is monolithic, i.e. the entire query is defined in one pass - no composition is used.

The solution to the problem requires composition - but to use composition one has to understand the binding system.

Now I can’t guarantee that this is correct as this is written freehand but I would build this stage after stage to see if I’m getting the expected results:

query =
  from(pipeline in CandidatePipeline, as: :pipeline)
  |> where([pipeline: p], p.job_opening_id == ^job_id)
  |> where([pipeline: p], p.job_stage_id == ^stage_id)
  |> where([pipeline: p], p.status == ^status)
  |> join(:inner, [pipeline: p], a in AppliedOpening, as: :opening,
    on: a.job_opening_id == p.job_opening_id and a.applicant_id == p.applicant_id,)
  |> join(:inner, [opening: o], r in Result, as: :result, on: r.applied_opening_id == o.id)
  |> where([result: r], r.state == ^result)
  |> join(:inner, [pipeline: p], a in Applicant, as: :applicant, a.id == p.applicant_id)
  |> order_by([result: r, applicant: a], desc_nulls_last: r.percent, asc: a.name)

query =
  case result do
    "drop" ->
      where([opening: o], o.state not in ["attempted", "unattempted"]
    "unattempted" ->
      where([opening: o], o.state == "unattempted"
    _ ->
      where([opening: o], o.state == "attempted")
  end

As @LostKobrakai pointed out the only named binding you actually need is :opening (in his example :joined). I just did the whole query in a compositional style to get the point across.

So given the original query:

# note the change to:
# join: a in AppliedOpening, as: :opening,
query = from(pipeline in CandidatePipeline,
        join: ap in assoc(pipeline, :applicant), # ??? is this actually needed ???
        where:
          pipeline.job_opening_id == ^job_id and pipeline.job_stage_id == ^stage_id and
            pipeline.status == ^status,
        preload: [applicant: ap],
        join: a in AppliedOpening, as: :opening,
        on: a.job_opening_id == pipeline.job_opening_id and a.applicant_id == pipeline.applicant_id,
        join: applicant in Applicant,              # ??? ... because this
        on: applicant.id == pipeline.applicant_id, # seems to do it all over again duplicating rows ???
        join: r in Result,
        on: r.applied_opening_id == a.id,
        where: r.state == ^result,
        order_by: [desc_nulls_last: r.percent, asc: applicant.name]
      )

query =
  case result do
    "drop" ->
      where([opening: o], o.state not in ["attempted", "unattempted"]
    "unattempted" ->
      where([opening: o], o.state == "unattempted"
    _ ->
      where([opening: o], o.state == "attempted")
  end
1 Like