mysql-1.0.0/0000755000232200023220000000000012642720136013226 5ustar debalancedebalancemysql-1.0.0/rebar.config0000644000232200023220000000017612642720136015514 0ustar debalancedebalance{erl_opts, [debug_info, {i, "include"}]}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: mysql-1.0.0/include/0000755000232200023220000000000012642720136014651 5ustar debalancedebalancemysql-1.0.0/include/p1_mysql.hrl0000644000232200023220000000015412642720136017125 0ustar debalancedebalance%% MySQL result record: -record(p1_mysql_result, {fieldinfo=[], rows=[], affectedrows=0, error=""}). mysql-1.0.0/src/0000755000232200023220000000000012642720136014015 5ustar debalancedebalancemysql-1.0.0/src/p1_mysql_auth.erl0000644000232200023220000001504112642720136017310 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql_auth.erl %%% Author : Fredrik Thulin %%% Descrip.: MySQL client authentication functions. %%% Created : 4 Aug 2005 by Fredrik Thulin %%% %%% Note : All MySQL code was written by Magnus Ahltorp, originally %%% in the file p1_mysql.erl - I just moved it here. %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%%------------------------------------------------------------------- -module(p1_mysql_auth). %%-------------------------------------------------------------------- %% External exports (should only be used by the 'p1_mysql_conn' module) %%-------------------------------------------------------------------- -export([ do_old_auth/7, do_new_auth/8 ]). %%-------------------------------------------------------------------- %% Macros %%-------------------------------------------------------------------- -define(LONG_PASSWORD, 1). -define(FOUND_ROWS, 2). -define(LONG_FLAG, 4). -define(PROTOCOL_41, 512). -define(TRANSACTIONS, 8192). -define(SECURE_CONNECTION, 32768). -define(CONNECT_WITH_DB, 8). -define(MAX_PACKET_SIZE, 1000000). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: do_old_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, %% LogFun) %% Sock = term(), gen_tcp socket %% RecvPid = pid(), receiver process pid %% SeqNum = integer(), first sequence number we should use %% User = string(), MySQL username %% Password = string(), MySQL password %% Salt1 = string(), salt 1 from server greeting %% LogFun = undefined | function() of arity 3 %% Descrip.: Perform old-style MySQL authentication. %% Returns : result of p1_mysql_conn:do_recv/3 %%-------------------------------------------------------------------- do_old_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, LogFun) -> Auth = password_old(Password, Salt1), Packet2 = make_auth(User, Auth), do_send(Sock, Packet2, SeqNum, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum). %%-------------------------------------------------------------------- %% Function: do_new_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, %% Salt2, LogFun) %% Sock = term(), gen_tcp socket %% RecvPid = pid(), receiver process pid %% SeqNum = integer(), first sequence number we should use %% User = string(), MySQL username %% Password = string(), MySQL password %% Salt1 = string(), salt 1 from server greeting %% Salt2 = string(), salt 2 from server greeting %% LogFun = undefined | function() of arity 3 %% Descrip.: Perform MySQL authentication. %% Returns : result of p1_mysql_conn:do_recv/3 %%-------------------------------------------------------------------- do_new_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, Salt2, LogFun) -> Auth = password_new(Password, Salt1 ++ Salt2), Packet2 = make_new_auth(User, Auth, none), do_send(Sock, Packet2, SeqNum, LogFun), case p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum) of {ok, Packet3, SeqNum2} -> case Packet3 of <<254:8>> -> AuthOld = password_old(Password, Salt1), do_send(Sock, <>, SeqNum2 + 1, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum2 + 1); _ -> {ok, Packet3, SeqNum2} end; {error, Reason} -> {error, Reason} end. %%==================================================================== %% Internal functions %%==================================================================== password_old(Password, Salt) -> {P1, P2} = hash(Password), {S1, S2} = hash(Salt), Seed1 = P1 bxor S1, Seed2 = P2 bxor S2, List = rnd(9, Seed1, Seed2), {L, [Extra]} = lists:split(8, List), list_to_binary(lists:map(fun (E) -> E bxor (Extra - 64) end, L)). %% part of do_old_auth/4, which is part of mysql_init/4 make_auth(User, Password) -> Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS bor ?FOUND_ROWS, Maxsize = 0, UserB = list_to_binary(User), PasswordB = Password, <>. %% part of do_new_auth/4, which is part of mysql_init/4 make_new_auth(User, Password, Database) -> DBCaps = case Database of none -> 0; _ -> ?CONNECT_WITH_DB end, Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS bor ?PROTOCOL_41 bor ?SECURE_CONNECTION bor DBCaps bor ?FOUND_ROWS, Maxsize = ?MAX_PACKET_SIZE, UserB = list_to_binary(User), PasswordL = size(Password), DatabaseB = case Database of none -> <<>>; _ -> list_to_binary(Database) end, <>. hash(S) -> hash(S, 1345345333, 305419889, 7). hash([C | S], N1, N2, Add) -> N1_1 = N1 bxor (((N1 band 63) + Add) * C + N1 * 256), N2_1 = N2 + ((N2 * 256) bxor N1_1), Add_1 = Add + C, hash(S, N1_1, N2_1, Add_1); hash([], N1, N2, _Add) -> Mask = (1 bsl 31) - 1, {N1 band Mask , N2 band Mask}. rnd(N, Seed1, Seed2) -> Mod = (1 bsl 30) - 1, rnd(N, [], Seed1 rem Mod, Seed2 rem Mod). rnd(0, List, _, _) -> lists:reverse(List); rnd(N, List, Seed1, Seed2) -> Mod = (1 bsl 30) - 1, NSeed1 = (Seed1 * 3 + Seed2) rem Mod, NSeed2 = (NSeed1 + Seed2 + 33) rem Mod, Float = (float(NSeed1) / float(Mod))*31, Val = trunc(Float)+64, rnd(N - 1, [Val | List], NSeed1, NSeed2). dualmap(_F, [], []) -> []; dualmap(F, [E1 | R1], [E2 | R2]) -> [F(E1, E2) | dualmap(F, R1, R2)]. bxor_binary(B1, B2) -> list_to_binary(dualmap(fun (E1, E2) -> E1 bxor E2 end, binary_to_list(B1), binary_to_list(B2))). password_new(Password, Salt) -> Stage1 = crypto:hash(sha, Password), Stage2 = crypto:hash(sha, Stage1), Res = crypto:hash_final( crypto:hash_update( crypto:hash_update(crypto:hash_init(sha), Salt), Stage2) ), bxor_binary(Res, Stage1). do_send(Sock, Packet, Num, LogFun) -> p1_mysql:log(LogFun, debug, "p1_mysql_auth send packet ~p: ~p", [Num, Packet]), Data = <<(size(Packet)):24/little, Num:8, Packet/binary>>, gen_tcp:send(Sock, Data). mysql-1.0.0/src/p1_mysql_conn.erl0000644000232200023220000007057412642720136017320 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql_conn.erl %%% Author : Fredrik Thulin %%% Descrip.: MySQL connection handler, handles de-framing of messages %%% received by the MySQL receiver process. %%% Created : 5 Aug 2005 by Fredrik Thulin %%% Modified: 11 Jan 2006 by Mickael Remond %%% %%% Note : All MySQL code was written by Magnus Ahltorp, originally %%% in the file p1_mysql.erl - I just moved it here. %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%% %%% This module handles a single connection to a single MySQL server. %%% You can use it stand-alone, or through the 'p1_mysql' module if you %%% want to have more than one connection to the server, or %%% connections to different servers. %%% %%% To use it stand-alone, set up the connection with %%% %%% {ok, Pid} = p1_mysql_conn:start(Host, Port, User, Password, %%% Database, LogFun) %%% %%% Host = string() %%% Port = integer() %%% User = string() %%% Password = string() %%% Database = string() %%% LogFun = undefined | (gives logging to console) %%% function() of arity 3 (Level, Fmt, Args) %%% %%% Note: In stand-alone mode you have to start Erlang crypto application by %%% yourself with crypto:start() %%% %%% and then make MySQL querys with %%% %%% Result = p1_mysql_conn:fetch(Pid, Query, self()) %%% %%% Result = {data, MySQLRes} | %%% {updated, MySQLRes} | %%% {error, MySQLRes} %%% Where: MySQLRes = #p1_mysql_result %%% %%% Actual data can be extracted from MySQLRes by calling the following API %%% functions: %%% - on data received: %%% FieldInfo = p1_mysql:get_result_field_info(MysqlRes) %%% AllRows = p1_mysql:get_result_rows(MysqlRes) %%% with FieldInfo = list() of {Table, Field, Length, Name} %%% and AllRows = list() of list() representing records %%% - on update: %%% Affected= p1_mysql:get_result_affected_rows(MysqlRes) %%% with Affected = integer() %%% - on error: %%% Reason = p1_mysql:get_result_reason(MysqlRes) %%% with Reason = string() %%%------------------------------------------------------------------- -module(p1_mysql_conn). %%-------------------------------------------------------------------- %% External exports %%-------------------------------------------------------------------- -export([start/6, start_link/6, fetch/3, fetch/4, squery/4, stop/1 ]). %%-------------------------------------------------------------------- %% External exports (should only be used by the 'p1_mysql_auth' module) %%-------------------------------------------------------------------- -export([do_recv/3 ]). -include("p1_mysql.hrl"). -record(state, { mysql_version, log_fun, recv_pid, socket, data }). -define(SECURE_CONNECTION, 32768). -define(MYSQL_QUERY_OP, 3). -define(DEFAULT_STANDALONE_TIMEOUT, 5000). -define(DEFAULT_RESULT_TYPE, list). -define(MYSQL_4_0, 40). %% Support for MySQL 4.0.x -define(MYSQL_4_1, 41). %% Support for MySQL 4.1.x et 5.0.x %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start(Host, Port, User, Password, Database, LogFun) %% Function: start_link(Host, Port, User, Password, Database, LogFun) %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() of arity 3 %% Descrip.: Starts a p1_mysql_conn process that connects to a MySQL %% server, logs in and chooses a database. %% Returns : {ok, Pid} | {error, Reason} %% Pid = pid() %% Reason = string() %%-------------------------------------------------------------------- start(Host, Port, User, Password, Database, LogFun) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> ConnPid = self(), Pid = spawn(fun () -> init(Host, Port, User, Password, Database, LogFun, ConnPid) end), post_start(Pid, LogFun). start_link(Host, Port, User, Password, Database, LogFun) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> ConnPid = self(), Pid = spawn_link(fun () -> init(Host, Port, User, Password, Database, LogFun, ConnPid) end), post_start(Pid, LogFun). %% part of start/6 or start_link/6: post_start(Pid, _LogFun) -> %%Timeout = get_option(timeout, Options, ?DEFAULT_STANDALONE_TIMEOUT), %%TODO find a way to get configured Options here Timeout= ?DEFAULT_STANDALONE_TIMEOUT, receive {p1_mysql_conn, Pid, ok} -> {ok, Pid}; {p1_mysql_conn, Pid, {error, Reason}} -> p1_mysql:log(_LogFun, error, "p1_mysql_conn: post_start error ~p~n", [Reason]), stop(Pid), {error, Reason} % Unknown -> % p1_mysql:log(_LogFun, error, "p1_mysql_conn: Received unknown signal, exiting"), % p1_mysql:log(_LogFun, debug, "p1_mysql_conn: Unknown signal : ~p", [Unknown]), % {error, "unknown signal received"} after Timeout -> p1_mysql:log(_LogFun, error, "p1_mysql_conn: post_start timeout~n", []), stop(Pid), timer:sleep(100), catch exit(Pid, kill), {error, "timed out"} end. %%-------------------------------------------------------------------- %% Function: fetch(Pid, Query, From) %% fetch(Pid, Query, From, Timeout) %% Pid = pid(), p1_mysql_conn to send fetch-request to %% Query = string(), MySQL query in verbatim %% From = pid() or term(), use a From of self() when %% using this module for a single connection, %% or pass the gen_server:call/3 From argument if %% using a gen_server to do the querys (e.g. the %% mysql_dispatcher) %% Timeout = integer() | infinity, gen_server timeout value %% Descrip.: Send a query and wait for the result if running stand- %% alone (From = self()), but don't block the caller if we %% are not running stand-alone (From = gen_server From). %% Returns : ok | (non-stand-alone mode) %% {data, #p1_mysql_result} | (stand-alone mode) %% {updated, #p1_mysql_result} | (stand-alone mode) %% {error, #p1_mysql_result} (stand-alone mode) %% FieldInfo = term() %% Rows = list() of [string()] %% Reason = term() %%-------------------------------------------------------------------- fetch(Pid, Query, From) -> squery(Pid, Query, From, []). fetch(Pid, Query, From, Timeout) -> squery(Pid, Query, From, [{timeout, Timeout}]). squery(Pid, Query, From, Options) when is_pid(Pid), is_list(Query) -> Self = self(), Timeout = get_option(timeout, Options, ?DEFAULT_STANDALONE_TIMEOUT), TRef = erlang:start_timer(Timeout, self(), timeout), Pid ! {fetch, TRef, Query, From, Options}, case From of Self -> %% We are not using a mysql_dispatcher, await the response wait_fetch_result(TRef, Pid); _ -> %% From is gen_server From, Pid will do gen_server:reply() %% when it has an answer ok end. wait_fetch_result(TRef, Pid) -> receive {fetch_result, TRef, Pid, Result} -> case erlang:cancel_timer(TRef) of false -> receive {timeout, TRef, _} -> ok after 0 -> ok end; _ -> ok end, Result; {fetch_result, _BadRef, Pid, _Result} -> wait_fetch_result(TRef, Pid); {timeout, TRef, _Info} -> stop(Pid), {error, #p1_mysql_result{error="query timed out"}} end. stop(Pid) -> Pid ! close. %%-------------------------------------------------------------------- %% Function: do_recv(LogFun, RecvPid, SeqNum) %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% SeqNum = undefined | integer() %% Descrip.: Wait for a frame decoded and sent to us by RecvPid. %% Either wait for a specific frame if SeqNum is an integer, %% or just any frame if SeqNum is undefined. %% Returns : {ok, Packet, Num} | %% {error, Reason} %% Reason = term() %% %% Note : Only to be used externally by the 'p1_mysql_auth' module. %%-------------------------------------------------------------------- do_recv(LogFun, RecvPid, SeqNum) when is_function(LogFun); LogFun == undefined, SeqNum == undefined -> receive {p1_mysql_recv, RecvPid, data, Packet, Num} -> %%p1_mysql:log(LogFun, debug, "p1_mysql_conn: recv packet ~p: %%~p", [Num, Packet]), {ok, Packet, Num}; {p1_mysql_recv, RecvPid, closed, _E} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: p1_mysql_recv:" " socket was closed ~p~n", [{RecvPid, _E}]), {error, "p1_mysql_recv: socket was closed"} end; do_recv(LogFun, RecvPid, SeqNum) when is_function(LogFun); LogFun == undefined, is_integer(SeqNum) -> ResponseNum = SeqNum + 1, receive {p1_mysql_recv, RecvPid, data, Packet, ResponseNum} -> %%p1_mysql:log(LogFun, debug, "p1_mysql_conn: recv packet ~p: %%~p", [ResponseNum, Packet]), {ok, Packet, ResponseNum}; {p1_mysql_recv, RecvPid, closed, _E} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: p1_mysql_recv:" " socket was closed 2 ~p~n", [{RecvPid, _E}]), {error, "p1_mysql_recv: socket was closed"} end. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Host, Port, User, Password, Database, LogFun, %% Parent) %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() of arity 3 %% Parent = pid() of process starting this p1_mysql_conn %% Descrip.: Connect to a MySQL server, log in and chooses a database. %% Report result of this to Parent, and then enter loop() if %% we were successfull. %% Returns : void() | does not return %%-------------------------------------------------------------------- init(Host, Port, User, Password, Database, LogFun, Parent) -> case p1_mysql_recv:start_link(Host, Port, LogFun, self()) of {ok, RecvPid, Sock} -> case mysql_init(Sock, RecvPid, User, Password, LogFun) of {ok, Version} -> case do_query(Sock, RecvPid, LogFun, "use " ++ Database, Version, [{result_type, binary}]) of {error, MySQLRes} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: Failed changing" " to database ~p : ~p", [Database, p1_mysql:get_result_reason(MySQLRes)]), gen_tcp:close(Sock), Parent ! {p1_mysql_conn, self(), {error, failed_changing_database}}; %% ResultType: data | updated {_ResultType, _MySQLRes} -> Parent ! {p1_mysql_conn, self(), ok}, State = #state{mysql_version=Version, recv_pid = RecvPid, socket = Sock, log_fun = LogFun, data = <<>> }, loop(State) end; {error, _Reason} -> Parent ! {p1_mysql_conn, self(), {error, login_failed}} end; E -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "Failed connecting to ~p:~p : ~p", [Host, Port, E]), Parent ! {p1_mysql_conn, self(), {error, connect_failed}} end. %%-------------------------------------------------------------------- %% Function: loop(State) %% State = state record() %% Descrip.: Wait for signals asking us to perform a MySQL query, or %% signals that the socket was closed. %% Returns : error | does not return %%-------------------------------------------------------------------- loop(State) -> RecvPid = State#state.recv_pid, receive {fetch, Ref, Query, GenSrvFrom, Options} -> %% GenSrvFrom is either a gen_server:call/3 From term(), %% or a pid if no gen_server was used to make the query Res = do_query(State, Query, Options), case is_pid(GenSrvFrom) of true -> %% The query was not sent using gen_server mechanisms GenSrvFrom ! {fetch_result, Ref, self(), Res}; false -> %% the timer is canceled in wait_fetch_result/2, but we wait on that funtion only if the query %% was not sent using the mysql gen_server. So we at least should try to cancel the timer here %% (no warranty, the gen_server can still receive timeout messages) erlang:cancel_timer(Ref), gen_server:reply(GenSrvFrom, Res) end, loop(State); {p1_mysql_recv, RecvPid, data, Packet, Num} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Received MySQL data when not expecting any " "(num ~p) - ignoring it", [Num]), p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Unexpected MySQL data (num ~p) :~n~p", [Num, Packet]), loop(State); {p1_mysql_recv, RecvPid, closed, _Reason} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Connection closed, exiting.", []), close_connection(State); close -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Received close signal, exiting.", []), close_connection(State); Unknown -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Received unknown signal, exiting : ~p", [Unknown]), close_connection(State), error end. %%-------------------------------------------------------------------- %% Function: mysql_init(Sock, RecvPid, User, Password, LogFun) %% Sock = term(), gen_tcp socket %% RecvPid = pid(), p1_mysql_recv process %% User = string() %% Password = string() %% LogFun = undefined | function() with arity 3 %% Descrip.: Try to authenticate on our new socket. %% Returns : ok | {error, Reason} %% Reason = string() %%-------------------------------------------------------------------- mysql_init(Sock, RecvPid, User, Password, LogFun) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, InitSeqNum} -> {Version, Salt1, Salt2, Caps} = greeting(Packet, LogFun), AuthRes = case Caps band ?SECURE_CONNECTION of ?SECURE_CONNECTION -> p1_mysql_auth:do_new_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1, Salt2, LogFun); _ -> p1_mysql_auth:do_old_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1, LogFun) end, case AuthRes of {ok, <<0:8, _Rest/binary>>, _RecvNum} -> {ok,Version}; {ok, <<255:8, Code:16/little, Message/binary>>, _RecvNum} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "init error ~p: ~p~n", [Code, binary_to_list(Message)]), {error, binary_to_list(Message)}; {ok, RecvPacket, _RecvNum} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "init unknown error ~p~n", [binary_to_list(RecvPacket)]), {error, binary_to_list(RecvPacket)}; {error, Reason} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "init failed receiving data : ~p~n", [Reason]), {error, Reason} end; {error, Reason} -> {error, Reason} end. %% part of mysql_init/4 greeting(Packet, LogFun) -> <> = Packet, {Version, Rest2} = asciz(Rest), <<_TreadID:32/little, Rest3/binary>> = Rest2, {Salt, Rest4} = asciz(Rest3), <> = Rest4, <> = Rest5, {Salt2, _Rest7} = asciz(Rest6), p1_mysql:log(LogFun, debug, "p1_mysql_conn: greeting version ~p (protocol ~p) " "salt ~p caps ~p serverchar ~p salt2 ~p", [Version, Protocol, Salt, Caps, ServerChar, Salt2]), {normalize_version(Version, LogFun), Salt, Salt2, Caps}. %% part of greeting/2 asciz(Data) when is_binary(Data) -> p1_mysql:asciz_binary(Data, []); asciz(Data) when is_list(Data) -> {String, [0 | Rest]} = lists:splitwith(fun (C) -> C /= 0 end, Data), {String, Rest}. %%-------------------------------------------------------------------- %% Function: get_query_response(LogFun, RecvPid) %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% Version = integer(), Representing MySQL version used %% Descrip.: Wait for frames until we have a complete query response. %% Returns : {data, #p1_mysql_result} %% {updated, #p1_mysql_result} %% {error, #p1_mysql_result} %% FieldInfo = list() of term() %% Rows = list() of [string()] %% AffectedRows = int() %% Reason = term() %%-------------------------------------------------------------------- get_query_response(LogFun, RecvPid, Version, Options) -> case do_recv(LogFun, RecvPid, undefined) of {ok, <>, _} -> case Fieldcount of 0 -> %% No Tabular data <> = Rest, {updated, #p1_mysql_result{affectedrows=AffectedRows}}; 255 -> <<_Code:16/little, Message/binary>> = Rest, {error, #p1_mysql_result{error=binary_to_list(Message)}}; _ -> %% Tabular data received ResultType = get_option(result_type, Options, ?DEFAULT_RESULT_TYPE), case get_fields(LogFun, RecvPid, [], Version, ResultType) of {ok, Fields} -> case get_rows(Fieldcount, LogFun, RecvPid, ResultType, []) of {ok, Rows} -> {data, #p1_mysql_result{fieldinfo=Fields, rows=Rows}}; {error, Reason} -> {error, #p1_mysql_result{error=Reason}} end; {error, Reason} -> {error, #p1_mysql_result{error=Reason}} end end; {error, Reason} -> {error, #p1_mysql_result{error=Reason}} end. %%-------------------------------------------------------------------- %% Function: get_fields(LogFun, RecvPid, [], Version) %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% Version = integer(), Representing MySQL version used %% Descrip.: Received and decode field information. %% Returns : {ok, FieldInfo} | %% {error, Reason} %% FieldInfo = list() of term() %% Reason = term() %%-------------------------------------------------------------------- %% Support for MySQL 4.0.x: get_fields(LogFun, RecvPid, Res, ?MYSQL_4_0, ResultType) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, _Num} -> case Packet of <<254:8>> -> {ok, lists:reverse(Res)}; <<254:8, Rest/binary>> when size(Rest) < 8 -> {ok, lists:reverse(Res)}; _ -> {Table, Rest} = get_with_length(Packet), {Field, Rest2} = get_with_length(Rest), {LengthB, Rest3} = get_with_length(Rest2), LengthL = size(LengthB) * 8, <> = LengthB, {Type, Rest4} = get_with_length(Rest3), {_Flags, _Rest5} = get_with_length(Rest4), if ResultType == list -> This = {binary_to_list(Table), binary_to_list(Field), Length, %% TODO: Check on MySQL 4.0 if types are specified %% using the same 4.1 formalism and could %% be expanded to atoms: binary_to_list(Type)}; ResultType == binary -> This = {Table, Field, Length, Type} end, get_fields(LogFun, RecvPid, [This | Res], ?MYSQL_4_0, ResultType) end; {error, Reason} -> {error, Reason} end; %% Support for MySQL 4.1.x and 5.x: get_fields(LogFun, RecvPid, Res, ?MYSQL_4_1, ResultType) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, _Num} -> case Packet of <<254:8>> -> {ok, lists:reverse(Res)}; <<254:8, Rest/binary>> when size(Rest) < 8 -> {ok, lists:reverse(Res)}; _ -> {_Catalog, Rest} = get_with_length(Packet), {_Database, Rest2} = get_with_length(Rest), {Table, Rest3} = get_with_length(Rest2), %% OrgTable is the real table name if Table is an alias {_OrgTable, Rest4} = get_with_length(Rest3), {Field, Rest5} = get_with_length(Rest4), %% OrgField is the real field name if Field is an alias {_OrgField, Rest6} = get_with_length(Rest5), <<_Metadata:8/little, _Charset:16/little, Length:32/little, Type:8/little, _Flags:16/little, _Decimals:8/little, _Rest7/binary>> = Rest6, if ResultType == list -> This = {binary_to_list(Table), binary_to_list(Field), Length, get_field_datatype(Type)}; ResultType == binary -> This = {Table, Field, Length, get_field_datatype(Type)} end, get_fields(LogFun, RecvPid, [This | Res], ?MYSQL_4_1, ResultType) end; {error, Reason} -> {error, Reason} end. %%-------------------------------------------------------------------- %% Function: get_rows(N, LogFun, RecvPid, []) %% N = integer(), number of rows to get %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% Descrip.: Receive and decode a number of rows. %% Returns : {ok, Rows} | %% {error, Reason} %% Rows = list() of [string()] %%-------------------------------------------------------------------- get_rows(N, LogFun, RecvPid, ResultType, Res) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, _Num} -> case Packet of <<254:8, Rest/binary>> when size(Rest) < 8 -> {ok, lists:reverse(Res)}; _ -> {ok, This} = get_row(N, Packet, ResultType, []), get_rows(N, LogFun, RecvPid, ResultType, [This | Res]) end; {error, Reason} -> {error, Reason} end. %% part of get_rows/4 get_row(0, _Data, _ResultType, Res) -> {ok, lists:reverse(Res)}; get_row(N, Data, ResultType, Res) -> {Col, Rest} = get_with_length(Data), This = case Col of null -> null; _ -> if ResultType == list -> binary_to_list(Col); ResultType == binary -> Col end end, get_row(N - 1, Rest, ResultType, [This | Res]). get_with_length(<<251:8, Rest/binary>>) -> {null, Rest}; get_with_length(<<252:8, Length:16/little, Rest/binary>>) -> split_binary(Rest, Length); get_with_length(<<253:8, Length:24/little, Rest/binary>>) -> split_binary(Rest, Length); get_with_length(<<254:8, Length:64/little, Rest/binary>>) -> split_binary(Rest, Length); get_with_length(<>) when Length < 251 -> split_binary(Rest, Length). close_connection(State) -> Result = gen_tcp:close(State#state.socket), p1_mysql:log(State#state.log_fun, normal, "Closing connection ~p: ~p~n", [State#state.socket, Result]), Result. %%-------------------------------------------------------------------- %% Function: do_query(State, Query) %% do_query(Sock, RecvPid, LogFun, Query) %% Sock = term(), gen_tcp socket %% RecvPid = pid(), p1_mysql_recv process %% LogFun = undefined | function() with arity 3 %% Query = string() %% Descrip.: Send a MySQL query and block awaiting it's response. %% Returns : result of get_query_response/2 | {error, Reason} %%-------------------------------------------------------------------- do_query(State, Query, Options) when is_record(State, state) -> do_query(State#state.socket, State#state.recv_pid, State#state.log_fun, Query, State#state.mysql_version, Options ). do_query(Sock, RecvPid, LogFun, Query, Version, Options) when is_pid(RecvPid), is_list(Query) -> Packet = list_to_binary([?MYSQL_QUERY_OP, Query]), case do_send(Sock, Packet, 0, LogFun) of ok -> get_query_response(LogFun, RecvPid, Version, Options); {error, Reason} -> Msg = io_lib:format("Failed sending data on socket : ~p", [Reason]), {error, Msg} end. %%-------------------------------------------------------------------- %% Function: do_send(Sock, Packet, SeqNum, LogFun) %% Sock = term(), gen_tcp socket %% Packet = binary() %% SeqNum = integer(), packet sequence number %% LogFun = undefined | function() with arity 3 %% Descrip.: Send a packet to the MySQL server. %% Returns : result of gen_tcp:send/2 %%-------------------------------------------------------------------- do_send(Sock, Packet, SeqNum, _LogFun) when is_binary(Packet), is_integer(SeqNum) -> Data = <<(size(Packet)):24/little, SeqNum:8, Packet/binary>>, %%p1_mysql:log(LogFun, debug, "p1_mysql_conn: send packet ~p: ~p", %%[SeqNum, Data]), gen_tcp:send(Sock, Data). %%-------------------------------------------------------------------- %% Function: normalize_version(Version, LogFun) %% Version = string() %% LogFun = undefined | function() with arity 3 %% Descrip.: Return a flag corresponding to the MySQL version used. %% The protocol used depends on this flag. %% Returns : Version = string() %%-------------------------------------------------------------------- normalize_version([$4,$.,$0|_T], LogFun) -> p1_mysql:log(LogFun, debug, "Switching to MySQL 4.0.x protocol.~n"), ?MYSQL_4_0; normalize_version([$4,$.,$1|_T], _LogFun) -> ?MYSQL_4_1; normalize_version([$5|_T], _LogFun) -> %% MySQL version 5.x protocol is compliant with MySQL 4.1.x: ?MYSQL_4_1; normalize_version(_Other, LogFun) -> p1_mysql:log(LogFun, error, "MySQL version not supported: MySQL Erlang " "module might not work correctly.~n"), %% Error, but trying the oldest protocol anyway: ?MYSQL_4_0. %%-------------------------------------------------------------------- %% Function: get_field_datatype(DataType) %% DataType = integer(), MySQL datatype %% Descrip.: Return MySQL field datatype as description string %% Returns : String, MySQL datatype %%-------------------------------------------------------------------- get_field_datatype(0) -> 'DECIMAL'; get_field_datatype(1) -> 'TINY'; get_field_datatype(2) -> 'SHORT'; get_field_datatype(3) -> 'LONG'; get_field_datatype(4) -> 'FLOAT'; get_field_datatype(5) -> 'DOUBLE'; get_field_datatype(6) -> 'NULL'; get_field_datatype(7) -> 'TIMESTAMP'; get_field_datatype(8) -> 'LONGLONG'; get_field_datatype(9) -> 'INT24'; get_field_datatype(10) -> 'DATE'; get_field_datatype(11) -> 'TIME'; get_field_datatype(12) -> 'DATETIME'; get_field_datatype(13) -> 'YEAR'; get_field_datatype(14) -> 'NEWDATE'; get_field_datatype(16) -> 'BIT'; get_field_datatype(246) -> 'DECIMAL'; get_field_datatype(247) -> 'ENUM'; get_field_datatype(248) -> 'SET'; get_field_datatype(249) -> 'TINYBLOB'; get_field_datatype(250) -> 'MEDIUM_BLOG'; get_field_datatype(251) -> 'LONG_BLOG'; get_field_datatype(252) -> 'BLOB'; get_field_datatype(253) -> 'VAR_STRING'; get_field_datatype(254) -> 'STRING'; get_field_datatype(255) -> 'GEOMETRY'. %%-------------------------------------------------------------------- %% Function: get_option(Key1, Options, Default) -> Value1 %% Options = [Option] %% Option = {Key2, Value2} %% Key1 = Key2 = atom() %% Value1 = Value2 = Default = term() %% Descrip.: Return the option associated with Key passed to squery/4 %%-------------------------------------------------------------------- get_option(Key, Options, Default) -> case lists:keysearch(Key, 1, Options) of {value, {_, Value}} -> Value; false -> Default end. mysql-1.0.0/src/p1_mysql_recv.erl0000644000232200023220000001367312642720136017317 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql_recv.erl %%% Author : Fredrik Thulin %%% Descrip.: Handles data being received on a MySQL socket. Decodes %%% per-row framing and sends each row to parent. %%% %%% Created : 4 Aug 2005 by Fredrik Thulin %%% %%% Note : All MySQL code was written by Magnus Ahltorp, originally %%% in the file p1_mysql.erl - I just moved it here. %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%% Signals this receiver process can send to it's parent %%% (the parent is a p1_mysql_conn connection handler) : %%% %%% {p1_mysql_recv, self(), data, Packet, Num} %%% {p1_mysql_recv, self(), closed, {error, Reason}} %%% {p1_mysql_recv, self(), closed, normal} %%% %%% Internally (from inside init/4 to start_link/4) the %%% following signals may be sent to the parent process : %%% %%% {p1_mysql_recv, self(), init, {ok, Sock}} %%% {p1_mysql_recv, self(), init, {error, E}} %%% %%%------------------------------------------------------------------- -module(p1_mysql_recv). %%-------------------------------------------------------------------- %% External exports (should only be used by the 'p1_mysql_conn' module) %%-------------------------------------------------------------------- -export([start_link/4 ]). -record(state, { socket, parent, log_fun, data }). -define(SECURE_CONNECTION, 32768). -define(CONNECT_TIMEOUT, 5000). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link(Host, Port, LogFun, Parent) %% Host = string() %% Port = integer() %% LogFun = undefined | function() of arity 3 %% Parent = pid(), process that should get received frames %% Descrip.: Start a process that connects to Host:Port and waits for %% data. When it has received a MySQL frame, it sends it to %% Parent and waits for the next frame. %% Returns : {ok, RecvPid, Socket} | %% {error, Reason} %% RecvPid = pid(), receiver process pid %% Socket = term(), gen_tcp socket %% Reason = atom() | string() %%-------------------------------------------------------------------- start_link(Host, Port, LogFun, Parent) when is_list(Host), is_integer(Port) -> RecvPid = spawn_link(fun () -> init(Host, Port, LogFun, Parent) end), %% wait for the socket from the spawned pid receive {p1_mysql_recv, RecvPid, init, {error, E}} -> {error, E}; {p1_mysql_recv, RecvPid, init, {ok, Socket}} -> {ok, RecvPid, Socket} after ?CONNECT_TIMEOUT -> catch exit(RecvPid, kill), {error, "timeout"} end. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init((Host, Port, LogFun, Parent) %% Host = string() %% Port = integer() %% LogFun = undefined | function() of arity 3 %% Parent = pid(), process that should get received frames %% Descrip.: Connect to Host:Port and then enter receive-loop. %% Returns : error | never returns %%-------------------------------------------------------------------- init(Host, Port, LogFun, Parent) -> case gen_tcp:connect(Host, Port, [binary, {packet, 0}]) of {ok, Sock} -> Parent ! {p1_mysql_recv, self(), init, {ok, Sock}}, State = #state{socket = Sock, parent = Parent, log_fun = LogFun, data = <<>> }, loop(State); E -> p1_mysql:log(LogFun, error, "p1_mysql_recv: Failed connecting to ~p:~p : ~p", [Host, Port, E]), Msg = lists:flatten(io_lib:format("connect failed : ~p", [E])), Parent ! {p1_mysql_recv, self(), init, {error, Msg}} end. %%-------------------------------------------------------------------- %% Function: loop(State) %% State = state record() %% Descrip.: The main loop. Wait for data from our TCP socket and act %% on received data or signals that our socket was closed. %% Returns : error | never returns %%-------------------------------------------------------------------- loop(State) -> Sock = State#state.socket, receive {tcp, Sock, InData} -> NewData = list_to_binary([State#state.data, InData]), %% send data to parent if we have enough data Rest = sendpacket(State#state.parent, NewData), loop(State#state{data = Rest}); {tcp_error, Sock, Reason} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_recv: " "Socket ~p closed : ~p", [Sock, Reason]), State#state.parent ! {p1_mysql_recv, self(), closed, {error, Reason}}, error; {tcp_closed, Sock} -> p1_mysql:log(State#state.log_fun, debug, "p1_mysql_recv: " "Socket ~p closed", [Sock]), State#state.parent ! {p1_mysql_recv, self(), closed, normal}, error end. %%-------------------------------------------------------------------- %% Function: sendpacket(Parent, Data) %% Parent = pid() %% Data = binary() %% Descrip.: Check if we have received one or more complete frames by %% now, and if so - send them to Parent. %% Returns : Rest = binary() %%-------------------------------------------------------------------- %% send data to parent if we have enough data sendpacket(Parent, Data) -> case Data of <> -> if Length =< size(D) -> {Packet, Rest} = split_binary(D, Length), Parent ! {p1_mysql_recv, self(), data, Packet, Num}, sendpacket(Parent, Rest); true -> Data end; _ -> Data end. mysql-1.0.0/src/p1_mysql.app.src0000644000232200023220000000116612642720136017056 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov %%% @copyright (C) 2013, Evgeniy Khramtsov %%% @doc %%% %%% @end %%% Created : 4 Apr 2013 by Evgeniy Khramtsov %%%------------------------------------------------------------------- {application, p1_mysql, [{description, "MySQL driver"}, {vsn, "1.0.0"}, {modules, []}, {registered, []}, {applications, [kernel, stdlib]}, {mod, {p1_mysql_app,[]}}]}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: mysql-1.0.0/src/p1_mysql_sup.erl0000644000232200023220000000361012642720136017155 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : p1_mysql_sup.erl %%% Author : Evgeniy Khramtsov %%% Purpose : MySQL erlang driver supervisor %%% Created : 15 May 2013 by Evgeniy Khramtsov %%% %%% %%% p1_mysql, Copyright (C) 2002-2015 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 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 General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- -module(p1_mysql_sup). -behaviour(supervisor). %% API -export([start_link/0]). %% Supervisor callbacks -export([init/1]). -define(SERVER, ?MODULE). %%%=================================================================== %%% API functions %%%=================================================================== start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([]) -> {ok, {{one_for_one, 10, 1}, []}}. %%%=================================================================== %%% Internal functions %%%=================================================================== mysql-1.0.0/src/p1_mysql_app.erl0000644000232200023220000000322512642720136017130 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : p1_mysql_app.erl %%% Author : Evgeniy Khramtsov %%% Purpose : MySQL erlang driver application %%% Created : 15 May 2013 by Evgeniy Khramtsov %%% %%% %%% p1_mysql, Copyright (C) 2002-2015 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 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 General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- -module(p1_mysql_app). -behaviour(application). %% Application callbacks -export([start/2, stop/1]). %%%=================================================================== %%% Application callbacks %%%=================================================================== start(_StartType, _StartArgs) -> p1_mysql_sup:start_link(). stop(_State) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== mysql-1.0.0/src/p1_mysql.erl0000644000232200023220000006704512642720136016302 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql.erl %%% Author : Magnus Ahltorp %%% Descrip.: MySQL client. %%% %%% Created : 4 Aug 2005 by Magnus Ahltorp %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%% Usage: %%% %%% %%% Call one of the start-functions before any call to fetch/2 %%% %%% start_link(Id, Host, User, Password, Database) %%% start_link(Id, Host, Port, User, Password, Database) %%% start_link(Id, Host, User, Password, Database, LogFun) %%% start_link(Id, Host, Port, User, Password, Database, LogFun) %%% %%% Id is a connection group identifier. If you want to have more %%% than one connection to a server (or a set of MySQL replicas), %%% add more with %%% %%% connect(Id, Host, Port, User, Password, Database, Reconnect) %%% %%% use 'undefined' as Port to get default MySQL port number (3306). %%% MySQL querys will be sent in a per-Id round-robin fashion. %%% Set Reconnect to 'true' if you want the dispatcher to try and %%% open a new connection, should this one die. %%% %%% When you have a p1_mysql_dispatcher running, this is how you make a %%% query : %%% %%% fetch(Id, "select * from hello") -> Result %%% Result = {data, MySQLRes} | {updated, MySQLRes} | %%% {error, MySQLRes} %%% %%% Actual data can be extracted from MySQLRes by calling the following API %%% functions: %%% - on data received: %%% FieldInfo = p1_mysql:get_result_field_info(MysqlRes) %%% AllRows = p1_mysql:get_result_rows(MysqlRes) %%% with FieldInfo = list() of {Table, Field, Length, Name} %%% and AllRows = list() of list() representing records %%% - on update: %%% Affected = p1_mysql:get_result_affected_rows(MysqlRes) %%% with Affected = integer() %%% - on error: %%% Reason = p1_mysql:get_result_reason(MysqlRes) %%% with Reason = string() %%% %%% If you just want a single MySQL connection, or want to manage your %%% connections yourself, you can use the p1_mysql_conn module as a %%% stand-alone single MySQL connection. See the comment at the top of %%% p1_mysql_conn.erl. %%% %%%------------------------------------------------------------------- -module(p1_mysql). -behaviour(gen_server). %%-------------------------------------------------------------------- %% External exports %%-------------------------------------------------------------------- -export([start_link/5, start_link/6, start_link/7, fetch/2, fetch/3, get_result_field_info/1, get_result_rows/1, get_result_affected_rows/1, get_result_reason/1, quote/1, asciz_binary/2, connect/7, stop/0, gc_each/1 ]). %%-------------------------------------------------------------------- %% Internal exports - just for p1_mysql_* modules %%-------------------------------------------------------------------- -export([log/3, log/4 ]). %%-------------------------------------------------------------------- %% Internal exports - gen_server callbacks %%-------------------------------------------------------------------- -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). %%-------------------------------------------------------------------- %% Records %%-------------------------------------------------------------------- -include("p1_mysql.hrl"). -record(state, { conn_list, %% list() of p1_mysql_connection record() log_fun, %% undefined | function for logging, gc_tref %% undefined | timer:TRef }). -record(p1_mysql_connection, { id, %% term(), user of 'p1_mysql' modules id of this socket group conn_pid, %% pid(), p1_mysql_conn process reconnect, %% true | false, should p1_mysql_dispatcher try to reconnect if this connection dies? host, %% undefined | string() port, %% undefined | integer() user, %% undefined | string() password, %% undefined | string() database %% undefined | string() }). %%-------------------------------------------------------------------- %% Macros %%-------------------------------------------------------------------- -define(SERVER, p1_mysql_dispatcher). -define(CONNECT_TIMEOUT, 5000). -define(LOCAL_FILES, 128). -define(PORT, 3306). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link(Id, Host, User, Password, Database) %% start_link(Id, Host, Port, User, Password, Database) %% start_link(Id, Host, User, Password, Database, LogFun) %% start_link(Id, Host, Port, User, Password, Database, %% LogFun) %% Id = term(), first connection-group Id %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() of arity 3 %% Descrip.: Starts the MySQL client gen_server process. %% Returns : {ok, Pid} | ignore | {error, Error} %%-------------------------------------------------------------------- start_link(Id, Host, User, Password, Database) when is_list(Host), is_list(User), is_list(Password), is_list(Database) -> start_link(Id, Host, ?PORT, User, Password, Database, undefined). start_link(Id, Host, Port, User, Password, Database) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> start_link(Id, Host, Port, User, Password, Database, undefined); start_link(Id, Host, User, Password, Database, LogFun) when is_list(Host), is_list(User), is_list(Password), is_list(Database) -> start_link(Id, Host, ?PORT, User, Password, Database, LogFun). start_link(Id, Host, Port, User, Password, Database, LogFun) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> crypto:start(), gen_server:start_link({local, ?SERVER}, ?MODULE, [Id, Host, Port, User, Password, Database, LogFun], []). stop() -> gen_server:call(?SERVER, stop). gc_each(Millisec) -> gen_server:call(?SERVER, {gc_each, Millisec}). %%-------------------------------------------------------------------- %% Function: fetch(Id, Query) %% fetch(Id, Query, Timeout) %% Id = term(), connection-group Id %% Query = string(), MySQL query in verbatim %% Timeout = integer() | infinity, gen_server timeout value %% Descrip.: Send a query and wait for the result. %% Returns : {data, MySQLRes} | %% {updated, MySQLRes} | %% {error, MySQLRes} %% MySQLRes = term() %%-------------------------------------------------------------------- fetch(Id, Query) when is_list(Query) -> gen_server:call(?SERVER, {fetch, Id, Query}). fetch(Id, Query, Timeout) when is_list(Query) -> gen_server:call(?SERVER, {fetch, Id, Query}, Timeout). %%-------------------------------------------------------------------- %% Function: get_result_field_info(MySQLRes) %% MySQLRes = term(), result of fetch function on "data" %% Descrip.: Extract the FieldInfo from MySQL Result on data received %% Returns : FieldInfo %% FieldInfo = list() of {Table, Field, Length, Name} %%-------------------------------------------------------------------- get_result_field_info(#p1_mysql_result{fieldinfo = FieldInfo}) -> FieldInfo. %%-------------------------------------------------------------------- %% Function: get_result_rows(MySQLRes) %% MySQLRes = term(), result of fetch function on "data" %% Descrip.: Extract the Rows from MySQL Result on data received %% Returns : Rows %% Rows = list() of list() representing records %%-------------------------------------------------------------------- get_result_rows(#p1_mysql_result{rows=AllRows}) -> AllRows. %%-------------------------------------------------------------------- %% Function: get_result_affected_rows(MySQLRes) %% MySQLRes = term(), result of fetch function on "updated" %% Descrip.: Extract the Rows from MySQL Result on update %% Returns : AffectedRows %% AffectedRows = integer() %%-------------------------------------------------------------------- get_result_affected_rows(#p1_mysql_result{affectedrows=AffectedRows}) -> AffectedRows. %%-------------------------------------------------------------------- %% Function: get_result_reason(MySQLRes) %% MySQLRes = term(), result of fetch function on "error" %% Descrip.: Extract the error Reason from MySQL Result on error %% Returns : Reason %% Reason = string() %%-------------------------------------------------------------------- get_result_reason(#p1_mysql_result{error=Reason}) -> Reason. %%-------------------------------------------------------------------- %% Function: quote(String) %% String = string() %% Descrip.: Quote a string so that it can be included safely in a %% MySQL query. %% Returns : Quoted = string() %%-------------------------------------------------------------------- quote(String) when is_list(String) -> [34 | lists:reverse([34 | quote(String, [])])]. %% 34 is $" quote([], Acc) -> Acc; quote([0 | Rest], Acc) -> quote(Rest, [$0, $\\ | Acc]); quote([10 | Rest], Acc) -> quote(Rest, [$n, $\\ | Acc]); quote([13 | Rest], Acc) -> quote(Rest, [$r, $\\ | Acc]); quote([$\\ | Rest], Acc) -> quote(Rest, [$\\ , $\\ | Acc]); quote([39 | Rest], Acc) -> %% 39 is $' quote(Rest, [39, $\\ | Acc]); %% 39 is $' quote([34 | Rest], Acc) -> %% 34 is $" quote(Rest, [34, $\\ | Acc]); %% 34 is $" quote([26 | Rest], Acc) -> quote(Rest, [$Z, $\\ | Acc]); quote([C | Rest], Acc) -> quote(Rest, [C | Acc]). %%-------------------------------------------------------------------- %% Function: asciz_binary(Data, Acc) %% Data = binary() %% Acc = list(), input accumulator %% Descrip.: Find the first zero-byte in Data and add everything %% before it to Acc, as a string. %% Returns : {NewList, Rest} %% NewList = list(), Acc plus what we extracted from Data %% Rest = binary(), whatever was left of Data, not %% including the zero-byte %%-------------------------------------------------------------------- asciz_binary(<<>>, Acc) -> {lists:reverse(Acc), <<>>}; asciz_binary(<<0:8, Rest/binary>>, Acc) -> {lists:reverse(Acc), Rest}; asciz_binary(<>, Acc) -> asciz_binary(Rest, [C | Acc]). %%-------------------------------------------------------------------- %% Function: connect(Id, Host, Port, User, Password, Database, %% Reconnect) %% Id = term(), connection-group Id %% Host = string() %% Port = undefined | integer() %% User = string() %% Password = string() %% Database = string() %% Reconnect = true | false %% Descrip.: Starts a MySQL connection and, if successfull, registers %% it with the p1_mysql_dispatcher. %% Returns : {ok, ConnPid} | {error, Reason} %%-------------------------------------------------------------------- connect(Id, Host, undefined, User, Password, Database, Reconnect) -> connect(Id, Host, ?PORT, User, Password, Database, Reconnect); connect(Id, Host, Port, User, Password, Database, Reconnect) -> {ok, LogFun} = gen_server:call(?SERVER, get_logfun), case p1_mysql_conn:start(Host, Port, User, Password, Database, LogFun) of {ok, ConnPid} -> MysqlConn = case Reconnect of true -> #p1_mysql_connection{id = Id, conn_pid = ConnPid, reconnect = true, host = Host, port = Port, user = User, password = Password, database = Database }; false -> #p1_mysql_connection{id = Id, conn_pid = ConnPid, reconnect = false } end, case gen_server:call(?SERVER, {add_mysql_connection, MysqlConn}) of ok -> {ok, ConnPid}; Res -> Res end; {error, Reason} -> {error, Reason} end. %%-------------------------------------------------------------------- %% Function: log(LogFun, Level, Format) %% log(LogFun, Level, Format, Arguments) %% LogFun = undefined | function() with arity 3 %% Level = debug | normal | error %% Format = string() %% Arguments = list() of term() %% Descrip.: Either call the function LogFun with the Level, Format %% and Arguments as parameters or log it to the console if %% LogFun is undefined. %% Returns : void() %% %% Note : Exported only for use by the p1_mysql_* modules. %% %%-------------------------------------------------------------------- log(LogFun, Level, Format) -> log(LogFun, Level, Format, []). log(LogFun, Level, Format, Arguments) when is_function(LogFun) -> LogFun(Level, Format, Arguments); log(undefined, _Level, Format, Arguments) -> %% default is to log to console io:format(Format, Arguments), io:format("~n", []). %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Args = [Id, Host, Port, User, Password, Database, LogFun] %% Id = term(), connection-group Id %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() with arity 3 %% Descrip.: Initiates the gen_server (MySQL dispatcher). %%-------------------------------------------------------------------- init([Id, Host, Port, User, Password, Database, LogFun]) -> case p1_mysql_conn:start(Host, Port, User, Password, Database, LogFun) of {ok, ConnPid} -> MysqlConn = #p1_mysql_connection{id = Id, conn_pid = ConnPid, reconnect = true, host = Host, port = Port, user = User, password = Password, database = Database }, case add_mysql_conn(MysqlConn, []) of {ok, ConnList} -> {ok, #state{log_fun = LogFun, conn_list = ConnList, gc_tref = undefined }}; error -> Msg = "p1_mysql: Failed adding first MySQL connection handler to my list, exiting", log(LogFun, error, Msg), {error, Msg} end; {error, Reason} -> log(LogFun, error, "p1_mysql: Failed starting first MySQL connection handler, exiting"), {stop, {error, Reason}} end. %%-------------------------------------------------------------------- %% Function: handle_call(Msg, From, State) %% Descrip.: Handling call messages. %% Returns : {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: handle_call({fetch, Id, Query}, From, State) %% Id = term(), connection-group id %% Query = string(), MySQL query %% Descrip.: Make a MySQL query. Use the first connection matching Id %% in our connection-list. Don't block the p1_mysql_dispatcher %% by returning {noreply, ...} here and let the p1_mysql_conn %% do gen_server:reply(...) when it has an answer. %% Returns : {noreply, NewState} | %% {reply, {error, Reason}, State} %% NewState = state record() %% Reason = atom() | string() %%-------------------------------------------------------------------- handle_call({fetch, Id, Query}, From, State) -> log(State#state.log_fun, debug, "p1_mysql: fetch ~p (id ~p)", [Query, Id]), case get_next_mysql_connection_for_id(Id, State#state.conn_list) of {ok, MysqlConn, RestOfConnList} when is_record(MysqlConn, p1_mysql_connection) -> p1_mysql_conn:fetch(MysqlConn#p1_mysql_connection.conn_pid, Query, From), %% move this mysql socket to the back of the list NewConnList = RestOfConnList ++ [MysqlConn], %% The ConnPid process does a gen_server:reply() when it has an answer {noreply, State#state{conn_list = NewConnList}}; nomatch -> %% we have no active connection matching Id {reply, {error, no_connection}, State} end; %%-------------------------------------------------------------------- %% Function: handle_call({add_mysql_connection, Conn}, From, State) %% Conn = p1_mysql_connection record() %% Descrip.: Add Conn to our list of connections. %% Returns : {reply, Reply, NewState} %% Reply = ok | {error, Reason} %% NewState = state record() %% Reason = string() %%-------------------------------------------------------------------- handle_call({add_mysql_connection, Conn}, _From, State) when is_record(Conn, p1_mysql_connection) -> case add_mysql_conn(Conn, State#state.conn_list) of {ok, NewConnList} -> {Id, ConnPid} = {Conn#p1_mysql_connection.id, Conn#p1_mysql_connection.conn_pid}, log(State#state.log_fun, normal, "p1_mysql: Added connection with id '~p' (pid ~p) to my list", [Id, ConnPid]), {reply, ok, State#state{conn_list = NewConnList}}; error -> {reply, {error, "failed adding MySQL connection to my list"}, State} end; %%-------------------------------------------------------------------- %% Function: handle_call(get_logfun, From, State) %% Descrip.: Fetch our logfun. %% Returns : {reply, {ok, LogFun}, State} %% LogFun = undefined | function() with arity 3 %%-------------------------------------------------------------------- handle_call(get_logfun, _From, State) -> {reply, {ok, State#state.log_fun}, State}; handle_call(stop, _From, State) -> {stop, normal, State}; handle_call({gc_each, Millisec}, _From, State) -> case State#state.gc_tref of undefined -> ok; TRef -> timer:cancel(TRef) end, case timer:send_interval(Millisec, gc) of {ok, NewTRef} -> {reply, ok, State#state{gc_tref = NewTRef}}; {error, Reason} -> {reply, {error, Reason}, State} end; handle_call(Unknown, _From, State) -> log(State#state.log_fun, error, "p1_mysql: Received unknown gen_server call : ~p", [Unknown]), {reply, {error, "unknown gen_server call in p1_mysql client"}, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) %% Descrip.: Handling cast messages %% Returns : {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(Unknown, State) -> log(State#state.log_fun, error, "p1_mysql: Received unknown gen_server cast : ~p", [Unknown]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Msg, State) %% Descrip.: Handling all non call/cast messages %% Returns : {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: handle_info({'DOWN', ...}, State) %% Descrip.: Handle a message that one of our monitored processes %% (p1_mysql_conn processes in our connection list) has exited. %% Remove the entry from our list. %% Returns : {noreply, NewState} | %% {stop, normal, State} %% NewState = state record() %% %% Note : For now, we stop if our connection list becomes empty. %% We should try to reconnect for a while first, to not %% eventually stop the whole OTP application if the MySQL- %% server is shut down and the p1_mysql_dispatcher was super- %% vised by an OTP supervisor. %%-------------------------------------------------------------------- handle_info({'DOWN', _MonitorRef, process, Pid, Info}, State) -> LogFun = State#state.log_fun, case remove_mysql_connection_using_pid(Pid, State#state.conn_list, []) of {ok, Conn, NewConnList} -> LogLevel = case Info of normal -> normal; _ -> error end, log(LogFun, LogLevel, "p1_mysql: MySQL connection pid ~p exited : ~p", [Pid, Info]), log(LogFun, normal, "p1_mysql: Removed MySQL connection with pid ~p from list", [Pid]), case Conn#p1_mysql_connection.reconnect of true -> start_reconnect(Conn, LogFun); false -> ok end, {noreply, State#state{conn_list = NewConnList}}; nomatch -> log(LogFun, error, "p1_mysql: Received 'DOWN' signal from pid ~p not in my list", [Pid]), {noreply, State} end; handle_info(gc, #state{conn_list = Connections} = State) -> [erlang:garbage_collect(C#p1_mysql_connection.conn_pid) || C <- Connections], erlang:garbage_collect(self()), {noreply, State}; handle_info(Info, State) -> log(State#state.log_fun, error, "p1_mysql: Received unknown signal : ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) %% Descrip.: Shutdown the server %% Returns : Reason %%-------------------------------------------------------------------- terminate(Reason, State) -> LogFun = State#state.log_fun, LogLevel = case Reason of normal -> debug; _ -> error end, log(LogFun, LogLevel, "p1_mysql: Terminating with reason : ~p", [Reason]), lists:foreach(fun(MysqlConn) -> MysqlConn#p1_mysql_connection.conn_pid ! close end, State#state.conn_list), Reason. %%-------------------------------------------------------------------- %% Function: code_change(_OldVsn, State, _Extra) %% Descrip.: Convert process state when code is changed %% Returns : {ok, State} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: add_mysql_conn(Conn, ConnList) %% Conn = p1_mysql_connection record() %% ConnList = list() of p1_mysql_connection record() %% Descrip.: Set up process monitoring of the p1_mysql_conn process and %% then add it (first) to ConnList. %% Returns : NewConnList = list() of p1_mysql_connection record() %%-------------------------------------------------------------------- add_mysql_conn(Conn, ConnList) when is_record(Conn, p1_mysql_connection), is_list(ConnList) -> erlang:monitor(process, Conn#p1_mysql_connection.conn_pid), {ok, [Conn | ConnList]}. %%-------------------------------------------------------------------- %% Function: remove_mysql_connection_using_pid(Pid, ConnList) %% Pid = pid() %% ConnList = list() of p1_mysql_connection record() %% Descrip.: Removes the first p1_mysql_connection in ConnList that has %% a pid matching Pid. %% Returns : {ok, Conn, NewConnList} | nomatch %% Conn = p1_mysql_connection record() %% NewConnList = list() of p1_mysql_connection record() %%-------------------------------------------------------------------- remove_mysql_connection_using_pid(Pid, [#p1_mysql_connection{conn_pid = Pid} = H | T], Res) -> {ok, H, lists:reverse(Res) ++ T}; remove_mysql_connection_using_pid(Pid, [H | T], Res) when is_record(H, p1_mysql_connection) -> remove_mysql_connection_using_pid(Pid, T, [H | Res]); remove_mysql_connection_using_pid(_Pid, [], _Res) -> nomatch. %%-------------------------------------------------------------------- %% Function: get_next_mysql_connection_for_id(Id, ConnList) %% Id = term(), connection-group id %% ConnList = list() of p1_mysql_connection record() %% Descrip.: Find the first p1_mysql_connection in ConnList that has an %% id matching Id. %% Returns : {ok, Conn, NewConnList} | nomatch %% Conn = p1_mysql_connection record() %% NewConnList = list() of p1_mysql_connection record(), same %% as ConnList but without Conn %%-------------------------------------------------------------------- get_next_mysql_connection_for_id(Id, ConnList) -> get_next_mysql_connection_for_id(Id, ConnList, []). get_next_mysql_connection_for_id(Id, [#p1_mysql_connection{id = Id} = H | T], Res) -> {ok, H, lists:reverse(Res) ++ T}; get_next_mysql_connection_for_id(Id, [H | T], Res) when is_record(H, p1_mysql_connection) -> get_next_mysql_connection_for_id(Id, T, [H | Res]); get_next_mysql_connection_for_id(_Id, [], _Res) -> nomatch. %%-------------------------------------------------------------------- %% Function: start_reconnect(Conn, LogFun) %% Conn = p1_mysql_connection record() %% LogFun = undefined | function() with arity 3 %% Descrip.: Spawns a process that will try to re-establish a new %% connection instead of the one in Conn which has just %% died. %% Returns : ok %%-------------------------------------------------------------------- start_reconnect(Conn, LogFun) when is_record(Conn, p1_mysql_connection) -> Pid = spawn(fun () -> reconnect_loop(Conn#p1_mysql_connection{conn_pid = undefined}, LogFun, 0) end), {Id, Host, Port} = {Conn#p1_mysql_connection.id, Conn#p1_mysql_connection.host, Conn#p1_mysql_connection.port}, log(LogFun, debug, "p1_mysql: Started pid ~p to try and reconnect to ~p:~s:~p (replacing " "connection with pid ~p)", [Pid, Id, Host, Port, Conn#p1_mysql_connection.conn_pid]), ok. %%-------------------------------------------------------------------- %% Function: reconnect_loop(Conn, LogFun, 0) %% Conn = p1_mysql_connection record() %% LogFun = undefined | function() with arity 3 %% Descrip.: Loop indefinately until we are able to reconnect to the %% server specified in the now dead connection Conn. %% Returns : ok %%-------------------------------------------------------------------- reconnect_loop(Conn, LogFun, N) when is_record(Conn, p1_mysql_connection) -> {Id, Host, Port} = {Conn#p1_mysql_connection.id, Conn#p1_mysql_connection.host, Conn#p1_mysql_connection.port}, case connect(Id, Host, Port, Conn#p1_mysql_connection.user, Conn#p1_mysql_connection.password, Conn#p1_mysql_connection.database, Conn#p1_mysql_connection.reconnect) of {ok, ConnPid} -> log(LogFun, debug, "p1_mysql_reconnect: Managed to reconnect to ~p:~s:~p (connection pid ~p)", [Id, Host, Port, ConnPid]), ok; {error, Reason} -> %% log every once in a while NewN = case N of 10 -> log(LogFun, debug, "p1_mysql_reconnect: Still unable to connect to ~p:~s:~p (~p)", [Id, Host, Port, Reason]), 0; _ -> N + 1 end, %% sleep between every unsuccessfull attempt timer:sleep(20 * 1000), reconnect_loop(Conn, LogFun, NewN) end. mysql-1.0.0/Makefile0000644000232200023220000000010612642720136014663 0ustar debalancedebalanceall: src src: rebar compile clean: rebar clean .PHONY: clean src mysql-1.0.0/COPYING0000644000232200023220000000630012642720136014260 0ustar debalancedebalance/* * Copyright (c) 2001-2003 Kungliga Tekniska Högskolan * (Royal Institute of Technology, Stockholm, Sweden). * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the Institute nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ Copyright (c) 2004, Sektionen för IT och media, Stockholms universitet All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.