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