Can't force_change_attribute a Decimal field that is curretly nil

I think I found a bug in Ash when trying to forcefully updating an attribute that is currently nil in the DB.

In my resource, I have this field:

   attribute :adjust_rate, :decimal do
      allow_nil? true
      writable? false
    end

This field is updated from an :update action that has a change that will calculate its value and update it with force_change_attribute:

    changeset
    |> Ash.Changeset.force_change_attribute(:adjust_rate, adjust_rate)

If the field, in the DB has a nil value and I’m running the above change to update it, I will get the following error:

** (Ash.Error.Unknown) Unknown Error

* %FunctionClauseError{module: Decimal, function: :decimal, arity: 1, kind: nil, args: nil, clauses: nil}
  (decimal 2.1.1) Decimal.decimal(nil)
  (decimal 2.1.1) lib/decimal.ex:386: Decimal.compare/2
  (decimal 2.1.1) lib/decimal.ex:447: Decimal.eq?/2
  (ash 3.4.34) lib/ash/changeset/changeset.ex:5302: Ash.Changeset.force_change_attribute/3
  (core 1.115.0) lib/core/marketplace/invoices/pro_forma/actions/common/changes/calculate_adjust_rate.ex:39: Core.Marketplace.Invoices.ProForma.Actions.Common.Changes.CalculateAdjustRate.change/1
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3718: anonymous fn/2 in Ash.Changeset.run_before_actions/1
  (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
  (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3693: Ash.Changeset.run_before_actions/1
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3833: Ash.Changeset.run_around_actions/2
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3385: anonymous fn/3 in Ash.Changeset.with_hooks/3
  (ecto_sql 3.12.0) lib/ecto/adapters/sql.ex:1382: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
  (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3383: anonymous fn/3 in Ash.Changeset.with_hooks/3
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3527: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
  (ash 3.4.34) lib/ash/changeset/changeset.ex:3364: Ash.Changeset.with_hooks/3
  (ash 3.4.34) lib/ash/actions/update/update.ex:403: Ash.Actions.Update.commit/3
  (ash 3.4.34) lib/ash/actions/update/update.ex:283: Ash.Actions.Update.do_run/4
  (ash 3.4.34) lib/ash/actions/update/update.ex:240: Ash.Actions.Update.run/4
  (ash 3.4.34) lib/ash.ex:2665: Ash.update/3
  (ash 3.4.34) lib/ash.ex:2605: Ash.update!/3
  (elixir 1.17.2) src/elixir.erl:386: :elixir.eval_external_handler/3
  (stdlib 6.1.1) erl_eval.erl:904: :erl_eval.do_apply/7
  (stdlib 6.1.1) erl_eval.erl:648: :erl_eval.expr/6
  (elixir 1.17.2) src/elixir.erl:364: :elixir.eval_forms/4
  (elixir 1.17.2) lib/module/parallel_checker.ex:112: Module.ParallelChecker.verify/1
  (iex 1.17.2) lib/iex/evaluator.ex:332: IEx.Evaluator.eval_and_inspect/3
  (iex 1.17.2) lib/iex/evaluator.ex:306: IEx.Evaluator.eval_and_inspect_parsed/3
  (iex 1.17.2) lib/iex/evaluator.ex:295: IEx.Evaluator.parse_eval_inspect/4
  (iex 1.17.2) lib/iex/evaluator.ex:187: IEx.Evaluator.loop/1
  (iex 1.17.2) lib/iex/evaluator.ex:32: IEx.Evaluator.init/5
  (stdlib 6.1.1) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
    (decimal 2.1.1) Decimal.decimal(nil)
    (decimal 2.1.1) lib/decimal.ex:386: Decimal.compare/2
    (decimal 2.1.1) lib/decimal.ex:447: Decimal.eq?/2
    (ash 3.4.34) lib/ash/changeset/changeset.ex:5302: Ash.Changeset.force_change_attribute/3
    (core 1.115.0) lib/core/marketplace/invoices/pro_forma/actions/common/changes/calculate_adjust_rate.ex:39: Core.Marketplace.Invoices.ProForma.Actions.Common.Changes.CalculateAdjustRate.change/1
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3718: anonymous fn/2 in Ash.Changeset.run_before_actions/1
    (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
    (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3693: Ash.Changeset.run_before_actions/1
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3833: Ash.Changeset.run_around_actions/2
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3385: anonymous fn/3 in Ash.Changeset.with_hooks/3
    (ecto_sql 3.12.0) lib/ecto/adapters/sql.ex:1382: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3383: anonymous fn/3 in Ash.Changeset.with_hooks/3
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3527: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
    (ash 3.4.34) lib/ash/changeset/changeset.ex:3364: Ash.Changeset.with_hooks/3
    (ash 3.4.34) lib/ash/actions/update/update.ex:403: Ash.Actions.Update.commit/3
    (ash 3.4.34) lib/ash/actions/update/update.ex:283: Ash.Actions.Update.do_run/4
    (ash 3.4.34) lib/ash/actions/update/update.ex:240: Ash.Actions.Update.run/4
    (ash 3.4.34) lib/ash.ex:2665: Ash.update/3
    (ash 3.4.34) lib/ash.ex:2605: Ash.update!/3
    iex:13: (file)

It seems to fail because in Ash changeset.ex:5302, it will try to compare the decimal values (the old value and new value) but the old value is nil so Decimal.compare/2 will fail.

1 Like

Actually, I just saw that I was in a slightly older version of Ash, upgrading to the latest fixed the issue