Lunarmask

Lunarmask

Is there any way to use Ecto fragment/1 in config.exs?

Hey everyone,

I’m wanting to apply a query fragment within the Phoenix application’s config.exs in order to properly pass a default value to the Ecto.Migration runner.

my_app/config/config.exs is setup like so:

config :my_app, MyApp.Repo,
  migration_primary_key: [name: :id, type: :uuid, default: "gen_random_uuid()"],
  migration_foreign_key: [column: :id, type: :uuid]

I’m trying to have the default :primary_key of every new table use UUID and have a default value that generates a random uuid.

And migrating produces this SQL statement in Ecto.Adapter, but it results in an error from the database. (using Postgres)

CREATE TABLE "users" ("id" uuid DEFAULT 'gen_random_uuid()', "name" varchar(255) NOT NULL, "type" integer NOT NULL, "inserted_at" timestamp(0) NOT NULL, "updated_at" timestamp(0) NOT NULL, PRIMARY KEY ("id"))

** (Postgrex.Error) ERROR 22P02 (invalid_text_representation) Invalid UUID: incorrect size

The migration is so very close, but sadly invalid because the default value is being passed as a fixed string but needs to be a Ecto.Query.API.fragment/1.

And sadly due to the config not having any dependencies available to pull from, importing the necessary Module to do this.

    error: module Ecto.Query.API is not loaded and could not be found
    │
    │ import Ecto.Query.API
    │ ^^^^^^^^^^^^^^^^^^^^^
    │
    └─ config/config.exs:10

It would be really awesome to have these migrations automatically have a working default value for primary ids without needing to manually apply them in every new migration.

If there is no way to do this in the Config, does anyone have a galaxy-brained solution for the outcome I’m looking for?

Thanks

Marked As Solved

Lunarmask

Lunarmask

Holy smokes I just figured it out! :exploding_head:

I was looking into the Ecto code that handles migrations and I found this the area that manages the Primary Key Configuration logic.

deps/ecto_sql/lib/ecto/migration.ex:1660

  @doc false
  def __primary_key__(table) do
    case table.primary_key do
      false ->
        false

      true ->
        case Runner.repo_config(:migration_primary_key, []) do
          false -> false
          opts when is_list(opts) -> pk_opts_to_tuple(opts)
        end

      opts when is_list(opts) ->
        pk_opts_to_tuple(opts)

      _ ->
        raise ArgumentError,
              ":primary_key option must be either a boolean or a keyword list of options"
    end
  end

  defp pk_opts_to_tuple(opts) do
    opts = Keyword.put(opts, :primary_key, true)
    {name, opts} = Keyword.pop(opts, :name, :id)
    {type, opts} = Keyword.pop(opts, :type, :bigserial)
    {name, type, opts}
  end

This is the logic block that pulls the migration_primary_key: [] from the configuration file. Pretty straight forward. Pulls out the :name and :type with a default value if the keys are not defined.

So with that found I was curious to see how the remaining opts are being parsed and coerced before the migration runs.

deps/ecto_sql/lib/ecto/migration.ex:1225

  @doc """
  Generates a fragment to be used as a default value.

  ## Examples

      create table("posts") do
        add :inserted_at, :naive_datetime, default: fragment("now()")
      end
  """
  def fragment(expr) when is_binary(expr) do
    {:fragment, expr}
  end

Finding that was WILD! It means we can likely just pass a tuple with the :fragment key in the config file!?

Tested it out and it worked!!!

priv/repo/migrations/create_users.exs

  def change do
    create table(:users) do
      add :name, :string
      add :type, :integer


      timestamps()
    end
  end

Produces this SQL statement during migration.

CREATE TABLE "users" ("id" uuid DEFAULT gen_random_uuid(), "name" varchar(255) NOT NULL, "type" integer NOT NULL, "inserted_at" timestamp(0) NOT NULL, "updated_at" timestamp(0) NOT NULL, PRIMARY KEY ("id"))

Which results in this Postgres table definition with the default value properly set!

my_app_dev=# \d users
                         Table "public.users"
    Column    |            Type             | Collation | Nullable |  Default
--------------+-----------------------------+-----------+----------+------------
 id           | bytea                       |           | not null | gen_random_uuid()
 name         | character varying(255)      |           |          |
 type         | integer                     |           |          |
 inserted_at  | timestamp without time zone |           | not null |
 updated_at   | timestamp without time zone |           | not null |

Absolutely perfect, I’m very stoked to get this knowledge gap figured out. :tada: :tada:

Also Liked

brettbeatty

brettbeatty

If you want to fiddle with deps like that you don’t need to nuke _build, you can just run tell mix to recompile that specific dependency.

mix deps.compile ecto_sql
LostKobrakai

LostKobrakai

There’s also CLI flags for that on mix ecto.migrate: mix ecto.migrate — Ecto SQL v3.12.1

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement