Ash calculations & aggregates surround strings with "?

Does any body know why Ash calculations like this one:

calculate :intent,
              :string,
              expr(
                first([:case, :document, :answers],
                  query: [filter: expr(question_id == ^@config.answer_slugs.intent)],
                  field: :value
                )
              ),
              public?: true

surround the resulting string with “” ? This is what it looks like when outputing it to html:

grafik

What does it look like in iex? If we’re actually wrapping strings in quotes in calculations then that is a bug.

If I just query the Answer directly it looks like this:

#ExEbau.Form.Answer<
    documents: #Ash.NotLoaded<:relationship, field: :documents>,
    question: #Ash.NotLoaded<:relationship, field: :question>,
    document: #Ash.NotLoaded<:relationship, field: :document>,
    documents_join_assoc: #Ash.NotLoaded<:relationship, field: :documents_join_assoc>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "caluma_form_answer">,
    id: "0571220a-75d1-44ea-b004-efead8419c38",
    question_id: "municipality",
    value: "1206",
    document_id: "d54b0af1-f33d-44c0-a9a9-b0b5237975a2",
    aggregates: %{},
    calculations: %{},
    ...
  >

If I query this aggregation:

defmodule SomeResource do
  aggregates do
    first :municipality_answer, [:case, :document, :answers], :value,
      filter: expr(question_id == ^@config.answer_slugs.municipality)
  end
end

I get this:

#ExEbau.Core.Instance<
    applicants: #Ash.NotLoaded<:calculation, field: :applicants>,
    submit_date: #Ash.NotLoaded<:calculation, field: :submit_date>,
    foobar2: #Ash.NotLoaded<:calculation, field: :foobar2>,
    foobar: #Ash.NotLoaded<:calculation, field: :foobar>,
    address: #Ash.NotLoaded<:calculation, field: :address>,
    intent: #Ash.NotLoaded<:calculation, field: :intent>,
    name: #Ash.NotLoaded<:calculation, field: :name>,
    form: #Ash.NotLoaded<:aggregate, field: :form>,
    object_street: #Ash.NotLoaded<:aggregate, field: :object_street>,
    applicants_list: #Ash.NotLoaded<:aggregate, field: :applicants_list>,
    municipality_answer: "\"1201\"",
    location: [],
    instance_state: #Ash.NotLoaded<:relationship, field: :instance_state>,
    personalien_answers: #Ash.NotLoaded<:relationship, field: :personalien_answers>,
    personalien_documents: #Ash.NotLoaded<:relationship, field: :personalien_documents>,
    case: #Ash.NotLoaded<:relationship, field: :case>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "INSTANCE">,
    id: 46268,
    instance_state_id: 21,
    case_id: "a12c32cc-8ac8-419c-b498-505aca548c46",
    aggregates: %{},
    calculations: %{},
    ...
  >

Does the Answer record (ie. the one being read in the aggregate) have those double quotes around the string?

Sry, my example wasn’t clear. It is comes from the value in the Answer module:

    value: "1206",

The column is actually a jsonb column.

Oh I totally missed that in your previous post, my apologies!

I suspect JSON might have something to do with it…

Wow, yeah this is definitely a bug. Can you please set up a reproduction project or PR a failing test? I will fix ASAP.

I’m not sure how I could provide an MR for this since it requires postgresql for the jsonb column type and ash runs against ETS in tests, right?

You’d create a MR against ash_postgres if you wanted to provide a failing test :slight_smile:

Question: what is your configured minimum postgres version? in your repo?

EDIT: nvm I understand. You said its a JSONB column, and @sevenseacat is right that this has something to do with casting jsonb.
Oh, and also, is :value a calculation or an attribute?

Ok, now it gets bad… but hear me out:

attribute :value, :answer_value

and AnswerValue looks like this:

defmodule MyAwfulCustomType.AnswerValue do
  use Ash.Type

  @moduledoc """
  Custom type for storing answer values as jsonb in the database.
  """

  @impl Ash.Type
  def storage_type(_), do: :jsonb

  @impl Ash.Type
  def cast_input(nil, _), do: {:ok, nil}

  def cast_input(value, _) do
    cast_value(value)
  end

  @impl Ash.Type
  @spec cast_stored(any(), any()) :: :error | {:error, keyword()} | {:ok, any()}
  def cast_stored(nil, _), do: {:ok, nil}

  def cast_stored(value, _) do
    cast_value(value)
  end

  @impl Ash.Type
  def dump_to_native(nil, _), do: {:ok, nil}

  def dump_to_native(value, _) do
    cast_value(value)
  end

  def cast_value(value) do
    with :error <- Ecto.Type.cast(:integer, value),
         :error <- Ecto.Type.cast(:float, value),
         :error <- Ecto.Type.cast(:string, value),
         :error <- Ecto.Type.cast(:map, value),
         :error <- Ecto.Type.cast({:array, :integer}, value),
         :error <- Ecto.Type.cast({:array, :float}, value),
         :error <- Ecto.Type.cast({:array, :string}, value),
         :error <- Ecto.Type.cast({:array, :map}, value) do
      :error
    end
  end
end

Basically value in the DB is a jsonb column that can contain values like:

"foo"
["foo"]
123.0
4
true
false

The reason for this is historic but not something we can change at all (because terrabytes of data would have to be migrated and it would cost thousands of work hours).

:scream:

Alright. This is an interesting challenge then :slight_smile: I think there is probably something we can do under the hood TBH, but will need a reproduction so I can tinker with it.