cover/mysql_conn.COVER.html

1 %% MySQL/OTP – MySQL client library for Erlang/OTP
2 %% Copyright (C) 2014-2021 Viktor Söderqvist
3 %%
4 %% This file is part of MySQL/OTP.
5 %%
6 %% MySQL/OTP is free software: you can redistribute it and/or modify it under
7 %% the terms of the GNU Lesser General Public License as published by the Free
8 %% Software Foundation, either version 3 of the License, or (at your option)
9 %% any later version.
10 %%
11 %% This program is distributed in the hope that it will be useful, but WITHOUT
12 %% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 %% FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 %% more details.
15 %%
16 %% You should have received a copy of the GNU Lesser General Public License
17 %% along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19 %% @doc This module implements parts of the MySQL client/server protocol.
20 %%
21 %% The protocol is described in the document "MySQL Internals" which can be
22 %% found under "MySQL Documentation: Expert Guides" on http://dev.mysql.com/.
23 %%
24 %% TCP communication is not handled in this module. Most of the public functions
25 %% take funs for data communitaction as parameters.
26 %% @private
27 -module(mysql_conn).
28
29 -behaviour(gen_server).
30 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
31 code_change/3, format_status/1]).
32
33 -if(?OTP_RELEASE < 25).
34 -export([format_status/2]).
35 -endif.
36
37 -define(default_host, "localhost").
38 -define(default_port, 3306).
39 -define(default_user, <<>>).
40 -define(default_password, <<>>).
41 -define(default_connect_timeout, 5000).
42 -define(default_query_timeout, infinity).
43 -define(default_query_cache_time, 60000). %% for query/3.
44 -define(default_ping_timeout, 60000).
45
46 -define(cmd_timeout, 3000). %% Timeout used for various commands to the server
47
48 %% Errors that cause "implicit rollback"
49 -define(ERROR_DEADLOCK, 1213).
50
51 %% --- Gen_server callbacks ---
52
53 -include("records.hrl").
54 -include("server_status.hrl").
55
56 %% Gen_server state
57 -record(state, {server_version, connection_id, socket, sockmod, tcp_opts, ssl_opts,
58 host, port, user, password, database, queries, prepares,
59 auth_plugin_name, auth_plugin_data, allowed_local_paths,
60 log_warnings, log_slow_queries,
61 connect_timeout, ping_timeout, query_timeout, query_cache_time,
62 affected_rows = 0, status = 0, warning_count = 0, insert_id = 0,
63 transaction_levels = [], ping_ref = undefined,
64 stmts = #{}, query_cache = mysql_cache:new(), cap_found_rows = false,
65 float_as_decimal = false, decode_decimal = auto}).
66
67 %% @private
68 init(Opts) ->
69 48 ok = logger:set_process_metadata(#{domain => [mysql]}),
70
71 %% Connect
72 48 Host = proplists:get_value(host, Opts, ?default_host),
73
74 48 DefaultPort = case Host of
75 2 {local, _LocalAddr} -> 0;
76 46 _NonLocalAddr -> ?default_port
77 end,
78 48 Port = proplists:get_value(port, Opts, DefaultPort),
79
80 48 User = proplists:get_value(user, Opts, ?default_user),
81 48 Password = proplists:get_value(password, Opts, ?default_password),
82 48 Database = proplists:get_value(database, Opts, undefined),
83 48 AllowedLocalPaths = proplists:get_value(allowed_local_paths, Opts, []),
84 48 LogWarn = proplists:get_value(log_warnings, Opts, true),
85 48 LogSlow = proplists:get_value(log_slow_queries, Opts, false),
86 48 KeepAlive = proplists:get_value(keep_alive, Opts, false),
87 48 ConnectTimeout = proplists:get_value(connect_timeout, Opts,
88 ?default_connect_timeout),
89 48 QueryTimeout = proplists:get_value(query_timeout, Opts,
90 ?default_query_timeout),
91 48 QueryCacheTime = proplists:get_value(query_cache_time, Opts,
92 ?default_query_cache_time),
93 48 TcpOpts = proplists:get_value(tcp_options, Opts, []),
94 48 SetFoundRows = proplists:get_value(found_rows, Opts, false),
95 48 SSLOpts = proplists:get_value(ssl, Opts, undefined),
96
97 48 Queries = proplists:get_value(queries, Opts, []),
98 48 Prepares = proplists:get_value(prepare, Opts, []),
99 48 FloatAsDecimal = proplists:get_value(float_as_decimal, Opts, false),
100 48 DecodeDecimal = proplists:get_value(decode_decimal, Opts, auto),
101
102 48 true = lists:all(fun mysql_protocol:valid_path/1, AllowedLocalPaths),
103
104 48 PingTimeout = case KeepAlive of
105 4 true -> ?default_ping_timeout;
106 43 false -> infinity;
107 1 N when N > 0 -> N
108 end,
109
110 48 State0 = #state{
111 tcp_opts = TcpOpts,
112 ssl_opts = SSLOpts,
113 host = Host, port = Port,
114 user = User, password = Password,
115 database = Database,
116 allowed_local_paths = AllowedLocalPaths,
117 queries = Queries, prepares = Prepares,
118 log_warnings = LogWarn, log_slow_queries = LogSlow,
119 connect_timeout = ConnectTimeout,
120 ping_timeout = PingTimeout,
121 query_timeout = QueryTimeout,
122 query_cache_time = QueryCacheTime,
123 cap_found_rows = (SetFoundRows =:= true),
124 float_as_decimal = FloatAsDecimal,
125 decode_decimal = DecodeDecimal
126 },
127
128 48 case proplists:get_value(connect_mode, Opts, synchronous) of
129 synchronous ->
130 45 case connect(State0) of
131 {ok, State1} ->
132 42 {ok, State1};
133 {error, Reason} ->
134 3 {stop, Reason}
135 end;
136 asynchronous ->
137 2 gen_server:cast(self(), connect),
138 2 {ok, State0};
139 lazy ->
140 1 {ok, State0}
141 end.
142
143 connect(#state{connect_timeout = ConnectTimeout} = State) ->
144 48 MainPid = self(),
145 48 Pid = spawn_link(
146 fun () ->
147 48 {ok, State1}=connect_socket(State),
148 48 case handshake(State1) of
149 {ok, #state{sockmod = SockMod, socket = Socket} = State2} ->
150 46 SockMod:controlling_process(Socket, MainPid),
151 46 MainPid ! {self(), {ok, State2}};
152 {error, _} = E ->
153 2 MainPid ! {self(), E}
154 end
155 end
156 ),
157 48 receive
158 {Pid, {ok, State3}} ->
159 46 post_connect(State3);
160 {Pid, {error, _} = E} ->
161 2 E
162 after ConnectTimeout ->
163
:-(
unlink(Pid),
164
:-(
exit(Pid, kill),
165
:-(
{error, timeout}
166 end.
167
168 connect_socket(#state{tcp_opts = TcpOpts, host = Host, port = Port} = State) ->
169 %% Connect socket
170 48 SockOpts = sanitize_tcp_opts(Host, TcpOpts),
171 48 {ok, Socket} = gen_tcp:connect(Host, Port, SockOpts),
172
173 %% If buffer wasn't specifically defined make it at least as
174 %% large as recbuf, as suggested by the inet:setopts() docs.
175 48 case proplists:is_defined(buffer, TcpOpts) of
176 true ->
177
:-(
ok;
178 false ->
179 48 {ok, [{buffer, Buffer}]} = inet:getopts(Socket, [buffer]),
180 48 {ok, [{recbuf, Recbuf}]} = inet:getopts(Socket, [recbuf]),
181 48 ok = inet:setopts(Socket, [{buffer, max(Buffer, Recbuf)}])
182 end,
183
184 48 {ok, State#state{socket = Socket}}.
185
186 sanitize_tcp_opts(Host, [{inet_backend, _} = InetBackend | TcpOpts0]) ->
187 %% This option is be used to turn on the experimental socket backend for
188 %% gen_tcp/inet (OTP/23). If given, it must remain the first option in the
189 %% list.
190 2 [InetBackend | sanitize_tcp_opts(Host, TcpOpts0)];
191 sanitize_tcp_opts(Host, TcpOpts0) ->
192 48 NodelaySupported = not is_tuple(Host) orelse element(1, Host) =/= local,
193 48 TcpOpts1 = lists:filter(
194 fun
195
:-(
({mode, _}) -> false;
196
:-(
(binary) -> false;
197
:-(
(list) -> false;
198
:-(
({packet, _}) -> false;
199
:-(
({active, _}) -> false;
200
:-(
({nodelay, _}) -> NodelaySupported;
201
:-(
(_) -> true
202 end,
203 TcpOpts0
204 ),
205 48 TcpOpts2 = case NodelaySupported andalso not lists:keymember(nodelay, 1, TcpOpts1) of
206 46 true -> [{nodelay, true} | TcpOpts1];
207 2 false -> TcpOpts1
208 end,
209 48 [binary, {packet, raw}, {active, false} | TcpOpts2].
210
211 handshake(#state{socket = Socket0, ssl_opts = SSLOpts,
212 host = Host, user = User, password = Password, database = Database,
213 cap_found_rows = SetFoundRows} = State0) ->
214 %% Exchange handshake communication.
215 48 Result = mysql_protocol:handshake(Host, User, Password, Database, gen_tcp,
216 SSLOpts, Socket0, SetFoundRows),
217 48 case Result of
218 {ok, Handshake, SockMod, Socket} ->
219 46 setopts(SockMod, Socket, [{active, once}]),
220 46 #handshake{server_version = Version, connection_id = ConnId,
221 status = Status,
222 auth_plugin_name = AuthPluginName,
223 auth_plugin_data = AuthPluginData} = Handshake,
224 46 State1 = State0#state{server_version = Version, connection_id = ConnId,
225 sockmod = SockMod,
226 socket = Socket,
227 auth_plugin_name = AuthPluginName,
228 auth_plugin_data = AuthPluginData,
229 status = Status},
230 46 {ok, State1};
231 #error{} = E ->
232 2 {error, error_to_reason(E)}
233 end.
234
235 post_connect(#state{queries = Queries, prepares = Prepares} = State) ->
236 54 case execute_on_connect(Queries, Prepares, State) of
237 {ok, State1} ->
238 50 process_flag(trap_exit, true),
239 50 State2 = schedule_ping(State1),
240 50 {ok, State2};
241 {error, _} = E ->
242 4 E
243 end.
244
245 execute_on_connect([], [], State) ->
246 50 {ok, State};
247 execute_on_connect([], [{Name, Stmt}|Prepares], State) ->
248 7 case named_prepare(Name, Stmt, State) of
249 {{ok, Name}, State1} ->
250 5 execute_on_connect([], Prepares, State1);
251 {{error, _} = E, _} ->
252 2 E
253 end;
254 execute_on_connect([Query|Queries], Prepares, State) ->
255 15 case query(Query, no_filtermap_fun, default_timeout, State) of
256 {ok, State1} ->
257 5 execute_on_connect(Queries, Prepares, State1);
258 {{ok, _}, State1} ->
259 4 execute_on_connect(Queries, Prepares, State1);
260 {{ok, _, _}, State1} ->
261 4 execute_on_connect(Queries, Prepares, State1);
262 {{error, _} = E, _} ->
263 2 E
264 end.
265
266 %% @private
267 %% @doc
268 %%
269 %% Query and execute calls:
270 %%
271 %% <ul>
272 %% <li>{query, Query, FilterMap, Timeout}</li>
273 %% <li>{param_query, Query, Params, FilterMap, Timeout}</li>
274 %% <li>{execute, Stmt, Args, FilterMap, Timeout}</li>
275 %% </ul>
276 %%
277 %% For the calls listed above, we return these values:
278 %%
279 %% <dl>
280 %% <dt>`ok'</dt>
281 %% <dd>Success without returning any table data (UPDATE, etc.)</dd>
282 %% <dt>`{ok, ColumnNames, Rows}'</dt>
283 %% <dd>Queries returning one result set of table data</dd>
284 %% <dt>`{ok, [{ColumnNames, Rows}, ...]}'</dt>
285 %% <dd>Queries returning more than one result set of table data</dd>
286 %% <dt>`{error, ServerReason}'</dt>
287 %% <dd>MySQL server error</dd>
288 %% <dt>`{implicit_commit, NestingLevel, Query}'</dt>
289 %% <dd>A DDL statement (e.g. CREATE TABLE, ALTER TABLE, etc.) results in
290 %% an implicit commit.
291 %%
292 %% If the caller is in a (nested) transaction, it must be aborted. To be
293 %% able to handle this in the caller's process, we also return the
294 %% nesting level.</dd>
295 %% <dt>`{implicit_rollback, NestingLevel, ServerReason}'</dt>
296 %% <dd>This errors results in an implicit rollback: `{1213, <<"40001">>,
297 %% <<"Deadlock found when trying to get lock; try restarting "
298 %% "transaction">>}'.
299 %%
300 %% If the caller is in a (nested) transaction, it must be aborted. To be
301 %% able to handle this in the caller's process, we also return the
302 %% nesting level.</dd>
303 %% </dl>
304 handle_call(is_connected, _, #state{socket = Socket} = State) ->
305 4 {reply, Socket =/= undefined, State};
306 handle_call(Msg, From, #state{socket = undefined} = State) ->
307 1 case connect(State) of
308 {ok, State1} ->
309 1 handle_call(Msg, From, State1);
310 {error, _} = E ->
311
:-(
{stop, E, State}
312 end;
313 handle_call({query, _Query, _FilterMap, _Timeout}, {FromPid, _},
314 State = #state{transaction_levels = [{OtherFromPid, _} | _]})
315 when FromPid =/= OtherFromPid ->
316 %% this conn is currently in transaction owned by another process
317
:-(
{reply, {error, busy}, State};
318 handle_call({query, Query, FilterMap, Timeout}, _From, State) ->
319 480 {Reply, State1} = query(Query, FilterMap, Timeout, State),
320 480 {reply, Reply, State1};
321 handle_call({param_query, Query, Params, FilterMap, default_timeout}, From,
322 State) ->
323 16 handle_call({param_query, Query, Params, FilterMap,
324 State#state.query_timeout}, From, State);
325 handle_call({param_query, _Query, _Params, _FilterMap, _Timeout}, {FromPid, _},
326 State = #state{transaction_levels = [{OtherFromPid, _} | _]})
327 when FromPid =/= OtherFromPid ->
328 %% this conn is currently in transaction owned by another process
329
:-(
{reply, {error, busy}, State};
330 handle_call({param_query, Query, Params, FilterMap, Timeout}, _From,
331 #state{socket = Socket, sockmod = SockMod} = State) ->
332 %% Parametrized query: Prepared statement cached with the query as the key
333 17 QueryBin = iolist_to_binary(Query),
334 17 Cache = State#state.query_cache,
335 17 {StmtResult, Cache1} = case mysql_cache:lookup(QueryBin, Cache) of
336 {found, FoundStmt, NewCache} ->
337 %% Found
338 2 {{ok, FoundStmt}, NewCache};
339 not_found ->
340 %% Prepare
341 15 setopts(SockMod, Socket, [{active, false}]),
342 15 Rec = mysql_protocol:prepare(Query, SockMod, Socket),
343 15 setopts(SockMod, Socket, [{active, once}]),
344 15 case Rec of
345 #error{} = E ->
346 1 {{error, error_to_reason(E)}, Cache};
347 #prepared{} = Stmt ->
348 %% If the first entry in the cache, start the timer.
349 14 mysql_cache:is_empty(Cache) andalso begin
350 9 When = State#state.query_cache_time * 2,
351 9 erlang:send_after(When, self(), query_cache)
352 end,
353 14 {{ok, Stmt}, mysql_cache:store(QueryBin, Stmt, Cache)}
354 end
355 end,
356 17 case StmtResult of
357 {ok, StmtRec} ->
358 16 State1 = State#state{query_cache = Cache1},
359 16 {Reply, State2} = execute_stmt(StmtRec, Params, FilterMap, Timeout, State1),
360 16 {reply, Reply, State2};
361 PrepareError ->
362 1 {reply, PrepareError, State}
363 end;
364 handle_call({execute, Stmt, Args, FilterMap, default_timeout}, From, State) ->
365 207 handle_call({execute, Stmt, Args, FilterMap, State#state.query_timeout},
366 From, State);
367 handle_call({execute, _Stmt, _Args, _FilterMap, _Timeout}, {FromPid, _},
368 State = #state{transaction_levels = [{OtherFromPid, _} | _]})
369 when FromPid =/= OtherFromPid ->
370 %% this conn is currently in transaction owned by another process
371
:-(
{reply, {error, busy}, State};
372 handle_call({execute, Stmt, Args, FilterMap, Timeout}, _From, State) ->
373 208 case State#state.stmts of
374 #{Stmt := StmtRec} ->
375 207 {Reply, State1} = execute_stmt(StmtRec, Args, FilterMap, Timeout, State),
376 207 {reply, Reply, State1};
377 #{} ->
378 1 {reply, {error, not_prepared}, State}
379 end;
380 handle_call({prepare, Query}, _From, State) ->
381 95 #state{socket = Socket, sockmod = SockMod} = State,
382 95 setopts(SockMod, Socket, [{active, false}]),
383 95 Rec = mysql_protocol:prepare(Query, SockMod, Socket),
384 95 setopts(SockMod, Socket, [{active, once}]),
385 95 State1 = #state{stmts = Stmts1} = update_state(Rec, State),
386 95 case Rec of
387 #error{} = E ->
388 1 {reply, {error, error_to_reason(E)}, State1};
389 #prepared{statement_id = Id} = Stmt ->
390 94 State2 = State#state{stmts = Stmts1#{Id => Stmt}},
391 94 {reply, {ok, Id}, State2}
392 end;
393 handle_call({prepare, Name, Query}, _From, State) when is_atom(Name);
394 is_binary(Name) ->
395 9 {Reply, State1} = named_prepare(Name, Query, State),
396 9 {reply, Reply, State1};
397 handle_call({unprepare, Stmt}, _From, State) when is_atom(Stmt);
398 is_integer(Stmt);
399 is_binary(Stmt) ->
400 93 case maps:take(Stmt, State#state.stmts) of
401 {StmtRec, Stmts1} ->
402 91 #state{socket = Socket, sockmod = SockMod} = State,
403 91 setopts(SockMod, Socket, [{active, false}]),
404 91 mysql_protocol:unprepare(StmtRec, SockMod, Socket),
405 91 setopts(SockMod, Socket, [{active, once}]),
406 91 State1 = State#state{stmts = Stmts1},
407 91 State2 = schedule_ping(State1),
408 91 {reply, ok, State2};
409 error ->
410 2 {reply, {error, not_prepared}, State}
411 end;
412 handle_call({change_user, Username, Password, Options}, From,
413 State = #state{transaction_levels = []}) ->
414 9 #state{socket = Socket, sockmod = SockMod,
415 auth_plugin_name = AuthPluginName,
416 auth_plugin_data = AuthPluginData,
417 server_version = ServerVersion} = State,
418 9 Database = proplists:get_value(database, Options, undefined),
419 9 Queries = proplists:get_value(queries, Options, []),
420 9 Prepares = proplists:get_value(prepare, Options, []),
421 9 setopts(SockMod, Socket, [{active, false}]),
422 9 Result = mysql_protocol:change_user(SockMod, Socket, Username, Password,
423 AuthPluginName, AuthPluginData, Database,
424 ServerVersion),
425 9 setopts(SockMod, Socket, [{active, once}]),
426 9 State1 = update_state(Result, State),
427 9 State1#state.warning_count > 0 andalso State1#state.log_warnings
428
:-(
andalso log_warnings(State1, "CHANGE USER"),
429 9 State2 = State1#state{query_cache = mysql_cache:new(), stmts = #{}},
430 9 case Result of
431 #ok{} ->
432 8 State3 = State2#state{user = Username, password = Password,
433 database=Database, queries=Queries,
434 prepares=Prepares},
435 8 case post_connect(State3) of
436 {ok, State4} ->
437 6 {reply, ok, State4};
438 {error, Reason} = E ->
439 2 gen_server:reply(From, E),
440 2 stop_server(Reason, State3)
441 end;
442 #error{} = E ->
443 1 gen_server:reply(From, {error, error_to_reason(E)}),
444 1 stop_server(change_user_failed, State2)
445 end;
446 handle_call(reset_connection, _From, #state{socket = Socket, sockmod = SockMod} = State) ->
447 1 setopts(SockMod, Socket, [{active, false}]),
448 1 Result = mysql_protocol:reset_connnection(SockMod, Socket),
449 1 setopts(SockMod, Socket, [{active, once}]),
450 1 State1 = update_state(Result, State),
451 1 Reply = case Result of
452 1 #ok{} -> ok;
453 #error{} = E ->
454 %% 'COM_RESET_CONNECTION' is added in MySQL 5.7 and MariaDB 10
455 %% "Unknown command" is returned when MySQL =< 5.6 or MariaDB =< 5.5
456
:-(
{error, error_to_reason(E)}
457 end,
458 1 {reply, Reply, State1};
459
460 handle_call(warning_count, _From, State) ->
461 2 {reply, State#state.warning_count, State};
462 handle_call(insert_id, _From, State) ->
463 3 {reply, State#state.insert_id, State};
464 handle_call(affected_rows, _From, State) ->
465 3 {reply, State#state.affected_rows, State};
466 handle_call(autocommit, _From, State) ->
467 3 {reply, State#state.status band ?SERVER_STATUS_AUTOCOMMIT /= 0, State};
468 handle_call(backslash_escapes_enabled, _From, State = #state{status = S}) ->
469 2 {reply, S band ?SERVER_STATUS_NO_BACKSLASH_ESCAPES == 0, State};
470 handle_call(in_transaction, _From, State) ->
471 25 {reply, State#state.status band ?SERVER_STATUS_IN_TRANS /= 0, State};
472 handle_call(start_transaction, {FromPid, _},
473 State = #state{transaction_levels = [{OtherFromPid, _}|_]})
474 when FromPid =/= OtherFromPid ->
475 %% the idea of "nested transaction" is that mysql:transaction can be called
476 %% in the transaction function, but all within the same process
477
:-(
{reply, {error, busy}, State};
478 handle_call(start_transaction, {FromPid, _},
479 State = #state{socket = Socket, sockmod = SockMod,
480 decode_decimal = DecodeDecimal,
481 transaction_levels = L, status = Status})
482 when Status band ?SERVER_STATUS_IN_TRANS == 0, L == [];
483 Status band ?SERVER_STATUS_IN_TRANS /= 0, L /= [] ->
484 29 MRef = erlang:monitor(process, FromPid),
485 29 Query = case L of
486 18 [] -> <<"BEGIN">>;
487 11 _ -> <<"SAVEPOINT s", (integer_to_binary(length(L)))/binary>>
488 end,
489 29 setopts(SockMod, Socket, [{active, false}]),
490 29 {ok, [Res = #ok{}]} = mysql_protocol:query(Query, SockMod, Socket,
491 [], DecodeDecimal, no_filtermap_fun,
492 ?cmd_timeout),
493 29 setopts(SockMod, Socket, [{active, once}]),
494 29 State1 = update_state(Res, State),
495 29 {reply, ok, State1#state{transaction_levels = [{FromPid, MRef} | L]}};
496 handle_call(rollback, {FromPid, _},
497 State = #state{socket = Socket, sockmod = SockMod, status = Status,
498 decode_decimal = DecodeDecimal,
499 transaction_levels = [{FromPid, MRef} | L]})
500 when Status band ?SERVER_STATUS_IN_TRANS /= 0 ->
501 6 erlang:demonitor(MRef, [flush]),
502 6 Query = case L of
503 5 [] -> <<"ROLLBACK">>;
504 1 _ -> <<"ROLLBACK TO s", (integer_to_binary(length(L)))/binary>>
505 end,
506 6 setopts(SockMod, Socket, [{active, false}]),
507 6 {ok, [Res = #ok{}]} = mysql_protocol:query(Query, SockMod, Socket,
508 [], DecodeDecimal, no_filtermap_fun,
509 ?cmd_timeout),
510 6 setopts(SockMod, Socket, [{active, once}]),
511 6 State1 = update_state(Res, State),
512 6 {reply, ok, State1#state{transaction_levels = L}};
513 handle_call(commit, {FromPid, _},
514 State = #state{socket = Socket, sockmod = SockMod, status = Status,
515 decode_decimal = DecodeDecimal,
516 transaction_levels = [{FromPid, MRef} | L]})
517 when Status band ?SERVER_STATUS_IN_TRANS /= 0 ->
518 16 erlang:demonitor(MRef, [flush]),
519 16 Query = case L of
520 10 [] -> <<"COMMIT">>;
521 6 _ -> <<"RELEASE SAVEPOINT s", (integer_to_binary(length(L)))/binary>>
522 end,
523 16 setopts(SockMod, Socket, [{active, false}]),
524 16 {ok, [Res = #ok{}]} = mysql_protocol:query(Query, SockMod, Socket,
525 [], DecodeDecimal, no_filtermap_fun,
526 ?cmd_timeout),
527 16 setopts(SockMod, Socket, [{active, once}]),
528 16 State1 = update_state(Res, State),
529 16 {reply, ok, State1#state{transaction_levels = L}}.
530
531 %% @private
532 handle_cast(connect, #state{socket = undefined} = State) ->
533 2 case connect(State) of
534 {ok, State1} ->
535 1 {noreply, State1};
536 {error, _} = E ->
537 1 {stop, E, State}
538 end;
539 handle_cast(connect, State) ->
540
:-(
{noreply, State};
541 handle_cast(_Msg, State) ->
542 1 {noreply, State}.
543
544 %% @private
545 handle_info(query_cache, #state{query_cache = Cache,
546 query_cache_time = CacheTime} = State) ->
547 %% Evict expired queries/statements in the cache used by query/3.
548 1 {Evicted, Cache1} = mysql_cache:evict_older_than(Cache, CacheTime),
549 %% Unprepare the evicted statements
550 1 #state{socket = Socket, sockmod = SockMod} = State,
551 1 setopts(SockMod, Socket, [{active, false}]),
552 1 lists:foreach(fun ({_Query, Stmt}) ->
553 1 mysql_protocol:unprepare(Stmt, SockMod, Socket)
554 end,
555 Evicted),
556 1 setopts(SockMod, Socket, [{active, once}]),
557 %% If nonempty, schedule eviction again.
558 1 mysql_cache:is_empty(Cache1) orelse
559
:-(
erlang:send_after(CacheTime, self(), query_cache),
560 1 {noreply, State#state{query_cache = Cache1}};
561 handle_info({'DOWN', _MRef, _, Pid, _Info}, State) ->
562 2 stop_server({application_process_died, Pid}, State);
563 handle_info(ping, #state{socket = Socket, sockmod = SockMod} = State) ->
564 4 setopts(SockMod, Socket, [{active, false}]),
565 4 #ok{} = mysql_protocol:ping(SockMod, Socket),
566 3 setopts(SockMod, Socket, [{active, once}]),
567 3 {noreply, schedule_ping(State)};
568 handle_info({tcp_closed, Socket}, #state{socket = Socket} = State) ->
569 1 {stop, tcp_closed, State#state{socket = undefined, connection_id = undefined}};
570 handle_info({ssl_closed, Socket}, #state{socket = Socket} = State) ->
571
:-(
{stop, ssl_closed, State#state{socket = undefined, connection_id = undefined}};
572 handle_info({tcp_error, Socket, Reason}, #state{socket = Socket} = State) ->
573 1 stop_server({tcp_error, Reason}, State);
574 handle_info({ssl_error, Socket, Reason}, #state{socket = Socket} = State) ->
575
:-(
stop_server({ssl_error, Reason}, State);
576 handle_info({Transport, Socket, _Data}, #state{socket = Socket,
577 sockmod = SockMod} = State)
578 when Transport =:= tcp; Transport =:= ssl ->
579 %% Ignore out-of-band messages sent by the server,
580 %% eg. before closing the connection because of a session timeout
581 2 setopts(SockMod, Socket, [{active, once}]),
582 2 {noreply, State};
583 handle_info(_Info, State) ->
584 2 {noreply, State}.
585
586 %% @private
587 terminate(Reason, #state{socket = Socket, sockmod = SockMod})
588 when Socket =/= undefined andalso (Reason == normal orelse Reason == shutdown) ->
589 %% Send the goodbye message for politeness.
590 36 setopts(SockMod, Socket, [{active, false}]),
591 36 mysql_protocol:quit(SockMod, Socket);
592 terminate(_Reason, _State) ->
593 10 ok.
594
595 %% @private
596 code_change(_OldVsn, State = #state{}, _Extra) ->
597 2 {ok, State};
598 code_change(_OldVsn, _State, _Extra) ->
599 2 {error, incompatible_state}.
600
601 %% --- Helpers ---
602
603 %% @doc Executes a prepared statement and returns {Reply, NewState}.
604 execute_stmt(Stmt, Args, FilterMap, Timeout, State) ->
605 223 #state{socket = Socket, sockmod = SockMod,
606 allowed_local_paths = AllowedPaths,
607 float_as_decimal = FloatAsDecimal,
608 decode_decimal = DecodeDecimal} = State,
609 223 Args1 = case FloatAsDecimal of
610 false ->
611 216 Args;
612 _ ->
613 7 [float_to_decimal(Arg, FloatAsDecimal) || Arg <- Args]
614 end,
615 223 setopts(SockMod, Socket, [{active, false}]),
616 223 {ok, Recs} = case mysql_protocol:execute(Stmt, Args1, SockMod, Socket,
617 AllowedPaths, DecodeDecimal,
618 FilterMap, Timeout) of
619 {error, timeout} when State#state.server_version >= [5, 0, 0] ->
620 2 kill_query(State),
621 2 mysql_protocol:fetch_execute_response(SockMod, Socket, [],
622 DecodeDecimal, FilterMap,
623 ?cmd_timeout);
624 {error, Reason} ->
625
:-(
exit(Reason);
626 QueryResult ->
627 221 QueryResult
628 end,
629 223 setopts(SockMod, Socket, [{active, once}]),
630 223 State1 = lists:foldl(fun update_state/2, State, Recs),
631 223 State1#state.warning_count > 0 andalso State1#state.log_warnings
632 5 andalso log_warnings(State1, Stmt#prepared.orig_query),
633 223 handle_query_call_result(Recs, Stmt#prepared.orig_query, State1).
634
635 %% @doc Formats floats as decimals, optionally with a given number of decimals.
636 float_to_decimal(Arg, true) when is_float(Arg) ->
637 4 {decimal, list_to_binary(io_lib:format("~w", [Arg]))};
638 float_to_decimal(Arg, N) when is_float(Arg), is_integer(N) ->
639 1 {decimal, float_to_binary(Arg, [{decimals, N}, compact])};
640 float_to_decimal(Arg, _) ->
641 2 Arg.
642
643 %% @doc Produces a tuple to return as an error reason.
644 -spec error_to_reason(#error{}) -> mysql:server_reason().
645 error_to_reason(#error{code = Code, state = State, msg = Msg}) ->
646 19 {Code, State, Msg}.
647
648 %% @doc Updates a state with information from a response. Also re-schedules
649 %% ping.
650 -spec update_state(#ok{} | #eof{} | any(), #state{}) -> #state{}.
651 update_state(Rec, State) ->
652 908 State1 = case Rec of
653 #ok{status = S, affected_rows = R, insert_id = Id, warning_count = W} ->
654 488 State#state{status = S, affected_rows = R, insert_id = Id,
655 warning_count = W};
656 #resultset{status = S, warning_count = W} ->
657 297 State#state{status = S, warning_count = W};
658 #prepared{warning_count = W} ->
659 107 State#state{warning_count = W};
660 _Other ->
661 %% This includes errors.
662 %% Reset some things. (Note: We don't reset status and insert_id.)
663 16 State#state{warning_count = 0, affected_rows = 0}
664 end,
665 908 schedule_ping(State1).
666
667 %% @doc executes an unparameterized query and returns {Reply, NewState}.
668 query(Query, FilterMap, default_timeout,
669 #state{query_timeout = DefaultTimeout} = State) ->
670 493 query(Query, FilterMap, DefaultTimeout, State);
671 query(Query, FilterMap, Timeout, State) ->
672 495 #state{sockmod = SockMod, socket = Socket,
673 allowed_local_paths = AllowedPaths,
674 decode_decimal = DecodeDecimal} = State,
675 495 setopts(SockMod, Socket, [{active, false}]),
676 495 Result = mysql_protocol:query(Query, SockMod, Socket, AllowedPaths,
677 DecodeDecimal, FilterMap, Timeout),
678 495 {ok, Recs} = case Result of
679 {error, timeout} when State#state.server_version >= [5, 0, 0] ->
680 1 kill_query(State),
681 1 mysql_protocol:fetch_query_response(SockMod, Socket,
682 [], DecodeDecimal, FilterMap,
683 ?cmd_timeout);
684 {error, Reason} ->
685
:-(
exit(Reason);
686 QueryResult ->
687 494 QueryResult
688 end,
689 495 setopts(SockMod, Socket, [{active, once}]),
690 495 State1 = lists:foldl(fun update_state/2, State, Recs),
691 495 State1#state.warning_count > 0 andalso State1#state.log_warnings
692 3 andalso log_warnings(State1, Query),
693 495 handle_query_call_result(Recs, Query, State1).
694
695 %% @doc Prepares a named query and returns {{ok, Name}, NewState} or
696 %% {{error, Reason}, NewState}.
697 named_prepare(Name, Query, State) ->
698 16 #state{socket = Socket, sockmod = SockMod} = State,
699 %% First unprepare if there is an old statement with this name.
700 16 setopts(SockMod, Socket, [{active, false}]),
701 16 State1 = case maps:take(Name, State#state.stmts) of
702 {OldStmt, Stmts1} ->
703 1 mysql_protocol:unprepare(OldStmt, SockMod, Socket),
704 1 State#state{stmts = Stmts1};
705 error ->
706 15 State
707 end,
708 16 Rec = mysql_protocol:prepare(Query, SockMod, Socket),
709 16 setopts(SockMod, Socket, [{active, once}]),
710 16 State2 = #state{stmts = Stmts2} = update_state(Rec, State1),
711 16 case Rec of
712 #error{} = E ->
713 3 {{error, error_to_reason(E)}, State2};
714 #prepared{} = Stmt ->
715 13 State3 = State2#state{stmts = Stmts2#{Name => Stmt}},
716 13 {{ok, Name}, State3}
717 end.
718
719 %% @doc Transforms result sets into a structure appropriate to be returned
720 %% to the client.
721 handle_query_call_result([_] = Recs, Query, State) ->
722 706 handle_query_call_result(Recs, not_applicable, Query, State, []);
723 handle_query_call_result(Recs, Query, State) ->
724 12 handle_query_call_result(Recs, 1, Query, State, []).
725
726 handle_query_call_result([], _RecNum, _Query, State, []) ->
727 419 {ok, State};
728 handle_query_call_result([], _RecNum, _Query, State, [{ColumnNames, Rows}]) ->
729 279 {{ok, ColumnNames, Rows}, State};
730 handle_query_call_result([], _RecNum, _Query, State, ResultSetsAcc) ->
731 8 {{ok, lists:reverse(ResultSetsAcc)}, State};
732 handle_query_call_result([Rec|Recs], RecNum, Query,
733 #state{transaction_levels = L} = State,
734 ResultSetsAcc) ->
735 734 RecNum1 = case RecNum of
736 706 not_applicable -> not_applicable;
737 28 _ -> RecNum + 1
738 end,
739 734 case Rec of
740 #ok{status = Status} when Status band ?SERVER_STATUS_IN_TRANS == 0,
741 L /= [] ->
742 %% DDL statements (e.g. CREATE TABLE, ALTER TABLE, etc.) result in
743 %% an implicit commit.
744 1 Length = length(L),
745 1 Reply = {implicit_commit, Length, Query},
746 1 [] = demonitor_processes(L, Length),
747 1 {Reply, State#state{transaction_levels = []}};
748 #ok{status = Status} ->
749 425 maybe_log_slow_query(State, Status, RecNum, Query),
750 425 handle_query_call_result(Recs, RecNum1, Query, State, ResultSetsAcc);
751 #resultset{cols = ColDefs, rows = Rows, status = Status} ->
752 297 Names = [Def#col.name || Def <- ColDefs],
753 297 ResultSetsAcc1 = [{Names, Rows} | ResultSetsAcc],
754 297 maybe_log_slow_query(State, Status, RecNum, Query),
755 297 handle_query_call_result(Recs, RecNum1, Query, State, ResultSetsAcc1);
756 #error{code = ?ERROR_DEADLOCK} when L /= [] ->
757 %% These errors result in an implicit rollback.
758 3 Reply = {implicit_rollback, length(L), error_to_reason(Rec)},
759 %% The transaction is rollbacked, except the BEGIN, so we're still
760 %% in a transaction. (In 5.7+, also the BEGIN has been rolled back,
761 %% but here we assume the old behaviour.)
762 3 NewMonitors = demonitor_processes(L, length(L) - 1),
763 3 {Reply, State#state{transaction_levels = NewMonitors}};
764 #error{} ->
765 8 {{error, error_to_reason(Rec)}, State}
766 end.
767
768 %% @doc Schedules (or re-schedules) ping.
769 schedule_ping(State = #state{ping_timeout = infinity}) ->
770 301 State;
771 schedule_ping(State = #state{ping_timeout = Timeout, ping_ref = Ref}) ->
772 751 is_reference(Ref) andalso erlang:cancel_timer(Ref),
773 751 State#state{ping_ref = erlang:send_after(Timeout, self(), ping)}.
774
775 %% @doc Fetches and logs warnings. Query is the query that gave the warnings.
776 log_warnings(#state{socket = Socket, sockmod = SockMod,
777 decode_decimal = DecodeDecimal}, Query) ->
778 8 setopts(SockMod, Socket, [{active, false}]),
779 8 {ok, [#resultset{rows = Rows}]} = mysql_protocol:query(<<"SHOW WARNINGS">>,
780 SockMod, Socket,
781 [], DecodeDecimal,
782 no_filtermap_fun,
783 ?cmd_timeout),
784 8 setopts(SockMod, Socket, [{active, once}]),
785 8 Lines = [[Level, " ", integer_to_binary(Code), ": ", Message, "\n"]
786 8 || [Level, Code, Message] <- Rows],
787 8 logger:warning("~s in ~s~n", [Lines, Query]).
788
789 %% @doc Logs slow queries. Query is the query that gave the warnings.
790 maybe_log_slow_query(#state{log_slow_queries = true}, S, RecNum, Query)
791 when S band ?SERVER_QUERY_WAS_SLOW /= 0 ->
792 3 IndexHint = if
793 S band ?SERVER_STATUS_NO_GOOD_INDEX_USED /= 0 ->
794
:-(
" (with no good index)";
795 S band ?SERVER_STATUS_NO_INDEX_USED /= 0 ->
796
:-(
" (with no index)";
797 true ->
798 3 ""
799 end,
800 3 QueryNumHint = case RecNum of
801 not_applicable ->
802 1 "";
803 _ ->
804 2 io_lib:format(" #~b", [RecNum])
805 end,
806 3 logger:warning("MySQL query~s~s was slow: ~s~n",
807 [QueryNumHint, IndexHint, Query]);
808 maybe_log_slow_query(_, _, _, _) ->
809 719 ok.
810
811 %% @doc Makes a separate connection and execute KILL QUERY. We do this to get
812 %% our main connection back to normal. KILL QUERY appeared in MySQL 5.0.0.
813 kill_query(#state{connection_id = ConnId, host = Host, port = Port,
814 user = User, password = Password, ssl_opts = SSLOpts,
815 cap_found_rows = SetFoundRows}) ->
816 %% Connect socket
817 3 SockOpts = [{active, false}, binary, {packet, raw}],
818 3 {ok, Socket0} = gen_tcp:connect(Host, Port, SockOpts),
819
820 %% Exchange handshake communication.
821 3 Result = mysql_protocol:handshake(Host, User, Password, undefined, gen_tcp,
822 SSLOpts, Socket0, SetFoundRows),
823 3 case Result of
824 {ok, #handshake{}, SockMod, Socket} ->
825 %% Kill and disconnect
826 3 IdBin = integer_to_binary(ConnId),
827 3 {ok, [#ok{}]} = mysql_protocol:query(<<"KILL QUERY ", IdBin/binary>>,
828 SockMod, Socket,
829 [], auto,
830 no_filtermap_fun,
831 ?cmd_timeout),
832 3 mysql_protocol:quit(SockMod, Socket);
833 #error{} = E ->
834
:-(
logger:error("Failed to connect to kill query: ~p",
835 [error_to_reason(E)])
836 end.
837
838 stop_server(Reason, #state{socket = undefined} = State) ->
839
:-(
{stop, Reason, State};
840 stop_server(Reason,
841 #state{socket = Socket, connection_id = ConnId, sockmod = SockMod} = State) ->
842 6 logger:error("Connection Id ~p closing with reason: ~p~n",
843 [ConnId, Reason]),
844 6 ok = SockMod:close(Socket),
845 6 {stop, Reason, State#state{socket = undefined, connection_id = undefined}}.
846
847 setopts(gen_tcp, Socket, Opts) ->
848 2078 inet:setopts(Socket, Opts);
849 setopts(SockMod, Socket, Opts) ->
850 23 SockMod:setopts(Socket, Opts).
851
852 demonitor_processes(List, 0) ->
853 4 List;
854 demonitor_processes([{_FromPid, MRef}|T], Count) ->
855 5 erlang:demonitor(MRef, [flush]),
856 5 demonitor_processes(T, Count - 1).
857
858 format_status(Status = #{state := State = #state{ssl_opts = SSLOpts}}) ->
859 12 Status#{state := State#state{password = hidden,
860 ssl_opts = obfuscate_ssl_options(SSLOpts)}}.
861
862 -if(?OTP_RELEASE < 25).
863 format_status(_, [_PDict, State = #state{ssl_opts = SSLOpts}]) ->
864 {data, [{"State", State#state{password = hidden,
865 ssl_opts = obfuscate_ssl_options(SSLOpts)}}]}.
866 -endif.
867
868 obfuscate_ssl_options(undefined) ->
869 11 undefined;
870 obfuscate_ssl_options(SSLOpts) ->
871 1 lists:map(
872 fun
873
:-(
({cert, _}) -> {cert, hidden};
874
:-(
({key, _}) -> {key, hidden};
875
:-(
({cacerts, _}) -> {cacerts, hidden};
876 2 (Other) -> Other
877 end,
878 SSLOpts
879 ).
Line Hits Source