How to I dump an Elixir tuple into a PostgreSQL composite type?

Hello :wave:

I’m using Ecto along with PostgreSQL. I’ve been trying to add a field containing a 2-tuple to an Ecto schema, but can’t seem to make it work.

There is no built-in :tuple type in Ecto, so I tried to create an Ecto.Type, but that does not seem to be enough:

defmodule MyType do
  use Ecto.Type

  @impl Ecto.Type
  def type, do: :my_type

  @impl Ecto.Type
  def cast({one, two}) when is_binary(one) and is_binary(two), do: {:ok, {one, two}}
  def cast(_), do: :error

  @impl Ecto.Type
  def cast({one, two}) when is_binary(one) and is_binary(two), do: {:ok, {one, two}}
  def cast(_), do: :error

  @impl Ecto.Type
  def load({one, two}) do
    {:ok, {one, two}}
  end
end

I create the field in an Ecto migration using this command:

execute "CREATE TYPE my_type AS (one text, two text)"

Then, when trying to insert a struct from a schema with a field of this type, I get the following message:

(DBConnection.EncodeError) cannot encode anonymous tuple {"one", "two"}. Please define a custom Postgrex extension that matches on its underlying type:

    use Postgrex.BinaryExtension, type: "typeinthedb"

I tried to follow the instructions and create an extension, but could never make it work.

What blows my mind is that the library :ex_money_sql (cc @kip) does not seem to have to create a Postgrex extension (or at least, I could never find it). See type source. Postgrex is pretty clear on the subject: “Anonymous composite types are decoded (read) as tuples but they cannot be encoded (written) to the database” (doc). So how does it work? Why does it work? I’m a bit lost here :confused:

If you see any flaw in my reasoning, please tell me what I’m doing wrong :pray:
Thanks for your time,

1 Like

There’s no dump/1 callback in your type. Not sure if that’s related, but should be fixed non the less.

1 Like

There is a mix task that defines the money_with_currency type. Hopefully that at least clears that up :slight_smile:

1 Like

Oh, I read a bit more closely. I suspect, as @LostKobrakai says, the lack of a dump/2 callback is the issue.