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.ex
Raw

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.
You can [read about SQLite's architecture
](https://www.sqlite.org/arch.html)
to learn about the SQLite bytecode compiler and other
modules within SQLite that you can utilize.
Depo provides `create/1` to create a new `Depo.DB`
object with which you can interact with the database.
Anywhere you provide a command, you can give either a valid
string of SQL or an atom registered to a cached statement.
## Usage Example
```
# Open a new in-memory database.
{:ok, db} = Depo.create(:memory)
Depo.write(db, "CREATE TABLE a (n, t)")
# Prepare, cache, and register named statements.
Depo.teach(db,
new_b: "INSERT INTO a VALUES(?1, 'b')",
n2: "SELECT * FROM a WHERE n = 2",
)
# Group multiple statements in nestable transactions.
Depo.transact(db, fn ->
# Write a named statement with bound variables.
for i <- 1..4, do: Depo.write(db, :new_b, i)
end)
Depo.read(db, :n2) == [%{n: 2, t: "b"}]
Depo.close(db)
```
"""
defmacrop is_command(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.
"""
def write(db, cmd) when is_command(cmd) do
GenServer.cast(db, {:write, cmd})
end
@doc """
Write an SQL statement to the database after binding
the values to the variables in the statement.
"""
def write(db, cmd, values) when is_command(cmd) do
GenServer.cast(db, {:write, cmd, values})
end
@doc """
Synchronously read an SQL query from the database
and return a list of the results.
"""
def read(db, query) when is_command(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 process will receive each result as a tuple
`{pid, value}` where `pid` is the PID of the `Depo.DB`
and `value` is a single result map.
"""
def stream(db, pid, query) when is_command(query) do
GenServer.cast(db, {:stream, pid, query})
end
@doc """
Prepare, 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 closure 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, code) do
Depo.write(db, "SAVEPOINT _depo_transaction;")
code.()
Depo.write(db, "RELEASE _depo_transaction;")
end
@doc """
Safely close the database connection.
"""
def close(db) do
GenServer.stop(db)
end
end