How to do a while like statement in Elixir?

I need basically to generate a random string to be inserted as value for a field called url_prefix in the DB on new record creation.

I want to ensure that the generated string is unique i.e. is not associated with any record in the DB’s table.

So I want to generate it, test if it is not already stored in DB, then make the INSERT query.

I have to instruct Elixir to keep generating a random string until the string on hand is unique.

Other languages has while statements which could be used for such scenario. How to do this in Elixir?

All this in the context of Phoenix.

Implement an infinite stream of id‘s and use reduce_while to reduce over it until a db insert is successful.

1 Like

You could do this in the db, assuming piostgres

defmodule App.Repo.Migrations.AddUUIDToSomeRecord do
  use Ecto.Migration

  def change do
    create table(:some_records) do
      add(:random_id_field, :uuid, default: fragment("uuid_generate_v4()"))
    end
  end
end
4 Likes

Otherwise, I’d do it recursively

defmodule MyMod do
  
  def try_generate_and_save_thing(:try_again, data) do
   random = random_string(64)
   case SomeOtherMod.save(data, random) do
     true ->
      :done
    false ->
      :try_again
    end
    |> try_generate_and_save_thing(data)
  end

  def try_generate_and_save_thing(data, :done), do: {:ok, data} 

  defp random_string(length) do
    :crypto.strong_rand_bytes(length) |> Base.url_encode64 |> binary_part(0, length)
  end
end
4 Likes

This is not an atomic operation, therefore you need to either find a way to do it on the database, or acquire a full table lock.

Anyway, then it’s either the infinite stream or recursion.

4 Likes

Doing in the db seems like it’s probably the best choice for most use cases, IMO.

5 Likes

Maybe a UUID would work better

3 Likes

I did it in the database using built in function and the value gets generated in the field in the database but Phoenix is not aware of that value, returns it to the front-end as null on creation.

Yes, this is how databases work… You’ll need to read after write.

1 Like

So maybe the UUID suggestion will work for you if you need to get the value before the insert.
Something like:

  def changeset(struct, params \\ %{}) do
    struct
    |> Map.update(:uuid, Ecto.UUID.generate,
       fn value -> value || Ecto.UUID.generate end)
    |> ....
  end
1 Like

Ecto has built in support for reading fields after writes, when using postgres:

field :id, :uuid, primary_key: true, read_after_writes: true

11 Likes

Ah ok that is nice. ^^

1 Like

This is an excellent approach

Recently I needed to get uuid before saving to database, for use with Arc Ecto. I did not set uuid in the db, but in the changeset.

@primary_key {:id, :binary_id, autogenerate: false}
@foreign_key_type Ecto.UUID
schema "pictures" do
  ...
end

def changeset(picture, attrs) do
  picture
  |> cast(attrs, [:name])
  |> ensure_uuid(:id)
  |> cast_attachments(attrs, [:display_image])
  |> validate_required([:name, :display_image])
end

defp ensure_uuid(changeset, field) do
  case get_field(changeset, field) do
    nil -> changeset |> put_change(field, Ecto.UUID.generate())
    _ -> changeset
  end
end

This way I could get uuid before saving to db, and use it to generate an Arc Ecto local storage path. It is working well in this situation.

2 Likes