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