Ecto: validate multiple related fields in a changeset

Beginner here.

I have the following changeset, where there are related fields whose values I would like to use in a validation in order to ensure valid values in the database.

defmodule MyApp.Basedata.CountryContext.Country do
  use Ecto.Schema
  import Ecto.Changeset

  schema "countries" do
    field :alpha2, :string
    field :alpha3, :string
    field :numeric3, :string
    field :name, :string

    timestamps()
  end

  @doc false
  def changeset(country, attrs) do
    country
    |> cast(attrs, [:alpha2, :alpha3, :numeric3, :name])
    |> validate_required([:name])
    |> validate_length(:alpha2, min: 2, max: 2)
    |> validate_length(:alpha3, min: 3, max: 3)
    |> validate_length(:numeric3, max: 3)
    |> validate_format(:alpha2, ~r/^$|[A-Z]{2}/)
    |> validate_format(:alpha3, ~r/^$|[A-Z]{3}/)
    |> validate_format(:numeric3, ~r/^$|[0-9]{2}/)
  end
end

Here some example data:

ZW;ZWE;716;Zimbabwe
XK;XKX;0;Kosovo
;;0;Turkish Republic Northern Cyprus

Zimbabwe has all three ISO codes
Kosovo only has preliminary :alpha2 and :alpha3 codes, but currently no :numeric3 value
Republic Northern Cyprus is only recognised by Turkey, and has no ISO codes.

What I would like to implement is a validator that check a non-empty :numeric3, and if the value is non-empty, then :alpha2 and :alpha3 should also be non-empty. From what I can tell the current ecto validators all take a field as a parameter, but my use case requires me to know other fields in relation to the current fields value within the validator.

I think what I will have to do is create a new function that takes a changeset as a parameter, then use code to extract the values that I need, and either

[:aplha2, "If a numeric is not empty, the alpha2 must also be not empty"]

in the case of an error

or

[]

in the case of a successfull validation. The issue I see here is Ecto will only show me the changes in the changeset, not the actual values of all the fields in the row. So, I will be able to trap some errors of this type, but not all permutations of changes, as if I do not modify the :numeric3 value, I will have nothing to compare the :alpha2 and :alpha3 against.

Perhaps I will just have to create a postgres constraint for this case.

Or is there another workflow I can follow in cases like this?

You can use get_field/3 or (fetch_field) for that but I think you should still create a constraint in PG though. You can then use check_constraint/3 to return an error.

1 Like

Thanks. I think the database constraint is the way to go.

 numeric_only_with_alpha_check = """
    ( coalesce(alpha2,'')='' and  coalesce(alpha3,'')='' and coalesce(numeric3,0)=0)
    or ( coalesce(alpha2,'')<>'' and  coalesce(alpha3,'')<>'' and coalesce(numeric3,0)<>0)
    or ( coalesce(alpha2,'')<>'' and  coalesce(alpha3,'')<>'' and coalesce(numeric3,0)=0)
    """

    create constraint(:countries, :numeric_only_with_alpha_check,
             check: numeric_only_with_alpha_check
           )
1 Like