|
@@ -28,8 +28,15 @@
|
|
|
-module(mysql_protocol).
|
|
|
|
|
|
-export([handshake/7, quit/2, ping/2,
|
|
|
- query/4, fetch_query_response/3,
|
|
|
- prepare/3, unprepare/3, execute/5, fetch_execute_response/3]).
|
|
|
+ query/4, query/5, fetch_query_response/3,
|
|
|
+ fetch_query_response/4, prepare/3, unprepare/3,
|
|
|
+ execute/5, execute/6, fetch_execute_response/3,
|
|
|
+ fetch_execute_response/4]).
|
|
|
+
|
|
|
+-type query_filtermap() :: no_filtermap_fun
|
|
|
+ | fun(([term()]) -> query_filtermap_res())
|
|
|
+ | fun(([term()], [term()]) -> query_filtermap_res()).
|
|
|
+-type query_filtermap_res() :: boolean() | {true, term()}.
|
|
|
|
|
|
%% How much data do we want per packet?
|
|
|
-define(MAX_BYTES_PER_PACKET, 16#1000000).
|
|
@@ -113,15 +120,23 @@ ping(SockModule, Socket) ->
|
|
|
-spec query(Query :: iodata(), atom(), term(), timeout()) ->
|
|
|
{ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
|
|
|
query(Query, SockModule, Socket, Timeout) ->
|
|
|
+ query(Query, SockModule, Socket, no_filtermap_fun, Timeout).
|
|
|
+
|
|
|
+-spec query(Query :: iodata(), atom(), term(), query_filtermap(), timeout()) ->
|
|
|
+ {ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
|
|
|
+query(Query, SockModule, Socket, FilterMap, Timeout) ->
|
|
|
Req = <<?COM_QUERY, (iolist_to_binary(Query))/binary>>,
|
|
|
SeqNum0 = 0,
|
|
|
{ok, _SeqNum1} = send_packet(SockModule, Socket, Req, SeqNum0),
|
|
|
- fetch_query_response(SockModule, Socket, Timeout).
|
|
|
+ fetch_query_response(SockModule, Socket, FilterMap, Timeout).
|
|
|
|
|
|
%% @doc This is used by query/4. If query/4 returns {error, timeout}, this
|
|
|
%% function can be called to retry to fetch the results of the query.
|
|
|
fetch_query_response(SockModule, Socket, Timeout) ->
|
|
|
- fetch_response(SockModule, Socket, Timeout, text, []).
|
|
|
+ fetch_query_response(SockModule, Socket, no_filtermap_fun, Timeout).
|
|
|
+
|
|
|
+fetch_query_response(SockModule, Socket, FilterMap, Timeout) ->
|
|
|
+ fetch_response(SockModule, Socket, Timeout, text, FilterMap, []).
|
|
|
|
|
|
%% @doc Prepares a statement.
|
|
|
-spec prepare(iodata(), atom(), term()) -> #error{} | #prepared{}.
|
|
@@ -168,8 +183,15 @@ unprepare(#prepared{statement_id = Id}, SockModule, Socket) ->
|
|
|
%% @doc Executes a prepared statement.
|
|
|
-spec execute(#prepared{}, [term()], atom(), term(), timeout()) ->
|
|
|
{ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
|
|
|
+execute(PrepStmt, ParamValues, SockModule, Socket, Timeout) ->
|
|
|
+ execute(PrepStmt, ParamValues, SockModule, Socket, no_filtermap_fun,
|
|
|
+ Timeout).
|
|
|
+-spec execute(#prepared{}, [term()], atom(), term(), query_filtermap(),
|
|
|
+ timeout()) ->
|
|
|
+ {ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
|
|
|
execute(#prepared{statement_id = Id, param_count = ParamCount}, ParamValues,
|
|
|
- SockModule, Socket, Timeout) when ParamCount == length(ParamValues) ->
|
|
|
+ SockModule, Socket, FilterMap, Timeout)
|
|
|
+ when ParamCount == length(ParamValues) ->
|
|
|
%% Flags Constant Name
|
|
|
%% 0x00 CURSOR_TYPE_NO_CURSOR
|
|
|
%% 0x01 CURSOR_TYPE_READ_ONLY
|
|
@@ -196,12 +218,15 @@ execute(#prepared{statement_id = Id, param_count = ParamCount}, ParamValues,
|
|
|
iolist_to_binary([Req1, TypesAndSigns, EncValues])
|
|
|
end,
|
|
|
{ok, _SeqNum1} = send_packet(SockModule, Socket, Req, 0),
|
|
|
- fetch_execute_response(SockModule, Socket, Timeout).
|
|
|
+ fetch_execute_response(SockModule, Socket, FilterMap, Timeout).
|
|
|
|
|
|
%% @doc This is used by execute/5. If execute/5 returns {error, timeout}, this
|
|
|
%% function can be called to retry to fetch the results of the query.
|
|
|
fetch_execute_response(SockModule, Socket, Timeout) ->
|
|
|
- fetch_response(SockModule, Socket, Timeout, binary, []).
|
|
|
+ fetch_execute_response(SockModule, Socket, no_filtermap_fun, Timeout).
|
|
|
+
|
|
|
+fetch_execute_response(SockModule, Socket, FilterMap, Timeout) ->
|
|
|
+ fetch_response(SockModule, Socket, Timeout, binary, FilterMap, []).
|
|
|
|
|
|
%% --- internal ---
|
|
|
|
|
@@ -413,9 +438,10 @@ parse_handshake_confirm(Packet) ->
|
|
|
%% @doc Fetches one or more results and and parses the result set(s) using
|
|
|
%% either the text format (for plain queries) or the binary format (for
|
|
|
%% prepared statements).
|
|
|
--spec fetch_response(atom(), term(), timeout(), text | binary, list()) ->
|
|
|
+-spec fetch_response(atom(), term(), timeout(), text | binary,
|
|
|
+ query_filtermap(), list()) ->
|
|
|
{ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
|
|
|
-fetch_response(SockModule, Socket, Timeout, Proto, Acc) ->
|
|
|
+fetch_response(SockModule, Socket, Timeout, Proto, FilterMap, Acc) ->
|
|
|
case recv_packet(SockModule, Socket, Timeout, any) of
|
|
|
{ok, Packet, SeqNum2} ->
|
|
|
Result = case Packet of
|
|
@@ -426,19 +452,14 @@ fetch_response(SockModule, Socket, Timeout, Proto, Acc) ->
|
|
|
ResultPacket ->
|
|
|
%% The first packet in a resultset is only the column count.
|
|
|
{ColCount, <<>>} = lenenc_int(ResultPacket),
|
|
|
- R0 = fetch_resultset(SockModule, Socket, ColCount, SeqNum2),
|
|
|
- case R0 of
|
|
|
- #error{} = E ->
|
|
|
- %% TODO: Find a way to get here + testcase
|
|
|
- E;
|
|
|
- #resultset{} = R ->
|
|
|
- parse_resultset(R, ColCount, Proto)
|
|
|
- end
|
|
|
+ fetch_resultset(SockModule, Socket, ColCount, Proto,
|
|
|
+ FilterMap, SeqNum2)
|
|
|
end,
|
|
|
Acc1 = [Result | Acc],
|
|
|
case more_results_exists(Result) of
|
|
|
true ->
|
|
|
- fetch_response(SockModule, Socket, Timeout, Proto, Acc1);
|
|
|
+ fetch_response(SockModule, Socket, Timeout, Proto,
|
|
|
+ FilterMap, Acc1);
|
|
|
false ->
|
|
|
{ok, lists:reverse(Acc1)}
|
|
|
end;
|
|
@@ -446,36 +467,60 @@ fetch_response(SockModule, Socket, Timeout, Proto, Acc) ->
|
|
|
{error, timeout}
|
|
|
end.
|
|
|
|
|
|
-%% @doc Fetches packets for a result set. The column definitions are parsed but
|
|
|
-%% the rows are unparsed binary packages. This function is used for both the
|
|
|
-%% text protocol and the binary protocol. This affects the way the rows need to
|
|
|
-%% be parsed.
|
|
|
--spec fetch_resultset(atom(), term(), integer(), integer()) ->
|
|
|
+%% @doc Fetches a result set.
|
|
|
+-spec fetch_resultset(atom(), term(), integer(), text | binary,
|
|
|
+ query_filtermap(), integer()) ->
|
|
|
#resultset{} | #error{}.
|
|
|
-fetch_resultset(SockModule, Socket, FieldCount, SeqNum) ->
|
|
|
- {ok, ColDefs, SeqNum1} = fetch_column_definitions(SockModule, Socket,
|
|
|
- SeqNum, FieldCount, []),
|
|
|
- {ok, DelimiterPacket, SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
|
|
|
- #eof{status = S, warning_count = W} = parse_eof_packet(DelimiterPacket),
|
|
|
- case fetch_resultset_rows(SockModule, Socket, SeqNum2, []) of
|
|
|
+fetch_resultset(SockModule, Socket, FieldCount, Proto, FilterMap, SeqNum0) ->
|
|
|
+ {ok, ColDefs0, SeqNum1} = fetch_column_definitions(SockModule, Socket,
|
|
|
+ SeqNum0, FieldCount, []),
|
|
|
+ {ok, DelimPacket, SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
|
|
|
+ #eof{status = S, warning_count = W} = parse_eof_packet(DelimPacket),
|
|
|
+ ColDefs1 = lists:map(fun parse_column_definition/1, ColDefs0),
|
|
|
+ case fetch_resultset_rows(SockModule, Socket, FieldCount, ColDefs1, Proto,
|
|
|
+ FilterMap, SeqNum2, []) of
|
|
|
{ok, Rows, _SeqNum3} ->
|
|
|
- ColDefs1 = lists:map(fun parse_column_definition/1, ColDefs),
|
|
|
- #resultset{cols = ColDefs1, rows = Rows,
|
|
|
- status = S, warning_count = W};
|
|
|
+ #resultset{cols = ColDefs1, rows = Rows, status = S,
|
|
|
+ warning_count = W};
|
|
|
#error{} = E ->
|
|
|
E
|
|
|
end.
|
|
|
|
|
|
-parse_resultset(#resultset{cols = ColDefs, rows = Rows} = R, ColumnCount,
|
|
|
- text) ->
|
|
|
- %% Parse the rows according to the 'text protocol' representation.
|
|
|
- Rows1 = [decode_text_row(ColumnCount, ColDefs, Row) || Row <- Rows],
|
|
|
- R#resultset{rows = Rows1};
|
|
|
-parse_resultset(#resultset{cols = ColDefs, rows = Rows} = R, ColumnCount,
|
|
|
- binary) ->
|
|
|
- %% Parse the rows according to the 'binary protocol' representation.
|
|
|
- Rows1 = [decode_binary_row(ColumnCount, ColDefs, Row) || Row <- Rows],
|
|
|
- R#resultset{rows = Rows1}.
|
|
|
+%% @doc Fetches the rows for a result set and decodes them using either the text
|
|
|
+%% format (for plain queries) or binary format (for prepared statements).
|
|
|
+-spec fetch_resultset_rows(atom(), term(), integer(), [#col{}], text | binary,
|
|
|
+ query_filtermap(), integer(), [[term()]]) ->
|
|
|
+ {ok, [[term()]], integer()} | #error{}.
|
|
|
+fetch_resultset_rows(SockModule, Socket, FieldCount, ColDefs, Proto,
|
|
|
+ FilterMap, SeqNum0, Acc) ->
|
|
|
+ {ok, Packet, SeqNum1} = recv_packet(SockModule, Socket, SeqNum0),
|
|
|
+ case Packet of
|
|
|
+ ?error_pattern ->
|
|
|
+ parse_error_packet(Packet);
|
|
|
+ ?eof_pattern ->
|
|
|
+ {ok, lists:reverse(Acc), SeqNum1};
|
|
|
+ RowPacket ->
|
|
|
+ Row0=decode_row(FieldCount, ColDefs, RowPacket, Proto),
|
|
|
+ Acc1 = case filtermap_resultset_row(FilterMap, ColDefs, Row0) of
|
|
|
+ false ->
|
|
|
+ Acc;
|
|
|
+ true ->
|
|
|
+ [Row0|Acc];
|
|
|
+ {true, Row1} ->
|
|
|
+ [Row1|Acc]
|
|
|
+ end,
|
|
|
+ fetch_resultset_rows(SockModule, Socket, FieldCount, ColDefs,
|
|
|
+ Proto, FilterMap, SeqNum1, Acc1)
|
|
|
+ end.
|
|
|
+
|
|
|
+-spec filtermap_resultset_row(query_filtermap(), [#col{}], [term()]) ->
|
|
|
+ query_filtermap_res().
|
|
|
+filtermap_resultset_row(no_filtermap_fun, _, _) ->
|
|
|
+ true;
|
|
|
+filtermap_resultset_row(Fun, _, Row) when is_function(Fun, 1) ->
|
|
|
+ Fun(Row);
|
|
|
+filtermap_resultset_row(Fun, ColDefs, Row) when is_function(Fun, 2) ->
|
|
|
+ Fun([Col#col.name || Col <- ColDefs], Row).
|
|
|
|
|
|
more_results_exists(#ok{status = S}) ->
|
|
|
S band ?SERVER_MORE_RESULTS_EXISTS /= 0;
|
|
@@ -497,24 +542,6 @@ fetch_column_definitions(SockModule, Socket, SeqNum, NumLeft, Acc)
|
|
|
fetch_column_definitions(_SockModule, _Socket, SeqNum, 0, Acc) ->
|
|
|
{ok, lists:reverse(Acc), SeqNum}.
|
|
|
|
|
|
-%% @doc Fetches rows in a result set. There is a packet per row. The row packets
|
|
|
-%% are not decoded. This function can be used for both the binary and the text
|
|
|
-%% protocol result sets.
|
|
|
--spec fetch_resultset_rows(atom(), term(), SeqNum :: integer(), Acc) ->
|
|
|
- {ok, Rows, integer()} | #error{}
|
|
|
- when Acc :: [binary()],
|
|
|
- Rows :: [binary()].
|
|
|
-fetch_resultset_rows(SockModule, Socket, SeqNum, Acc) ->
|
|
|
- {ok, Packet, SeqNum1} = recv_packet(SockModule, Socket, SeqNum),
|
|
|
- case Packet of
|
|
|
- ?error_pattern ->
|
|
|
- parse_error_packet(Packet);
|
|
|
- ?eof_pattern ->
|
|
|
- {ok, lists:reverse(Acc), SeqNum1};
|
|
|
- Row ->
|
|
|
- fetch_resultset_rows(SockModule, Socket, SeqNum1, [Row | Acc])
|
|
|
- end.
|
|
|
-
|
|
|
%% Parses a packet containing a column definition (part of a result set)
|
|
|
parse_column_definition(Data) ->
|
|
|
{<<"def">>, Rest1} = lenenc_str(Data), %% catalog (always "def")
|
|
@@ -540,6 +567,13 @@ parse_column_definition(Data) ->
|
|
|
#col{name = Name, type = Type, charset = Charset, length = Length,
|
|
|
decimals = Decimals, flags = Flags}.
|
|
|
|
|
|
+%% @doc Decodes a row using either the text or binary format.
|
|
|
+-spec decode_row(integer(), [#col{}], binary(), text | binary) -> [term()].
|
|
|
+decode_row(FieldCount, ColDefs, RowPacket, text) ->
|
|
|
+ decode_text_row(FieldCount, ColDefs, RowPacket);
|
|
|
+decode_row(FieldCount, ColDefs, RowPacket, binary) ->
|
|
|
+ decode_binary_row(FieldCount, ColDefs, RowPacket).
|
|
|
+
|
|
|
%% -- text protocol --
|
|
|
|
|
|
-spec decode_text_row(NumColumns :: integer(),
|