Packages
ash_postgres
0.2.1
2.10.0
2.9.1
2.9.0
2.8.0
2.7.0
2.6.32
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.12
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.22
2.5.21
2.5.20
2.5.19
2.5.18
2.5.17
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.1
2.3.0
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.19
2.1.18
2.1.17
2.1.15
2.1.14
2.1.13
2.1.12
2.1.11
2.1.10
2.1.9
2.1.8
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.5.30
1.5.29
1.5.28
1.5.27
1.5.26
1.5.25
1.5.24
1.5.23
1.5.22
1.5.21
1.5.20
1.5.19
1.5.18
1.5.17
1.5.16
1.5.15
1.5.14
1.5.13
1.5.12
1.5.11
1.5.10
1.5.9
1.5.8
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.0
1.3.68
1.3.67
1.3.66
1.3.65
1.3.64
1.3.63
1.3.62
1.3.61
1.3.60
1.3.59
1.3.58
1.3.56
1.3.55
1.3.54
1.3.53
1.3.52
1.3.51
1.3.50
1.3.49
1.3.48
1.3.47
1.3.46
1.3.45
1.3.44
1.3.43
1.3.42
1.3.41
1.3.40
1.3.39
1.3.38
1.3.37
1.3.36
1.3.35
1.3.34
1.3.33
1.3.32
1.3.31
1.3.30
1.3.29
1.3.28
1.3.26
1.3.25
1.3.24
1.3.23
1.3.22
1.3.21
1.3.20
1.3.19
1.3.18
1.3.17
1.3.16
1.3.15
1.3.14
1.3.12
1.3.11
1.3.10
1.3.9
1.3.8
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.3.0-rc.4
1.3.0-rc.3
1.3.0-rc.2
1.3.0-rc.0
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.1
1.2.0-rc.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
1.0.0-pre.3
1.0.0-pre.2
1.0.0-pre.1
1.0.0-pre.0
0.43.0
0.42.0-rc.7
0.42.0-rc.6
0.42.0-rc.5
0.42.0-rc.4
0.42.0-rc.3
0.42.0-rc.1
0.42.0-rc.0
0.41.7
0.41.6
0.41.5
0.41.4
0.41.3
0.41.2
0.41.1
0.41.0-rc0
retired
0.41.0-rc.9
0.41.0-rc.8
0.41.0-rc.7
0.41.0-rc.6
0.41.0-rc.5
0.41.0-rc.4
0.41.0-rc.3
0.41.0-rc.2
0.41.0-rc.1
0.41.0-rc.0
0.40.11
0.40.10
0.40.9
0.40.8
0.40.7
0.40.6
0.40.5
0.40.4
0.40.3
0.40.2
0.40.1
0.40.0-rc5
0.40.0-rc4
0.40.0-rc3
0.40.0-rc2
0.40.0-rc1
0.39.0-rc0
0.38.11
0.38.10
0.38.9
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.8
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.5
0.35.4
0.35.3
0.35.1
0.34.5
0.34.4
0.34.3
0.34.2
0.34.1
0.34.0
0.33.1
0.33.0
0.32.2
0.32.1
0.32.0
0.31.1
0.30.1
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.28.1
0.28.0
0.27.0
0.26.2
0.26.1
0.26.0
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.0
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.1
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.2
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.1
0.2.0
0.1.4
0.1.3
0.1.2
0.1.1
0.1.1-rc.2
0.1.1-rc.1
0.1.1-rc.0
0.1.0
The PostgreSQL data layer for Ash Framework
Current section
Files
Jump to
Current section
Files
lib/data_layer.ex
defmodule AshPostgres.DataLayer do
@moduledoc """
A postgres data layer that levereges Ecto's postgres capabilities.
"""
@postgres %Ash.Dsl.Section{
name: :postgres,
describe: """
Postgres data layer configuration
""",
schema: [
repo: [
type: {:custom, AshPostgres.DataLayer, :validate_repo, []},
required: true,
doc:
"The repo that will be used to fetch your data. See the `Ecto.Repo` documentation for more"
],
table: [
type: :string,
required: true,
doc: "The table to store and read the resource from"
]
]
}
alias Ash.Filter.{And, Eq, In, NotEq, NotIn, Or}
alias AshPostgres.Predicates.Trigram
import AshPostgres, only: [table: 1, repo: 1]
@behaviour Ash.DataLayer
use Ash.Dsl.Extension, sections: [@postgres]
@doc false
def validate_repo(repo) do
if repo.__adapter__() == Ecto.Adapters.Postgres do
{:ok, repo}
else
{:error, "Expected a repo using the postgres adapter `Ecto.Adapters.Postgres`"}
end
end
@impl true
def custom_filters(resource) do
config = repo(resource).config()
add_pg_trgm_search(%{}, config)
end
defp add_pg_trgm_search(filters, config) do
if "pg_trgm" in config[:installed_extensions] do
Map.update(filters, :string, [{:trigram, AshPostgres.Predicates.Trigram}], fn filters ->
[{:trigram, AshPostgres.Predicates.Trigram} | filters]
end)
else
filters
end
end
import Ecto.Query, only: [from: 2]
@impl true
def can?(_, capability), do: can?(capability)
defp can?(:query_async), do: true
defp can?(:transact), do: true
defp can?(:composite_primary_key), do: true
defp can?(:upsert), do: true
defp can?({:filter, :in}), do: true
defp can?({:filter, :not_in}), do: true
defp can?({:filter, :not_eq}), do: true
defp can?({:filter, :eq}), do: true
defp can?({:filter, :and}), do: true
defp can?({:filter, :or}), do: true
defp can?({:filter, :not}), do: true
defp can?({:filter, :trigram}), do: true
defp can?({:filter_related, _}), do: true
defp can?(_), do: false
@impl true
def limit(query, nil, _), do: {:ok, query}
def limit(query, limit, _resource) do
{:ok, from(row in query, limit: ^limit)}
end
@impl true
def offset(query, nil, _), do: query
def offset(query, offset, _resource) do
{:ok, from(row in query, offset: ^offset)}
end
@impl true
def run_query(%{__impossible__: true}, _) do
{:ok, []}
end
@impl true
def run_query(query, resource) do
{:ok, repo(resource).all(query)}
end
@impl true
def resource_to_query(resource),
do: Ecto.Queryable.to_query({table(resource), resource})
@impl true
def create(resource, changeset) do
changeset =
Map.update!(changeset, :action, fn
:create -> :insert
action -> action
end)
changeset =
Map.update!(changeset, :data, fn data ->
Map.update!(data, :__meta__, &Map.put(&1, :source, table(resource)))
end)
repo(resource).insert(changeset)
rescue
e ->
{:error, e}
end
@impl true
def upsert(resource, changeset) do
changeset =
Map.update!(changeset, :action, fn
:create -> :insert
action -> action
end)
changeset =
Map.update!(changeset, :data, fn data ->
Map.update!(data, :__meta__, &Map.put(&1, :source, table(resource)))
end)
repo(resource).insert(changeset,
on_conflict: :replace_all,
conflict_target: Ash.primary_key(resource)
)
rescue
e ->
{:error, e}
end
@impl true
def update(resource, changeset) do
repo(resource).update(changeset)
rescue
e ->
{:error, e}
end
@impl true
def destroy(%resource{} = record) do
case repo(resource).delete(record) do
{:ok, _record} -> :ok
{:error, error} -> {:error, error}
end
rescue
e ->
{:error, e}
end
@impl true
def sort(query, sort, _resource) do
{:ok,
from(row in query,
order_by: ^sanitize_sort(sort)
)}
end
defp sanitize_sort(sort) do
sort
|> List.wrap()
|> Enum.map(fn
{sort, order} -> {order, sort}
sort -> sort
end)
end
@impl true
def filter(query, filter, _resource) do
new_query =
query
|> Map.put(:bindings, %{})
|> join_all_relationships(filter)
|> add_filter_expression(filter)
impossible_query =
if filter.impossible? do
Map.put(new_query, :__impossible__, true)
else
new_query
end
{:ok, impossible_query}
end
defp join_all_relationships(query, filter, path \\ []) do
query =
Map.put_new(query, :__ash_bindings__, %{current: Enum.count(query.joins) + 1, bindings: %{}})
Enum.reduce(filter.relationships, query, fn {name, relationship_filter}, query ->
join_type = :left
case {join_type, relationship_filter} do
{join_type, relationship_filter} ->
relationship = Ash.relationship(filter.resource, name)
current_path = [relationship | path]
joined_query = join_relationship(query, current_path, join_type)
joined_query_with_distinct =
join_and_add_distinct(relationship, join_type, joined_query, filter)
join_all_relationships(joined_query_with_distinct, relationship_filter, current_path)
end
end)
end
defp join_and_add_distinct(relationship, join_type, joined_query, filter) do
if relationship.cardinality == :many and join_type == :left && !joined_query.distinct do
from(row in joined_query,
distinct: ^Ash.primary_key(filter.resource)
)
else
joined_query
end
end
defp join_relationship(query, path, join_type) do
path_names = Enum.map(path, & &1.name)
case Map.get(query.__ash_bindings__.bindings, path_names) do
%{type: existing_join_type} when join_type != existing_join_type ->
raise "unreachable?"
nil ->
do_join_relationship(query, path, join_type)
_ ->
query
end
end
defp do_join_relationship(query, relationships, kind, path \\ [])
defp do_join_relationship(_, [], _, _), do: nil
defp do_join_relationship(query, [%{type: :many_to_many} = relationship | rest], :inner, path) do
relationship_through = maybe_get_resource_query(relationship.through)
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
join: through in ^relationship_through,
on:
field(row, ^relationship.source_field) ==
field(through, ^relationship.source_field_on_join_table),
join: destination in ^relationship_destination,
on:
field(destination, ^relationship.destination_field) ==
field(through, ^relationship.destination_field_on_join_table)
)
join_path =
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
full_path = Enum.reverse([relationship.name | path])
new_query
|> add_binding(join_path, :inner)
|> add_binding(full_path, :inner)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [relationship | rest], :inner, path) do
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
join: destination in ^relationship_destination,
on:
field(row, ^relationship.source_field) ==
field(destination, ^relationship.destination_field)
)
new_query
|> add_binding(Enum.reverse([relationship.name | path]), :inner)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [%{type: :many_to_many} = relationship | rest], :left, path) do
relationship_through = maybe_get_resource_query(relationship.through)
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
left_join: through in ^relationship_through,
on:
field(row, ^relationship.source_field) ==
field(through, ^relationship.source_field_on_join_table),
left_join: destination in ^relationship_destination,
on:
field(destination, ^relationship.destination_field) ==
field(through, ^relationship.destination_field_on_join_table)
)
join_path =
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
full_path = Enum.reverse([relationship.name | path])
new_query
|> add_binding(join_path, :left)
|> add_binding(full_path, :left)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [relationship | rest], :left, path) do
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
left_join: destination in ^relationship_destination,
on:
field(row, ^relationship.source_field) ==
field(destination, ^relationship.destination_field)
)
new_query
|> add_binding(Enum.reverse([relationship.name | path]), :left)
|> merge_bindings(relationship_destination)
end
defp add_filter_expression(query, filter) do
{params, expr} = filter_to_expr(filter, query.__ash_bindings__.bindings, [])
if expr do
boolean_expr = %Ecto.Query.BooleanExpr{
expr: expr,
op: :and,
params: params
}
%{query | wheres: [boolean_expr | query.wheres]}
else
query
end
end
defp join_exprs(nil, nil, _op), do: nil
defp join_exprs(expr, nil, _op), do: expr
defp join_exprs(nil, expr, _op), do: expr
defp join_exprs(left_expr, right_expr, op), do: {op, [], [left_expr, right_expr]}
defp filter_to_expr(filter, bindings, params, current_binding \\ 0, path \\ [])
# A completely empty filter means "everything"
defp filter_to_expr(%{impossible?: true}, _, _, _, _), do: {[], false}
defp filter_to_expr(
%{ands: [], ors: [], not: nil, attributes: attrs, relationships: rels},
_,
_,
_,
_
)
when attrs == %{} and rels == %{} do
{[], true}
end
defp filter_to_expr(filter, bindings, params, current_binding, path) do
{params, expr} =
Enum.reduce(filter.attributes, {params, nil}, fn {attribute, filter},
{params, existing_expr} ->
{params, new_expr} = filter_value_to_expr(attribute, filter, current_binding, params)
{params, join_exprs(existing_expr, new_expr, :and)}
end)
{params, expr} =
Enum.reduce(filter.relationships, {params, expr}, fn {relationship, relationship_filter},
{params, existing_expr} ->
full_path = path ++ [relationship]
binding =
Map.get(bindings, full_path) ||
raise "unbound relationship #{inspect(full_path)} referenced! #{inspect(bindings)}"
{params, new_expr} =
filter_to_expr(relationship_filter, bindings, params, binding.binding, full_path)
{params, join_exprs(new_expr, existing_expr, :and)}
end)
{params, expr} =
Enum.reduce(filter.ors, {params, expr}, fn or_filter, {params, existing_expr} ->
{params, new_expr} = filter_to_expr(or_filter, bindings, params, current_binding, path)
{params, join_exprs(existing_expr, new_expr, :or)}
end)
{params, expr} =
case filter.not do
nil ->
{params, expr}
not_filter ->
{params, new_expr} = filter_to_expr(not_filter, bindings, params, current_binding, path)
{params, join_exprs(expr, {:not, [], [new_expr]}, :and)}
end
{params, expr} =
Enum.reduce(filter.ands, {params, expr}, fn and_filter, {params, existing_expr} ->
{params, new_expr} = filter_to_expr(and_filter, bindings, params, current_binding, path)
{params, join_exprs(existing_expr, new_expr, :and)}
end)
if expr do
{params, expr}
else
# A filter that was not empty, but didn't generate an expr for some reason, should default to `false`
# AFAIK this shouldn't actually be possible
{params, false}
end
end
# THe fact that we keep counting params here is very silly.
defp filter_value_to_expr(attribute, %Eq{value: value}, current_binding, params) do
{params ++ [{value, {current_binding, attribute}}],
{:==, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(attribute, %NotEq{value: value}, current_binding, params) do
{params ++ [{value, {current_binding, attribute}}],
{:==, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(attribute, %In{values: values}, current_binding, params) do
{params ++ [{values, {:in, {current_binding, attribute}}}],
{:in, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(
attribute,
%NotIn{values: values},
current_binding,
params
) do
{params ++ [{values, {:in, {current_binding, attribute}}}],
{:not,
{:in, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}}
end
defp filter_value_to_expr(
attribute,
%Trigram{} = trigram,
current_binding,
params
) do
param_count = Enum.count(params)
case trigram do
%{equals: equals, greater_than: nil, less_than: nil, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {equals, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") = ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {greater_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") > ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {less_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") < ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
{params ++
[{text, {current_binding, attribute}}, {less_than, :float}, {greater_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") BETWEEN ",
expr: {:^, [], [param_count + 1]},
raw: " AND ",
expr: {:^, [], [param_count + 2]},
raw: ""
]}}
end
end
defp filter_value_to_expr(
attribute,
%And{left: left, right: right},
current_binding,
params
) do
{params, left_expr} = filter_value_to_expr(attribute, left, current_binding, params)
{params, right_expr} = filter_value_to_expr(attribute, right, current_binding, params)
{params, join_exprs(left_expr, right_expr, :and)}
end
defp filter_value_to_expr(
attribute,
%Or{left: left, right: right},
current_binding,
params
) do
{params, left_expr} = filter_value_to_expr(attribute, left, current_binding, params)
{params, right_expr} = filter_value_to_expr(attribute, right, current_binding, params)
{params, join_exprs(left_expr, right_expr, :or)}
end
defp merge_bindings(query, %{__ash_bindings__: ash_bindings}) do
ash_bindings
|> Map.get(:bindings)
|> Enum.reduce(query, fn {path, data}, query ->
add_binding(query, path, data)
end)
end
defp merge_bindings(query, _) do
query
end
defp add_binding(query, path, type) do
current = query.__ash_bindings__.current
bindings = query.__ash_bindings__.bindings
new_ash_bindings = %{
query.__ash_bindings__
| bindings: do_add_binding(bindings, path, current, type),
current: current + 1
}
%{query | __ash_bindings__: new_ash_bindings}
end
defp do_add_binding(bindings, path, current, type) do
Map.put(bindings, path, %{binding: current, type: type})
end
@impl true
def can_query_async?(resource) do
repo(resource).in_transaction?()
end
@impl true
def transaction(resource, func) do
repo(resource).transaction(func)
end
defp maybe_get_resource_query(resource) do
if Ash.resource_module?(resource) do
{table(resource), resource}
else
resource
end
end
end