Multi update after insert

Hi!

I am working with a legacy database. I want to insert a record and update a field in the same record using the inserted_at timestamp.

Specifically I have a field named order_date which I want to assign the value of the date part in inserted_at.

I tried using a Multi.insert followed by a Multi.update but the field is not updated.

This is a simplified version of my code:

statements.ex

def create_account_statement
    Multi.new()
    |> Multi.insert(:account_header, &insert_account_header/1)
    |> Multi.update(:order_date, &update_order_date/1)
    |> Repo.transaction()
end

defp insert_account_header(%{patient: patient}) do
  AccountHeader.new()
  |> AccountHeader.update_patient_changeset(patient.oid)
end

defp update_order_date(%{account_header: account_header}) do
  AccountHeader.update_order_date_changeset(account_header, account_header.transactiondate)
end

account_header.ex

  def new() do
    %__MODULE__{}
  end  

  def update_patient_changeset(changeset, patient_oid) do
    change(changeset, %{patient_oid: patient_oid})
  end

  def update_order_date_changeset(changeset, datetime) do
    change(changeset, %{order_date: datetime})
  end

The transaction runs with any error but the order_date is nil.

Is this the right way to do this or am I totally lost?

Any comments or ideas will be helpful.

Maybe this is nil, did you check?

Thanks Dimitar,

I checked with an inspect at the end of update_order_date_changeset:

#Ecto.Changeset<
  action: nil,
  changes: %{order_date: ~N[2022-11-14 18:09:27]},
  errors: [],
  data: #Medik.Accounts.AccountHeader<>,
  valid?: true

and with an assertion in a test:

     Assertion with == failed
     code:  assert account_header.order_date == account_header.transactiondate
     left:  nil
     right: ~N[2022-11-14 18:09:27]

Changeset.change/2 doesn’t do any type-casting - does adding an explicit conversion change things?

  def update_order_date_changeset(changeset, datetime) do
    change(changeset, %{order_date: NaiveDateTime.to_date(datetime)})
  end
1 Like

Well in that case IO.inspect or dbg to the rescue! Put them in update_order_date_changeset and update_order_date and you’ll see if the value is the right one?

and with an assertion in a test:

You say “in a test”, but IMO that does not really assert that the transactiondate field is actually set within your code, does it ? Maybe your test is setup in a way that allows this field to be set in the context of your test…

Is transactiondate a field auto-magically set by the database in some way ? Doesn’t it need e.g. read_after_writes to be set to true to read the actual value ?

In any case, to debug that, I would start e.g. by adding some instrumentation at the moment the :order_date step runs, replacing the Multi.update in create_account_statement with a Multi.run :

def create_account_statement
    Multi.new()
    |> Multi.insert(:account_header, &insert_account_header/1)
    |> Multi.run(:order_date, &instrumented_update_fun/2)
    |> Repo.transaction()
end

defp instrumented_update_fun(repo, changes) do
  with ok = {:ok, updated} <- changes |> update_order_date() |> repo.update do
    # ... this is strictly equivalent to `Multi.update` and you can now add
    # some instrumentation code here : IO.inspect, dbg, IEx.pry, ...
    ok
  end
end

You can also try to trace back where that nil value comes from by letting the caller raise if datetime is nil at any point, e.g by adding a guard to the update_order_date_changeset function :

def update_order_date_changeset(changeset, datetime) when not is_nil(datetime) do
  change(changeset, %{order_date: datetime})
end

(and then do the same thing within the caller, if applicable)

Good luck with that !

1 Like

Dimitar, Matt, Theenglishway

I appreciate a lot your help and I am ashamed that everything was my mistake. The code works as expected. As a last resort I enabled SQL logging in the test environment and noticed that the insert and update were exactly as I expected.

The problem, is that at the end of the Multi (which I not posted previously) I was returning the result from the :account_header step instead of using the result of the :order_date step.

This is the wrong code

def create_account_statement
    Multi.new()
    |> Multi.insert(:account_header, &insert_account_header/1)
    |> Multi.update(:order_date, &update_order_date/1)
    |> Repo.transaction()
    |> case do
      {:ok, %{account_header: account_header}} ->
        {:ok, account_header}
    end
end

And this is the fixed code:

def create_account_statement
    Multi.new()
    |> Multi.insert(:account_header, &insert_account_header/1)
    |> Multi.update(:order_date, &update_order_date/1)
    |> Repo.transaction()
    |> case do
      {:ok, %{order_date: account_header}} ->
        {:ok, account_header}
    end
end

The problem is that I added a new step and kept using the result from the previous step.

I thank you again for your help.
Best Regards,
Hector

2 Likes