Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested relationships #17

Merged
merged 3 commits into from
May 25, 2020
Merged
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
66 changes: 33 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
name: CI
on: [pull_request, push]
jobs:
mix_test:
name: mix test (Elixir ${{ matrix.elixir }} OTP ${{ matrix.otp }})
strategy:
matrix:
elixir: ['1.7.4', '1.10.1']
include:
- elixir: '1.7.4'
otp: '19.x'
- elixir: '1.10.1'
otp: '22.x'
runs-on: ubuntu-16.04
steps:
- name: Setup PostgreSQL
uses: Harmon758/postgresql-action@v1.0.0
with:
postgresql db: ex_sieve_test
postgresql user: ex_sieve_user
postgresql password: ex_sieve_password
- uses: actions/checkout@v1
- uses: actions/setup-elixir@v1
with:
otp-version: ${{ matrix.otp }}
elixir-version: ${{ matrix.elixir }}
- name: Install Dependencies
run: mix deps.get
env:
MIX_ENV: test
- name: Run Tests
run: mix ecto.migrate && mix test
env:
MIX_ENV: test
DB_USER: ex_sieve_user
DB_PASSWORD: ex_sieve_password
mix_test:
name: mix test (Elixir ${{ matrix.elixir }} OTP ${{ matrix.otp }})
strategy:
matrix:
elixir: ["1.7.4", "1.10.1"]
include:
- elixir: "1.7.4"
otp: "19.x"
- elixir: "1.10.1"
otp: "22.x"
runs-on: ubuntu-latest
env:
MIX_ENV: test
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Setup PostgreSQL
uses: Harmon758/postgresql-action@v1.0.0
with:
postgresql db: ex_sieve_test
postgresql user: ex_sieve_user
postgresql password: ex_sieve_password
- uses: actions/checkout@v1
- uses: actions/setup-elixir@v1
with:
otp-version: ${{ matrix.otp }}
elixir-version: ${{ matrix.elixir }}
- name: Install Dependencies
run: mix deps.get
- name: Run Tests
run: mix ecto.migrate && mix coveralls.github
env:
DB_USER: ex_sieve_user
DB_PASSWORD: ex_sieve_password
8 changes: 8 additions & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"skip_files": [
"test"
],
"coverage_options": {
"treat_no_relevant_lines_as_covered": true
}
}
29 changes: 1 addition & 28 deletions lib/ex_sieve/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,9 @@ defmodule ExSieve.Builder do

@spec call(Ecto.Queryable.t(), Grouping.t(), list(Sort.t())) :: Ecto.Query.t()
def call(query, grouping, sorts) do
relations = build_relations(grouping, sorts)

query
|> Join.build(relations)
|> Join.build(grouping, sorts)
|> Where.build(grouping)
|> OrderBy.build(sorts)
end

defp build_relations(grouping, sorts) do
sorts_parents = Enum.map(sorts, & &1.attribute.parent)

grouping
|> List.wrap()
|> get_grouping_conditions()
|> Enum.flat_map(& &1.attributes)
|> Enum.map(& &1.parent)
|> Enum.concat(sorts_parents)
|> Enum.uniq()
|> List.delete(:query)
end

defp get_grouping_conditions(groupings, acc \\ [])

defp get_grouping_conditions([%Grouping{conditions: conditions, groupings: []} | t], acc) do
get_grouping_conditions(t, acc ++ conditions)
end

defp get_grouping_conditions([%Grouping{conditions: conditions, groupings: groupings} | t], acc) do
get_grouping_conditions(t ++ groupings, acc ++ conditions)
end

defp get_grouping_conditions([], acc), do: acc
end
70 changes: 65 additions & 5 deletions lib/ex_sieve/builder/join.ex
Original file line number Diff line number Diff line change
@@ -1,22 +1,82 @@
defmodule ExSieve.Builder.Join do
@moduledoc false
alias Ecto.Query.Builder.Join
alias ExSieve.Node.{Grouping, Sort}

@spec build(Ecto.Queryable.t(), Macro.t()) :: Ecto.Query.t()
def build(query, relations) do
@spec build(Ecto.Queryable.t(), Grouping.t(), list(Sort.t())) :: Ecto.Query.t()
def build(query, grouping, sorts) do
relations = build_relations(grouping, sorts)
Enum.reduce(relations, query, &apply_join/2)
end

@spec apply_join(Macro.t(), Ecto.Queryable.t()) :: Ecto.Query.t() | no_return
defp apply_join(relation, query) do
defp build_relations(grouping, sorts) do
sorts_parents = Enum.map(sorts, & &1.attribute.parent)

grouping
|> get_grouping_conditions()
|> Enum.flat_map(& &1.attributes)
|> Enum.map(& &1.parent)
|> Enum.concat(sorts_parents)
|> all_possible_relations()
|> Enum.concat()
|> Enum.uniq()
|> Enum.sort(&(length(&1) <= length(&2)))
|> to_parent_relation_tuple()
end

defp get_grouping_conditions(groupings, acc \\ [])

defp get_grouping_conditions(%Grouping{} = grouping, acc), do: get_grouping_conditions([grouping], acc)

defp get_grouping_conditions([%Grouping{conditions: conditions, groupings: []} | t], acc) do
get_grouping_conditions(t, acc ++ conditions)
end

defp get_grouping_conditions([%Grouping{conditions: conditions, groupings: groupings} | t], acc) do
get_grouping_conditions(t ++ groupings, acc ++ conditions)
end

defp get_grouping_conditions([], acc), do: acc

defp all_possible_relations(parents) do
Enum.map(parents, fn parent_list ->
{relations, _} =
Enum.map_reduce(parent_list, [], fn el, acc ->
acc = [el | acc]
{acc, acc}
end)

relations
end)
end

defp to_parent_relation_tuple(parents) do
Enum.map(parents, fn parent_list ->
case parent_list do
[] -> {nil, :query}
[head] -> {nil, head}
[head | tail] -> {tail |> Enum.reverse() |> Enum.join("_"), head}
end
end)
end

defp apply_join({parent, relation} = pr, query) do
query
|> Macro.escape()
|> Join.build(:inner, [Macro.var(:query, __MODULE__)], expr(relation), nil, nil, relation, nil, nil, __ENV__)
|> Join.build(:inner, join_binding(parent), expr(relation), nil, nil, join_as(pr), nil, nil, __ENV__)
|> elem(0)
|> Code.eval_quoted()
|> elem(0)
end

defp join_binding(nil), do: [Macro.var(:query, __MODULE__)]

defp join_binding(parent), do: [{String.to_atom(parent), Macro.var(:query, __MODULE__)}]

defp join_as({nil, relation}), do: relation

defp join_as({parent, relation}), do: :"#{parent}_#{relation}"

defp expr(relation) do
quote do
unquote(Macro.var(relation, __MODULE__)) in assoc(query, unquote(relation))
Expand Down
9 changes: 7 additions & 2 deletions lib/ex_sieve/builder/order_by.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ defmodule ExSieve.Builder.OrderBy do
end)
end

defp dynamic_sort(:query, name), do: dynamic([p], field(p, ^name))
defp dynamic_sort(parent, name), do: dynamic([{^parent, p}], field(p, ^name))
defp dynamic_sort([], name), do: dynamic([p], field(p, ^name))
defp dynamic_sort([parent], name), do: dynamic([{^parent, p}], field(p, ^name))

defp dynamic_sort(parents, name) do
parent = parents |> Enum.join("_") |> String.to_atom()
dynamic([{^parent, p}], field(p, ^name))
end
end
Loading