Skip to content

Commit e19a44d

Browse files
committed
WIP on dynamic refactor
1 parent 1dcef72 commit e19a44d

File tree

9 files changed

+2050
-2004
lines changed

9 files changed

+2050
-2004
lines changed

lib/aggregate.ex

Lines changed: 588 additions & 0 deletions
Large diffs are not rendered by default.

lib/data_layer.ex

Lines changed: 68 additions & 2002 deletions
Large diffs are not rendered by default.

lib/expr.ex

Lines changed: 512 additions & 0 deletions
Large diffs are not rendered by default.

lib/join.ex

Lines changed: 504 additions & 0 deletions
Large diffs are not rendered by default.

lib/sort.ex

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule AshPostgres.Sort do
2+
@moduledoc false
3+
require Ecto.Query
4+
5+
def sort(query, sort, resource) do
6+
{:ok, query}
7+
query = AshPostgres.DataLayer.default_bindings(query, resource)
8+
9+
sort
10+
|> sanitize_sort()
11+
|> Enum.reduce_while({:ok, []}, fn
12+
{order, %Ash.Query.Calculation{} = calc}, {:ok, query_expr} ->
13+
type =
14+
if calc.type do
15+
AshPostgres.Types.parameterized_type(calc.type, [])
16+
else
17+
nil
18+
end
19+
20+
calc.opts
21+
|> calc.module.expression(calc.context)
22+
|> Ash.Filter.hydrate_refs(%{
23+
resource: resource,
24+
aggregates: query.__ash_bindings__.aggregate_defs,
25+
calculations: %{},
26+
public?: false
27+
})
28+
|> case do
29+
{:ok, expr} ->
30+
expr = AshPostgres.Expr.dynamic_expr(expr, query.__ash_bindings__, false, type)
31+
32+
{:cont, {:ok, query_expr ++ [{order, expr}]}}
33+
34+
{:error, error} ->
35+
{:halt, {:error, error}}
36+
end
37+
38+
{order, sort}, {:ok, query_expr} ->
39+
expr =
40+
case Map.fetch(query.__ash_bindings__.aggregates, sort) do
41+
{:ok, binding} ->
42+
aggregate =
43+
Ash.Resource.Info.aggregate(resource, sort) ||
44+
raise "No such aggregate for query aggregate #{inspect(sort)}"
45+
46+
{:ok, field_type} =
47+
if aggregate.field do
48+
related = Ash.Resource.Info.related(resource, aggregate.relationship_path)
49+
50+
attr = Ash.Resource.Info.attribute(related, aggregate.field)
51+
52+
if attr && related do
53+
{:ok, AshPostgres.Types.parameterized_type(attr.type, attr.constraints)}
54+
else
55+
{:ok, nil}
56+
end
57+
else
58+
{:ok, nil}
59+
end
60+
61+
default_value =
62+
aggregate.default || Ash.Query.Aggregate.default_value(aggregate.kind)
63+
64+
if is_nil(default_value) do
65+
Ecto.Query.dynamic(field(as(^binding), ^sort))
66+
else
67+
if field_type do
68+
Ecto.Query.dynamic(
69+
coalesce(field(as(^binding), ^sort), type(^default_value, ^field_type))
70+
)
71+
else
72+
Ecto.Query.dynamic(coalesce(field(as(^binding), ^sort), ^default_value))
73+
end
74+
end
75+
76+
:error ->
77+
Ecto.Query.dynamic(field(as(^0), ^sort))
78+
end
79+
80+
{:cont, {:ok, query_expr ++ [{order, expr}]}}
81+
end)
82+
|> case do
83+
{:ok, []} ->
84+
{:ok, query}
85+
86+
{:ok, sort_exprs} ->
87+
new_query = Ecto.Query.order_by(query, ^sort_exprs)
88+
89+
sort_expr = List.last(new_query.order_bys)
90+
91+
new_query =
92+
new_query
93+
|> Map.update!(:windows, fn windows ->
94+
order_by_expr = %{sort_expr | expr: [order_by: sort_expr.expr]}
95+
Keyword.put(windows, :order, order_by_expr)
96+
end)
97+
98+
{:ok, new_query}
99+
100+
{:error, error} ->
101+
{:error, error}
102+
end
103+
end
104+
105+
def order_to_postgres_order(dir) do
106+
case dir do
107+
:asc -> nil
108+
:asc_nils_last -> " ASC NULLS LAST"
109+
:asc_nils_first -> " ASC NULLS FIRST"
110+
:desc -> " DESC"
111+
:desc_nils_last -> " DESC NULLS LAST"
112+
:desc_nils_first -> " DESC NULLS FIRST"
113+
end
114+
end
115+
116+
defp sanitize_sort(sort) do
117+
sort
118+
|> List.wrap()
119+
|> Enum.map(fn
120+
{sort, {order, context}} ->
121+
{ash_to_ecto_order(order), {sort, context}}
122+
123+
{sort, order} ->
124+
{ash_to_ecto_order(order), sort}
125+
126+
sort ->
127+
sort
128+
end)
129+
end
130+
131+
defp ash_to_ecto_order(:asc_nils_last), do: :asc_nulls_last
132+
defp ash_to_ecto_order(:asc_nils_first), do: :asc_nulls_first
133+
defp ash_to_ecto_order(:desc_nils_last), do: :desc_nulls_last
134+
defp ash_to_ecto_order(:desc_nils_first), do: :desc_nulls_first
135+
defp ash_to_ecto_order(other), do: other
136+
end

lib/types/calculation.ex

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
defmodule AshPostgres.Calculation do
2+
@moduledoc false
3+
4+
import Ecto.Query, only: [from: 2]
5+
6+
def add_calculation(query, calculation, expression, resource) do
7+
query = AshPostgres.DataLayer.default_bindings(query, resource)
8+
9+
query =
10+
if query.select do
11+
query
12+
else
13+
from(row in query,
14+
select: row,
15+
select_merge: %{aggregates: %{}, calculations: %{}}
16+
)
17+
end
18+
19+
expr =
20+
AshPostgres.Expr.dynamic_expr(
21+
expression,
22+
query.__ash_bindings__
23+
)
24+
25+
{:ok,
26+
query
27+
|> Map.update!(:select, &add_to_calculation_select(&1, expr, calculation))}
28+
end
29+
30+
defp add_to_calculation_select(
31+
%{
32+
expr:
33+
{:merge, _,
34+
[
35+
first,
36+
{:%{}, _,
37+
[{:aggregates, {:%{}, [], agg_fields}}, {:calculations, {:%{}, [], fields}}]}
38+
]}
39+
} = select,
40+
expr,
41+
%{load: nil} = calculation
42+
) do
43+
field = expr |> IO.inspect()
44+
45+
name =
46+
if calculation.sequence == 0 do
47+
calculation.name
48+
else
49+
String.to_existing_atom("#{calculation.name}_#{calculation.sequence}")
50+
end
51+
52+
new_fields = [
53+
{name, field}
54+
| fields
55+
]
56+
57+
%{
58+
select
59+
| expr:
60+
{:merge, [],
61+
[
62+
first,
63+
{:%{}, [],
64+
[{:aggregates, {:%{}, [], agg_fields}}, {:calculations, {:%{}, [], new_fields}}]}
65+
]}
66+
}
67+
end
68+
69+
defp add_to_calculation_select(
70+
%{expr: select_expr} = select,
71+
expr,
72+
%{load: load_as} = calculation
73+
) do
74+
field =
75+
Ecto.Query.dynamic(type(^expr, ^AshPostgres.Types.parameterized_type(calculation.type, [])))
76+
77+
load_as =
78+
if calculation.sequence == 0 do
79+
load_as
80+
else
81+
"#{load_as}_#{calculation.sequence}"
82+
end
83+
84+
%{
85+
select
86+
| expr: {:merge, [], [select_expr, {:%{}, [], [{load_as, field}]}]}
87+
}
88+
end
89+
end

lib/types/types.ex

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
defmodule AshPostgres.Types do
2+
@moduledoc false
3+
4+
alias Ash.Query.Ref
5+
6+
def parameterized_type({:array, type}, constraints) do
7+
{:array, parameterized_type(type, constraints[:items] || [])}
8+
end
9+
10+
def parameterized_type(Ash.Type.CiString, constraints) do
11+
parameterized_type(Ash.Type.CiStringWrapper, constraints)
12+
end
13+
14+
def parameterized_type(type, constraints) do
15+
if Ash.Type.ash_type?(type) do
16+
parameterized_type(Ash.Type.ecto_type(type), constraints)
17+
else
18+
if is_atom(type) && :erlang.function_exported(type, :type, 1) do
19+
{:parameterized, type, constraints}
20+
else
21+
type
22+
end
23+
end
24+
end
25+
26+
def determine_types(mod, values) do
27+
Code.ensure_compiled(mod)
28+
29+
cond do
30+
:erlang.function_exported(mod, :types, 0) ->
31+
mod.types()
32+
33+
:erlang.function_exported(mod, :args, 0) ->
34+
mod.args()
35+
36+
true ->
37+
[:any]
38+
end
39+
|> Enum.map(fn types ->
40+
case types do
41+
:same ->
42+
types =
43+
for _ <- values do
44+
:same
45+
end
46+
47+
closest_fitting_type(types, values)
48+
49+
:any ->
50+
for _ <- values do
51+
:any
52+
end
53+
54+
types ->
55+
closest_fitting_type(types, values)
56+
end
57+
end)
58+
|> Enum.min_by(fn types ->
59+
types
60+
|> Enum.map(&vagueness/1)
61+
|> Enum.sum()
62+
end)
63+
end
64+
65+
defp closest_fitting_type(types, values) do
66+
types_with_values = Enum.zip(types, values)
67+
68+
types_with_values
69+
|> fill_in_known_types()
70+
|> clarify_types()
71+
end
72+
73+
defp clarify_types(types) do
74+
basis =
75+
types
76+
|> Enum.map(&elem(&1, 0))
77+
|> Enum.min_by(&vagueness(&1))
78+
79+
Enum.map(types, fn {type, _value} ->
80+
replace_same(type, basis)
81+
end)
82+
end
83+
84+
defp replace_same({:in, type}, basis) do
85+
{:in, replace_same(type, basis)}
86+
end
87+
88+
defp replace_same(:same, :same) do
89+
:any
90+
end
91+
92+
defp replace_same(:same, {:in, :same}) do
93+
{:in, :any}
94+
end
95+
96+
defp replace_same(:same, basis) do
97+
basis
98+
end
99+
100+
defp replace_same(other, _basis) do
101+
other
102+
end
103+
104+
defp fill_in_known_types(types) do
105+
Enum.map(types, &fill_in_known_type/1)
106+
end
107+
108+
defp fill_in_known_type(
109+
{vague_type, %Ref{attribute: %{type: type, constraints: constraints}}} = ref
110+
)
111+
when vague_type in [:any, :same] do
112+
if Ash.Type.ash_type?(type) do
113+
type = type |> Ash.Type.ecto_type() |> parameterized_type(constraints) |> array_to_in()
114+
{type, ref}
115+
else
116+
type =
117+
if is_atom(type) && :erlang.function_exported(type, :type, 1) do
118+
{:parameterized, type, []} |> array_to_in()
119+
else
120+
type |> array_to_in()
121+
end
122+
123+
{type, ref}
124+
end
125+
end
126+
127+
defp fill_in_known_type(
128+
{{:array, type}, %Ref{attribute: %{type: {:array, type}} = attribute} = ref}
129+
) do
130+
{:in, fill_in_known_type({type, %{ref | attribute: %{attribute | type: type}}})}
131+
end
132+
133+
defp fill_in_known_type({type, value}), do: {array_to_in(type), value}
134+
135+
defp array_to_in({:array, v}), do: {:in, array_to_in(v)}
136+
137+
defp array_to_in({:parameterized, type, constraints}),
138+
do: {:parameterized, array_to_in(type), constraints}
139+
140+
defp array_to_in(v), do: v
141+
142+
defp vagueness({:in, type}), do: vagueness(type)
143+
defp vagueness(:same), do: 2
144+
defp vagueness(:any), do: 1
145+
defp vagueness(_), do: 0
146+
end

mix.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ defmodule AshPostgres.MixProject do
9393
# Run "mix help deps" to learn about dependencies.
9494
defp deps do
9595
[
96-
{:ecto_sql, "~> 3.7"},
96+
{:ecto_sql, "~> 3.7", override: true},
97+
# {:ecto, github: "zachdaniel/ecto", branch: "dynamic-bindings", override: true},
98+
{:ecto, path: "../ecto", override: true},
9799
{:jason, "~> 1.0"},
98100
{:postgrex, ">= 0.0.0"},
99101
{:ash, ash_version("~> 1.50 and >= 1.50.5")},

0 commit comments

Comments
 (0)