lib/kino/ets.ex

defmodule Kino.ETS do
  @moduledoc """
  A kino for interactively viewing an ETS table.

  ## Examples

      tid = :ets.new(:users, [:set, :public])
      Kino.ETS.new(tid)

      Kino.ETS.new(:elixir_config)
  """

  @behaviour Kino.Table

  @type t :: Kino.Table.t()

  @doc """
  Creates a new kino displaying the given ETS table.

  Note that private tables cannot be read by an arbitrary process,
  so the given table must have either public or protected access.
  """
  @spec new(:ets.tid()) :: t()
  def new(tid) do
    case :ets.info(tid, :protection) do
      :private ->
        raise ArgumentError,
              "the given table must be either public or protected, but a private one was given"

      :undefined ->
        raise ArgumentError,
              "the given table identifier #{inspect(tid)} does not refer to an existing ETS table"

      _ ->
        :ok
    end

    Kino.Table.new(__MODULE__, {tid})
  end

  @impl true
  def init({tid}) do
    table_name = :ets.info(tid, :name)
    name = "ETS #{inspect(table_name)}"
    info = %{name: name, features: [:refetch, :pagination]}
    {:ok, info, %{tid: tid}}
  end

  @impl true
  def get_data(rows_spec, state) do
    records = get_records(state.tid, rows_spec)
    data = Enum.map(records, fn record -> [inspect(record)] end)
    total_rows = :ets.info(state.tid, :size)
    columns = [%{key: 0, label: "row", type: "tuple"}]
    {:ok, %{columns: columns, data: {:rows, data}, total_rows: total_rows}, state}
  end

  defp get_records(tid, rows_spec) do
    query = :ets.table(tid)
    cursor = :qlc.cursor(query)

    if rows_spec.offset > 0 do
      :qlc.next_answers(cursor, rows_spec.offset)
    end

    records = :qlc.next_answers(cursor, rows_spec.limit)
    :qlc.delete_cursor(cursor)
    records
  end
end