From 0ff01f99e55f50d8c23180e0ce00d8663621889a Mon Sep 17 00:00:00 2001 From: Steve Brambilla Date: Thu, 2 Oct 2025 15:33:43 -0400 Subject: [PATCH] test: add repro for ash_sql issue --- .../test_repo/rsvps/20251002180954.json | 43 +++++++++++ .../20251002180954_migrate_resources62.exs | 20 +++++ test/support/domain.ex | 1 + test/support/resources/rsvp.ex | 42 +++++++++++ test/support/types/response.ex | 75 +++++++++++++++++++ test/type_test.exs | 9 +++ 6 files changed, 190 insertions(+) create mode 100644 priv/resource_snapshots/test_repo/rsvps/20251002180954.json create mode 100644 priv/test_repo/migrations/20251002180954_migrate_resources62.exs create mode 100644 test/support/resources/rsvp.ex create mode 100644 test/support/types/response.ex diff --git a/priv/resource_snapshots/test_repo/rsvps/20251002180954.json b/priv/resource_snapshots/test_repo/rsvps/20251002180954.json new file mode 100644 index 00000000..31e6d18f --- /dev/null +++ b/priv/resource_snapshots/test_repo/rsvps/20251002180954.json @@ -0,0 +1,43 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "0", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "response", + "type": "integer" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "8ADC3A631361A18B1B9A2070D5E8477428EDE39DE3B43FA5FF8E50CE7710B9E5", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "rsvps" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20251002180954_migrate_resources62.exs b/priv/test_repo/migrations/20251002180954_migrate_resources62.exs new file mode 100644 index 00000000..fe90b42c --- /dev/null +++ b/priv/test_repo/migrations/20251002180954_migrate_resources62.exs @@ -0,0 +1,20 @@ +defmodule AshPostgres.TestRepo.Migrations.MigrateResources62 do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:rsvps, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:response, :integer, null: false, default: 0) + end + end + + def down do + drop(table(:rsvps)) + end +end diff --git a/test/support/domain.ex b/test/support/domain.ex index 2bf55a0b..ec992cbe 100644 --- a/test/support/domain.ex +++ b/test/support/domain.ex @@ -53,6 +53,7 @@ defmodule AshPostgres.Test.Domain do resource(AshPostgres.Test.Order) resource(AshPostgres.Test.Chat) resource(AshPostgres.Test.Message) + resource(AshPostgres.Test.RSVP) end authorization do diff --git a/test/support/resources/rsvp.ex b/test/support/resources/rsvp.ex new file mode 100644 index 00000000..965f053d --- /dev/null +++ b/test/support/resources/rsvp.ex @@ -0,0 +1,42 @@ +defmodule AshPostgres.Test.RSVP do + @moduledoc false + use Ash.Resource, + domain: AshPostgres.Test.Domain, + data_layer: AshPostgres.DataLayer + + postgres do + table "rsvps" + repo AshPostgres.TestRepo + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + + # Uses an expression with an array of atoms for a custom type backed by integers. + update :clear_response do + change( + atomic_update( + :response, + expr( + if response in [:accepted, :declined] do + :awaiting + else + response + end + ) + ) + ) + end + end + + attributes do + uuid_primary_key(:id) + + attribute(:response, AshPostgres.Test.Types.Response, + allow_nil?: false, + public?: true, + default: 0 + ) + end +end diff --git a/test/support/types/response.ex b/test/support/types/response.ex new file mode 100644 index 00000000..51f475e0 --- /dev/null +++ b/test/support/types/response.ex @@ -0,0 +1,75 @@ +defmodule AshPostgres.Test.Types.Response do + @moduledoc false + use Ash.Type + use AshPostgres.Type + require Ash.Expr + + @atoms_to_ints %{accepted: 1, declined: 2, awaiting: 0} + @ints_to_atoms Map.new(@atoms_to_ints, fn {k, v} -> {v, k} end) + @atom_values Map.keys(@atoms_to_ints) + @string_values Enum.map(@atom_values, &to_string/1) + + @impl Ash.Type + def storage_type, do: :integer + + @impl Ash.Type + def cast_input(nil, _), do: {:ok, nil} + + def cast_input(value, _) when value in @atom_values, do: {:ok, value} + def cast_input(value, _) when value in @string_values, do: {:ok, String.to_existing_atom(value)} + + def cast_input(integer, _) when is_integer(integer), + do: Map.fetch(@ints_to_atoms, integer) + + def cast_input(_, _), do: :error + + @impl Ash.Type + def matches_type?(value, _) when is_atom(value) and value in @atom_values, do: true + def matches_type?(_, _), do: false + + @impl Ash.Type + def cast_stored(nil, _), do: {:ok, nil} + def cast_stored(integer, _) when is_integer(integer), do: Map.fetch(@ints_to_atoms, integer) + def cast_stored(_, _), do: :error + + @impl Ash.Type + def dump_to_native(nil, _), do: {:ok, nil} + def dump_to_native(atom, _) when is_atom(atom), do: Map.fetch(@atoms_to_ints, atom) + def dump_to_native(_, _), do: :error + + @impl Ash.Type + def cast_atomic(new_value, constraints) do + if Ash.Expr.expr?(new_value) do + {:atomic, new_value} + else + case cast_input(new_value, constraints) do + {:ok, value} -> {:atomic, value} + {:error, error} -> {:error, error} + end + end + end + + @impl Ash.Type + def apply_atomic_constraints(new_value, _constraints) do + {:ok, + Ash.Expr.expr( + if ^new_value in ^@atom_values do + ^new_value + else + error( + Ash.Error.Changes.InvalidChanges, + message: "must be one of %{values}", + vars: %{values: ^Enum.join(@atom_values, ", ")} + ) + end + )} + end + + @impl AshPostgres.Type + def value_to_postgres_default(_, _, value) do + case Map.fetch(@atoms_to_ints, value) do + {:ok, integer} -> {:ok, Integer.to_string(integer)} + :error -> :error + end + end +end diff --git a/test/type_test.exs b/test/type_test.exs index 4c3731e7..6355fe92 100644 --- a/test/type_test.exs +++ b/test/type_test.exs @@ -1,6 +1,7 @@ defmodule AshPostgres.Test.TypeTest do use AshPostgres.RepoCase, async: false alias AshPostgres.Test.Post + alias AshPostgres.Test.RSVP require Ash.Query @@ -107,4 +108,12 @@ defmodule AshPostgres.Test.TypeTest do post = Ash.Query.for_read(Post, :with_version_check, version: 1) |> Ash.read!() refute is_nil(post) end + + test "array expressions work with custom types that map atoms to integers" do + rsvp = RSVP |> Ash.Changeset.for_create(:create, %{response: :accepted}) |> Ash.create!() + + updated = rsvp |> Ash.Changeset.for_update(:clear_response, %{}) |> Ash.update!() + + assert updated.response == :awaiting + end end