%% MySQL/OTP – MySQL client library for Erlang/OTP
%% Copyright (C) 2014-2015, 2018 Viktor Söderqvist,
%% 2016 Johan Lövdahl
%% 2017 Piotr Nosek, Michal Slaski
%%
%% 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 MySQL client.
%%
%% The `connection()' type is a gen_server reference as described in the
%% documentation for `gen_server:call/2,3', e.g. the pid or the name if the
%% gen_server is locally registered.
-module(mysql).
-export([start_link/1, stop/1, stop/2,
query/2, query/3, query/4, query/5,
execute/3, execute/4, execute/5,
prepare/2, prepare/3, unprepare/2,
warning_count/1, affected_rows/1, autocommit/1, insert_id/1,
encode/2, in_transaction/1,
transaction/2, transaction/3, transaction/4,
change_user/3, change_user/4]).
-export_type([connection/0, server_reason/0, query_result/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 server_reason() :: {Code :: integer(), SQLState :: binary() | undefined,
Message :: binary()}.
-type column_names() :: [binary()].
-type row() :: [term()].
-type rows() :: [row()].
-type query_filtermap_fun() :: fun((row()) -> query_filtermap_res())
| fun((column_names(), row()) -> query_filtermap_res()).
-type query_filtermap_res() :: boolean()
| {true, term()}.
-type query_result() :: ok
| {ok, column_names(), rows()}
| {ok, [{column_names(), rows()}, ...]}
| {error, server_reason()}.
-define(default_connect_timeout, 5000).
-include("exception.hrl").
%% @doc Starts a connection gen_server process and connects to a database. To
%% disconnect use `mysql:stop/1,2'.
%%
%% Options:
%%
%%
%% - `{name, ServerName}'
%% - If a name is provided, the gen_server will be registered with this
%% name. For details see the documentation for the first argument of
%% gen_server:start_link/4.
%% - `{host, Host}'
%% - Hostname of the MySQL database. Since OTP version 19, it is also
%% possible to specify a local (Unix) Socket by specifying
%% `{local, SocketFile}'. Default `"localhost"'.
%% - `{port, Port}'
%% - Port; default 3306 for non-local or 0 for local (Unix) sockets.
%% - `{user, User}'
%% - Username.
%% - `{password, Password}'
%% - Password.
%% - `{database, Database}'
%% - The name of the database AKA schema to use. This can be changed later
%% using the query `USE '.
%% - `{connect_timeout, Timeout}'
%% - The maximum time to spend for start_link/1.
%% - `{log_warnings, boolean()}'
%% - Whether to fetch warnings and log them using error_logger; default
%% true.
%% - `{keep_alive, boolean() | timeout()}'
%% - Send ping when unused for a certain time. Possible values are `true',
%% `false' and `integer() > 0' for an explicit interval in milliseconds.
%% The default is `false'. For `true' a default ping timeout is used.
%%
%% - `{prepare, NamedStatements}'
%% - Named prepared statements to be created as soon as the connection is
%% ready.
%% - `{queries, Queries}'
%% - Queries to be executed as soon as the connection is ready. Any results
%% are discarded. Typically, this is used for setting time zone and other
%% session variables.
%% - `{query_timeout, Timeout}'
%% - The default time to wait for a response when executing a query or a
%% prepared statement. This can be given per query using `query/3,4' and
%% `execute/4'. The default is `infinity'.
%% - `{found_rows, boolean()}'
%% - If set to true, the connection will be established with
%% CLIENT_FOUND_ROWS capability. affected_rows/1 will now return the
%% number of found rows, not the number of rows changed by the
%% query.
%% - `{query_cache_time, Timeout}'
%% - The minimum number of milliseconds to cache prepared statements used
%% for parametrized queries with query/3.
%% - `{tcp_options, Options}'
%% - Additional options for `gen_tcp:connect/3'. You may want to set
%% `{recbuf, Size}' and `{sndbuf, Size}' if you send or receive more than
%% the default (typically 8K) per query.
%% - `{ssl, Options}'
%% - Additional options for `ssl:connect/3'.
%%
-spec start_link(Options) -> {ok, pid()} | ignore | {error, term()}
when Options :: [Option],
Option :: {name, ServerName} |
{host, inet:socket_address() | inet:hostname()} | {port, integer()} |
{user, iodata()} | {password, iodata()} |
{database, iodata()} |
{connect_timeout, timeout()} |
{log_warnings, boolean()} |
{keep_alive, boolean() | timeout()} |
{prepare, NamedStatements} |
{queries, [iodata()]} |
{query_timeout, timeout()} |
{found_rows, boolean()} |
{query_cache_time, non_neg_integer()} |
{tcp_options, [gen_tcp:connect_option()]} |
{ssl, term()},
ServerName :: {local, Name :: atom()} |
{global, GlobalName :: term()} |
{via, Module :: atom(), ViaName :: term()},
NamedStatements :: [{StatementName :: atom(), Statement :: iodata()}].
start_link(Options) ->
GenSrvOpts = [{timeout, proplists:get_value(connect_timeout, Options,
?default_connect_timeout)}],
case proplists:get_value(name, Options) of
undefined ->
gen_server:start_link(mysql_conn, Options, GenSrvOpts);
ServerName ->
gen_server:start_link(ServerName, mysql_conn, Options, GenSrvOpts)
end.
%% @see stop/2.
-spec stop(Conn) -> ok
when Conn :: connection().
stop(Conn) ->
stop(Conn, infinity).
%% @doc Stops a connection process and closes the connection. The
%% process calling `stop' will be blocked until the connection
%% process stops or the given timeout expires.
%%
%% If the connection is not stopped within the given timeout,
%% an exit exception is raised with reason `timeout'.
%%
%% If the connection process exits with any other reason than `normal',
%% an exit exception is raised with that reason.
-spec stop(Conn, Timeout) -> ok
when Conn :: connection(),
Timeout :: timeout().
stop(Conn, Timeout) ->
case erlang:function_exported(gen_server, stop, 3) of
true -> gen_server:stop(Conn, normal, Timeout); %% OTP >= 18
false -> backported_gen_server_stop(Conn, normal, Timeout) %% OTP < 18
end.
-spec backported_gen_server_stop(Conn, Reason, Timeout) -> ok
when Conn :: connection(),
Reason :: term(),
Timeout :: timeout().
backported_gen_server_stop(Conn, Reason, Timeout) ->
Monitor=monitor(process, Conn),
exit(Conn, Reason),
receive
{'DOWN', Monitor, process, Conn, Reason} ->
ok;
{'DOWN', Monitor, process, Conn, UnexpectedReason} ->
exit(UnexpectedReason)
after Timeout ->
exit(Conn, kill),
receive
{'DOWN', Monitor, process, Conn, killed} ->
exit(timeout)
end
end.
%% @see query/5.
-spec query(Conn, Query) -> Result
when Conn :: connection(),
Query :: iodata(),
Result :: query_result().
query(Conn, Query) ->
query(Conn, Query, no_params, no_filtermap_fun, default_timeout).
%% @see query/5.
-spec query(Conn, Query, Params | FilterMap | Timeout) -> Result
when Conn :: connection(),
Query :: iodata(),
Timeout :: default_timeout | timeout(),
Params :: no_params | [term()],
FilterMap :: no_filtermap_fun | query_filtermap_fun(),
Result :: query_result().
query(Conn, Query, Params) when Params == no_params;
is_list(Params) ->
query(Conn, Query, Params, no_filtermap_fun, default_timeout);
query(Conn, Query, FilterMap) when FilterMap == no_filtermap_fun;
is_function(FilterMap, 1);
is_function(FilterMap, 2) ->
query(Conn, Query, no_params, FilterMap, default_timeout);
query(Conn, Query, Timeout) when Timeout == default_timeout;
is_integer(Timeout);
Timeout == infinity ->
query(Conn, Query, no_params, no_filtermap_fun, Timeout).
%% @see query/5.
-spec query(Conn, Query, Params, Timeout) -> Result
when Conn :: connection(),
Query :: iodata(),
Timeout :: default_timeout | timeout(),
Params :: no_params | [term()],
Result :: query_result();
(Conn, Query, FilterMap, Timeout) -> Result
when Conn :: connection(),
Query :: iodata(),
Timeout :: default_timeout | timeout(),
FilterMap :: no_filtermap_fun | query_filtermap_fun(),
Result :: query_result();
(Conn, Query, Params, FilterMap) -> Result
when Conn :: connection(),
Query :: iodata(),
Params :: no_params | [term()],
FilterMap :: no_filtermap_fun | query_filtermap_fun(),
Result :: query_result().
query(Conn, Query, Params, Timeout) when (Params == no_params orelse
is_list(Params)) andalso
(Timeout == default_timeout orelse
is_integer(Timeout) orelse
Timeout == infinity) ->
query(Conn, Query, Params, no_filtermap_fun, Timeout);
query(Conn, Query, FilterMap, Timeout) when (FilterMap == no_filtermap_fun orelse
is_function(FilterMap, 1) orelse
is_function(FilterMap, 2)) andalso
(Timeout == default_timeout orelse
is_integer(Timeout) orelse
Timeout=:=infinity) ->
query(Conn, Query, no_params, FilterMap, Timeout);
query(Conn, Query, Params, FilterMap) when (Params == no_params orelse
is_list(Params)) andalso
(FilterMap == no_filtermap_fun orelse
is_function(FilterMap, 1) orelse
is_function(FilterMap, 2)) ->
query(Conn, Query, Params, FilterMap, default_timeout).
%% @doc Executes a parameterized query with a timeout and applies a filter/map
%% function to the result rows.
%%
%% A prepared statement is created, executed and then cached for a certain
%% time. If the same query is executed again when it is already cached, it does
%% not need to be prepared again.
%%
%% The minimum time the prepared statement is cached can be specified using the
%% option `{query_cache_time, Milliseconds}' to start_link/1.
%%
%% Results are returned in the form `{ok, ColumnNames, Rows}' if there is one
%% result set. If there are more than one result sets, they are returned in the
%% form `{ok, [{ColumnNames, Rows}, ...]}'.
%%
%% For queries that don't return any rows (INSERT, UPDATE, etc.) only the atom
%% `ok' is returned.
%%
%% The `Params', `FilterMap' and `Timeout' arguments are optional.
%%
%% - If the `Params' argument is the atom `no_params' or is omitted, a plain
%% query will be executed instead of a parameterized one.
%% - If the `FilterMap' argument is the atom `no_filtermap_fun' or is
%% omitted, no row filtering/mapping will be applied and all result rows
%% will be returned unchanged.
%% - If the `Timeout' argument is the atom `default_timeout' or is omitted,
%% the timeout given in `start_link/1' is used.
%%
%%
%% If the `FilterMap' argument is used, it must be a function of arity 1 or 2
%% that returns either `true', `false', or `{true, Value}'.
%%
%% Each result row is handed to the given function as soon as it is received
%% from the server, and only when the function has returned, the next row is
%% fetched. This provides the ability to prevent memory exhaustion; on the
%% other hand, it can cause the server to time out on sending if your function
%% is doing something slow (see the MySQL documentation on `NET_WRITE_TIMEOUT').
%%
%% If the function is of arity 1, only the row is passed to it as the single
%% argument, while if the function is of arity 2, the column names are passed
%% in as the first argument and the row as the second.
%%
%% The value returned is then used to decide if the row is to be included in
%% the result(s) returned from the `query' call (filtering), or if something
%% else is to be included in the result instead (mapping). You may also use
%% this function for side effects, like writing rows to disk or sending them
%% to another process etc.
%%
%% Here is an example showing some of the things that are possible:
%% ```
%% Query = "SELECT a, b, c FROM foo",
%% FilterMap = fun
%% %% Include all rows where the first column is < 10.
%% ([A|_]) when A < 10 ->
%% true;
%% %% Exclude all rows where the first column is >= 10 and < 20.
%% ([A|_]) when A < 20 ->
%% false;
%% %% For rows where the first column is >= 20 and < 30, include
%% %% the atom 'foo' in place of the row instead.
%% ([A|_]) when A < 30 ->
%% {true, foo}};
%% %% For rows where the first row is >= 30 and < 40, send the
%% %% row to a gen_server via call (ie, wait for a response),
%% %% and do not include the row in the result.
%% (R=[A|_]) when A < 40 ->
%% gen_server:call(Pid, R),
%% false;
%% %% For rows where the first column is >= 40 and < 50, send the
%% %% row to a gen_server via cast (ie, do not wait for a reply),
%% %% and include the row in the result, also.
%% (R=[A|_]) when A < 50 ->
%% gen_server:cast(Pid, R),
%% true;
%% %% Exclude all other rows from the result.
%% (_) ->
%% false
%% end,
%% query(Conn, Query, no_params, FilterMap, default_timeout).
%% '''
-spec query(Conn, Query, Params, FilterMap, Timeout) -> Result
when Conn :: connection(),
Query :: iodata(),
Timeout :: default_timeout | timeout(),
Params :: no_params | [term()],
FilterMap :: no_filtermap_fun | query_filtermap_fun(),
Result :: query_result().
query(Conn, Query, no_params, FilterMap, Timeout) ->
query_call(Conn, {query, Query, FilterMap, Timeout});
query(Conn, Query, Params, FilterMap, Timeout) ->
case mysql_protocol:valid_params(Params) of
true ->
query_call(Conn,
{param_query, Query, Params, FilterMap, Timeout});
false ->
error(badarg)
end.
%% @doc Executes a prepared statement with the default query timeout as given
%% to start_link/1.
%% @see prepare/2
%% @see prepare/3
%% @see prepare/4
%% @see execute/5
-spec execute(Conn, StatementRef, Params) -> Result | {error, not_prepared}
when Conn :: connection(),
StatementRef :: atom() | integer(),
Params :: [term()],
Result :: query_result().
execute(Conn, StatementRef, Params) ->
execute(Conn, StatementRef, Params, no_filtermap_fun, default_timeout).
%% @doc Executes a prepared statement.
%% @see prepare/2
%% @see prepare/3
%% @see prepare/4
%% @see execute/5
-spec execute(Conn, StatementRef, Params, FilterMap | Timeout) ->
Result | {error, not_prepared}
when Conn :: connection(),
StatementRef :: atom() | integer(),
Params :: [term()],
FilterMap :: no_filtermap_fun | query_filtermap_fun(),
Timeout :: default_timeout | timeout(),
Result :: query_result().
execute(Conn, StatementRef, Params, Timeout) when Timeout == default_timeout;
is_integer(Timeout);
Timeout=:=infinity ->
execute(Conn, StatementRef, Params, no_filtermap_fun, Timeout);
execute(Conn, StatementRef, Params, FilterMap) when FilterMap == no_filtermap_fun;
is_function(FilterMap, 1);
is_function(FilterMap, 2) ->
execute(Conn, StatementRef, Params, FilterMap, default_timeout).
%% @doc Executes a prepared statement.
%%
%% The `FilterMap' and `Timeout' arguments are optional.
%%
%% - If the `FilterMap' argument is the atom `no_filtermap_fun' or is
%% omitted, no row filtering/mapping will be applied and all result rows
%% will be returned unchanged.
%% - If the `Timeout' argument is the atom `default_timeout' or is omitted,
%% the timeout given in `start_link/1' is used.
%%
%%
%% See `query/5' for an explanation of the `FilterMap' argument.
%%
%% @see prepare/2
%% @see prepare/3
%% @see prepare/4
%% @see query/5
-spec execute(Conn, StatementRef, Params, FilterMap, Timeout) ->
Result | {error, not_prepared}
when Conn :: connection(),
StatementRef :: atom() | integer(),
Params :: [term()],
FilterMap :: no_filtermap_fun | query_filtermap_fun(),
Timeout :: default_timeout | timeout(),
Result :: query_result().
execute(Conn, StatementRef, Params, FilterMap, Timeout) ->
case mysql_protocol:valid_params(Params) of
true ->
query_call(Conn,
{execute, StatementRef, Params, FilterMap, Timeout});
false ->
error(badarg)
end.
%% @doc Creates a prepared statement from the passed query.
%% @see prepare/3
-spec prepare(Conn, Query) -> {ok, StatementId} | {error, Reason}
when Conn :: connection(),
Query :: iodata(),
StatementId :: integer(),
Reason :: server_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 prepare/2
-spec prepare(Conn, Name, Query) -> {ok, Name} | {error, Reason}
when Conn :: connection(),
Name :: atom(),
Query :: iodata(),
Reason :: server_reason().
prepare(Conn, Name, Query) ->
gen_server:call(Conn, {prepare, Name, Query}).
%% @doc Deallocates a prepared statement.
-spec unprepare(Conn, StatementRef) -> ok | {error, Reason}
when Conn :: connection(),
StatementRef :: atom() | integer(),
Reason :: server_reason() | not_prepared.
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. If found_rows is set on the
%% connection, for update operation the return value will equal to the number
%% of rows matched by the query.
-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, "BEGIN")'.
%% @see transaction/2
%% @see transaction/4
-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/4
-spec transaction(connection(), fun()) -> {atomic, term()} | {aborted, term()}.
transaction(Conn, Fun) ->
transaction(Conn, Fun, [], infinity).
%% @doc This function executes the functional object Fun as a transaction.
%% @see transaction/4
-spec transaction(connection(), fun(), Retries) -> {atomic, term()} |
{aborted, term()}
when Retries :: non_neg_integer() | infinity.
transaction(Conn, Fun, Retries) ->
transaction(Conn, Fun, [], Retries).
%% @doc This function executes the functional object Fun with arguments Args as
%% a transaction.
%%
%% The semantics are as close as possible to mnesia's transactions. Transactions
%% can be nested and are restarted automatically when deadlocks are detected.
%% MySQL's savepoints are used to implement nested transactions.
%%
%% Fun must be a function and Args must be a list of the same length as the
%% arity of Fun.
%%
%% 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.
%%
%% Note that an error response from a query does not cause a transaction to be
%% rollbacked. To force a rollback on a MySQL error you can trigger a `badmatch'
%% using e.g. `ok = mysql:query(Pid, "SELECT some_non_existent_value")'. An
%% exception to this is the error 1213 "Deadlock", after the specified number
%% of retries, all failed. In this case, the transaction is aborted and the
%% error is retured as the reason for the aborted transaction, along with a
%% stacktrace pointing to where the last deadlock was detected. (In earlier
%% versions, up to and including 1.3.2, transactions where automatically
%% restarted also for the error 1205 "Lock wait timeout". This is no longer the
%% case.)
%%
%% Some queries such as ALTER TABLE cause an *implicit commit* on the server.
%% If such a query is executed within a transaction, an error on the form
%% `{implicit_commit, Query}' is raised. This means that the transaction has
%% been committed prematurely. This also happens if an explicit COMMIT is
%% executed as a plain query within a managed transaction. (Don't do that!)
%%
%%
%%
%% Class of exception | Return value |
%%
%%
%%
%% `error' with reason `ErrorReason' |
%% `{aborted, {ErrorReason, Stack}}' |
%%
%% `exit(Term)' | `{aborted, Term}' |
%% `throw(Term)' | `{aborted, {throw, Term}}' |
%%
%%
-spec transaction(connection(), fun(), list(), Retries) -> {atomic, term()} |
{aborted, term()}
when Retries :: non_neg_integer() | infinity.
transaction(Conn, Fun, Args, Retries) 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 = gen_server:call(Conn, start_transaction, infinity),
execute_transaction(Conn, Fun, Args, Retries).
%% @private
%% @doc This is a helper for transaction/2,3,4. It performs everything except
%% executing the BEGIN statement. It is called recursively when a transaction
%% is retried.
%%
%% "When a transaction rollback occurs due to a deadlock or lock wait timeout,
%% it cancels the effect of the statements within the transaction. But if the
%% start-transaction statement was START TRANSACTION or BEGIN statement,
%% rollback does not cancel that statement."
%% (https://dev.mysql.com/doc/refman/5.6/en/innodb-error-handling.html)
%%
%% Lock Wait Timeout:
%% "InnoDB rolls back only the last statement on a transaction timeout by
%% default. If --innodb_rollback_on_timeout is specified, a transaction timeout
%% causes InnoDB to abort and roll back the entire transaction (the same
%% behavior as in MySQL 4.1)."
%% (https://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html)
execute_transaction(Conn, Fun, Args, Retries) ->
try apply(Fun, Args) of
ResultOfFun ->
ok = gen_server:call(Conn, commit, infinity),
{atomic, ResultOfFun}
catch
%% We are at the top level, try to restart the transaction if there are
%% retries left
?EXCEPTION(throw, {implicit_rollback, 1, _}, _Stacktrace)
when Retries == infinity ->
execute_transaction(Conn, Fun, Args, infinity);
?EXCEPTION(throw, {implicit_rollback, 1, _}, _Stacktrace)
when Retries > 0 ->
execute_transaction(Conn, Fun, Args, Retries - 1);
?EXCEPTION(throw, {implicit_rollback, 1, Reason}, Stacktrace)
when Retries == 0 ->
%% No more retries. Return 'aborted' along with the deadlock error
%% and a the trace to the line where the deadlock occured.
Trace = ?GET_STACK(Stacktrace),
ok = gen_server:call(Conn, rollback, infinity),
{aborted, {Reason, Trace}};
?EXCEPTION(throw, {implicit_rollback, N, Reason}, Stacktrace)
when N > 1 ->
%% Nested transaction. Bubble out to the outermost level.
erlang:raise(throw, {implicit_rollback, N - 1, Reason},
?GET_STACK(Stacktrace));
?EXCEPTION(error, {implicit_commit, _Query} = E, Stacktrace) ->
%% The called did something like ALTER TABLE which resulted in an
%% implicit commit. The server has already committed. We need to
%% jump out of N levels of transactions.
%%
%% Returning 'atomic' or 'aborted' would both be wrong. Raise an
%% exception is the best we can do.
erlang:raise(error, E, ?GET_STACK(Stacktrace));
?EXCEPTION(error, change_user_in_transaction = E, Stacktrace) ->
%% The called tried to change user inside the transaction, which
%% is not allowed and a serious mistake. We roll back and raise
%% an error.
ok = gen_server:call(Conn, rollback, infinity),
erlang:raise(error, E, ?GET_STACK(Stacktrace));
?EXCEPTION(Class, Reason, Stacktrace) ->
%% We must be able to rollback. Otherwise let's crash.
ok = gen_server:call(Conn, rollback, infinity),
%% These forms for throw, error and exit mirror Mnesia's behaviour.
Aborted = case Class of
throw -> {throw, Reason};
error -> {Reason, ?GET_STACK(Stacktrace)};
exit -> Reason
end,
{aborted, Aborted}
end.
%% @doc Equivalent to `change_user(Conn, Username, Password, [])'.
%% @see change_user/4
-spec change_user(Conn, Username, Password) -> Result
when Conn :: connection(),
Username :: iodata(),
Password :: iodata(),
Result :: ok.
change_user(Conn, Username, Password) ->
change_user(Conn, Username, Password, []).
%% @doc Changes the user of the active connection without closing and
%% and re-opening it. The currently active session will be reset (ie,
%% user variables, temporary tables, prepared statements, etc will
%% be lost) independent of whether the operation succeeds or fails.
%%
%% If change user is called when a transaction is active (ie, neither
%% committed nor rolled back), calling `change_user' will fail with
%% an error exception and `change_user_in_transaction' as the error
%% message.
%%
%% If the change user operation fails, `{error, Reason}' will be
%% returned. Specifically, if the operation itself fails (eg
%% authentication failure), `change_user_failed' will be returned as
%% the reason, while if the operation itself succeeds but one of
%% the given initial queries or prepares fails, the reason will
%% reflect the cause for the failure. In any case, the connection
%% process will exit with the same reason and cannot be used any longer.
%%
%% For a description of the `database', `queries' and `prepare'
%% options, see `start_link/1'.
%%
%% @see start_link/1
-spec change_user(Conn, Username, Password, Options) -> Result
when Conn :: connection(),
Username :: iodata(),
Password :: iodata(),
Options :: [Option],
Result :: ok,
Option :: {database, iodata()}
| {queries, [iodata()]}
| {prepare, [NamedStatement]},
NamedStatement :: {StatementName :: atom(), Statement :: iodata()}.
change_user(Conn, Username, Password, Options) ->
case in_transaction(Conn) of
true -> error(change_user_in_transaction);
false -> ok
end,
gen_server:call(Conn, {change_user, Username, Password, Options}).
%% @doc Encodes a term as a MySQL literal so that it can be used to inside a
%% query. If backslash escapes are enabled, backslashes and single quotes in
%% strings and binaries are escaped. Otherwise only single quotes are escaped.
%%
%% Note that the preferred way of sending values is by prepared statements or
%% parametrized queries with placeholders.
%%
%% @see query/3
%% @see execute/3
-spec encode(connection(), term()) -> iodata().
encode(Conn, Term) ->
Term1 = case (is_list(Term) orelse is_binary(Term)) andalso
gen_server:call(Conn, backslash_escapes_enabled) of
true -> mysql_encode:backslash_escape(Term);
false -> Term
end,
mysql_encode:encode(Term1).
%% --- Helpers ---
%% @doc Makes a gen_server call for a query (plain, parametrized or prepared),
%% checks the reply and sometimes throws an exception when we need to jump out
%% of a transaction.
query_call(Conn, CallReq) ->
case gen_server:call(Conn, CallReq, infinity) of
{implicit_commit, _NestingLevel, Query} ->
error({implicit_commit, Query});
{implicit_rollback, _NestingLevel, _ServerReason} = ImplicitRollback ->
throw(ImplicitRollback);
Result ->
Result
end.