Packages

ash_postgres

0.1.0
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
ash_postgres lib ash_postgres.ex
Raw

lib/ash_postgres.ex

defmodule AshPostgres do
@behaviour Ash.DataLayer
defmacro __using__(opts) do
quote bind_quoted: [repo: opts[:repo]] do
@data_layer AshPostgres
@repo repo
unless repo do
raise "You must pass the `repo` option to `use AshPostgres` for #{__MODULE__}"
end
unless repo.__adapter__() == Ecto.Adapters.Postgres do
raise "Only Ecto.Adapters.Postgres is supported with AshPostgres for now"
end
def repo() do
@repo
end
end
end
def repo(resource) do
resource.repo()
end
import Ecto.Query, only: [from: 2]
@impl true
def limit(query, limit, _resource) do
{:ok, from(row in query, limit: ^limit)}
end
@impl true
def offset(query, offset, _resource) do
{:ok, from(row in query, offset: ^offset)}
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(resource)
@impl true
def create(resource, changeset, relationships) do
repo = repo(resource)
repo.transaction(fn ->
changeset = cast_assocs(changeset, repo, resource, relationships)
result =
try do
changeset
|> repo.insert()
|> case do
{:ok, result} -> result
{:error, changeset} -> repo.rollback(changeset)
end
rescue
e ->
repo.rollback(e)
end
case changeset do
%{__after_action__: [_ | _] = after_action_hooks} ->
Enum.each(after_action_hooks, fn hook ->
case hook.(changeset, result, repo) do
:ok -> :ok
{:error, error} -> repo.rollback(error)
:error -> repo.rollback(:error)
end
end)
result
_ ->
result
end
end)
end
@impl true
def sort(query, sort, _resource) do
{:ok,
from(row in query,
order_by: ^sort
)}
end
@impl true
def filter(query, filter, resource) do
Enum.reduce(filter, {:ok, query}, fn
_, {:error, error} ->
{:error, error}
{key, value}, {:ok, query} ->
do_filter(query, key, value, resource)
end)
end
defp do_filter(
query,
:from_related,
{records, %{cardinality: :many_to_many} = relationship},
_resource
) do
ids = Enum.map(records, &Map.get(&1, relationship.source_field))
{:ok,
from(row in query,
join: join_row in ^relationship.through,
on:
field(join_row, ^relationship.destination_field_on_join_table) ==
field(row, ^relationship.destination_field),
where: field(join_row, ^relationship.source_field_on_join_table) in ^ids,
select_merge: %{__related_id__: field(join_row, ^relationship.source_field_on_join_table)}
)}
end
defp do_filter(query, :from_related, {records, relationship}, _resource) do
ids = Enum.map(records, &Map.get(&1, relationship.source_field))
{:ok,
from(row in query,
where: field(row, ^relationship.destination_field) in ^ids
)}
end
# TODO This is a really dumb implementation of this.
defp do_filter(query, key, value, resource) do
cond do
attr = Ash.attribute(resource, key) ->
filter_attribute(query, attr, value, resource)
rel = Ash.relationship(resource, key) ->
filter_relationship(query, rel, value, resource)
true ->
{:error, "No such filter"}
end
end
defp filter_attribute(query, attribute, value, _resource) do
query =
from(row in query,
where: field(row, ^attribute.name) == ^value
)
{:ok, query}
end
# Only supports a single id for now
defp filter_relationship(query, %{name: name}, id, _resource) do
query =
from(row in query,
join: related in assoc(row, ^name),
where: related.id == ^id
)
{:ok, query}
end
@impl true
def can_query_async?(resource) do
repo(resource).in_transaction?()
end
def cast_assocs(changeset, repo, resource, relationships) do
Enum.reduce(relationships, changeset, fn {relationship, value}, changeset ->
case Ash.relationship(resource, relationship) do
%{type: :belongs_to, source_field: source_field} ->
belongs_to_assoc_update(changeset, source_field, value)
%{type: :has_one} = rel ->
has_one_assoc_update(changeset, repo, rel, value)
%{type: :has_many} = rel ->
has_many_assoc_update(changeset, rel, value)
%{type: :many_to_many} = rel ->
many_to_many_assoc_update(changeset, rel, value, repo)
_ ->
changeset
end
end)
end
defp has_one_assoc_update(
changeset,
repo,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
},
identifier
) do
Ecto.Changeset.prepare_changes(changeset, fn changeset ->
value =
case identifier do
%{id: id} -> id
nil -> nil
_ -> raise "what"
end
query =
from(row in destination,
where:
field(row, ^destination_field) == ^Ecto.Changeset.get_field(changeset, source_field)
)
repo.update_all(query, set: [{destination_field, value}])
changeset
end)
end
defp belongs_to_assoc_update(changeset, source_field, %{id: id}) do
Ecto.Changeset.cast(changeset, %{source_field => id}, [source_field])
end
defp belongs_to_assoc_update(changeset, source_field, nil) do
Ecto.Changeset.cast(changeset, %{source_field => nil}, [source_field])
end
defp has_many_assoc_update(
changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
},
values
) do
ids = values |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
add_after_action_hook(changeset, fn _changeset, result, repo ->
field_value = Map.get(result, source_field)
query =
from(row in destination,
where: row.id in ^ids
)
repo.update_all(query, set: [{destination_field, field_value}])
:ok
end)
end
defp many_to_many_assoc_update(
changeset,
%{
through: through,
source_field: source_field,
source_field_on_join_table: source_field_on_join_table,
destination_field_on_join_table: destination_field_on_join_table
},
values,
repo
) do
ids = values |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
source_id =
Ecto.Changeset.get_field(
changeset,
source_field
)
changeset
|> Ecto.Changeset.prepare_changes(fn changeset ->
delete_now_unrelated_ids(
repo,
source_id,
through,
source_field_on_join_table,
destination_field_on_join_table,
ids
)
changeset
end)
|> add_after_action_hook(fn _changeset, _result, repo ->
upsert_join_table_rows(
repo,
source_id,
through,
values,
source_field_on_join_table,
destination_field_on_join_table
)
end)
end
defp delete_now_unrelated_ids(
repo,
source_id,
through,
source_field_on_join_table,
destination_field_on_join_table,
ids
) do
query =
from(join_row in through,
where: field(join_row, ^destination_field_on_join_table) not in ^ids,
where: field(join_row, ^source_field_on_join_table) == ^source_id
)
repo.delete_all(query)
end
defp upsert_join_table_rows(
repo,
source_id,
through,
values,
source_field_on_join_table,
destination_field_on_join_table
) do
Enum.each(values, fn fields ->
update_fields = Map.delete(fields, :id)
cond do
update_fields == %{} && is_nil(Map.get(fields, :id)) ->
:ok
is_bitstring(through) ->
:ok
true ->
# TODO: This needs to be wired up properly
# (fields/changes need to come from resource/relationship config)
attributes =
update_fields
|> Map.put(source_field_on_join_table, source_id)
|> Map.put(destination_field_on_join_table, Map.get(fields, :id))
changeset =
through
|> struct()
|> Ecto.Changeset.cast(attributes, Map.keys(attributes))
repo.insert(changeset,
on_conflict: :replace_all_except_primary_key,
conflict_target: [source_field_on_join_table, destination_field_on_join_table]
)
:ok
end
end)
end
defp add_after_action_hook(changeset, hook) do
changeset
|> Map.put_new(:__after_action__, [])
|> Map.update!(:__after_action__, fn list -> [hook | list] end)
end
# Copied from an older file, to be added as more functionality is added back in
# @impl true
# def update(%resource{} = record, _action, attributes, relationships, _params) do
# @repo.transaction(fn ->
# changeset =
# record
# |> Ecto.Changeset.cast(attributes, Map.keys(attributes))
# |> AshPostgres.DataLayer.cast_assocs(@repo, resource, relationships)
# result =
# case @repo.update(changeset) do
# {:ok, result} -> result
# {:error, changeset} -> @repo.rollback(changeset)
# end
# case changeset do
# %{__after_action__: [_ | _] = after_action_hooks} ->
# Enum.each(after_action_hooks, fn hook ->
# case hook.(changeset, result, @repo) do
# :ok -> :ok
# {:error, error} -> @repo.rollback(error)
# :error -> @repo.rollback(:error)
# end
# end)
# result
# _other ->
# result
# end
# end)
# end
# @impl true
# def append_related(record, relationship, resource_identifiers) do
# @repo.transaction(fn ->
# AshPostgres.DataLayer.append_related(@repo, record, relationship, resource_identifiers)
# end)
# end
# @impl true
# def delete_related(record, relationship, resource_identifiers) do
# @repo.transaction(fn ->
# AshPostgres.DataLayer.delete_related(@repo, record, relationship, resource_identifiers)
# end)
# end
# @impl true
# def replace_related(record, relationship, resource_identifiers) do
# @repo.transaction(fn ->
# AshPostgres.DataLayer.replace_related(@repo, record, relationship, resource_identifiers)
# end)
# end
# @impl true
# def delete(record, _action, _params) do
# @repo.delete(record)
# end
# @doc false
# def replace_related(
# repo,
# record,
# %{
# type: :many_to_many,
# through: through,
# source_field: source_field,
# source_field_on_join_table: source_field_on_join_table,
# destination_field_on_join_table: destination_field_on_join_table
# },
# identifiers
# ) do
# ids = identifiers |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
# source_id = Map.get(record, source_field)
# delete_now_unrelated_ids(
# repo,
# source_id,
# through,
# source_field_on_join_table,
# destination_field_on_join_table,
# ids
# )
# upsert_join_table_rows(
# repo,
# source_id,
# through,
# identifiers,
# source_field_on_join_table,
# destination_field_on_join_table
# )
# record
# end
# def replace_related(
# repo,
# record,
# %{
# type: :has_many,
# source_field: source_field,
# destination: destination,
# destination_field: destination_field
# },
# identifiers
# ) do
# ids = identifiers |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
# field_value = Map.get(record, source_field)
# query =
# from(row in destination,
# where: row.id in ^ids
# )
# repo.update_all(query, set: [{destination_field, field_value}])
# record
# end
# def replace_related(
# repo,
# record,
# %{
# type: :belongs_to,
# source_field: source_field
# },
# identifier
# ) do
# value =
# case identifier do
# %{id: id} -> id
# nil -> nil
# _ -> raise "what do"
# end
# record
# |> Ecto.Changeset.cast(%{source_field => value}, [source_field])
# |> repo.update()
# |> case do
# {:ok, record} -> record
# {:error, error} -> repo.rollback(error)
# end
# end
# def replace_related(
# repo,
# record,
# %{
# type: :has_one,
# source_field: source_field,
# destination_field: destination_field,
# destination: destination
# },
# identifier
# ) do
# value =
# case identifier do
# %{id: id} -> id
# nil -> nil
# _ -> raise "what"
# end
# query =
# from(row in destination,
# where: field(row, ^destination_field) == ^Map.get(record, source_field)
# )
# repo.update_all(query, set: [{destination_field, value}])
# record
# end
# @doc false
# def append_related(repo, record, %{type: :many_to_many} = relationship, identifiers) do
# source_id = Map.get(record, relationship.source_field)
# upsert_join_table_rows(
# repo,
# source_id,
# relationship.through,
# identifiers,
# relationship.source_field_on_join_table,
# relationship.destination_field_on_join_table
# )
# record
# end
# def append_related(
# repo,
# record,
# %{type: :has_many, destination: destination, destination_field: destination_field},
# identifiers
# ) do
# ids =
# identifiers
# |> Enum.map(&Map.get(&1, :id))
# |> Enum.reject(&is_nil/1)
# query =
# from(related in destination,
# where: related.id in ^ids,
# where: field(related, ^destination_field) != ^record.id
# )
# repo.update_all(query, set: [{destination_field, record.id}])
# end
# @doc false
# def delete_related(repo, record, %{type: :many_to_many} = relationship, identifiers) do
# source_id = Map.get(record, relationship.source_field)
# ids =
# identifiers
# |> Enum.map(&Map.get(&1, :id))
# |> Enum.reject(&is_nil/1)
# delete_related_ids(
# repo,
# source_id,
# relationship.through,
# relationship.source_field_on_join_table,
# relationship.destination_field_on_join_table,
# ids
# )
# record
# end
# def delete_related(
# repo,
# record,
# %{type: :has_many, destination: destination, destination_field: destination_field},
# identifiers
# ) do
# ids =
# identifiers
# |> Enum.map(&Map.get(&1, :id))
# |> Enum.reject(&is_nil/1)
# query =
# from(related in destination,
# where: related.id in ^ids,
# where: field(related, ^destination_field) != ^record.id
# )
# # TODO: Validate the a delete_related action doesn't exist for has_many relationships
# # where the destination field is not nullable. That will only ever error.
# repo.update_all(query, set: [{destination_field, record.id}])
# end
# defp delete_related_ids(
# repo,
# source_id,
# through,
# source_field_on_join_table,
# destination_field_on_join_table,
# ids
# ) do
# query =
# from(join_row in through,
# where: field(join_row, ^destination_field_on_join_table) in ^ids,
# where: field(join_row, ^source_field_on_join_table) == ^source_id
# )
# repo.delete_all(query)
# end
# def add_through_schema_fields(repo, resource, includes) when is_list(includes) do
# Enum.flat_map(includes, fn {rel, further} ->
# case Ash.relationship(resource, rel) do
# %{type: :many_to_many, destination: destination} = relationship ->
# [
# {rel, &fetch_and_add_through_row(&1, repo, relationship)},
# {rel, add_through_schema_fields(repo, destination, further)}
# ]
# %{destination: destination} ->
# [
# {rel, add_through_schema_fields(repo, destination, further)}
# ]
# end
# end)
# end
# def add_through_schema_fields(_repo, _resource, includes), do: includes
# defp fetch_and_add_through_row(source_ids, repo, relationship) do
# query =
# from(join_row in relationship.through,
# where:
# type(field(join_row, ^relationship.source_field_on_join_table), :binary_id) in ^source_ids,
# join: destination_row in ^relationship.destination,
# on:
# type(field(join_row, ^relationship.destination_field_on_join_table), :binary_id) ==
# field(destination_row, ^relationship.destination_field)
# )
# query
# |> add_select(relationship)
# |> repo.all()
# |> Enum.map(fn {destination_row, join_row} ->
# {Map.get(join_row, relationship.source_field_on_join_table),
# Map.put(destination_row, :__join_row__, join_row)}
# end)
# end
# defp add_select(query, relationship) do
# case relationship.through do
# string when is_bitstring(string) ->
# from([join_row, destination_row] in query,
# select:
# {destination_row,
# %{
# ^relationship.source_field_on_join_table =>
# type(field(join_row, ^relationship.source_field_on_join_table), :binary_id),
# ^relationship.destination_field_on_join_table =>
# type(field(join_row, ^relationship.destination_field_on_join_table), :binary_id)
# }}
# )
# module when is_atom(module) ->
# from([join_row, destination_row] in query,
# select: {destination_row, join_row}
# )
# end
# end
end