(ArgumentError) cannot load `0.9` as type #Ash.Type.Float.EctoType<[]>

Hello,

I must be missing something very simple, but I’m really puzzled by this inability to create a resource with a float number. I’m using sqlite, and I have the following definitions:

defmodule AshFloatDomain do
  use Ash.Domain

  resources do
    resource AshFloatError
  end

end

defmodule AshFloatError do
  use Ash.Resource,
    domain: AshFloatDomain,
    data_layer: AshSqlite.DataLayer

  actions do
    defaults [:read]

    create :create do
      accept [:temperature]
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :temperature, :float, allow_nil?: true
  end

  sqlite do
    table "ash_float_error"
    repo Elani.Repo
  end
end

I ran ash_sqlite.generate_migrations, which generated the following migration:

defmodule Elani.Repo.Migrations.MigrateResources3 do
  @moduledoc """
  Updates resources based on their most recent snapshots.

  This file was autogenerated with `mix ash_sqlite.generate_migrations`
  """

  use Ecto.Migration

  def up do
    create table(:ash_float_error, primary_key: false) do
      add :temperature, :float
      add :id, :uuid, null: false, primary_key: true
    end
  end

  def down do
    drop table(:ash_float_error)
  end
end

and after running ash_sqlite.migrate, I can verify the the table was created properly:

sqlite3 .elani.db
-- Loading resources from /Users/victor/.sqliterc
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .schema ash_float_error
CREATE TABLE IF NOT EXISTS "ash_float_error" ("temperature" NUMERIC, "id" TEXT NOT NULL PRIMARY KEY);

When creating a changeset, I can see that it’s valid:

iex(24)> AshFloatError |> Ash.Changeset.for_create(:create, %{temperature: 0.9}) 
#Ash.Changeset<
  domain: AshFloatDomain,
  action_type: :create,
  action: :create,
  attributes: %{temperature: 0.9},
  relationships: %{},
  errors: [],
  data: #AshFloatError<
    __meta__: #Ecto.Schema.Metadata<:built, "ash_float_error">,
    id: nil,
    temperature: nil,
    aggregates: %{},
    calculations: %{},
    ...
  >,
  valid?: true
>

yet when I’m trying to persist, I get the following error:

iex(25)> AshFloatError |> Ash.Changeset.for_create(:create, %{temperature: 0.9}) |> Ash.create!()
** (Ash.Error.Unknown) 
Bread Crumbs:
  > Error returned from: AshFloatError.create

Unknown Error

* ** (ArgumentError) cannot load `0.9` as type #Ash.Type.Float.EctoType<[]> for field :temperature in #AshFloatError<__meta__: #Ecto.Schema.Metadata<:loaded, "ash_float_error">, id: nil, temperature: nil, aggregates: %{}, calculations: %{}, ...>
  (ecto 3.12.5) lib/ecto/repo/queryable.ex:431: Ecto.Repo.Queryable.struct_load!/6
  (ecto 3.12.5) lib/ecto/repo/schema.ex:76: anonymous fn/4 in Ecto.Repo.Schema.postprocess/5
  (elixir 1.18.0) lib/enum.ex:1714: Enum."-map/2-lists^map/1-1-"/2
  (ecto 3.12.5) lib/ecto/repo/schema.ex:61: Ecto.Repo.Schema.do_insert_all/7
  (ash_sqlite 0.2.1) lib/data_layer.ex:720: anonymous fn/4 in AshSqlite.DataLayer.bulk_create/3
  (elixir 1.18.0) lib/enum.ex:4964: Enumerable.List.reduce/3
  (elixir 1.18.0) lib/enum.ex:2600: Enum.reduce_while/3
  (ash_sqlite 0.2.1) lib/data_layer.ex:651: AshSqlite.DataLayer.bulk_create/3
    (ash 3.4.48) lib/ash/error/unknown.ex:3: Ash.Error.Unknown."exception (overridable 2)"/1
    (ash 3.4.48) /Users/victor/src/elani/deps/splode/lib/splode.ex:264: Ash.Error.to_class/2
    (ash 3.4.48) lib/ash/error/error.ex:108: Ash.Error.to_error_class/2
    (ash 3.4.48) lib/ash/actions/create/create.ex:161: Ash.Actions.Create.do_run/4
    (ash 3.4.48) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4
    (ash 3.4.48) lib/ash.ex:2134: Ash.create!/3
    iex:25: (file)

Does it look like a bug? Should I file a bug report in ash_sqlite?

I’m using

  ash 3.4.48
  ash_sql 0.2.42
  ash_sqlite 0.2.1

which I think are the latest version.

Many thanks for helping to debug this,
Victor.

1 Like

And when changing the data layer to Ets, everything works, so it does seem to be an error with AshSqlite:

diff --git a/lib/ash_float_error.ex b/lib/ash_float_error.ex
index 393bb18..66dc308 100644
--- a/lib/ash_float_error.ex
+++ b/lib/ash_float_error.ex
@@ -10,7 +10,7 @@ end
 defmodule AshFloatError do
   use Ash.Resource,
     domain: AshFloatDomain,
-    data_layer: AshSqlite.DataLayer
+    data_layer: Ash.DataLayer.Ets
 
   actions do
     defaults [:read]
@@ -25,8 +25,8 @@ defmodule AshFloatError do
     attribute :temperature, :float, allow_nil?: true
   end
 
-  sqlite do
-    table "ash_float_error"
-    repo Elani.Repo
-  end
+  # sqlite do
+  #   table "ash_float_error"
+  #   repo Elani.Repo
+  # end
 end
iex(1)> AshFloatError |> Ash.Changeset.for_create(:create, %{temperature: 0.9}) |> Ash.create!()
#AshFloatError<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "8b27f6a4-14fd-4d6f-9576-7cb8bb0102f9",
  temperature: 0.9,
  aggregates: %{},
  calculations: %{},
  ...
>

Today I stumbled upon this with my app as well. I also use the AshSqlite.DataLayer and get the same error when I want to save a float attribute, e.g:

cannot load `163.4` as type #Ash.Type.Float.EctoType<[]> for field :duration

It seems to work with whole numbers. If I save an integer like 163 instead of a floating number the insert works. This sure looks like a bug and not the intended behavior.

Thank you for double checking, @toberoni ! I filed a bug report here: Cannot persist a float number · Issue #114 · ash-project/ash_sqlite · GitHub

This is pretty strange…

I can see the type loading logic returning {:ok, 0.5}. This feels like it may be a bug in the ecto sqlite adapter?

In fact I can see that calling Repo.all(Resource) is failing also. I think we may need to open a bug w/ ecto_sqlite3 with a demo repo showing the issue :slight_smile:

1 Like

Thanks Zach for pointing me in the right direction, as always. The issue is indeed in the interaction between Ash and Sqlite3 libraries.

By putting a few dbg statements here and there (see below), I can see that Ash is using ParameterizedType internally for floats (to support max/min constraints I assume), so that’s what SQLite3.Codec.float_decode is called on.

However, looks like Codec.float_decode does not support parameterized types - it is defined as

  def float_decode(nil), do: {:ok, nil}
  def float_decode(%Decimal{} = decimal), do: {:ok, Decimal.to_float(decimal)}
  def float_decode(x) when is_integer(x), do: {:ok, x / 1}
  def float_decode(_), do: :error

and it seems it’s the last clause that produces the error we see, as the ones above don’t match.

That’s as far as I went so far - I don’t fully understand yet the relationship between the main Ecto library and the adapter in handling ParameterizedTypes (and for example, I can’t see anything special that postgrex does to enable ParameterizedType support), so I just created an issue in ecto_sqlite3 repository.

iex(1)> AshFloatError |> Ash.Changeset.for_create(:create, %{temperature: 0.9}) |> Ash.create!()
[(ecto 3.13.0-dev) lib/ecto/type.ex:1020: Ecto.Type.adapter_load/3]
type(type) #=> :float

[(ecto 3.13.0-dev) lib/ecto/type.ex:1021: Ecto.Type.adapter_load/3]
type #=> {:parameterized, {Ash.Type.Float.EctoType, []}}

[(ecto 3.13.0-dev) lib/ecto/type.ex:1022: Ecto.Type.adapter_load/3]
adapter.loaders(type(type), type) #=> [
  &Ecto.Adapters.SQLite3.Codec.float_decode/1,
  {:parameterized, {Ash.Type.Float.EctoType, []}}
]

[(ecto 3.13.0-dev) lib/ecto/repo/queryable.ex:431: Ecto.Repo.Queryable.load!/5]
binding() #=> [
  adapter: Ecto.Adapters.SQLite3,
  field: " for field :temperature",
  struct: " in #AshFloatError<__meta__: #Ecto.Schema.Metadata<:loaded, \"ash_float_error\">, id: nil, temperature: nil, aggregates: %{}, calculations: %{}, ...>",
  type: {:parameterized, {Ash.Type.Float.EctoType, []}},
  value: 0.9
]

** (Ash.Error.Unknown) 
Bread Crumbs:
  > Error returned from: AshFloatError.create

Unknown Error

* ** (ArgumentError) cannot load `0.9` as type #Ash.Type.Float.EctoType<[]> for field :temperature in #AshFloatError<__meta__: #Ecto.Schema.Metadata<:loaded, "ash_float_error">, id: nil, temperature: nil, aggregates: %{}, calculations: %{}, ...>

Meanwhile, can you think of any workaround until the issue is fixed upstream?

To be honest, I’m a bit puzzled here: every other type in Ash is also an instance of ParameterizedType; why then it’s only float that causes the issue?

Looks like replacing this line with

 def float_decode(x), do: {:ok, x}

as they do in myxql adapter for Ecto_sql fixes the issue for me. @toberoni you might try doing the same and see if that fixes the issue for you, too.

Changed the behavior in ecto_sqlite3 to pass through the unrecognized value.

2 Likes