Skip to content
This repository was archived by the owner on Mar 19, 2021. It is now read-only.

Allow esqlite’s timeout to be specified #65

Merged
merged 2 commits into from
May 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions lib/sqlitex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,49 @@ defmodule Sqlitex do
{:ok, [%{a: 1, b: 2, c: 3}]}

```

## Configuration

Sqlitex uses the Erlang library [esqlite](https://github.com/mmzeeman/esqlite)
which accepts a timeout parameter for almost all interactions with the database.
The default value for this timeout is 5000 ms. Many functions in Sqlitex accept
a `:db_timeout` option that is passed on to the esqlite calls and that also defaults
to 5000 ms. If required, this default value can be overridden globally with the
following in your `config.exs`:

```
config :sqlitex,
esqlite3_timeout: 10_000 # or other positive integer number of ms
```
"""

@spec close(connection) :: :ok
def close(db) do
:esqlite3.close(db)
alias Sqlitex.Config

@spec close(connection, Keyword.t) :: :ok
def close(db, opts \\ []) do
timeout = Keyword.get(opts, :db_timeout, Config.esqlite3_timeout())
:esqlite3.close(db, timeout)
end

@spec open(charlist | String.t) :: {:ok, connection} | {:error, {atom, charlist}}
def open(path) when is_binary(path), do: open(string_to_charlist(path))
def open(path) do
:esqlite3.open(path)
@spec open(charlist | String.t, Keyword.t) :: {:ok, connection} | {:error, {atom, charlist}}
def open(path, opts \\ [])
def open(path, opts) when is_binary(path), do: open(string_to_charlist(path), opts)
def open(path, opts) do
timeout = Keyword.get(opts, :db_timeout, Config.esqlite3_timeout())
:esqlite3.open(path, timeout)
end

def with_db(path, fun) do
{:ok, db} = open(path)
def with_db(path, fun, opts \\ []) do
{:ok, db} = open(path, opts)
res = fun.(db)
close(db)
close(db, opts)
res
end

@spec exec(connection, string_or_charlist) :: :ok | sqlite_error
def exec(db, sql) do
:esqlite3.exec(sql, db)
@spec exec(connection, string_or_charlist, Keyword.t) :: :ok | sqlite_error
def exec(db, sql, opts \\ []) do
timeout = Keyword.get(opts, :db_timeout, Config.esqlite3_timeout())
:esqlite3.exec(sql, db, timeout)
end

def query(db, sql, opts \\ []), do: Sqlitex.Query.query(db, sql, opts)
Expand All @@ -72,9 +92,9 @@ defmodule Sqlitex do
**id: :integer, name: {:text, [:not_null]}**

"""
def create_table(db, name, table_opts \\ [], cols) do
def create_table(db, name, table_opts \\ [], cols, call_opts \\ []) do
stmt = Sqlitex.SqlBuilder.create_table(name, table_opts, cols)
exec(db, stmt)
exec(db, stmt, call_opts)
end

if Version.compare(System.version, "1.3.0") == :lt do
Expand Down
9 changes: 9 additions & 0 deletions lib/sqlitex/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Sqlitex.Config do
@moduledoc false

def esqlite3_timeout do
Application.get_env(:sqlitex, :esqlite3_timeout, default_esqlite3_timeout())
end

def default_esqlite3_timeout, do: 5_000
end
18 changes: 11 additions & 7 deletions lib/sqlitex/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule Sqlitex.Query do
* `bind` - If your query has parameters in it, you should provide the options
to bind as a list.
* `into` - The collection to put results into. This defaults to a list.
* `db_timeout` - The timeout (in ms) to apply to each of the underlying SQLite operations. Defaults
to 5000, or to `Application.get_env(:sqlitex, :esqlite3_timeout)` if set.

## Returns
* [results...] on success
Expand All @@ -28,8 +30,8 @@ defmodule Sqlitex.Query do
@spec query(Sqlitex.connection, String.t | charlist) :: {:ok, [[]]} | {:error, term()}
@spec query(Sqlitex.connection, String.t | charlist, [{atom, term}]) :: {:ok, [[]]} | {:error, term()}
def query(db, sql, opts \\ []) do
with {:ok, stmt} <- Statement.prepare(db, sql),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, [])),
with {:ok, stmt} <- Statement.prepare(db, sql, opts),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), opts),
{:ok, res} <- Statement.fetch_all(stmt, Keyword.get(opts, :into, [])),
do: {:ok, res}
end
Expand All @@ -40,7 +42,7 @@ defmodule Sqlitex.Query do
Returns the results otherwise.
"""
@spec query!(Sqlitex.connection, String.t | charlist) :: [[]]
@spec query!(Sqlitex.connection, String.t | charlist, [bind: [], into: Enum.t]) :: [Enum.t]
@spec query!(Sqlitex.connection, String.t | charlist, [bind: [], into: Enum.t, db_timeout: integer()]) :: [Enum.t]
def query!(db, sql, opts \\ []) do
case query(db, sql, opts) do
{:error, reason} -> raise Sqlitex.QueryError, reason: reason
Expand All @@ -62,17 +64,19 @@ defmodule Sqlitex.Query do

* `bind` - If your query has parameters in it, you should provide the options
to bind as a list.
* `db_timeout` - The timeout (in ms) to apply to each of the underlying SQLite operations. Defaults
to 5000, or to `Application.get_env(:sqlitex, :esqlite3_timeout)` if set.

## Returns
* {:ok, %{rows: [[1, 2], [2, 3]], columns: [:a, :b], types: [:INTEGER, :INTEGER]}} on success
* {:error, _} on failure.
"""

@spec query_rows(Sqlitex.connection, String.t | charlist) :: {:ok, %{}} | Sqlitex.sqlite_error
@spec query_rows(Sqlitex.connection, String.t | charlist, [bind: []]) :: {:ok, %{}} | Sqlitex.sqlite_error
@spec query_rows(Sqlitex.connection, String.t | charlist, [bind: [], db_timeout: integer()]) :: {:ok, %{}} | Sqlitex.sqlite_error
def query_rows(db, sql, opts \\ []) do
with {:ok, stmt} <- Statement.prepare(db, sql),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, [])),
with {:ok, stmt} <- Statement.prepare(db, sql, opts),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), opts),
{:ok, rows} <- Statement.fetch_all(stmt, :raw_list),
do: {:ok, %{rows: rows, columns: stmt.column_names, types: stmt.column_types}}
end
Expand All @@ -83,7 +87,7 @@ defmodule Sqlitex.Query do
Returns the results otherwise.
"""
@spec query_rows!(Sqlitex.connection, String.t | charlist) :: %{}
@spec query_rows!(Sqlitex.connection, String.t | charlist, [bind: []]) :: %{}
@spec query_rows!(Sqlitex.connection, String.t | charlist, [bind: [], db_timeout: integer()]) :: %{}
def query_rows!(db, sql, opts \\ []) do
case query_rows(db, sql, opts) do
{:error, reason} -> raise Sqlitex.QueryError, reason: reason
Expand Down
81 changes: 46 additions & 35 deletions lib/sqlitex/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,66 +47,73 @@ defmodule Sqlitex.Server do

alias Sqlitex.Statement
alias Sqlitex.Server.StatementCache, as: Cache
alias Sqlitex.Config

@doc """
Starts a SQLite Server (GenServer) instance.

In addition to the options that are typically provided to `GenServer.start_link/3`,
you can also specify `stmt_cache_size: (positive_integer)` to override the default
limit (20) of statements that are cached when calling `prepare/3`.
you can also specify:

- `stmt_cache_size: (positive_integer)` to override the default limit (20) of statements
that are cached when calling `prepare/3`.
- `db_timeout: (positive_integer)` to override `:esqlite3`'s default timeout of 5000 ms for
interactions with the database. This can also be set in `config.exs` as
`config :sqlitex, esqlite3_timeout: 5_000`.
"""
def start_link(db_path, opts \\ []) do
stmt_cache_size = Keyword.get(opts, :stmt_cache_size, 20)
GenServer.start_link(__MODULE__, {db_path, stmt_cache_size}, opts)
timeout = Keyword.get(opts, :db_timeout, Config.esqlite3_timeout())
GenServer.start_link(__MODULE__, {db_path, stmt_cache_size, timeout}, opts)
end

## GenServer callbacks

def init({db_path, stmt_cache_size})
def init({db_path, stmt_cache_size, timeout})
when is_integer(stmt_cache_size) and stmt_cache_size > 0
do
case Sqlitex.open(db_path) do
{:ok, db} -> {:ok, {db, __MODULE__.StatementCache.new(db, stmt_cache_size)}}
case Sqlitex.open(db_path, [db_timeout: timeout]) do
{:ok, db} -> {:ok, {db, __MODULE__.StatementCache.new(db, stmt_cache_size), timeout}}
{:error, reason} -> {:stop, reason}
end
end

def handle_call({:exec, sql}, _from, {db, stmt_cache}) do
result = Sqlitex.exec(db, sql)
{:reply, result, {db, stmt_cache}}
def handle_call({:exec, sql}, _from, {db, stmt_cache, timeout}) do
result = Sqlitex.exec(db, sql, [db_timeout: timeout])
{:reply, result, {db, stmt_cache, timeout}}
end

def handle_call({:query, sql, opts}, _from, {db, stmt_cache}) do
case query_impl(sql, opts, stmt_cache) do
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache}}
err -> {:reply, err, {db, stmt_cache}}
def handle_call({:query, sql, opts}, _from, {db, stmt_cache, timeout}) do
case query_impl(sql, opts, stmt_cache, timeout) do
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, timeout}}
err -> {:reply, err, {db, stmt_cache, timeout}}
end
end

def handle_call({:query_rows, sql, opts}, _from, {db, stmt_cache}) do
case query_rows_impl(sql, opts, stmt_cache) do
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache}}
err -> {:reply, err, {db, stmt_cache}}
def handle_call({:query_rows, sql, opts}, _from, {db, stmt_cache, timeout}) do
case query_rows_impl(sql, opts, stmt_cache, timeout) do
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, timeout}}
err -> {:reply, err, {db, stmt_cache, timeout}}
end
end

def handle_call({:prepare, sql}, _from, {db, stmt_cache}) do
case prepare_impl(sql, stmt_cache) do
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache}}
err -> {:reply, err, {db, stmt_cache}}
def handle_call({:prepare, sql}, _from, {db, stmt_cache, timeout}) do
case prepare_impl(sql, stmt_cache, timeout) do
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, timeout}}
err -> {:reply, err, {db, stmt_cache, timeout}}
end
end

def handle_call({:create_table, name, table_opts, cols}, _from, {db, stmt_cache}) do
result = Sqlitex.create_table(db, name, table_opts, cols)
{:reply, result, {db, stmt_cache}}
def handle_call({:create_table, name, table_opts, cols}, _from, {db, stmt_cache, timeout}) do
result = Sqlitex.create_table(db, name, table_opts, cols, [db_timeout: timeout])
{:reply, result, {db, stmt_cache, timeout}}
end

def handle_cast(:stop, {db, stmt_cache}) do
{:stop, :normal, {db, stmt_cache}}
def handle_cast(:stop, {db, stmt_cache, timeout}) do
{:stop, :normal, {db, stmt_cache, timeout}}
end

def terminate(_reason, {db, _stmt_cache}) do
def terminate(_reason, {db, _stmt_cache, _timeout}) do
Sqlitex.close(db)
:ok
end
Expand Down Expand Up @@ -157,24 +164,28 @@ defmodule Sqlitex.Server do

## Helpers

defp query_impl(sql, opts, stmt_cache) do
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, [])),
defp query_impl(sql, opts, stmt_cache, db_timeout) do
db_opts = [db_timeout: db_timeout]

with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, db_opts),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), db_opts),
{:ok, rows} <- Statement.fetch_all(stmt, Keyword.get(opts, :into, [])),
do: {:ok, rows, new_cache}
end

defp query_rows_impl(sql, opts, stmt_cache) do
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, [])),
defp query_rows_impl(sql, opts, stmt_cache, db_timeout) do
db_opts = [db_timeout: db_timeout]

with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, db_opts),
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), db_opts),
{:ok, rows} <- Statement.fetch_all(stmt, :raw_list),
do: {:ok,
%{rows: rows, columns: stmt.column_names, types: stmt.column_types},
new_cache}
end

defp prepare_impl(sql, stmt_cache) do
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql),
defp prepare_impl(sql, stmt_cache, db_timeout) do
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, [db_timeout: db_timeout]),
do: {:ok, %{columns: stmt.column_names, types: stmt.column_types}, new_cache}
end

Expand Down
8 changes: 4 additions & 4 deletions lib/sqlitex/server/statement_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ defmodule Sqlitex.Server.StatementCache do

Will return `{:error, reason}` if SQLite is unable to prepare the statement.
"""
def prepare(%__MODULE__{cached_stmts: cached_stmts} = cache, sql)
def prepare(%__MODULE__{cached_stmts: cached_stmts} = cache, sql, opts \\ [])
when is_binary(sql) and byte_size(sql) > 0
do
case Map.fetch(cached_stmts, sql) do
{:ok, stmt} -> {update_cache_for_read(cache, sql), stmt}
:error -> prepare_new_statement(cache, sql)
:error -> prepare_new_statement(cache, sql, opts)
end
end

defp prepare_new_statement(%__MODULE__{db: db} = cache, sql) do
case Sqlitex.Statement.prepare(db, sql) do
defp prepare_new_statement(%__MODULE__{db: db} = cache, sql, opts \\ []) do
case Sqlitex.Statement.prepare(db, sql, opts) do
{:ok, prepared} ->
cache = cache
|> store_new_stmt(sql, prepared)
Expand Down
Loading