Browse Source

Implements simple mnesia style transactions (#7)

Viktor Söderqvist 10 years ago
parent
commit
60d40a99fe
3 changed files with 118 additions and 10 deletions
  1. 17 9
      README.md
  2. 52 1
      src/mysql.erl
  3. 49 0
      test/mysql_tests.erl

+ 17 - 9
README.md

@@ -56,15 +56,23 @@ WarningCount = mysql:warning_count(Pid),
 {ok, ColumnNames, Rows} =
 {ok, ColumnNames, Rows} =
     mysql:query(Pid, <<"SELECT * FROM mytable WHERE id=?">>, [42]),
     mysql:query(Pid, <<"SELECT * FROM mytable WHERE id=?">>, [42]),
 
 
-%% Mnesia style transactions (nestable and restartable).
-%% NOT IMPLEMENTED YET. See issue #7.
-%% This example will obviously fail with a badmatch.
-{atomic, ResultOfFun} = mysql:transaction(Pid,
-        fun () ->
-            ok = mysql:query(Pid, "INSERT INTO mytable (foo) VALUES (1)"),
-            throw(foo),
-            ok = mysql:query(Pid, "INSERT INTO mytable (foo) VALUES (1)")
-        end).
+%% Simple Mnesia style transactions.
+%%
+%% Note 1: Cannot be nested.
+%% Note 2: Not automatically restarted when a deadlock is detected.
+%%
+%% There are plans to implement nested and restartable transactions. (Issue #7)
+Result = mysql:transaction(Pid, fun () ->
+    ok = mysql:query(Pid, "INSERT INTO mytable (foo) VALUES (1)"),
+    throw(foo),
+    ok = mysql:query(Pid, "INSERT INTO mytable (foo) VALUES (1)")
+end),
+case Result of
+    {atomic, ResultOfFun} ->
+        io:format("Inserted 2 rows.~n");
+    {aborted, Reason} ->
+        io:format("Inserted 0 rows.~n")
+end
 ```
 ```
 
 
 Value representation
 Value representation

+ 52 - 1
src/mysql.erl

@@ -25,7 +25,7 @@
 -module(mysql).
 -module(mysql).
 
 
 -export([start_link/1, query/2, execute/3, prepare/2, warning_count/1,
 -export([start_link/1, query/2, execute/3, prepare/2, warning_count/1,
-         affected_rows/1, insert_id/1]).
+         affected_rows/1, insert_id/1, transaction/2, transaction/3]).
 
 
 %% MySQL error with the codes and message returned from the server.
 %% MySQL error with the codes and message returned from the server.
 -type reason() :: {Code :: integer(), SQLState :: binary(),
 -type reason() :: {Code :: integer(), SQLState :: binary(),
@@ -33,6 +33,10 @@
 
 
 %% @doc Starts a connection process and connects to a database. To disconnect
 %% @doc Starts a connection process and connects to a database. To disconnect
 %% do `exit(Pid, normal)'.
 %% do `exit(Pid, normal)'.
+%%
+%% This is just a wrapper for `gen_server:start_link(mysql_connection, Options,
+%% [])'. If you need to specify gen_server options, use gen_server:start_link/3
+%% directly.
 -spec start_link(Options) -> {ok, pid()} | ignore | {error, term()}
 -spec start_link(Options) -> {ok, pid()} | ignore | {error, term()}
     when Options :: [Option],
     when Options :: [Option],
          Option :: {host, iodata()} | {port, integer()} | {user, iodata()} |
          Option :: {host, iodata()} | {port, integer()} | {user, iodata()} |
@@ -69,3 +73,50 @@ affected_rows(Conn) ->
 -spec insert_id(pid()) -> integer().
 -spec insert_id(pid()) -> integer().
 insert_id(Conn) ->
 insert_id(Conn) ->
     gen_server:call(Conn, insert_id).
     gen_server:call(Conn, insert_id).
+
+%% @doc This function executes the functional object Fun as a transaction.
+%% @see transaction/2
+-spec transaction(pid(), fun()) -> {atomic, term()} | {aborted, term()}.
+transaction(Conn, Fun) ->
+    transaction(Conn, Fun, []).
+
+%% @doc This function executes the functional object Fun with arguments Args as
+%% a transaction. 
+%%
+%% The semantics are the sames as for mnesia's transactions.
+%%
+%% The Fun must be a function and Args must be a list with the same length
+%% as the arity of Fun. 
+%%
+%% Current limitations:
+%%
+%% <ul>
+%%   <li>Transactions cannot be nested</li>
+%%   <li>They are not automatically restarted when deadlocks are detected.</li>
+%% </ul>
+%%
+%% TODO: Implement nested transactions
+%% TODO: Automatic restart on deadlocks
+-spec transaction(pid(), fun(), list()) -> {atomic, term()} | {aborted, term()}.
+transaction(Conn, Fun, Args) when is_list(Args),
+                                  is_function(Fun, length(Args)) ->
+    %% The guard makes sure that we can apply Fun to Args. Any error we catch
+    %% in the try-catch are actual errors that occurred in Fun.
+    ok = query(Conn, <<"BEGIN">>),
+    try apply(Fun, Args) of
+        ResultOfFun ->
+            %% We must be able to rollback. Otherwise let's go mad.
+            ok = query(Conn, <<"COMMIT">>),
+            {atomic, ResultOfFun}
+    catch
+        Class:Reason ->
+            %% We must be able to rollback. Otherwise let's go mad.
+            ok = query(Conn, <<"ROLLBACK">>),
+            %% These forms for throw, error and exit mirror Mnesia's behaviour.
+            Aborted = case Class of
+                throw -> {throw, Reason};
+                error -> {Reason, erlang:get_stacktrace()};
+                exit  -> Reason
+            end,
+            {aborted, Aborted}
+    end.

+ 49 - 0
test/mysql_tests.erl

@@ -275,3 +275,52 @@ run_test_microseconds(Pid) ->
     ?assertEqual({ok, [<<"t">>], E3}, mysql:execute(Pid, S3, [])),
     ?assertEqual({ok, [<<"t">>], E3}, mysql:execute(Pid, S3, [])),
     ok.
     ok.
 
 
+%% --------------------------------------------------------------------------
+
+%% Transaction tests
+
+transaction_single_connection_test_() ->
+    {setup,
+     fun () ->
+         {ok, Pid} = mysql:start_link([{user, ?user}, {password, ?password}]),
+         ok = mysql:query(Pid, <<"DROP DATABASE IF EXISTS otptest">>),
+         ok = mysql:query(Pid, <<"CREATE DATABASE otptest">>),
+         ok = mysql:query(Pid, <<"USE otptest">>),
+         ok = mysql:query(Pid, <<"CREATE TABLE foo (bar INT) engine=InnoDB">>),
+         Pid
+     end,
+     fun (Pid) ->
+         ok = mysql:query(Pid, <<"DROP DATABASE otptest">>),
+         exit(Pid, normal)
+     end,
+     {with, [fun transaction_simple_success/1,
+             fun transaction_simple_aborted/1]}}.
+
+transaction_simple_success(Pid) ->
+    Result = mysql:transaction(Pid, fun () ->
+                 ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
+                 hello
+             end),
+    ?assertEqual({atomic, hello}, Result),
+    ok = mysql:query(Pid, "DELETE FROM foo").
+
+transaction_simple_aborted(Pid) ->
+    ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
+    ?assertEqual({ok, [<<"bar">>], [[9]]},
+                 mysql:query(Pid, "SELECT bar FROM foo")),
+    Result = mysql:transaction(Pid, fun () ->
+                 ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
+                 ?assertMatch({ok, _, [[2]]},
+                              mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
+                 error(hello)
+             end),
+    ?assertMatch({aborted, {hello, Stacktrace}} when is_list(Stacktrace),
+                 Result),
+    ?assertEqual({ok, [<<"bar">>], [[9]]},
+                 mysql:query(Pid, "SELECT bar FROM foo")),
+    ok = mysql:query(Pid, "DELETE FROM foo"),
+    %% Also check the abort Reason for throw and exit.
+    ?assertEqual({aborted, {throw, foo}},
+                 mysql:transaction(Pid, fun () -> throw(foo) end)),
+    ?assertEqual({aborted, foo},
+                 mysql:transaction(Pid, fun () -> exit(foo) end)).