embeds_one won't dump; says it's an invalid type
Closed this issue · 8 comments
I am trying sqlite3 out for the first time in Livebook. I believe that I have parity with how an app would use it, but I cannot get an embeds_one field to be inserted. I've appended an example of the failure on a 'buildings' table that has an embeds_one 'size' field. The error occurs in dump_field!:
(Ecto.ChangeError) value `%Satis.Model.BuildingSize{height: 0, length: 0, width: 0}` for `Satis.Model.Building.size`
in `insert` does not match type #Ecto.Embedded<%Ecto.Embedded{cardinality: :one, field: :size, owner:
Satis.Model.Building, related: Satis.Model.BuildingSize, on_cast: nil, on_replace: :raise, unique: true, ordered: true}>
I've used embeds before with postgres, and it feels like i'm missing something obvious, but I don't know what.
Below is the Livebook file (.livemd file not attachable).
embeds in sqlite3 won't dump
Mix.install([:kino, :ecto_sqlite3, {:ecto, "~> 3.12"}])Section
sqlite3_db_file = "Documents/codex.sqlite3"
Application.put_env(:satis, Satis.Repo,
# Cannot use :memory sqlite3 bc you can't migrate it
database: sqlite3_db_file,
# Turn off foreign key constraints to ease data importing
foreign_keys: :off,
log: false
)
defmodule Satis.Repo do
@otp_app :satis
use Ecto.Repo, otp_app: @otp_app, adapter: Ecto.Adapters.SQLite3
end
defmodule Satis.Model do
defmacro __using__(_) do
quote do
use Ecto.Schema
import Ecto.Changeset
alias unquote(__MODULE__)
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(_) do
quote do
def changeset(params \\ %{}) do
changeset(%__MODULE__{}, params)
end
def changeset(model, params) do
fields = __MODULE__.__schema__(:fields)
associations = __MODULE__.__schema__(:associations)
embeds = __MODULE__.__schema__(:embeds)
cs = cast(model, params, fields -- (embeds ++ associations))
cs = Enum.reduce(embeds, cs, &cast_embed(&2, &1))
Enum.reduce(associations, cs, &cast_assoc(&2, &1))
end
end
end
end
defmodule Satis.Model.BuildingSize do
use Satis.Model
@primary_key false
embedded_schema do
field(:height, :integer)
field(:length, :integer)
field(:width, :integer)
end
end
defmodule Satis.Model.Building do
use Satis.Model
@primary_key {:class_name, :string, autogenerate: false}
schema "buildings" do
embeds_one(:size, Model.BuildingSize)
end
defmodule Migration do
use Ecto.Migration
def change do
create table("buildings", primary_key: false) do
add(:class_name, :string, primary_key: true)
add(:size, :map)
end
end
end
end{:module, Satis.Model.Building, <<70, 79, 82, 49, 0, 0, 61, ...>>,
{:module, Satis.Model.Building.Migration, <<70, 79, 82, ...>>, {:change, 0}}}
Ready Database
alias Satis.Model
File.rm(sqlite3_db_file) |> dbg
Kino.start_child!(Satis.Repo) |> dbg
[
Model.Building
]
|> Enum.with_index(1)
|> Enum.each(fn {migration, idx} ->
:ok = Ecto.Migrator.up(Satis.Repo, idx, Module.concat([migration, "Migration"]))
end):ok
#PID<0.265.0>
09:42:48.019 [info] == Running 1 Satis.Model.Building.Migration.change/0 forward
09:42:48.021 [info] create table buildings
09:42:48.022 [info] == Migrated 1 in 0.0s
:ok
data =
%{
"class_name" => "Desc_QuarterPipeMiddleOutCorner_Ficsit_4x1_C",
"size" => %{"height" => 0, "length" => 0, "width" => 0},
}
Model.Building.changeset(data)
|> Satis.Repo.insert!()** (Ecto.ChangeError) value `%Satis.Model.BuildingSize{height: 0, length: 0, width: 0}` for `Satis.Model.Building.size` in `insert` does not match type #Ecto.Embedded<%Ecto.Embedded{cardinality: :one, field: :size, owner: Satis.Model.Building, related: Satis.Model.BuildingSize, on_cast: nil, on_replace: :raise, unique: true, ordered: true}>
(ecto 3.12.5) lib/ecto/repo/schema.ex:1115: Ecto.Repo.Schema.dump_field!/6
(ecto 3.12.5) lib/ecto/repo/schema.ex:1124: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
(stdlib 6.2) maps.erl:860: :maps.fold_1/4
(ecto 3.12.5) lib/ecto/repo/schema.ex:1122: Ecto.Repo.Schema.dump_fields!/5
(ecto 3.12.5) lib/ecto/repo/schema.ex:1055: Ecto.Repo.Schema.dump_changes!/7
(ecto 3.12.5) lib/ecto/repo/schema.ex:406: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(ecto 3.12.5) lib/ecto/repo/schema.ex:1099: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
#cell:zrd25i7t5zceldyc:8: (file)
What's the changeset function defined as?
The given example is clipped from about 6 more tables / migrations, so to save space in the livebook they are all being auto generated in the __before_compile__ hook of Satis.Model. I tried manually defining the changsets as below:
Model.BuildingSize
def changeset(params) do
%__MODULE__{} |> changeset(params)
end
def changeset(data, params) do
data
|> cast(params, [:height, :length, :width])
endand Model.Building
def changeset(params) do
%__MODULE__{} |> changeset(params)
end
def changeset(data, params) do
data
|> cast(params, [:class_name])
|> cast_embed(:size)
endand received the same error
I know this is a stretch, but have you tried this against the postgres adapter?
Edit: I am asking because I think you may be running into an issue with Ecto itself, and not this adapter.
I had been avoiding it (was trying out sqlite3), but thankfully it wasn't too hard to change adapters, and it worked with postgres (i've attached below for completeness), but that also showed me the actual error!
Punchline: I was missing Jason. Here was Postgres' error without :jason being installed:
** (UndefinedFunctionError) function Jason.encode_to_iodata!/1 is undefined (module Jason is not available). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
Jason.encode_to_iodata!(%{length: 0, width: 0, height: 0})
(postgrex 0.20.0) lib/postgrex/type_module.ex:1084: Postgrex.DefaultTypes.encode_params/3
(postgrex 0.20.0) lib/postgrex/query.ex:75: DBConnection.Query.Postgrex.Query.encode/3
(db_connection 2.7.0) lib/db_connection.ex:1449: DBConnection.encode/5
(db_connection 2.7.0) lib/db_connection.ex:1549: DBConnection.run_prepare_execute/5
(db_connection 2.7.0) lib/db_connection.ex:772: DBConnection.parsed_prepare_execute/5
(db_connection 2.7.0) lib/db_connection.ex:764: DBConnection.prepare_execute/4
#cell:icoyuvuwifcyj22c:8: (file)
Once I added :jason to the livebook in the original post, it worked like expected. Perhaps a more clear error could be surfaced, but this issue seems closed as is, thanks for the assistance!
embeds in sqlite3 won't dump
Mix.install([:kino, :jason, :ecto_sql, :postgrex, {:ecto, "~> 3.12"}])Section
sqlite3_db_file = ""
Application.put_env(:satis, Satis.Repo,
hostname: "localhost",
username: "postgres",
password: "postgres",
database: "codex-test",
log: false
)
defmodule Satis.Repo do
@otp_app :satis
use Ecto.Repo, otp_app: @otp_app, adapter: Ecto.Adapters.Postgres
end
defmodule Satis.Model do
defmacro __using__(_) do
quote do
use Ecto.Schema
import Ecto.Changeset
alias unquote(__MODULE__)
end
end
end
defmodule Satis.Model.BuildingSize do
use Satis.Model
@primary_key false
embedded_schema do
field(:height, :integer)
field(:length, :integer)
field(:width, :integer)
end
def changeset(params) do
%__MODULE__{} |> changeset(params)
end
def changeset(data, params) do
data
|> cast(params, [:height, :length, :width])
end
end
defmodule Satis.Model.Building do
use Satis.Model
@primary_key {:class_name, :string, autogenerate: false}
schema "buildings" do
embeds_one(:size, Model.BuildingSize)
end
def changeset(params) do
%__MODULE__{} |> changeset(params)
end
def changeset(data, params) do
data
|> cast(params, [:class_name])
|> cast_embed(:size)
end
defmodule Migration do
use Ecto.Migration
def change do
create table("buildings", primary_key: false) do
add(:class_name, :string, primary_key: true)
add(:size, :map)
end
end
end
end{:module, Satis.Model.Building, <<70, 79, 82, 49, 0, 0, 57, ...>>,
{:module, Satis.Model.Building.Migration, <<70, 79, 82, ...>>, {:change, 0}}}
Ready Database
alias Satis.Model
repo = Satis.Repo
repo.__adapter__().storage_up(repo.config()) |> dbg
Kino.start_child!(Satis.Repo) |> dbg
[
Model.Building
]
|> Enum.with_index(1)
|> Enum.each(fn {migration, idx} ->
Ecto.Migrator.up(Satis.Repo, idx, Module.concat([migration, "Migration"])) |> dbg
end){:error, :already_up}
#PID<0.1827.0>
:already_up
:ok
data =
%{
"class_name" => "Desc_QuarterPipeMiddleOutCorner_Ficsit_4x1_C",
"size" => %{"height" => 0, "length" => 0, "width" => 0},
}
Model.Building.changeset(data)
|> Satis.Repo.insert!()%Satis.Model.Building{
__meta__: #Ecto.Schema.Metadata<:loaded, "buildings">,
class_name: "Desc_QuarterPipeMiddleOutCorner_Ficsit_4x1_C",
size: %Satis.Model.BuildingSize{height: 0, length: 0, width: 0}
}
As an aside, I tried adding the below to Mix.install:
config: [ecto_sqlite3: [json_library: :json]]
to see if i could get around Jason (no strong reason, just trying new things in livebook), but i got the same error yet again. I could not get the native :json library to be ecto_sqlite's json library after a few other random attempts. I was not installing ':json' because i think that's a hex.pm package that's not the new erlang module. I'm on Livebook 15.2 (Elixir 1.18.2, and erlang 27.something, which should have :json)
Just gonna fall back to Jason. This is always the push-pull of hobby projects, you wanna try new things, but how much time do you wanna spend debugging new problems.
You gotta set the config to the module to use
So in your case
config: [ecto_sqlite3: [json_library: JSON]]
Worked! Oh, I had no idea the JSON was in the Elixir space; I'd used :json.decode elsewhere in the Livebook, so I assumed it was the usual 'when it's available to both erlang and elixir, you use the lowercase name, eg. :telemetry'. I see it here https://hexdocs.pm/elixir/1.18.2/JSON.html now. Thanks much!
The JSON module is backed by :json, just has some elixir'y things added.