Ash stopped working with ExMachina. Ash 2.5.10
Myrmyr:
Hello, I’ve been using Ash with ExMachina successfully up to this point. Recently I’ve wanted to upgrade Ash from 2.5.9 to 2.6.0, alongside AshPostgres from 1.3.3 to 1.3.8. I’ve tested combinations of different Ash and AshPostgres combinations to narrow down the issue. It appears to be caused by Ash 2.5.10. Specifically this commit https://github.com/ash-project/ash/commit/2787b5074b8b339057af6f0bc4ee3db5abf7c60d
Our factory:
defmodule MyApp.Factory do
@moduledoc false
use ExMachina.Ecto, repo: MyApp.Repo
def resource_factory do
%MyApp.Resource{
id: Ecto.UUID.generate(),
name: sequence(:name, &"Resource #{&1}"),
...
}
end
When I try to do
insert(:resource, some_field: "some_value)
I get the following error
** (UndefinedFunctionError) function Ash.NotLoaded.__schema__/0 is undefined or private
(ash 2.6.0) Ash.NotLoaded.__schema__()
iex:1: (file)
Is there any possibility to bring back the support for ExMachina?
ZachDaniel:
🤔 I don’t think that we can undo that change realistically.
ZachDaniel:
but hopefully we can find some other way to get compatibility
ZachDaniel:
Is there a stacktrace for that error?
Myrmyr:
Yeah, sorry.
** (UndefinedFunctionError) function Ash.NotLoaded.__schema__/1 is undefined or private
code: insert(:resource,
stacktrace:
(ash 2.6.0) Ash.NotLoaded.__schema__(:fields)
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:141: ExMachina.EctoStrategy.schema_fields/1
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:56: ExMachina.EctoStrategy.cast_all_fields/1
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:49: ExMachina.EctoStrategy.cast/1
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:109: anonymous fn/2 in ExMachina.EctoStrategy.cast_all_assocs/1
(elixir 1.14.1) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:25: ExMachina.EctoStrategy.handle_insert/2
test/my_app/resources/resource_test.exs:xxx: (test)
ZachDaniel:
I think we may need to clone the EctoStrategy and make an
AshStrategy
, or make a PR to the ecto strategy to give
%Ash.NotLoaded{}
the same treatment that
Ecto.NotLoaded
gets
Myrmyr:
Yeah, that was my first step, but that kinda seems like a lot of unnecessary work to just handle that case.
My second thought was to allow developer to opt-out of adding Ecto Relationship by wrapping the added
case
statement in an
unless
. Something like
unless Ash.Resource.Info.disable_ecto_relationship?(__MODULE__) do
case relationship do
%{no_attributes?: true} ->
:ok
%{manual?: true} ->
:ok
%{manual: manual} when not is_nil(manual) ->
:ok
%{type: :belongs_to} ->
belongs_to relationship.name, relationship.destination,
define_field: false,
references: relationship.destination_attribute,
foreign_key: relationship.source_attribute
%{type: :has_many} ->
has_many relationship.name, relationship.destination,
foreign_key: relationship.destination_attribute,
references: relationship.source_attribute
%{type: :has_one} ->
has_one relationship.name, relationship.destination,
foreign_key: relationship.destination_attribute,
references: relationship.source_attribute
%{type: :many_to_many} ->
many_to_many relationship.name, relationship.destination,
join_through: relationship.through,
join_keys: [
{relationship.source_attribute_on_join_resource,
relationship.source_attribute},
{relationship.destination_attribute_on_join_resource,
relationship.destination_attribute}
]
end
end
And then either in the resource itself or by config file pass
disable_ecto_relationship: true
Myrmyr:
But I lack the knowledge of Ash internals to be sure if it’s a viable solution
ZachDaniel:
🤔 you might also be able to do this
ZachDaniel:
its a bit of a hack, but might work
ZachDaniel:
AshPostgres.DataLayer.to_ecto(%YourStuff{})
ZachDaniel:
in the factory, I mean
ZachDaniel:
Ash also has some seeding tools FWIW
Ash HQ Bot:
Found 1 Code results in all libraries:
Myrmyr:
The
AshPostgres.DataLayer.to_ecto(%YourStuff{})
works. 🎉 Thanks!
I’ve written simple ExMachina strategy to do that on every insert. But I do not know to which repo(if at all) should it go.
defmodule Ash.ExMachina do
@moduledoc false
defmacro __using__(opts) do
quote do
use ExMachina.Ecto
# We want all the usefull functions that `ExMachina.Ecto` provides, but have to override inserts
defoverridable insert: 1
defoverridable insert: 2
defoverridable insert: 3
defoverridable insert_pair: 1
defoverridable insert_pair: 2
defoverridable insert_pair: 3
defoverridable insert_list: 2
defoverridable insert_list: 3
defoverridable insert_list: 4
use Ash.ExMachina.InsertStrategy,
repo: unquote(Keyword.get(opts, :repo))
end
end
end
defmodule Ash.ExMachina.InsertStrategy do
@moduledoc false
use ExMachina.Strategy, function_name: :insert
def handle_insert(record, opts) do
record
|> AshPostgres.DataLayer.to_ecto()
|> ExMachina.EctoStrategy.handle_insert(opts)
|> AshPostgres.DataLayer.from_ecto()
end
end
EDIT:
Added
from_ecto()
at the end of
handle_insert
as Zach suggested
ZachDaniel:
You might want to add a
from_ecto/1
call at the end also so it will definitely play nicely with the rest of Ash stuff.
ZachDaniel:
I’m also not sure where that should go 😆 Where does the
Ecto
related stuff go fro
ex_machina
? Do they just have it all in one repo? For now, I might just keep it in your app, this forum thread will be searchable for others with similar issues 🙂
Myrmyr:
ExMachina has built-in Ecto support so it’s in the main library, but I don’t think we will manage to add Ash support there 😆 Yeah I think it’s kinda too specific to add it to Ash. But if there’s a need for that later I think it could go into
ash_postgres
ZachDaniel:
Yeah, thats a good point. We can conditionally compile it if
ExMachina
is compiled too
alex88:
Hi Myrmyr, I have the same issue, where do you put this file? Do you just
use Ash.ExMachina
instead of
use ExMachina.Ecto
?
Myrmyr:
I actually put these modules in a separate files in
support
folder of
test
directory. Then, just as you’ve described, in factory I do
use Ash.ExMachina, repo: MyApp.Repo
instead of
ExMachina.Ecto, repo: MyApp.Repo
Terryble:
Not sure if anything changed recently, but I tried this solution and it doesn’t work for some reason. None of the records I try to create are persisted in the database.
ZachDaniel:
they should only live for the life of each test, is that what you’re seeing?
brittonjb:
You have to do a bit more with the ExMachina.Ecto and ExMachina.EctoStrategy based on when I took a swing at this a little over a month ago.
I’ll try to do a writeup in the next week, but if you’re not already leveraging ExMachina throughout your codebase you may better off giving Ash.Seed a shot.
ZachDaniel:
👆
Terryble:
I didn’t know Ash.Seed exists. I’ll check this out. Thanks!
Terryble:
It doesn’t exist even inside the test. As in the struct says it’s loaded, but there is no
id
,
inserted_at
, and
updated_at
which implies the record didn’t get created.
ZachDaniel:
🤔 Yeah, hard to say whats up there. For my piece, I’d like to just improve
Ash.Seed
until there is no compelling reason to use ex_machina.
Terryble:
I tried
Ash.Seed
and it looks like it’s enough for me. I wish I’d known about that module a lot earlier lol