AE
Ash Elixirβ€’2y ago
kernel

Issues using Ash.Type.Union

So I'm trying to use Ash.Type.Union and I keep getting errors when attempting to load the resource, I have defined an embedded resource
defmodule VMX.Transactions.TransactionData.Type.Call do
use Ash.Resource, data_layer: :embedded

attributes do
attribute :call_uuid, :string
attribute :plivo_recording_url, :string
attribute :receiver, :string
attribute :recording_duration, :string
attribute :recording_url, :string
attribute :recording_uuid, :string
attribute :sender, :string
end
end
defmodule VMX.Transactions.TransactionData.Type.Call do
use Ash.Resource, data_layer: :embedded

attributes do
attribute :call_uuid, :string
attribute :plivo_recording_url, :string
attribute :receiver, :string
attribute :recording_duration, :string
attribute :recording_url, :string
attribute :recording_uuid, :string
attribute :sender, :string
end
end
22 Replies
kernel
kernelOPβ€’2y ago
the parent resource has this data in the attribute which is a union type
%{"__schema": "call", "call_uuid" => "9ba4f656-e85f-11ec-827b-8f363c318b4c", "plivo_recording_url" => "some.mp3", "receiver" => "122345674", "recording_duration" => "21240", "recording_url" => "meow.mp3", "recording_uuid" => "f67797ef-3fb7-4dc7-819d-46b905e6230f", "sender" => "1234567"}
%{"__schema": "call", "call_uuid" => "9ba4f656-e85f-11ec-827b-8f363c318b4c", "plivo_recording_url" => "some.mp3", "receiver" => "122345674", "recording_duration" => "21240", "recording_url" => "meow.mp3", "recording_uuid" => "f67797ef-3fb7-4dc7-819d-46b905e6230f", "sender" => "1234567"}
and the parent resource attribute is defined as such
attribute :data, :union do
constraints types: [
call: [
type: __MODULE__.Type.Call,
tag: :__schema,
tag_value: :call
]
]
end
attribute :data, :union do
constraints types: [
call: [
type: __MODULE__.Type.Call,
tag: :__schema,
tag_value: :call
]
]
end
I get this error when tryig to load
[error] GenServer #PID<0.1133.0> terminating
** (Ash.Error.Unknown) Unknown Error

* ** (ArgumentError) cannot load <<the json>> as type #Ash.Type.Union.EctoType<[types: [call: [type: VMX.Transactions.TransactionData.Type.Call, constraints: [], tag: :type, tag_value: :call], email: [type: VMX.Transactions.TransactionData.Type.Email, constraints: [], tag: :type, tag_value: :email]]]> for field :data in #VMX.Transactions.TransactionData<transaction: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<:loaded, "transaction_datum">, type: nil, data: nil, transaction_id: nil, aggregates: %{}, calculations: %{}, ...>
[error] GenServer #PID<0.1133.0> terminating
** (Ash.Error.Unknown) Unknown Error

* ** (ArgumentError) cannot load <<the json>> as type #Ash.Type.Union.EctoType<[types: [call: [type: VMX.Transactions.TransactionData.Type.Call, constraints: [], tag: :type, tag_value: :call], email: [type: VMX.Transactions.TransactionData.Type.Email, constraints: [], tag: :type, tag_value: :email]]]> for field :data in #VMX.Transactions.TransactionData<transaction: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<:loaded, "transaction_datum">, type: nil, data: nil, transaction_id: nil, aggregates: %{}, calculations: %{}, ...>
Am I doing something obviously wrong or? cast_input works
barnabasj
barnabasjβ€’2y ago
how did you save the data using ash or are you trying to load existing data from the database? Ash persists the data wraped inside union type like so:
{ type: "the type of the union", value: {"your": "data"} }
{ type: "the type of the union", value: {"your": "data"} }
so if you're trying to load existing data that's just the json, that won't work because it expects the format from above
kernel
kernelOPβ€’2y ago
ah right yeah I'm trying to load existing data πŸ™‚ which is just json hmmm :thinkies:
barnabasj
barnabasjβ€’2y ago
In that case you probably have to migrate it first, into a different column or something, load it as map, and persist it as ash union
kernel
kernelOPβ€’2y ago
that sounds like a lot of work why does ash choose to save stuff that way tbh? I would have thought just plain would be enough
barnabasj
barnabasjβ€’2y ago
not sure, tbh. have to wait for zach to answer that πŸ˜‰
kernel
kernelOPβ€’2y ago
performs the ritual to summon zach daniel leinad hcaz, leinad hcaz, leinad hcaz
barnabasj
barnabasjβ€’2y ago
Depending on how much data you have you could just do something like this for the migration
Resource
|> Ash.Query.for_read(:paginated)
|> Api.stream!()
|> Enum.each(fn resource ->
resource
|> Ash.Changeset.for_update(:update, %{union_attribute: resource.map_attribute})
|> Api.update!()
end)
Resource
|> Ash.Query.for_read(:paginated)
|> Api.stream!()
|> Enum.each(fn resource ->
resource
|> Ash.Changeset.for_update(:update, %{union_attribute: resource.map_attribute})
|> Api.update!()
end)
kernel
kernelOPβ€’2y ago
only a few million rows πŸ˜† could do it with a db query I guess I'll migrate the data for now into Ash's format I dont necessarily like it, but it may be for a good reason maybe Zach can enlighten me as to why it was chosen to be saved like that, as opposed to just raw json with the type tag not being made into an attribute, or why the type tag wasn't able to be on the resource containing the union type (probably easier and more portable to have it tied to the type) when you say this, do you mean type is the configured type tag? i.e: for me it would be {"__schema": "call", "value": {...}}
barnabasj
barnabasjβ€’2y ago
I think it is the key for you type, which is the same in your case
types: [
# the typename should be the type in the union struct
typename: [
config...
types: [
# the typename should be the type in the union struct
typename: [
config...
But unions have been more trial and error for me than really understanding so far tbh πŸ˜…
kernel
kernelOPβ€’2y ago
yeah it's type
kernel
kernelOPβ€’2y ago
works
No description
kernel
kernelOPβ€’2y ago
mmm I don't know if I like this 😜 , it feels like 'application level' stuff starting to dictate how I do my database at least it's just a type tag, it would be terrible if it was saving the actual module name of my type 🀣 thanks @barnabasj πŸ™‚
ZachDaniel
ZachDanielβ€’2y ago
We can make this configurable potentially πŸ™‚ The main reason is that you can’t always tell what type something is when loading from the database. Like a value may be valid for multiple types But when saving, we selected one. It would be as easy as adding a constraint to the type TBH
kernel
kernelOPβ€’2y ago
personally I would like something that either has the type in the actual json - or something with the type tag on the resource, so in my case I can say this is transaction data for a 'email' transaction or a 'call' transaction and the type can just be a FK to the transactions table and if it's in the json, I would just something like this in json
{__type: 'email', ...restOfData}
{__type: 'email', ...restOfData}
ZachDaniel
ZachDanielβ€’2y ago
Yeah, that makes sense, but only for some unions i.e a union of only objects it works
kernel
kernelOPβ€’2y ago
yeah hadn't thought of the others πŸ™‚
ZachDaniel
ZachDanielβ€’2y ago
But still, we could make these storage options a constraint with little-to-no hassle πŸ™‚ Can you make a GH issue describing the behavior you’d want to get out of your unions? Changing the underlying storage of unions is easy.
kernel
kernelOPβ€’2y ago
it would be opt-in right? just because unions are already out in the wild
kernel
kernelOPβ€’2y ago
GitHub
configurable storage for union type Β· Issue #694 Β· ash-project/ash
Following on from our conversations on Discord, it would be helpful to be able to configure a simpler storage type for the union type. Ideally we would just store the json directly in the database ...
kernel
kernelOPβ€’2y ago
there we go
ZachDaniel
ZachDanielβ€’2y ago
Yep! It would be opt-in πŸ‘

Did you find this page helpful?