Current section
Files
Jump to
Current section
Files
lib/depo.ex
defmodule Depo do
@moduledoc """
Depo provides lightweight storage and querying capabilities
in Elixir by providing a minimal and polished API
that builds on the unique advantages of SQLite.
There are many ways to use Depo, but the recommended
way is to use `teach/2` and `transact/2`so that
you can reap the performance benefit of [SQLite transactions
](https://sqlite.org/transactional.html) and
pre-compiled SQL statements.
It also has the benefit of forming a sort of
DSL for your database.
Here are the basic steps to use Depo in the recommended way:
1. Open or create a database using `open/1` to get a
new `Depo.DB` object that manages the database connection.
2. Enclose all database actions in a function you pass to
`transact/2` to group them in a transaction.
3. Use `write/2` to create any desired tables (data types are
optional because SQLite uses [dynamic data typing.](https://sqlite.org/datatype3.html))
4. Use `teach/2` to create a set of pre-compiled SQL
statements you can use to write to or read from the database.
You can include numbered arguments in the statements by using
`?1` to refer to the first argument, and
`?2` for the second argument, and so on.
5. Use `write/3` to populate your database with data using
the statements you taught the database and arguments to the
statements supplied as the third argument.
6. Use `read/2` and `read/3` to run (optionally parameterized)
queries and get the results synchronously.
7. Use `stream/3` and `stream/4` to run the same queries and
stream the results asynchrously to any process.
Anywhere you give a command, you can give either a valid
string of SQL or an atom registered to a cached statement.
You can also include numbered variables like `?1` in your
SQL statements and pass a list of values as the third
parameter to `write/3`, `read/3`, and `stream/3`.
## Usage Example
```
# Set up the database.
{:ok, db} = Depo.open(:memory)
# {:ok, #PID<0.155.0>}
# Enclose operations within a transaction.
Depo.transact(db, fn ->
# Write SQL statements to the database.
Depo.write(db, "CREATE TABLE greetings (phrase)")
# Teach the database statements to cache them.
Depo.teach(db, %{
new_greeting: "INSERT INTO greetings VALUES (?1)",
# By default, queries will return a map for each row.
greetings: "SELECT * FROM greetings",
# You can return a list of row value tuples.
phrases: {:values, "SELECT phrase FROM greetings"},
# You can return a single value (from one column in one row).
first_phrase: {:single, "SELECT phrase FROM greetings LIMIT 1"},
})
Enum.each(["hola", "bonjour", "今日は"], fn phrase ->
Depo.write(db, :new_greeting, phrase)
end)
end)
Depo.read(db, :greetings)
# [
# %{phrase: "今日は"},
# %{phrase: "bonjour"},
# %{phrase: "hola"},
# ]
# Any query can be streamed to a PID.
stream_id = Depo.stream(db, self(), :phrases)
:timer.sleep(5)
phrases = Enum.reduce(1..3, [], fn _i, phrases ->
receive do
{^stream_id, phrase} -> phrases ++ [phrase]
end
end)
# phrases == [
# {"hola"},
# {"bonjour"},
# {"今日は"},
# ]
Depo.read(db, :first_phrase)
# "hola"
"""
defmacrop is_cmd(cmd) do
quote do
is_binary(unquote(cmd)) or is_atom(unquote(cmd))
end
end
@doc """
Open a connection to a database and return a new
`Depo.DB` object to manage the database connection.
There are a few ways you can open a database:
- pass a `path` to open an existing on-disk database
- pass `create: path` to create and open a database at the path
- pass `:memory` to create a new in-memory database
"""
def open(:memory) do
GenServer.start_link(Depo.DB, :memory)
end
def open(create: path) when is_binary(path) do
GenServer.start_link(Depo.DB, create: path)
end
def open(path) when is_binary(path) do
GenServer.start_link(Depo.DB, path)
end
@doc """
Asynchronously write SQL statements to the database.
Optionally supply a list of values as the third
argument to bind to variables in the statement.
"""
def write(db, cmd, values) when is_cmd(cmd) do
GenServer.cast(db, {:write, cmd, values})
end
def write(db, cmd) when is_cmd(cmd) do
GenServer.cast(db, {:write, cmd})
end
@doc """
Synchronously read an SQL query from the database
and return a list of the results.
Optionally supply a list of values as the third
argument to bind to variables in the query.
"""
def read(db, query, values) when is_cmd(query) do
GenServer.call(db, {:read, query, values})
end
def read(db, query) when is_cmd(query) do
GenServer.call(db, {:read, query})
end
@doc """
Asynchronously stream the results of an SQL query from
the database to the given PID.
The given process will receive each result as a tuple
`{stream_id, value}` where `stream_id` is the PID of the
stream process that uniquely identifies the stream,
and `value` is a single result map.
Optionally supply a list of values as the fourth
argument to bind to variables in the query.
"""
def stream(db, pid, query, values) when is_cmd(query) do
GenServer.call(db, {:stream, pid, query, values})
end
def stream(db, pid, query) when is_cmd(query) do
GenServer.call(db, {:stream, pid, query})
end
@doc """
Pre-compile, cache, and register named SQL statements for
more efficient repeated use.
`statements` should be a keyword list, where the keys
are atoms and the values are SQL statements.
"""
def teach(db, stmts) do
GenServer.cast(db, {:teach, stmts})
end
@doc """
Wrap any operations within the given anonymous function
in a nestable transaction.
If any error occurs within, the transaction will be
automatically rolled back.
You can [read about SQLite's transactions in depth in its
documentation.](https://sqlite.org/lang_transaction.html)
"""
def transact(db, func) do
Depo.write(db, "SAVEPOINT _depo_transaction;")
func.()
Depo.write(db, "RELEASE _depo_transaction;")
end
@doc """
Safely close the database connection.
"""
def close(db) do
GenServer.stop(db)
end
end