Current section
Files
Jump to
Current section
Files
lib/depo_db.ex
defmodule Depo.DB do
@moduledoc """
A DB is a GenServer that manages an open database connection
and a cache of prepared SQL statements.
"""
use GenServer
def init(:memory) do
{:ok, db} = :esqlite3.open(':memory:')
{:ok, %{
# An open connection to the database.
db: db,
# An atom-keyed cache of named prepared SQL statements.
cache: %{}
}}
end
def init(create: path) when is_binary(path) do
if File.exists?(path) do
{:stop, :file_already_exists}
else
{:ok, db} = path
|> to_charlist()
|> :esqlite3.open()
{:ok, %{
# An open connection to the database.
db: db,
}}
end
end
def init(path) when is_binary(path) do
if File.exists?(path) do
{:ok, db} = path
|> to_charlist()
|> :esqlite3.open()
{:ok, %{
# An open connection to the database.
db: db,
}}
else
{:stop, :no_such_file}
end
end
def terminate(_reason, %{db: db}) do
:ok = :esqlite3.close(db)
end
def handle_cast({:write, sql}, state)
when is_binary(sql) do
:esqlite3.exec(to_charlist(sql), state.db)
{:noreply, state}
end
def handle_cast({:write, command}, state)
when is_atom(command) do
if Map.has_key?(state.cache, command) do
:"$done" = :esqlite3.step(state.cache[command])
end
{:noreply, state}
end
def handle_cast({:write, sql, values}, state)
when is_binary(sql) do
{:ok, stmt} = :esqlite3.prepare(to_charlist(sql), state.db)
:ok = :esqlite3.bind(stmt, values)
:"$done" = :esqlite3.step(stmt)
{:noreply, state}
end
def handle_cast({:write, command, values}, state)
when is_atom(command) do
if Map.has_key?(state.cache, command) do
stmt = state.cache[command]
:ok = :esqlite3.bind(stmt, values)
:"$done" = :esqlite3.step(stmt)
end
{:noreply, state}
end
def handle_cast({:teach, commands}, state)
when is_map(commands) do
# Prepare the statements and add them to the cache.
state = Map.update!(state, :cache, fn cache ->
Enum.reduce(commands, cache, fn {name, sql}, cache ->
true = is_atom(name) && is_binary(sql)
{:ok, stmt} = :esqlite3.prepare(to_charlist(sql), state.db)
Map.put(cache, name, stmt)
end) |> Enum.into(%{})
end)
{:noreply, state}
end
def handle_call({:read, sql}, _from, state)
when is_binary(sql) do
case :esqlite3.prepare(to_char_list(sql), state.db) do
{:error, reason} ->
{:reply, {:error, reason}, state}
{:ok, stmt} ->
{:reply, get_values(stmt), state}
end
end
def handle_call({:read, command}, _from, state)
when is_atom(command) do
if Map.has_key?(state.cache, command) do
{:reply, get_values(state.cache[command]), state}
else
{:reply, {:error, :unknown_command}, state}
end
end
defp get_values(stmt) do
keys = :esqlite3.column_names(stmt)
case all_rows(stmt) do
{:error, reason} ->
{:error, reason}
{:ok, rows} ->
# Convert the rows into maps with column names as keys.
Enum.map(rows, fn row ->
Enum.reduce(0..(tuple_size(keys)-1), %{}, fn(i, m) ->
Map.put(m, elem(keys, i), elem(row, i))
end)
end)
end
end
defp all_rows(stmt) do
all_rows(stmt, [])
end
defp all_rows(stmt, rows) do
case :esqlite3.step(stmt) do
{:row, values} ->
all_rows(stmt, [values | rows])
:"$done" -> {:ok, rows}
:"$busy" -> {:error, :busy}
end
end
end