Today I Learned

hashrocket A Hashrocket project

259 posts about #elixir surprise

Shorthand Elixir Anonymous Functions

Elixir has a really cool syntax for writing anonymous functions (unnamed functions). It goes like this:

# The '&' operator is used to define the function and its arguments
putter = &(IO.puts &1)
putter.("Today I learned")
#...> Today I learned
#...> :ok

&1 refers to the functions first argument; in this example, we used “Today I learned". You can use more arguments with &2, &3, etc… For example:

combiner = &(&1 <> &2)
combiner.("Foo", "Bar")
#...> "FooBar"

Also notice that we are not calling either of these functions like function(arg1, arg2), instead we are calling them like function.(arg1, arg2). We have to use . because we are not actually naming these functions, we are only assigning them a reference, hence “anonymous" functions.

Pattern Match Keyword List in Function Def

TIL that you can pattern match a keyword list in a function definition.

Sometimes you’ll receive the last argument as an empty keyword list; in this case, we’re calling it opts. You can pattern match key-values by defining the shape in a function.

  defmodule Example do
    def hello(opts \\ [])
 
   # will match when message is the only keyword included in opts
    def hello([message: message]) do
      IO.puts("Hello #{message}")
    end

    # will match when there are multiple keywords but message is the first
    def hello([{:message, message} | _rest] = opts) do
      IO.puts("Hello #{message}")
    end
  end

> Example.hello(message: "World")
Hello World
:ok

# could also call in the more verbose way
> Example.hello([message: "World"])
Hello World
:ok

# :message but with other args after
> Example.hello(message: "World", something_else: "hi")
Hello World
:ok

Note: As a TIL reader pointed out, pattern matching Keywords will make your function args order dependent. The following would not work:

> Example.hello(foo: "bar", message: "World")
** (FunctionClauseError) no function clause matching in Example.hello/1 

If you need them to be order independent, use a map or just match on single argument, then check for each option appropriately with the Keyword module.

https://elixir-lang.org/getting-started/keywords-and-maps.html

Composite Primary Keys using Elixir Ecto

Ecto allows us to map a table with composite primary keys into our schemas. The way to do that is by using the primary_key: true option for the Ecto.Schema.field/3 instead of dealing with the special module attr @primary_key. Check this out:

defmodule MyApp.OrderItem do
  use Ecto.Schema

  @primary_key false
  schema "order_items" do
    field :product_id, :integer, primary_key: true
    field :order_id, :integer, primary_key: true
    field :quantity, :integer
  end
end

Identity Primary Keys on Ecto

Ecto, allow the type :identity to be used since 3.5 which is cool:

create table(:user, primary_key: false) do
  add :id, :identity, primary_key: true
  add :name, :string, null: false

  timestamps()
end

That generates this SQL:

CREATE TABLE public.users (
    id bigint NOT NULL,
    name character varying(255) NOT NULL,
    inserted_at timestamp(0) without time zone NOT NULL,
    updated_at timestamp(0) without time zone NOT NULL
);

ALTER TABLE public.users ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
    SEQUENCE NAME public.users_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);

The only issue is that there’s no option to change from BY DEFAULT to ALWAYS 😕

Phoenix component attr definition

The new Phoenix.Componentattr/3 function is awesome. It does compile time validations to all the places that are calling the component and also it comes with nice and useful options. For instance the default option:

attr :name, :string, default: "there"

def greet(assigns) do
  ~H"""
  

Hey <%= @name %>!

""" end

That’s very useful. That would totally replace most usages of assign_new like I used to do:

def greet(assigns) do
  assigns = assign_new(assigns, :name, fn -> "there" end)

  ~H"""
  

Hey <%= @name %>!

""" end

This way we can call:

<.greet />

And have this html generated:

Hey there!

Migrating Data in Ecto

I usually have created a mix task for data migrations to avoid putting the app down, but today I learned that ecto migrations accept an arg --migrations-path to their commands which allow us to have 2 separate folders for migrations.

With that we can easily use the default priv/repo/migrations folder for automatic migrations (for running on deployments) and a separate folder, let’s say priv/repo/data_migrations that we can run when it’s more convenient.

So in prod we run migrations on deploy and data_migrations on a quite time for the app to avoid downtime. And in dev and test env we just run them all as we usually have smaller dataset, so not a big deal.

Here’s a good post about this strategy.

Use created_at in Ecto

You can use created_at in Ecto/phoenix app with timestamps/1. When migrating data from a rails application to a phoenix application you will have many tables with a created_at column.

defmodule Phoenix.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :utc_datetime

    timestamps(inserted_at: :created_at, type: :utc_datetime)
  end

end

Elixir Compilation Cycles with `--fail-above`

Elixir 1.13.0 introduced a --fail-above flag for the mix xref task which will fail that task execution under a certain criteria.

With that we can verify, for example, that our code is ok to have 1 cycle that goes over length of 4, but not 2 cycles. Let’s see how it works:

$ mix xref graph --format cycles --min-cycle-size 4 --fail-above 1
2 cycles found. Showing them in decreasing size:

Cycle of length 6:

    lib/my_app_web/endpoint.ex
    lib/my_app_web/router.ex
    lib/my_app_web/views/layout_view.ex
    lib/my_app_web/live/components/sorting_link.ex
    lib/my_app_web/live/components/icon.ex
    lib/my_app_web/endpoint.ex

Cycle of length 5:

    lib/my_app_web/live/components/sorting_link.ex
    lib/my_app_web/live/components/icon.ex
    lib/my_app_web/router.ex
    lib/my_app_web/views/layout_view.ex
    lib/my_app_web/live/components/sorting_link.ex

** (Mix) Too many cycles (found: 2, permitted: 1)

In this case xref found 2 cycles with a length greater than 4, and as I allowed only 1 then we can see the error.

Elixir __struct__/0

When we define a struct via defstruct macro we end up getting a __struct__/0 function on both struct module definition and on each struct map. The intriguing part is that the implementation of the module and the map are different, check this out:

iex(1)> defmodule Book do
...(1)>   defstruct title: nil, pages_count: 0
...(1)> end

iex(2)> Book.__struct__()
%Book{pages_count: 0, title: nil}

iex(3)> Book.__struct__().__struct__()
Book

As we can see Book.__struct__() returns a new %Book{} struct with its defaults, meanwhile %Book{}.__struct() returns the Book module.

Elixir IEX multi-line command

A change on Elixir 1.12.0 made possible to pipe |> multi-line commands in iex where the |> operator is in the beginning of new lines.

That means that we can:

iex(1)> :foo
:foo
iex(2)>       |> to_string()
"foo"
iex(3)>       |> String.upcase()
"FOO"

The docs also mention that all other binary operators works the same way, except+/2 and -/2, so that’s also valid:

iex(1)> [:foo]
[:foo]
iex(2)> ++ [:bar]
[:foo, :bar]
iex(3)> |> Enum.join(" ")
"foo bar"
iex(4)> |> String.upcase()
"FOO BAR"

Each line will run at a new command, so if you assign a variable in the first line you may not have what you expect, so watch out.

Phoenix Live View enable Profiling

Phoenix LiveView has a way to enable Profiling in the client side by just adding this into the app.js file:

// app.js
liveSocket.enableProfiling();

That will enable a log into your browser console such as:

toString diff (update): 1.224853515625 ms
premorph container prep: 0.006103515625 ms
morphdom: 397.676025390625 ms
full patch complete: 411.117919921875 ms

In this case we can see that the morphdom library is taking a considerable time to apply my DOM patches as my page has a huge html table full of data.

BTW, this function adds to 2 other very useful ones for debugging the client:

  • enableDebug ()
  • enableLatencySim(ms)

Get the Values for a Ecto Schema Enum Column

I recently started learning Elixir and had a model with an enum column with the following attributes:

 schema "keyboards" do
   field :nickname, :string
   field :form_factor, Ecto.Enum, values: [:macro, :num, :custom, :split, :forty, :sixty, :sixty_five, :seventy_five, :tkl, :full]

   timestamps()
 end

In my view, I had a form object where I wanted to have a select input with the values from my enum column, form_factor. Luckily, the Ecto.Enum module has a few functions that can help with this - mappings/2, values/2, and dump_values/2.

I ended up using the following in my form:

  <%= label f, :form_factor %>
  <%= select f, :form_factor, Ecto.Enum.mappings(Keyboard, :form_factor)  %>
  <%= error_tag f, :form_factor %>

https://hexdocs.pm/ecto/Ecto.Enum.html

Get Elixir GenServer current state

Today I learned that we can use :sys.get_state/1 to get the current state of a GenServer.

Check this out:

iex(1)> pid = Process.whereis(MyApp.Repo)
iex(2)> :sys.get_state(pid)

{:state, {:local, MyApp.Repo}, :one_for_one,
 {[DBConnection.ConnectionPool],
  %{
    DBConnection.ConnectionPool => {:child, #PID<0.421.0>,
     DBConnection.ConnectionPool,
     {Ecto.Repo.Supervisor, :start_child,
      [
        {DBConnection.ConnectionPool, :start_link,
         [
           {Postgrex.Protocol,
            [
              types: Postgrex.DefaultTypes,
              repo: MyApp.Repo,
              telemetry_prefix: [:my_app, :repo],
              otp_app: :my_app,
              timeout: 15000,
              database: "my_app_dev",
              hostname: "localhost",
              port: 5432,
              show_sensitive_data_on_connection_error: true,
              pool_size: 10,
              pool: DBConnection.ConnectionPool
            ]}
         ]},
        MyApp.Repo,
        Ecto.Adapters.Postgres,
        %{
          cache: #Reference<0.1645792067.2416050178.91334>,
          opts: [
            timeout: 15000,
            pool_size: 10,
            pool: DBConnection.ConnectionPool
          ],
          repo: MyApp.Repo,
          sql: Ecto.Adapters.Postgres.Connection,
          telemetry: {MyApp.Repo, :debug, [:my_app, :repo, :query]}
        }
      ]}, :permanent, false, 5000, :worker, [Ecto.Repo.Supervisor]}
  }}, :undefined, 0, 5, [], 0, :never, Ecto.Repo.Supervisor,
 {MyApp.Repo, MyApp.Repo, :my_app, Ecto.Adapters.Postgres, []}}

Split a string in elixir

You can split a string in elixir with String.split

defmodule Phoenix.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def up do
    execute_sql("""
      create extension citext;

      create table users (
        id bigint generated by default as identity primary key,
        email citext unique not null,
        inserted_at timestamptz not null default now(),
        updated_at timestamptz not null default now()
      );
    """)
  end

  def down do
    execute_sql("""
      drop table users;
      drop extension citext;
    """)
  end
   

  def execute_sql(sql_statements) do
     sql_statements
     |> String.split(";")
     |> Enum.filter(fn s -> String.trim(s) != "" end)
     |> Enum.each(&execute/1)
  end
end

Elixir: Sandboxing an `iex` session

We can change the default .iex.exs file for another to be pre-loaded by iex using --dot-iex:

MIX_ENV=test iex --dot-iex .iex-sandbox.exs -S mix

This way we could create a rudimentary way to “protect" our existing database of changes on the iex session. Check this out:

# .iex-sandbox.exs
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo, ownership_timeout: 300_000)

This approach works for existing databases, so if the app we are working has a very difficult data setup then maybe this is the way to go. Although, making the db to hold data changes for as long as iex sessions are running it can be a bit too much in terms of db memory. Row & Table locks can definately be a problem as well.

Another important note is that we may have to call Sandbox.allow/3 if we need to call a GenServer or another process that touches the db.

The way to go

If we have a simpler data, with a nice seeds file, we’d be much better this way:

(export DB_NAME=my_app_sandbox && mix ecto.reset && iex -S mix)

Don’t forget to change our ecto config to use that ENV var.

Hibernating a GenServer

I was reading a great blog post from @cloud8421 this morning and I learned that Elixir GenServer has the capability to hibernate itself, which it means that it halts the continuous looping if there’s no incoming message and it runs a garbage collection to release some memory generated by that process.

Use that with moderation, and it’s always nice to evaluate if we really need to put a GenServer to hibernate, but in some cases it’s very handy.

How to force reload associations in Ecto

Ecto by default doesn’t load associations and you have to explicitly call preload:

id = 1
user = Repo.get(User, id) |> Repo.preload(:posts)

If you call preload again on the same user, Ecto won’t make the SQL query again because the association is already loaded:

user = user |> Repo.preload(:posts)

But in some cases you do want to reload a preloaded association then you can use force: true:

user = user |> Repo.preload(:posts, force: true)

Disable capture in Elixir Regex

Today I came across a regex that had to use the ( parenthesis to match a sequence of chars but I wanted just to match them, so I’d love if I could ignore that piece from the “capture" part.

So I learned that we could use ?: in the beginning of the ( and that would turn the capture off for that piece. Check this out;

iex> Regex.scan(~r/aa(bb)(?:cc|CC)/, "aabbccdd aabbCCdd")
[["aabbcc", "bb"], ["aabbCC", "bb"]]

In this case I am matching and capturing the bb sequence and I am matching but not capturing the cc|CC sequence options.

Preloading data for Phoenix LiveView components

One of the most common problems in web development is N+1. Graphql has taught me the DataLoader pattern and since then I never had to worry about N+1 as long as I kept using that pattern.

With Phoenix LiveView you don’t need to think about APIs anymore but I’m glad that it has support for preloading data kind of the same way as if it was a DataLoader. You can define a preload function that will receive a list of all the assigns so you can preload all the data needed for multiple components in one single batch before they are mounted. Here is the sequence of a component lifecycle:

preload(list_of_assigns) -> mount(socket) -> update(assigns, socket) -> render(assigns)

On this example only one SQL query is made to load all products by id:

@impl true
def preload(list_of_assigns) do
  product_ids = Enum.map(list_of_assigns, & &1.product_id)

  products =
    from(p in Product, where: p.id in ^product_ids, select: {p.id, p})
    |> Repo.all()
    |> Map.new()

  Enum.map(list_of_assigns, fn assigns ->
    Map.put(assigns, :product, products[assigns.product_id])
  end)
end

Elixir Supervisor default child_spec

When using Supervisor elixir already declares a child_spec/1 for us with the right type: :supervisor. The generated code will be something like:

defmodule MyApp.MySupervisor do
  use Supervisor, opts
  
  ...

  def child_spec(init_arg) do
    default = %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [init_arg]},
      type: :supervisor
    }

    Supervisor.child_spec(default, unquote(Macro.escape(opts)))
  end
end

Keep in mind that’s a pseudo-code as I left the Macro.escape for better understanding of the “expanded" code that happens on the __using__/1 function.

I tried to get this info from the docs, which I could not find that easily so I dig into the code, and as a good surprise this was so easy to find. Check this out.

By the way, the generated code is very similar to the GenServer one for the same function, the only difference is that on the GenServer there’s no :type key, and it works fine because the default value for the :type key is :worker.

Using `@describetag` in Elixir Tests

Wow! ExUnit have some special tags for @moduletag and @describetag which are self explanatory if you know what tags are used for in ExUnit. They are so useful in case you want to, for example, only run that section of tests:

defmodule MyApp.AccountsTest do
  use MyApp.DataCase

  @moduletag :module_tag

  describe "search" do
    @describetag :describe_tag

    @tag :test_tag
    test "return all users" do
      user_1 = insert!(User)
      user_2 = insert!(User)

      assert Accounts.search(User) == [user_1, user_2]
    end
    ...
  end
  ...
end

In case you want to run only tests that match with a module (file) level @moduletag:

my_app git:(main) ✗ mix test --only my_module_tag
Excluding tags: [:test]
Including tags: [:my_module_tag]
............
Finished in 0.2 seconds
22 tests, 0 failures, 10 excluded

Or if we want to run all tests that match with a describe block tagged with @describetag:

my_app git:(main) ✗ mix test --only my_describe_tag
Excluding tags: [:test]
Including tags: [:my_describe_tag]
....
Finished in 0.2 seconds
22 tests, 0 failures, 18 excluded

We can see by the number of excluded tests that this works great as expected.

Phoenix LiveView with some JSX flavor

Today I learned from Vinny about this Elixir library called Surface that allows you to write LiveView components that look a lot like JSX from React.

You first create a module that uses Surface.Component and define some props:

defmodule MyApp.WelcomeMessage do
  use Surface.Component

  prop name, :string, required: true

  def render(assigns) do
    ~F"""
    Hello, {@name}!
    """
  end
end

Then your LiveView module needs to use Surface.LiveView and you are ready to render that new “JSX" component passing some props:

defmodule MyApp.ExampleLive do
  use Surface.LiveView
  alias MyApp.WelcomeMessage

  def render(assigns) do
    ~F"""
    
      
    
    """
  end
end

Surface also handles state and has a way to handle portals, which are called slots. I might play with this library in one of my side-projects.

Nested attributes in Phoenix (Live) Views:

To use nested attributes in a form you have to use the inputs_for helper:

<%= f = form_for @changeset, Routes.user_path(@socket, :create), phx_submit: :submit, as: :user %>
  <%= input f, :name %>
  <%= inputs_for f, :address, fn a -> %>
    <%= input a, :city %>
  	<%= input a, :zipcode %>
	<% end %><
  <%= submit "Create" %>

And then you can use cast_assoc and specify the changeset for the nested schema you want to validate:

defmodule MyApp.User do
  def registration_changeset(user, attrs, opts \\ []) do
    user
      |> cast(attrs, [:name])
      |> cast_assoc(:address, with: &MyApp.Address.changeset/2)
      |> validate_required([:name, :address])
  end
end

defmodule MyApp.Address do
  def changeset(address, attrs, opts \\ []) do
    user
      |> cast(attrs, [:city, :zipcode])
      |> validate_required([:city, :zipcode])
  end
end

Session cookie with Phoenix LiveView

You can use phx-trigger-action to make sure a form submit goes through http instead through the socket.

On the example below, when @trigger_submit is true then the form will be sumitted to the path Routes.user_session_path(@socket, :create), which will hit the controller and set the cookie:

<%= f = form_for @changeset, Routes.user_session_path(@socket, :create), phx_trigger_action: @trigger_submit, phx_change: :validate, phx_submit: :submit, as: :user %>
  <%= input f, :email %>
  <%= input f, :password %>
  <%= submit "Sign In", disabled: !@changeset.valid? %>

Count Occurrences in Elixir

Elixir recently introduced an useful couple of functions to count how many times a value appears in an Enumerable. It comes in two formats: frequencies/1 and frequencies_by/2. Here’s an example:

iex> [
...>   %{name: "Falcon", power: "Flight"},
...>   %{name: "Titan Spirit", power: "Flight"},
...>   %{name: "Atom Claw", power: "Strength"},
...>   %{name: "Electro", power: "Electricity Control"},
...>   %{name: "Loki Brain", power: "Telekinesis"},
...> ] |> Enum.frequencies_by(& &1.power)
%{
  "Electricity Control" => 1,
  "Flight" => 2,
  "Strength" => 1,
  "Telekinesis" => 1
}

Elixir Date and Time conversion into US standards

Today I learned how to manually convert a Date and Time into US standards: mm/dd/YYYY and hh:mm am|pm. Here’s my code snippet:

defmodule Utils.Converter do
  def to_usa_date(%Date{day: day, month: month, year: year}) do
    "~2..0B/~2..0B/~4..0B"
    |> :io_lib.format([month, day, year])
    |> to_string()
  end

  def to_usa_time(%Time{} = time) do
    period = (time.hour < 12 && "am") || "pm"
    hour = time.hour |> rem(12)

    "~2..0B:~2..0B ~2..0s"
    |> :io_lib.format([hour, time.minute, period])
    |> to_string()
  end
end

This way I can convert dates like ~D[2020-05-29] into 05/29/2020" and times like ~T[11:00:07.001] into 11:00 am" and ~T[23:00:07.001] into 11:00 pm".

Here’s the erlang :io_lib documentation

Elixir Inspecting Big Structs and Lists

IO inspects limits your data by default in 50, so 50 Map keys and values, or 50 List items, etc. Sometimes you have a larger data, so you can play with this number by setting an option to the IO.inspect/2 function, or you can set it to :infinity. Use :infinity with moderation of course:

big_map = 1..100 |> Enum.map(&{ &1, "value #{&1}"}) |> Enum.into(%{})
IO.inspect(big_map)
IO.inspect(big_map, limit: 100)
IO.inspect(big_map, limit: :infinity)

Convert HTTPoison Request into curl command

Today I learned how to convert a HTTPoison Request into a curl command, useful for logs. This might be useful to someone else, so I am sharing:

defmodule MyHTTP do
  def curl(%HTTPoison.Request{} = req) do
    headers = req.headers |> Enum.map(fn {k, v} -> "-H \"#{k}: #{v}\"" end) |> Enum.join(" ")
    body = req.body && req.body != "" && "-d '#{req.body}'" || nil
    params = URI.encode_query(req.params || [])
    url = [req.url, params] |> Enum.filter(& &1 != "") |> Enum.join("?")

    [
      "curl -v -X",
      to_string(req.method),
      headers,
      body,
      url,
      ";"
    ]
    |> Enum.filter(& &1)
    |> Enum.map(&String.trim/1)
    |> Enum.filter(& &1 != "")
    |> Enum.join(" ")
  end
end

And here’s an example of usage:

MyHTTP.curl(%HTTPoison.Request{
  url: "https://til.hashrocket.com",
  params: [q: "elixir"]
})

"curl -v -X get https://til.hashrocket.com?q=elixir ;"

Print stacktrace in Elixir #ElixirLang

When you are trying to debug something it is usually helpful to see the stacktrace of what called a function.

If you’re inside of a try expression, and within the catch or rescue block, that’s easy - use the __STACKTRACE__ Special Form.

try do
	# NOPE
rescue
  ArgumentError ->
    IO.puts("Stacktrace #{inspect(__STACKTRACE__)}")
catch
  value ->
    IO.puts("Stacktrace #{inspect(__STACKTRACE__)}")
else
  # NOPE
after
  # NOPE
end

For all other cases the incantation is a little more complicated (and if you don’t think you can remember it, perhaps set up a snippet):

self()
|> Process.info(:current_stacktrace)
|> IO.inspect(label: "---------> stacktrace")

Ecto queries can be combined

Queries can take other queries to build on top of, which is very useful for conditional query logic:

def find_posts(group_id, title) do
  posts =
    from(posts in Phoenix.Post,
      where: posts.group_id == ^group_id,
    )
    |> title_search(title)
    |> Phoenix.Repo.all()
end

defp title_search(query, title) do
  query
    |> where([posts], posts.title == ^title)
end

defp title_search(query, nil) do
  query
end

How to remove unused deps from mix.lock

When you remove a dep from the mix.exs file it will remain in the mix.lock file. Unused deps need to be “unlocked" in order to be removed from the lock file.

For example:

mix deps.clean --unused

will only remove the unused dependencies’ files, but will keep the library in the mix.lock file. If you run the clean command the --unlock option, it will also remove it from the lock file.

For example:

mix deps.clean --unlock --unused

How to change the request body size in Phoenix

Plug.Parsers accepts a length: key with a value of approximate bytes to accept and can be used in an endpoint file.

# endpoint.ex
plug Plug.Parsers,
  pasrsers: [:urlencoded, :multipart],
  length: 100_000_000 # 100 MB body size (approximately)

You can also configure the length for a specific parser:

plug Plug.Parsers,
  parsers: [:urlencoded, {:multipart, length: 100_000_000}] # 100 MB body size (approximately)

Ecto can only execute 1 sql statement at a time

Ecto can only ever execute 1 sql statement at a time, by design. For performance concerns, every statement is wrapped in a prepared statement
*some “security" is a fringe benefit of prepare statements.

In regards to the performance concerns of the prepared statement, PostgreSQL will force re-analysis of the statement when the objects in the statement have undergone definitional changes (DDL), making its use in a migration useless.

An example of a migration if you need to perform multiple statements:

def up do
  execute("create extension if not exists \"uuid-ossp\";")
  execute("alter table schedules add column user_id uuid;")
  execute("create unique index on schedules (id, user_id);")
end

def down do
  execute("alter table schedules drop column user_id;")
  execute("drop extension if exists \"uuid-ossp\";")
end

What happens on Ecto.Repo.rollback?

The rollback function interupts the execution of the function that you pass to transaction, but what is the mechanism for interupting execution?

When you call rollback you are essentially calling this code:

throw({DBConnection, conn_ref, reason})

throw is no good without a catch and you can find the try ... catch in the DBConnection module as well. It can be boiled down to this:

try do
  # call function passed to Ecto.Repo.transaction
catch
  :throw, {__MODULE__, ^conn_ref, reason} ->
    # send rollback request to database and return with {:error, reason}
    return {:error, reason}
  kind, reason ->
    # send rollback request to database and raise with reason
    :erlang.raise(kind, reason, stack)
end

This code exploits the ability of catch to both handle a throw and handle a raise.

`ets` table gets deleted when owning process dies

You can create a new ets table with:

:ets.new(:chris_table, [:named_table])

And you can confirm it was created with:

:ets.info(:chris_table)
[
id: #Reference<0.197283434.4219076611.147360>,
...
]

Now check this:

spawn(fn -> :ets.new(:spawn_table, [:named_table]) end)

Let’s see if it was created:

:ets.info(:spawn_table)
# returns :undefined

What gives? The erlang ets docs say this:

> Each table is created by a process. When the process terminates, the table is automatically destroyed.

So, spawn created the process and then terminated, so :spawn_table got deleted when the process died.

Get a writable temporary directory in Elixir

When you need to write a temporary file to disk it is common to assume a particular path exists based on the operating system the app is running on:

tmp_file = Path.join("/tmp", filename)
File.write!(tmp_file)

However depending on the configuration of the app as it is deployed in production, it may not have access to the /tmp directory. Fortunately Elixir provides a function that returns a writable path for temporary files:

dir = System.tmp_dir!()
tmp_file = Path.join(dir, filename)
File.write!(tmp_file)

From the docs (h System.tmp_dir):

Writable temporary directory.

Returns a writable temporary directory. Searches for directories in the following order:

    1. the directory named by the TMPDIR environment variable
    2. the directory named by the TEMP environment variable
    3. the directory named by the TMP environment variable
    4. C:\TMP on Windows or /tmp on Unix
    5. as a last resort, the current working directory

Returns nil if none of the above are writable.

I prefer to use the “bang" version System.tmp_dir! described as:

> Same as tmp_dir/0 but raises RuntimeError instead of returning nil if no temp dir is set.

On my machine it returns something like this:

"/var/folders/66/dj7rwns53vn4db4_1npvqtrh0000gn/T/"