improvement: identities w/ calculations and where clauses in upserts

This commit is contained in:
Zach Daniel 2024-06-17 11:38:28 -04:00
parent 0db1b29c23
commit 97e538bf63
4 changed files with 90 additions and 16 deletions

View file

@ -1721,6 +1721,7 @@ defmodule AshPostgres.DataLayer do
:conflict_target, :conflict_target,
conflict_target( conflict_target(
resource, resource,
options[:identity],
options[:upsert_keys] || Ash.Resource.Info.primary_key(resource) options[:upsert_keys] || Ash.Resource.Info.primary_key(resource)
) )
) )
@ -2469,7 +2470,7 @@ defmodule AshPostgres.DataLayer do
end end
@impl true @impl true
def upsert(resource, changeset, keys \\ nil) do def upsert(resource, changeset, keys, identity) do
if AshPostgres.DataLayer.Info.manage_tenant_update?(resource) do if AshPostgres.DataLayer.Info.manage_tenant_update?(resource) do
{:error, "Cannot currently upsert a resource that owns a tenant"} {:error, "Cannot currently upsert a resource that owns a tenant"}
else else
@ -2491,6 +2492,7 @@ defmodule AshPostgres.DataLayer do
single?: true, single?: true,
upsert?: true, upsert?: true,
tenant: changeset.tenant, tenant: changeset.tenant,
identity: identity,
upsert_keys: keys, upsert_keys: keys,
upsert_fields: upsert_fields, upsert_fields: upsert_fields,
return_records?: true return_records?: true
@ -2504,23 +2506,73 @@ defmodule AshPostgres.DataLayer do
end end
end end
defp conflict_target(resource, keys) do defp conflict_target(resource, identity, keys) do
if Ash.Resource.Info.base_filter(resource) do identity_where =
case identity do
%{name: name, where: where} when not is_nil(where) ->
AshPostgres.DataLayer.Info.identity_where_to_sql(resource, name) ||
raise(
"Must provide an entry for :#{identity.name} in `postgres.identity_wheres_to_sql` to use it as an upsert_identity"
)
_ ->
nil
end
base_filter_sql = base_filter_sql =
case Ash.Resource.Info.base_filter(resource) do
nil ->
nil
_base_filter ->
AshPostgres.DataLayer.Info.base_filter_sql(resource) || AshPostgres.DataLayer.Info.base_filter_sql(resource) ||
raise """ raise """
Cannot use upserts with resources that have a base_filter without also adding `base_filter_sql` in the postgres section. Cannot use upserts with resources that have a base_filter without also adding `base_filter_sql` in the postgres section.
""" """
sources =
Enum.map(keys, fn key ->
~s("#{Ash.Resource.Info.attribute(resource, key).source || key}")
end)
{:unsafe_fragment, "(" <> Enum.join(sources, ", ") <> ") WHERE (#{base_filter_sql})"}
else
keys
end end
where =
case {base_filter_sql, identity_where} do
{nil, nil} ->
nil
{base_filter_sql, nil} ->
" WHERE (#{base_filter_sql})"
{nil, identity_where} ->
" WHERE (#{identity_where})"
{base_filter_sql, identity_where} ->
" WHERE ((#{base_filter_sql}) AND (#{identity_where}))"
end
if is_nil(where) && Enum.all?(keys, &Ash.Resource.Info.attribute(resource, &1)) do
keys
else
sources = sources_to_sql(resource, keys)
{:unsafe_fragment, "(" <> Enum.join(sources, ", ") <> ")#{where}"}
end
end
defp sources_to_sql(resource, keys) do
Enum.map(keys, fn key ->
case Ash.Resource.Info.field(resource, key) do
%Ash.Resource.Attribute{source: source, name: name} ->
~s("#{source || name}")
%Ash.Resource.Calculation{name: name} ->
if sql = AshPostgres.DataLayer.Info.calculations_to_sql(name) do
"(" <> sql <> ")"
else
raise ArgumentError,
"Calculation #{inspect(key)} used in `AshPostgres.DataLayer` conflict target must have its sql defined in `calculations_to_sql`"
end
_other ->
raise ArgumentError,
"Unsupported field #{inspect(key)} used in `AshPostgres.DataLayer` conflict target"
end
end)
end end
defp update_defaults(resource) do defp update_defaults(resource) do

View file

@ -162,7 +162,7 @@ defmodule AshPostgres.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:ash, ash_version("~> 3.0 and >= 3.0.7")}, {:ash, ash_version("~> 3.0 and >= 3.0.13")},
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.4")}, {:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.4")},
{:ecto_sql, "~> 3.9"}, {:ecto_sql, "~> 3.9"},
{:ecto, "~> 3.9"}, {:ecto, "~> 3.9"},

View file

@ -43,7 +43,7 @@
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"sourceror": {:hex, :sourceror, "1.3.0", "70ab9e8bf6df085a1effba4b49ad621b7153b065f69ef6cdb82e6088f2026029", [:mix], [], "hexpm", "1794c3ceeca4eb3f9437261721e4d9cbf846d7c64c7aee4f64062b18d5ce1eac"}, "sourceror": {:hex, :sourceror, "1.3.0", "70ab9e8bf6df085a1effba4b49ad621b7153b065f69ef6cdb82e6088f2026029", [:mix], [], "hexpm", "1794c3ceeca4eb3f9437261721e4d9cbf846d7c64c7aee4f64062b18d5ce1eac"},
"spark": {:hex, :spark, "2.2.1", "b4ec56c99b3a750f7db0d2651fc59dc59c4002a5e0d658dde968fc69bd451136", [:mix], [{:igniter, ">= 0.1.7 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a6052f08ebe1ef152f86489151401210755bd81a9f0cbd737dd50819c65a0fc1"}, "spark": {:hex, :spark, "2.2.1", "b4ec56c99b3a750f7db0d2651fc59dc59c4002a5e0d658dde968fc69bd451136", [:mix], [{:igniter, ">= 0.1.7 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a6052f08ebe1ef152f86489151401210755bd81a9f0cbd737dd50819c65a0fc1"},
"spitfire": {:hex, :spitfire, "0.1.1", "249c8ea38d9e313e636670f2f692df5158b231e5986cbd13390498741d33cc0b", [:mix], [], "hexpm", "13782a1f902bfa52f5058d71752f4aa78c6056667a66bda5804480b7355e3aa9"}, "spitfire": {:hex, :spitfire, "0.1.2", "49b85d59c170d671e7e49649f62f6fe0771743a61bc42bd7a203f98f322d99e2", [:mix], [], "hexpm", "21f04cf02df601d75e1551e393ee5c927e3986fbb7598f3e59f71d4ca544fd9b"},
"splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"}, "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"}, "stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},

View file

@ -42,4 +42,26 @@ defmodule AshPostgres.Test.UniqueIdentityTest do
assert new_post.id == post.id assert new_post.id == post.id
assert new_post.price == 10 assert new_post.price == 10
end end
test "a unique constraint can be used to upsert when backed by a calculation" do
post =
Post
|> Ash.Changeset.for_create(:create, %{
title: "title",
uniq_if_contains_foo: "abcfoodef",
price: 10
})
|> Ash.create!()
new_post =
Post
|> Ash.Changeset.for_create(:create, %{
title: "title2",
uniq_if_contains_foo: "abcfoodef"
})
|> Ash.create!(upsert?: true, upsert_identity: :uniq_if_contains_foo)
assert new_post.id == post.id
assert new_post.price == 10
end
end end