AE
Ash Elixirβ€’3y ago
Myrmyr

Ash stopped working with ExMachina. Ash 2.5.10

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
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)
** (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?
19 Replies
ZachDaniel
ZachDanielβ€’3y ago
πŸ€” I don't think that we can undo that change realistically. but hopefully we can find some other way to get compatibility Is there a stacktrace for that error?
Myrmyr
MyrmyrOPβ€’3y ago
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)
** (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
ZachDanielβ€’3y ago
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
MyrmyrOPβ€’3y ago
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
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 But I lack the knowledge of Ash internals to be sure if it's a viable solution
ZachDaniel
ZachDanielβ€’3y ago
πŸ€” you might also be able to do this its a bit of a hack, but might work
AshPostgres.DataLayer.to_ecto(%YourStuff{})
AshPostgres.DataLayer.to_ecto(%YourStuff{})
in the factory, I mean Ash also has some seeding tools FWIW
Ash HQ Bot
Ash HQ Botβ€’3y ago
Found 1 Code results in all libraries: * Ash.Seed: https://ash-hq.org/docs/module/ash/2.6.0/ash-seed
Myrmyr
MyrmyrOPβ€’3y ago
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
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
ZachDanielβ€’3y ago
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. 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
MyrmyrOPβ€’3y ago
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
ZachDanielβ€’3y ago
Yeah, thats a good point. We can conditionally compile it if ExMachina is compiled too
alex88
alex88β€’3y ago
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
MyrmyrOPβ€’3y ago
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
Terrybleβ€’3y ago
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
ZachDanielβ€’3y ago
they should only live for the life of each test, is that what you're seeing?
brittonjb
brittonjbβ€’3y ago
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
ZachDanielβ€’3y ago
πŸ‘†
Terryble
Terrybleβ€’3y ago
I didn't know Ash.Seed exists. I'll check this out. Thanks! 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
ZachDanielβ€’3y ago
πŸ€” 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
Terrybleβ€’3y ago
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

Did you find this page helpful?