Skip to content

Commit

Permalink
improvement: identities w/ calculations and where clauses in upserts
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jun 17, 2024
1 parent 0db1b29 commit 97e538b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 16 deletions.
80 changes: 66 additions & 14 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,7 @@ defmodule AshPostgres.DataLayer do
:conflict_target,
conflict_target(
resource,
options[:identity],
options[:upsert_keys] || Ash.Resource.Info.primary_key(resource)
)
)
Expand Down Expand Up @@ -2469,7 +2470,7 @@ defmodule AshPostgres.DataLayer do
end

@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
{:error, "Cannot currently upsert a resource that owns a tenant"}
else
Expand All @@ -2491,6 +2492,7 @@ defmodule AshPostgres.DataLayer do
single?: true,
upsert?: true,
tenant: changeset.tenant,
identity: identity,
upsert_keys: keys,
upsert_fields: upsert_fields,
return_records?: true
Expand All @@ -2504,25 +2506,75 @@ defmodule AshPostgres.DataLayer do
end
end

defp conflict_target(resource, keys) do
if Ash.Resource.Info.base_filter(resource) do
base_filter_sql =
AshPostgres.DataLayer.Info.base_filter_sql(resource) ||
raise """
Cannot use upserts with resources that have a base_filter without also adding `base_filter_sql` in the postgres section.
"""
defp conflict_target(resource, identity, keys) 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"
)

sources =
Enum.map(keys, fn key ->
~s("#{Ash.Resource.Info.attribute(resource, key).source || key}")
end)
_ ->
nil
end

{:unsafe_fragment, "(" <> Enum.join(sources, ", ") <> ") WHERE (#{base_filter_sql})"}
else
base_filter_sql =
case Ash.Resource.Info.base_filter(resource) do
nil ->
nil

_base_filter ->
AshPostgres.DataLayer.Info.base_filter_sql(resource) ||
raise """
Cannot use upserts with resources that have a base_filter without also adding `base_filter_sql` in the postgres section.
"""
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

defp update_defaults(resource) do
attributes =
resource
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ defmodule AshPostgres.MixProject do
# Run "mix help deps" to learn about dependencies.
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")},
{:ecto_sql, "~> 3.9"},
{:ecto, "~> 3.9"},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"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"},
"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"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},
Expand Down
22 changes: 22 additions & 0 deletions test/unique_identity_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,26 @@ defmodule AshPostgres.Test.UniqueIdentityTest do
assert new_post.id == post.id
assert new_post.price == 10
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

0 comments on commit 97e538b

Please sign in to comment.