lib/archeometer/repo.ex

defmodule Archeometer.Repo do
  @moduledoc """
  Common utilities for interacting with the database.
  """

  @db_prefix "archeometer_"
  @db_file_format ".db"

  alias Exqlite.Sqlite3, as: DB
  alias Archeometer.{Query, Repo.Result, Explore.Project}

  def default_db_name() do
    env_db = Application.fetch_env(:archeometer, :default_db)

    cond do
      is_archeometer_test?() ->
        Application.fetch_env!(:archeometer, :test_db)

      env_db == :error ->
        @db_prefix <> Project.get!() <> @db_file_format

      {:ok, db} = env_db ->
        db
    end
  end

  def execute_raw_query(conn, query, bindings) do
    {:ok, query} = DB.prepare(conn, query)
    :ok = DB.bind(conn, query, bindings)
    {:ok, rows} = DB.fetch_all(conn, query)
    DB.release(conn, query)
    rows
  end

  def execute_raw(conn, query, bindings) do
    {:ok, query} = DB.prepare(conn, query)
    :ok = DB.bind(conn, query, bindings)
    :done = DB.step(conn, query)
    DB.release(conn, query)
  end

  @doc """
  Execute the given query and returns a `Archeometer.Result` structure with all
  rows.

  The query can be a raw string to be executed directly with the given bindings.
  In that case the resulting structure won't have header information.

  The query can also be a tuple `{:ok, %Archeometer.Query{}}`. Consecuentyl the
  result of `Archeoemter.Query.from` can be piped directly. In this case the
  `bindings` field will be ignored, as the bindings are stored on the `Query`
  structure.
  """
  def all(query, bindings \\ [], db_name \\ default_db_name())

  def all(query, bindings, db_name)
      when is_bitstring(query) do
    with {:ok, conn} <- DB.open(db_name) do
      try do
        %Result{
          rows: execute_raw_query(conn, query, bindings)
        }
      after
        DB.close(conn)
      end
    end
  end

  def all(query, _bindings, db_name) do
    with {:ok, query_string} <- Query.Serializer.to_sql(query) do
      all(query_string, [], db_name)
      |> Map.put(:headers, build_query_headers(query))
    end
  end

  defp build_query_headers({:ok, %Query{select: exprs}}) do
    exprs
    |> Enum.concat()
    |> Enum.map(fn
      {key, _val} -> key
      expr -> expr |> Query.Term.to_ast() |> Macro.to_string()
    end)
  end

  defmacro __using__(_opts) do
    quote do
      alias Exqlite.Sqlite3, as: DB

      defp default_db_name(), do: Archeometer.Repo.default_db_name()

      defp execute(conn, query, bindings),
        do: Archeometer.Repo.execute_raw(conn, query, bindings)

      defp execute_query(conn, query, bindings),
        do: Archeometer.Repo.execute_raw_query(conn, query, bindings)
    end
  end

  defp table_exists?(table_name, db_name) do
    {:ok, conn} = DB.open(db_name)

    {:ok, statement} =
      DB.prepare(
        conn,
        "SELECT name
         FROM sqlite_master
         WHERE type='table'
         AND name=?;"
      )

    :ok = DB.bind(conn, statement, [table_name])

    row = DB.step(conn, statement)
    DB.release(conn, statement)
    DB.close(conn)

    case row do
      :done -> false
      {:row, _} -> true
    end
  end

  def db_ready?(mode, db_name \\ default_db_name())

  def db_ready?(:basic, db_name) do
    File.exists?(db_name) and db_has_tables?(["modules", "functions", "macros"], db_name)
  end

  def db_ready?(:full, db_name) do
    db_ready?(:basic, db_name) and db_has_tables?(["xrefs", "behaviours", "apps"], db_name)
  end

  def db_has_tables?(tables, db_name \\ default_db_name()) do
    Enum.all?(tables, &table_exists?(&1, db_name))
  end

  defp is_archeometer_test?() do
    Mix.env() == :test and Keyword.get(Mix.Project.config(), :app) == :archeometer
  end
end