🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir ETS Continuations

An ETS continuation is a bookmark that lets a select walk a table page by page. Each call returns one page of rows plus the bookmark for the next page.

Take the first page of a table

An ordered set table holds five rows. Select with a limit of two returns the first two keys.

table = :ets.new(:pages, [:ordered_set, :public])
:ets.insert(table, [{1, "a"}, {2, "b"}, {3, "c"}, {4, "d"}, {5, "e"}])
spec = [{{:"$1", :"$2"}, [], [:"$1"]}]
{page, cont} = :ets.select(table, spec, 2)
IO.inspect(page)
IO.inspect(cont == :"$end_of_table")
:ets.delete(table)

Walk pages until the end marker

Each continuation call returns the next page. The last page pairs its rows with the end marker.

table = :ets.new(:walk, [:ordered_set, :public])
:ets.insert(table, [{1, "a"}, {2, "b"}, {3, "c"}, {4, "d"}, {5, "e"}])
spec = [{{:"$1", :"$2"}, [], [:"$1"]}]
{_page1, cont1} = :ets.select(table, spec, 2)
{page2, cont2} = :ets.select(cont1)
IO.inspect(page2)
{page3, cont3} = :ets.select(cont2)
IO.inspect(page3)
IO.inspect(cont3 == :"$end_of_table")
:ets.delete(table)

A guard filters rows before paging

A guard in the match spec keeps only rows whose value is above 25. Paging then walks only the kept rows.

table = :ets.new(:filtered, [:ordered_set, :public])
:ets.insert(table, [{1, 10}, {2, 20}, {3, 30}, {4, 40}, {5, 50}])
spec = [{{:"$1", :"$2"}, [{:>, :"$2", 25}], [:"$1"]}]
{page1, cont1} = :ets.select(table, spec, 2)
IO.inspect(page1)
{page2, cont2} = :ets.select(cont1)
IO.inspect(page2)
IO.inspect(cont2 == :"$end_of_table")
:ets.delete(table)

Example

defmodule Pager do
  def all(table, spec, limit) do
    {page, cont} = :ets.select(table, spec, limit)
    collect(page, cont)
  end
  defp collect(rows, :"$end_of_table"), do: rows
  defp collect(rows, cont) do
    {page, next} = :ets.select(cont)
    collect(rows ++ page, next)
  end
end
table = :ets.new(:all_rows, [:ordered_set, :public])
:ets.insert(table, [{1, "a"}, {2, "b"}, {3, "c"}, {4, "d"}, {5, "e"}])
spec = [{{:"$1", :"$2"}, [], [{{:"$1", :"$2"}}]}]
rows = Pager.all(table, spec, 2)
IO.inspect(rows)
IO.puts("count: #{length(rows)}")
:ets.delete(table)

Key points

  • Continuation: a bookmark returned with a page of select results.
  • Use :ets.select/3 with a limit to take only the first page of rows.
  • Use :ets.select/1 with a continuation to take the next page.
  • A page result pairs the rows with either a new continuation or the end marker.
  • Use ordered_set tables when page order must follow key order.

In this example, Pager takes the first page with a limit of two, then keeps taking pages and joining them until the end marker arrives. The hand-written match spec returns each full row, so the collected result holds all five rows in key order. The final line prints the count of the collected rows.

Note

never print a continuation itself, since it holds internal references.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A select with limit 2 over keys 1 to 5 returns the first page [1, 2] with a continuation. What does the next continuation call return?