diff --git a/lib/sqlitex.ex b/lib/sqlitex.ex index e8e76c0..17a1897 100644 --- a/lib/sqlitex.ex +++ b/lib/sqlitex.ex @@ -45,10 +45,10 @@ defmodule Sqlitex do Sqlitex.query(db, "select * from mytable", db_chunk_size: 500) ``` in this case all rows will be passed from native sqlite OS thread to the erlang process in two passes. - Each pass will contain 500 rows. + Each pass will contain 500 rows. This parameter decrease overhead of transmitting rows from native OS sqlite thread to the erlang process by - chunking list of result rows. - Please, decrease this value if rows are heavy. Default value is 5000. + chunking list of result rows. + Please, decrease this value if rows are heavy. Default value is 5000. If you in doubt what to do with this parameter, please, do nothing. Default value is ok. ``` config :sqlitex, db_chunk_size: 500 # if most of the database rows are heavy @@ -150,9 +150,46 @@ defmodule Sqlitex do exec(db, stmt, call_opts) end + @doc """ + Runs `fun` inside a transaction. If `fun` returns without raising an exception, + the transaction will be commited via `commit`. Otherwise, `rollback` will be called. + + ## Examples + iex> {:ok, db} = Sqlitex.open(":memory:") + iex> Sqlitex.with_transaction(db, fn(db) -> + ...> Sqlitex.exec(db, "create table foo(id integer)") + ...> Sqlitex.exec(db, "insert into foo (id) values(42)") + ...> end) + iex> Sqlitex.query(db, "select * from foo") + {:ok, [[{:id, 42}]]} + """ + @spec with_transaction(Sqlitex.connection, (Sqlitex.connection -> any()), Keyword.t) :: any + def with_transaction(db, fun, opts \\ []) do + with :ok <- exec(db, "begin", opts), + {:ok, result} <- apply_rescuing(fun, [db]), + :ok <- exec(db, "commit", opts) + do + {:ok, result} + else + err -> + :ok = exec(db, "rollback", opts) + err + end + end + if Version.compare(System.version, "1.3.0") == :lt do defp string_to_charlist(string), do: String.to_char_list(string) else defp string_to_charlist(string), do: String.to_charlist(string) end + + ## Private Helpers + + defp apply_rescuing(fun, args) do + try do + {:ok, apply(fun, args)} + rescue + error -> {:error, error} + end + end end diff --git a/lib/sqlitex/server.ex b/lib/sqlitex/server.ex index 51d4b66..875af4e 100644 --- a/lib/sqlitex/server.ex +++ b/lib/sqlitex/server.ex @@ -119,6 +119,11 @@ defmodule Sqlitex.Server do {:reply, result, {db, stmt_cache, config}} end + def handle_call({:with_transaction, fun}, _from, {db, stmt_cache, config}) do + result = Sqlitex.with_transaction(db, fun) + {:reply, result, {db, stmt_cache, config}} + end + def handle_cast(:stop, {db, stmt_cache, config}) do {:stop, :normal, {db, stmt_cache, config}} end @@ -191,6 +196,27 @@ defmodule Sqlitex.Server do GenServer.cast(pid, :stop) end + @doc """ + Runs `fun` inside a transaction. If `fun` returns without raising an exception, + the transaction will be commited via `commit`. Otherwise, `rollback` will be called. + + Be careful if `fun` might take a long time to run. The function is executed in the + context of the server and therefore blocks other requests until it's finished. + + ## Examples + iex> {:ok, server} = Sqlitex.Server.start_link(":memory:") + iex> Sqlitex.Server.with_transaction(server, fn(db) -> + ...> Sqlitex.exec(db, "create table foo(id integer)") + ...> Sqlitex.exec(db, "insert into foo (id) values(42)") + ...> end) + iex> Sqlitex.Server.query(server, "select * from foo") + {:ok, [[{:id, 42}]]} + """ + @spec with_transaction(pid(), (Sqlitex.connection -> any()), Keyword.t) :: any + def with_transaction(pid, fun, opts \\ []) do + GenServer.call(pid, {:with_transaction, fun}, Config.call_timeout(opts)) + end + ## Helpers defp query_impl(sql, stmt_cache, opts) do diff --git a/test/server_test.exs b/test/server_test.exs index 7bcf44c..1743c38 100644 --- a/test/server_test.exs +++ b/test/server_test.exs @@ -1,4 +1,35 @@ defmodule Sqlitex.ServerTest do use ExUnit.Case doctest Sqlitex.Server + + test "with_transaction commit" do + alias Sqlitex.Server + + {:ok, server} = Server.start_link(':memory:') + :ok = Server.exec(server, "create table foo(id integer)") + + Server.with_transaction(server, fn db -> + :ok = Sqlitex.exec(db, "insert into foo (id) values (42)") + end) + + assert Server.query(server, "select * from foo") == {:ok, [[{:id, 42}]]} + end + + test "with_transaction rollback" do + alias Sqlitex.Server + + {:ok, server} = Server.start_link(':memory:') + :ok = Server.exec(server, "create table foo(id integer)") + + try do + Server.with_transaction(server, fn db -> + :ok = Sqlitex.exec(db, "insert into foo (id) values (42)") + raise "Error to roll back transaction" + end) + rescue + _ -> nil + end + + assert Server.query(server, "select * from foo") == {:ok, []} + end end diff --git a/test/sqlitex_test.exs b/test/sqlitex_test.exs index 2a6a08a..43f133f 100644 --- a/test/sqlitex_test.exs +++ b/test/sqlitex_test.exs @@ -250,4 +250,31 @@ defmodule Sqlitex.Test do assert row[:b] == nil assert row[:c] == nil end + + test "with_transaction commit" do + {:ok, db} = Sqlitex.open(":memory:") + :ok = Sqlitex.exec(db, "create table foo(id integer)") + + Sqlitex.with_transaction(db, fn db -> + :ok = Sqlitex.exec(db, "insert into foo (id) values (42)") + end) + + assert Sqlitex.query(db, "select * from foo") == {:ok, [[{:id, 42}]]} + end + + test "with_transaction rollback" do + {:ok, db} = Sqlitex.open(':memory:') + :ok = Sqlitex.exec(db, "create table foo(id integer)") + + try do + Sqlitex.with_transaction(db, fn db -> + :ok = Sqlitex.exec(db, "insert into foo (id) values (42)") + raise "Error to roll back transaction" + end) + rescue + _ -> nil + end + + assert Sqlitex.query(db, "select * from foo") == {:ok, []} + end end