DEV Community

NDREAN
NDREAN

Posted on • Edited on

TIL : "select" queries with ETS

The following notes are complementary to this ETS lesson with a focus on "ets.select / match_spec" and the Ex2ms library to use with ETS, the Erlang built-in in-memory database.

Suppose we have an ETS table that saves messages exchanged between two users. The records will be in the form:

{timestamp, user, receiver, msg}
Enter fullscreen mode Exit fullscreen mode

We will use match_spec for .select based queries.

Remove "old" messages

We want to run periodical cleanups to limit the size of the table. We use the Erlang-ETS function :ets.select_delete/2 that takes the table name (we named it in the options) and a match_spec.

A record to be deleted should have a "match_spec" that returns true whenever this record matches. A match here is when the first element of the record - the timestamp - is lower than a given constant.
For example, if the constant is 1, a "match_spec" to be used in the function :ets.select_delete is:

ms = 
[{
  {:"$1", :"$2", :"$3", :"$4"},
  [
   {:<, :"$1", {:const, 1}}
  ], 
  [true]
}]
Enter fullscreen mode Exit fullscreen mode

and use it: :ets.select_delete(:table, ms).

We can test this against the table with the function :ets.test_ms/2. It will detect errors if any.

We can advantageously use the package Ex2ms to rewrite this into a function form with Ex2ms.fun. It is more readable, but also allows you to use variables in the scope. Add the package to the MixProject.

With this package, the "match_spec" is built by passing a function to Ex2ms.fun that returns the same "match_spec":

ms = Ex2ms.fun do
  {t, _e, _u, _r, _m} when t < 1 -> true
end
Enter fullscreen mode Exit fullscreen mode

you can use the built-in function fm2ms when you use constants, but when you want to use variables in scope, then you should use the Ex2ms package.

We can use a variable in scope and use this "match_spec" in the :ets.select_delete function:

# module MyApp.ChatCache
import Ex2ms, only: [fun: 1]

def clean_up(delay) do
  some_time_ago = System.os_time(:second) - delay * 60

  ms =
    fun do
      {t, _e, _u, _r, _m} when t < ^some_time_ago -> true
    end

  :ets.select_delete(:chat, ms)
end
Enter fullscreen mode Exit fullscreen mode

This function returns the number of records deleted.

Another example

We want to :ets.select from our table all the messages exchanged between a couple of users. In other words, we want the records where the user and receiver - the second and third element (of the record) - are equal to some values, regardless of the order.
Given two constants values a and b that represent our couple of users, our constraint is:

($2 == "a" and  $3 == "b") or ($2 == "b" and $3 == "a")
Enter fullscreen mode Exit fullscreen mode

Such a "match_spec" would be written (carefully!) in Erlang for our table:

[{
  {:"$1", :"$2", :"$3", :"$4"},
  [{
    :orelse,
      {:andalso, 
        {:==, :"$2", {:const, "a"}},
        {:==, :"$3", {:const, "b"}}
      },
      {:andalso, 
        {:==, :"$2", {:const, "b"}},
        {:==, :"$3", {:const, "a"}}
      }
  }], 
  [{
    {:"$1", :"$2", :"$3", :"$4"}
  }]
}]
Enter fullscreen mode Exit fullscreen mode

The list of Erlang's allowed function descriptions is here. Not so easy? Ex2ms makes it easy:

import Ex2ms, only: [fun: 1]

def get_messages_by_channel(val_e, val_r) do

  ms = fun do {t, e, r, m} when
      (e == ^val_e and r == ^val_r) 
      or (e == ^val_r and r == ^val_e) 
    -> {t, e, r, m}

  :ets.select(:chat, ms)
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)