Packages

Depo provides lightweight storage and querying capabilities in Elixir by providing a minimal and polished API that builds on the unique advantages of SQLite.

Current section

Files

Jump to
depo lib depo_db.ex
Raw

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
@doc """
Bind the values to the statement, execute it,
and return a stream of the row values.
"""
def bind_stream(stmt, values) do
:ok = :esqlite3.bind(stmt, values)
Stream.unfold(stmt, fn stmt ->
case :esqlite3.step(stmt) do
{:row, row} -> {row, stmt}
_ -> nil
end
end)
end
# Bind the values to variables, then run the query.
defp bind_query(stmt, values) do
keys = :esqlite3.column_names(stmt)
# Convert the rows into maps with column names as keys.
bind_stream(stmt, values)
|> Stream.map(fn row ->
Enum.reduce(0..(tuple_size(keys)-1), %{},
fn(i, m) ->
Map.put(m, elem(keys, i), elem(row, i))
end)
end)
|> Enum.into([])
end
# Get a stream of results from a compiled SQL statement.
defp results_stream(stmt) do
keys = :esqlite3.column_names(stmt)
# Convert the row stream into a map stream and return it.
rows_stream(stmt)
|> Stream.map(&(row_to_map(&1, keys)))
end
# Get a stream of row tuples from the compiled statement.
defp rows_stream(stmt) do
Stream.unfold(stmt, fn stmt ->
case :esqlite3.step(stmt) do
{:row, row} -> {row, stmt}
_ -> nil
end
end)
end
# Convert a tuple of row values and a list of keys to a map.
defp row_to_map(row, keys) do
columns = 0..(tuple_size(keys)-1)
Enum.reduce(columns, %{}, fn(i, m) ->
Map.put(m, elem(keys, i), elem(row, i))
end)
end
# Get a list of results from a compiled statement.
defp results_list(stmt) do
stmt
|> results_stream()
|> Enum.into([])
|> Enum.reverse()
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, cmd}, state)
when is_atom(cmd) do
if Map.has_key?(state.cache, cmd) do
:"$done" = :esqlite3.step(state.cache[cmd])
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)
Stream.run(bind_stream(stmt, values))
{:noreply, state}
end
def handle_cast({:write, cmd, values}, state)
when is_atom(cmd) do
if Map.has_key?(state.cache, cmd) do
stmt = state.cache[cmd]
Stream.run(bind_stream(stmt, values))
end
{:noreply, state}
end
def handle_cast({:teach, cmds}, state)
when is_map(cmds) do
# Prepare the statements and add them to the cache.
state = Map.update!(state, :cache, fn cache ->
Enum.reduce(cmds, 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, results_list(stmt), state}
end
end
def handle_call({:read, cmd}, _from, state)
when is_atom(cmd) do
if Map.has_key?(state.cache, cmd) do
stmt = state.cache[cmd]
{:reply, results_list(stmt), state}
else
{:reply, {:error, :unknown_command}, state}
end
end
def handle_call({:read, sql, values}, _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, bind_query(stmt, values), state}
end
end
def handle_call({:read, query, values}, _from, state)
when is_atom(query) do
if Map.has_key?(state.cache, query) do
stmt = state.cache[query]
{:reply, bind_query(stmt, values), state}
else
{:reply, {:error, :unknown_query}, state}
end
end
def handle_call({:stream, pid, query}, _from, state)
when is_binary(query) do
case :esqlite3.prepare(to_char_list(query), state.db) do
{:error, _} ->
{:noreply, state}
{:ok, stmt} ->
{:reply, spawn_streamer(pid, stmt), state}
end
end
def handle_call({:stream, pid, query}, _from, state)
when is_atom(query) do
if Map.has_key?(state.cache, query) do
stmt = state.cache[query]
{:reply, spawn_streamer(pid, stmt), state}
else
{:reply, {:error, :unknown_query}, state}
end
end
def handle_call({:stream, pid, query, values}, _from, state)
when is_binary(query) do
case :esqlite3.prepare(to_char_list(query), state.db) do
{:error, _} ->
{:noreply, state}
{:ok, stmt} ->
:ok = :esqlite3.bind(stmt, values)
{:reply, spawn_streamer(pid, stmt), state}
end
end
def handle_call({:stream, pid, query, values}, _from, state)
when is_atom(query) do
if Map.has_key?(state.cache, query) do
stmt = state.cache[query]
:ok = :esqlite3.bind(stmt, values)
{:reply, spawn_streamer(pid, stmt), state}
else
{:reply, {:error, :unknown_query}, state}
end
end
# Stream the results from an isolated process.
defp spawn_streamer(pid, stmt) do
spawn_link(fn ->
results_stream(stmt)
|> Enum.each(fn result ->
send(pid, {self(), result})
end)
end)
end
end