Current section

Files

Jump to
exqlite lib exqlite sqlite3.ex
Raw

lib/exqlite/sqlite3.ex

defmodule Exqlite.Sqlite3 do
@moduledoc """
The interface to the NIF implementation.
"""
#
# TODO: If the database reference is closed, any prepared statements should be
# dereferenced as well. It is entirely possible that an application does
# not properly remove a stale reference.
#
# Will need to add a test for this and think of possible solution.
#
# TODO: Need to figure out if we can just stream results where we use this
# module as a sink.
alias Exqlite.Sqlite3NIF
@type db() :: reference()
@type statement() :: reference()
@type reason() :: atom() | String.t()
@type row() :: []
@doc """
Opens a new sqlite database at the Path provided.
If `path` can be `":memory"` to keep the sqlite database in memory.
"""
@spec open(String.t()) :: {:ok, db()} | {:error, reason()}
def open(path), do: Sqlite3NIF.open(String.to_charlist(path))
@spec close(nil) :: :ok
def close(nil), do: :ok
@doc """
Closes the database and releases any underlying resources.
"""
@spec close(db()) :: :ok | {:error, reason()}
def close(conn), do: Sqlite3NIF.close(conn)
@doc """
Executes an sql script. Multiple stanzas can be passed at once.
"""
@spec execute(db(), String.t()) :: :ok | {:error, reason()}
def execute(conn, sql) do
case Sqlite3NIF.execute(conn, String.to_charlist(sql)) do
:ok -> :ok
{:error, reason} -> {:error, reason}
_ -> {:error, "unhandled error"}
end
end
@doc """
Get the number of changes recently.
**Note**: If triggers are used, the count may be larger than expected.
See: https://sqlite.org/c3ref/changes.html
"""
@spec changes(db()) :: {:ok, integer()}
def changes(conn), do: Sqlite3NIF.changes(conn)
@spec prepare(db(), String.t()) :: {:ok, statement()} | {:error, reason()}
def prepare(conn, sql) do
Sqlite3NIF.prepare(conn, String.to_charlist(sql))
end
@spec bind(db(), statement(), nil) :: :ok | {:error, reason()}
def bind(conn, statement, nil), do: bind(conn, statement, [])
@spec bind(db(), statement(), []) :: :ok | {:error, reason()}
def bind(conn, statement, args) do
Sqlite3NIF.bind(conn, statement, Enum.map(args, &convert/1))
end
@spec columns(db(), statement()) :: {:ok, []} | {:error, reason()}
def columns(conn, statement), do: Sqlite3NIF.columns(conn, statement)
@spec step(db(), statement()) :: :done | :busy | {:row, []}
def step(conn, statement), do: Sqlite3NIF.step(conn, statement)
@spec multi_step(db(), statement()) :: :busy | {:rows, [row()]} | {:done, [row()]}
def multi_step(conn, statement) do
chunk_size = Application.get_env(:exqlite, :default_chunk_size, 50)
multi_step(conn, statement, chunk_size)
end
@spec multi_step(db(), statement(), integer()) ::
:busy | {:rows, [row()]} | {:done, [row()]}
def multi_step(conn, statement, chunk_size) do
case Sqlite3NIF.multi_step(conn, statement, chunk_size) do
:busy ->
{:error, "Database busy"}
{:error, reason} ->
{:error, reason}
{:rows, rows} ->
{:rows, Enum.reverse(rows)}
{:done, rows} ->
{:done, Enum.reverse(rows)}
end
end
@spec last_insert_rowid(db()) :: {:ok, integer()}
def last_insert_rowid(conn), do: Sqlite3NIF.last_insert_rowid(conn)
@spec transaction_status(db()) :: {:ok, :idle | :transaction}
def transaction_status(conn), do: Sqlite3NIF.transaction_status(conn)
@doc """
Causes the database connection to free as much memory as it can. This is
useful if you are on a memory restricted system.
"""
@spec shrink_memory(db()) :: :ok | {:error, reason()}
def shrink_memory(conn) do
Sqlite3NIF.execute(conn, String.to_charlist("PRAGMA shrink_memory"))
end
@spec fetch_all(db(), statement(), integer()) :: {:ok, [row()]} | {:error, reason()}
def fetch_all(conn, statement, chunk_size) do
fetch_all(conn, statement, chunk_size, [])
end
@spec fetch_all(db(), statement()) :: {:ok, [row()]} | {:error, reason()}
def fetch_all(conn, statement) do
# TODO: Should this be done in the NIF? It can be _much_ faster to build a
# list there, but at the expense that it could block other dirty nifs from
# getting work done.
#
# For now this just works
chunk_size = Application.get_env(:exqlite, :default_chunk_size, 50)
fetch_all(conn, statement, chunk_size, [])
end
defp fetch_all(conn, statement, chunk_size, accum) do
case multi_step(conn, statement, chunk_size) do
{:done, rows} ->
{:ok, accum ++ rows}
{:rows, rows} ->
fetch_all(conn, statement, chunk_size, accum ++ rows)
{:error, reason} ->
{:error, reason}
:busy ->
{:error, "Database busy"}
end
end
@doc """
Serialize the contents of the database to a binary.
"""
@spec serialize(db(), String.t()) :: {:ok, binary()} | {:error, reason()}
def serialize(conn, database \\ "main") do
Sqlite3NIF.serialize(conn, String.to_charlist(database))
end
@doc """
Disconnect from database and then reopen as an in-memory database based on the serialized binary.
"""
@spec deserialize(db(), String.t(), binary()) :: :ok | {:error, reason()}
def deserialize(conn, database \\ "main", serialized) do
Sqlite3NIF.deserialize(conn, String.to_charlist(database), serialized)
end
defp convert(%Date{} = val), do: Date.to_iso8601(val)
defp convert(%DateTime{} = val), do: DateTime.to_iso8601(val)
defp convert(%Time{} = val), do: Time.to_iso8601(val)
defp convert(%NaiveDateTime{} = val), do: NaiveDateTime.to_iso8601(val)
defp convert(val), do: val
end