%% MySQL/OTP – MySQL client library for Erlang/OTP
%% Copyright (C) 2014 Viktor Söderqvist
%%
%% This file is part of MySQL/OTP.
%%
%% MySQL/OTP is free software: you can redistribute it and/or modify it under
%% the terms of the GNU Lesser General Public License as published by the Free
%% Software Foundation, either version 3 of the License, or (at your option)
%% any later version.
%%
%% This program is distributed in the hope that it will be useful, but WITHOUT
%% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
%% FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
%% more details.
%%
%% You should have received a copy of the GNU Lesser General Public License
%% along with this program. If not, see .
%% @doc API for communicating with MySQL databases.
%%
%% Most of the functions are wrappers for `gen_server' calls. The
%% `connection()' type is the same as returned by `gen_server:start_link/2,3'.
-module(mysql).
-export([start_link/1, query/2, execute/3, prepare/2, prepare/3, unprepare/2,
warning_count/1, affected_rows/1, autocommit/1, insert_id/1,
in_transaction/1,
transaction/2, transaction/3]).
-export_type([connection/0]).
%% A connection is a ServerRef as in gen_server:call/2,3.
-type connection() :: Name :: atom() |
{Name :: atom(), Node :: atom()} |
{global, GlobalName :: term()} |
{via, Module :: atom(), ViaName :: term()} |
pid().
%% MySQL error with the codes and message returned from the server.
-type reason() :: {Code :: integer(), SQLState :: binary(),
Message :: binary()}.
%% @doc Starts a connection gen_server process and connects to a database. To
%% disconnect just 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()}
when Options :: [Option],
Option :: {host, iodata()} | {port, integer()} | {user, iodata()} |
{password, iodata()} | {database, iodata()}.
start_link(Opts) ->
gen_server:start_link(mysql_connection, Opts, []).
%% @doc Executes a query.
-spec query(Conn, Query) -> ok | {ok, ColumnNames, Rows} | {error, Reason}
when Conn :: connection(),
Query :: iodata(),
ColumnNames :: [binary()],
Rows :: [[term()]],
Reason :: reason().
query(Conn, Query) ->
gen_server:call(Conn, {query, Query}).
%% @doc Executes a prepared statement.
%% @see prepare/2
execute(Conn, StatementId, Args) ->
gen_server:call(Conn, {execute, StatementId, Args}).
%% @doc Creates a prepared statement from the passed query.
%% @see execute/3
-spec prepare(Conn :: connection(), Query :: iodata()) ->
{ok, StatementId :: integer()} | {error, Reason :: reason()}.
prepare(Conn, Query) ->
gen_server:call(Conn, {prepare, Query}).
%% @doc Creates a prepared statement from the passed query and associates it
%% with the given name.
%% @see execute/3
-spec prepare(Conn :: connection(), Name :: term(), Query :: iodata()) ->
{ok, Name :: term()} | {error, Reason :: reason()}.
prepare(Conn, Name, Query) ->
gen_server:call(Conn, {prepare, Name, Query}).
%% @doc Deallocates a prepared statement.
%% @see prepare/3
-spec unprepare(Conn :: connection(), StatementRef :: term()) ->
ok | {error, not_prepared} | {error, Reason :: reason()}.
unprepare(Conn, StatementRef) ->
gen_server:call(Conn, {unprepare, StatementRef}).
%% @doc Returns the number of warnings generated by the last query/2 or
%% execute/3 calls.
-spec warning_count(connection()) -> integer().
warning_count(Conn) ->
gen_server:call(Conn, warning_count).
%% @doc Returns the number of inserted, updated and deleted rows of the last
%% executed query or prepared statement.
-spec affected_rows(connection()) -> integer().
affected_rows(Conn) ->
gen_server:call(Conn, affected_rows).
%% @doc Returns true if auto-commit is enabled and false otherwise.
-spec autocommit(connection()) -> boolean().
autocommit(Conn) ->
gen_server:call(Conn, autocommit).
%% @doc Returns the last insert-id.
-spec insert_id(connection()) -> integer().
insert_id(Conn) ->
gen_server:call(Conn, insert_id).
%% @doc Returns true if the connection is in a transaction and false otherwise.
%% This works regardless of whether the transaction has been started using
%% transaction/2,3 or using a plain `mysql:query(Connection, "START
%% TRANSACTION")'.
%% @see transaction/2
%% @see transaction/3
-spec in_transaction(connection()) -> boolean().
in_transaction(Conn) ->
gen_server:call(Conn, in_transaction).
%% @doc This function executes the functional object Fun as a transaction.
%% @see transaction/3
%% @see in_transaction/1
-spec transaction(connection(), 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 same 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:
%%
%%
%% - Transactions cannot be nested
%% - They are not automatically restarted when deadlocks are detected.
%%
%%
%% If an exception occurs within Fun, the exception is caught and `{aborted,
%% Reason}' is returned. The value of `Reason' depends on the class of the
%% exception.
%%
%%
%%
%% Class of exception | Return value |
%%
%%
%%
%% `error' with reason `ErrorReason' |
%% `{aborted, {ErrorReason, Stack}}' |
%%
%% `exit(Term)' | `{aborted, Term}' |
%% `throw(Term)' | `{aborted, {throw, Term}}' |
%%
%%
%%
%% TODO: Implement nested transactions
%% TODO: Automatic restart on deadlocks
%% @see in_transaction/1
-spec transaction(connection(), 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.