Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export ECTO=elixir-ecto
42 changes: 31 additions & 11 deletions lib/dx.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ defmodule Dx do
doesn't need to be loaded again. Can be initialized using `Loaders.Dataloader.init/0`.
"""

alias Dx.{Engine, Result, Util}
alias Dx.{Engine, Result, Schema, Util}
alias Dx.Evaluation, as: Eval

@doc """
Expand All @@ -46,9 +46,15 @@ defmodule Dx do
Does not load any additional data.
"""
def get(records, predicates, opts \\ []) do
eval = Eval.from_options(opts)
type = type(records)
eval = Eval.from_options(opts) |> Map.put(:root_type, type)

{expanded, _type} =
predicates
|> expand()
|> Schema.expand_mapping(type, eval)

do_get(records, predicates, eval)
do_get(records, expanded, eval)
|> Result.to_simple_if(not eval.return_cache?)
end

Expand All @@ -57,16 +63,12 @@ defmodule Dx do
end

defp do_get(record, predicates, eval) when is_list(predicates) do
Result.map(predicates, &Engine.resolve_predicate(&1, record, eval))
Result.map(predicates, &Engine.execute(&1, record, eval))
|> Result.transform(&Util.Map.zip(predicates, &1))
end

defp do_get(record, predicate, eval) when is_atom(predicate) do
Engine.resolve_predicate(predicate, record, eval)
end

defp do_get(record, result, eval) do
Engine.map_result(result, %{eval | root_subject: record})
Engine.execute(result, record, eval)
end

@doc """
Expand All @@ -81,12 +83,30 @@ defmodule Dx do
Like `get/3`, but loads additional data if needed.
"""
def load(records, predicates, opts \\ []) do
eval = Eval.from_options(opts)
type = type(records)
eval = Eval.from_options(opts) |> Map.put(:root_type, type)

{expanded, _type} =
predicates
|> expand()
|> Schema.expand_mapping(type, eval)

do_load(records, predicates, eval)
do_load(records, expanded, eval)
|> Result.to_simple_if(not eval.return_cache?)
end

defp expand(predicates) when is_list(predicates) do
predicates
|> Map.new(&{&1, {:ref, &1}})
end

defp expand(other) do
other
end

defp type([%type{} | _]), do: type
defp type(%type{}), do: type

defp do_load(records, predicates, eval) do
load_all_data_reqs(eval, fn eval ->
do_get(records, predicates, eval)
Expand Down
94 changes: 45 additions & 49 deletions lib/dx/ecto/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Dx.Ecto.Query do
Functions to dynamically generate Ecto query parts.
"""

alias Dx.{Result, Util}
alias Dx.Result
alias Dx.Evaluation, as: Eval
alias __MODULE__.Builder

Expand All @@ -29,7 +29,22 @@ defmodule Dx.Ecto.Query do
@doc """
Add predicate-based filters to a queryable and return it.
"""

def where(queryable, condition, opts \\ []) when is_list(opts) do
type = queryable
eval = Eval.from_options(opts) |> Map.put(:root_type, type)

{expanded, _binds} =
condition
|> Dx.Schema.expand_condition(type, eval)

case apply_condition(queryable, expanded, eval) do
{queryable, true} -> queryable
{queryable, condition} -> raise TranslationError, queryable: queryable, condition: condition
end
end

def execute_where(queryable, condition, opts \\ []) when is_list(opts) do
eval = Eval.from_options(opts)

case apply_condition(queryable, condition, eval) do
Expand Down Expand Up @@ -121,10 +136,6 @@ defmodule Dx.Ecto.Query do
end
end

defp map_condition(builder, conditions) when is_map(conditions) do
map_condition(builder, {:all, conditions})
end

defp map_condition(builder, {:all, conditions}) do
Enum.reduce_while(conditions, {builder, true}, fn condition, {builder, acc_query} ->
case map_condition(builder, condition) do
Expand Down Expand Up @@ -160,10 +171,10 @@ defmodule Dx.Ecto.Query do
:error
end

defp map_condition(builder, {key, val}) when is_atom(key) do
case field_info(key, builder) do
:field ->
left = Builder.field(builder, key)
defp map_condition(builder, {key, val}) do
case key do
{:field, key} ->
left = Builder.field(builder, {:field, key})

case val do
vals when is_list(vals) ->
Expand All @@ -183,7 +194,7 @@ defmodule Dx.Ecto.Query do
Enum.reduce_while(grouped_vals, {builder, false}, fn val, {builder, acc_query} ->
case val do
vals when is_list(vals) -> {builder, compare(left, :eq, vals, builder)}
val -> map_condition(builder, {key, val})
val -> map_condition(builder, {{:field, key}, val})
end
|> case do
:error -> {:halt, :error}
Expand All @@ -210,19 +221,19 @@ defmodule Dx.Ecto.Query do
end
end

{:predicate, rules} ->
{:predicate, _meta, rules} ->
case rules_for_value(rules, val, builder) do
:error -> :error
condition -> map_condition(builder, condition)
end

{:assoc, :one, _assoc} ->
{:assoc, :one, _type, _assoc} ->
Builder.with_join(builder, key, fn builder ->
map_condition(builder, val)
end)

{:assoc, :many, assoc} ->
%{queryable: queryable, related_key: related_key, owner_key: owner_key} = assoc
{:assoc, :many, queryable, assoc} ->
%{related_key: related_key, owner_key: owner_key} = assoc

as = Builder.current_alias(builder)

Expand Down Expand Up @@ -261,8 +272,8 @@ defmodule Dx.Ecto.Query do
end

defp do_ref(builder, [field | path]) do
case field_info(field, builder) do
{:assoc, :one, _assoc} -> Builder.with_join(builder, field, &do_ref(&1, path))
case field do
{:assoc, :one, _type, _assoc} -> Builder.with_join(builder, field, &do_ref(&1, path))
_other -> :error
end
end
Expand Down Expand Up @@ -321,46 +332,14 @@ defmodule Dx.Ecto.Query do
defp compare(left, op, val, %{negate?: true}) when op in @gt_ops,
do: dynamic(^left <= ^val)

defp field_info(predicate, %Builder{} = builder) do
type = Builder.current_type(builder)

case Util.rules_for_predicate(predicate, type, builder.eval) do
[] ->
case Util.Ecto.association_details(type, predicate) do
%_{cardinality: :one} = assoc ->
{:assoc, :one, assoc}

%_{cardinality: :many} = assoc ->
{:assoc, :many, assoc}

_other ->
case Util.Ecto.field_details(type, predicate) do
nil ->
raise ArgumentError,
"""
Unknown field #{inspect(predicate)} on #{inspect(type)}.
Path: #{inspect(builder.path)}
Types: #{inspect(builder.types)}
"""

_other ->
:field
end
end

rules ->
{:predicate, rules}
end
end

# maps a comparison of "predicate equals value" to a Dx condition
defp rules_for_value(rules, val, %{negate?: false}) do
vals = List.wrap(val)

rules
|> Enum.reverse()
|> Enum.reduce_while(false, fn
{condition, val}, acc when is_simple(val) ->
{val, condition}, acc when is_simple(val) ->
if val in vals do
{:cont, [condition, acc]}
else
Expand Down Expand Up @@ -459,12 +438,29 @@ defmodule Dx.Ecto.Query do
end
end

def apply_expanded_options(queryable, opts) do
Enum.reduce(opts, {queryable, []}, fn
{:where, conditions}, {query, opts} -> {execute_where(query, conditions), opts}
{:limit, limit}, {query, opts} -> {limit(query, limit), opts}
{:order_by, order}, {query, opts} -> {order_by(query, order), opts}
other, {query, opts} -> {query, [other | opts]}
end)
|> case do
{queryable, opts} -> {queryable, Enum.reverse(opts)}
end
end

@doc "Apply all options to the given `queryable`, raise on any unknown option."
def from_options(queryable, opts) do
{queryable, []} = apply_options(queryable, opts)
queryable
end

def execute_options(queryable, opts) do
{queryable, []} = apply_expanded_options(queryable, opts)
queryable
end

def limit(queryable, limit) do
from(q in queryable, limit: ^limit)
end
Expand Down
19 changes: 9 additions & 10 deletions lib/dx/ecto/query/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,16 @@ defmodule Dx.Ecto.Query.Builder do
def root_type(%Builder{aliases: {_, type, _}}), do: type

def field(builder, key, maybe_parent? \\ false)
def field(%{path: [{:parent, as} | _]}, key, _), do: dynamic(field(parent_as(^as), ^key))

def field(%{path: [as | _], in_subquery?: true}, key, true),
def field(%{path: [{:parent, as} | _]}, {:field, key}, _),
do: dynamic(field(parent_as(^as), ^key))

def field(%{path: [as | _]}, key, _), do: dynamic(field(as(^as), ^key))
def field(%{path: [as | _], in_subquery?: true}, {:field, key}, true),
do: dynamic(field(parent_as(^as), ^key))

def field(%{path: [as | _]}, {:field, key}, _), do: dynamic(field(as(^as), ^key))

def field(%{path: [], aliases: {as, _, _}}, key, _), do: dynamic(field(as(^as), ^key))
def field(%{path: [], aliases: {as, _, _}}, {:field, key}, _), do: dynamic(field(as(^as), ^key))

def current_alias(%{path: [as | _]}), do: as
def current_alias(%{path: [], aliases: {as, _, _}}), do: as
Expand Down Expand Up @@ -181,16 +183,13 @@ defmodule Dx.Ecto.Query.Builder do
{builder, as}
end

def add_aliased_join(builder, key) do
def add_aliased_join(builder, assoc) do
{builder, as} = next_alias(builder)
left = current_alias(builder)

type =
case Dx.Util.Ecto.association_details(current_type(builder), key) do
%_{related: type} -> type
end
{:assoc, _, type, %{name: name}} = assoc

builder = update_query(builder, &aliased_join(&1, left, key, as))
builder = update_query(builder, &aliased_join(&1, left, name, as))
%{builder | path: [as | builder.path], types: [type | builder.types]}
end

Expand Down
Loading