defmodule Ash.Resource.Verifiers.ValidateRelationshipAttributesMatch do @moduledoc """ Shows a warning on potentially incompatible relationship attributes. """ use Spark.Dsl.Verifier def verify(dsl) do module = Spark.Dsl.Verifier.get_persisted(dsl, :module) dsl |> Ash.Resource.Info.relationships() |> Enum.reject(&(&1.type == :many_to_many)) |> Enum.flat_map(fn %{ source_attribute: source_attribute, destination_attribute: destination_attribute, destination: destination, validate_destination_attribute?: true } = rel -> case Code.ensure_compiled(destination) do {:module, destination} -> [{rel, source_attribute, destination_attribute, destination}] _ -> # older versions of ash/spark/elixir can get here, but not newer ones [] end _ -> [] end) |> Enum.flat_map(fn {rel, source, dest, resource} -> source = Ash.Resource.Info.attribute(dsl, source) dest = Ash.Resource.Info.attribute(resource, dest) if source && dest && !compatible_types?(source, dest) do [warning(rel, module, source, dest, resource)] else [] end end) |> case do [] -> :ok warnings -> {:warn, warnings} end end defp warning(rel, module, source, dest, resource) do """ Source attribute may not be compatible with destination attribute for #{inspect(module)}.#{rel.name} to #{inspect(resource)} `source_attribute` type: `#{inspect(source.type)}` `destination_attribute` type: `#{inspect(dest.type)}` Types are considered compatible if: 1. They are exactly the same 2. Their `storage_type/0` callback returns the same value 3. The storage types are `:text` and `:string` 4. The relationship has `validate_destination_attribute?` set to `false`. 5. They are explicitly configured as compatible. To do so in this instance, add it to your config like so: config :ash, :compatible_foreign_key_types, [ {#{inspect(source.type)}, #{inspect(dest.type)}} ] """ end defp compatible_types?(%{type: source}, %{type: dest}) do left_storage_type = Ash.Type.storage_type(source) right_storage_type = Ash.Type.storage_type(dest) cond do source == dest -> true left_storage_type == right_storage_type -> true left_storage_type == :text and right_storage_type == :string -> true left_storage_type == :string and right_storage_type == :text -> true true -> configured_as_compatible?(source, dest) end end @compatible_foreign_key_types Application.compile_env(:ash, :compatible_foreign_key_types, []) defp configured_as_compatible?(source, dest) do Enum.find(@compatible_foreign_key_types, fn {left, right} -> (source == left and dest == right) or (source == right and dest == left) end) end end