1: %%==============================================================================
    2: %% Copyright 2016 Erlang Solutions Ltd.
    3: %%
    4: %% Licensed under the Apache License, Version 2.0 (the "License");
    5: %% you may not use this file except in compliance with the License.
    6: %% You may obtain a copy of the License at
    7: %%
    8: %% http://www.apache.org/licenses/LICENSE-2.0
    9: %%
   10: %% Unless required by applicable law or agreed to in writing, software
   11: %% distributed under the License is distributed on an "AS IS" BASIS,
   12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13: %% See the License for the specific language governing permissions and
   14: %% limitations under the License.
   15: %%==============================================================================
   16: 
   17: -module(rdbms_SUITE).
   18: -compile([export_all, nowarn_export_all]).
   19: 
   20: -include_lib("eunit/include/eunit.hrl").
   21: 
   22: %% We need assert from it
   23: -include("mam_helper.hrl").
   24: 
   25: -import(domain_helper, [host_type/0]).
   26: 
   27: %%--------------------------------------------------------------------
   28: %% Suite configuration
   29: %%--------------------------------------------------------------------
   30: 
   31: all() ->
   32:     [{group, rdbms_queries}].
   33: 
   34: groups() ->
   35:     [{rdbms_queries, [], rdbms_queries_cases()}].
   36: 
   37: rdbms_queries_cases() ->
   38:     [select_one_works_case,
   39:      select_ascii_string_works_case,
   40:      read_int32_case,
   41:      read_int64_case,
   42:      read_unicode_case,
   43:      read_unicode250_case,
   44:      read_ascii_char_case,
   45:      read_ascii_string_case,
   46:      read_binary_8k_case,
   47:      read_binary_65k_case,
   48:      read_binary_16m_case,
   49:      read_enum_char_case,
   50:      read_boolean_case,
   51: 
   52:      read_prep_int32_case,
   53:      read_prep_int64_case,
   54:      read_prep_unicode_case,
   55:      read_prep_unicode250_case,
   56:      read_prep_ascii_char_case,
   57:      read_prep_ascii_string_case,
   58:      read_prep_binary_8k_case,
   59:      read_prep_binary_65k_case,
   60:      read_prep_binary_16m_case,
   61:      read_prep_enum_char_case,
   62:      read_prep_boolean_case,
   63: 
   64:      select_like_case,
   65:      select_like_prep_case,
   66: 
   67:      insert_batch_with_null_case,
   68:      test_cast_insert,
   69:      test_request_insert,
   70:      test_incremental_upsert,
   71:      arguments_from_two_tables].
   72: 
   73: suite() ->
   74:     escalus:suite().
   75: 
   76: %%--------------------------------------------------------------------
   77: %% Init & teardown
   78: %%--------------------------------------------------------------------
   79: init_per_suite(Config) ->
   80:     case not ct_helper:is_ct_running()
   81:          orelse mongoose_helper:is_rdbms_enabled(host_type()) of
   82:         false -> {skip, rdbms_or_ct_not_running};
   83:         true -> escalus:init_per_suite(Config)
   84:     end.
   85: 
   86: end_per_suite(Config) ->
   87:     escalus:end_per_suite(Config).
   88: 
   89: init_per_testcase(test_incremental_upsert, Config) ->
   90:     sql_query(Config, <<"TRUNCATE TABLE inbox">>),
   91:     escalus:init_per_testcase(test_incremental_upsert, Config);
   92: init_per_testcase(CaseName, Config) ->
   93:     escalus:init_per_testcase(CaseName, Config).
   94: 
   95: end_per_testcase(test_incremental_upsert, Config) ->
   96:     sql_query(Config, <<"TRUNCATE TABLE inbox">>),
   97:     escalus:end_per_testcase(test_incremental_upsert, Config);
   98: end_per_testcase(CaseName, Config) ->
   99:     escalus:end_per_testcase(CaseName, Config).
  100: 
  101: %%--------------------------------------------------------------------
  102: %% Data for cases
  103: %%--------------------------------------------------------------------
  104: 
  105: int32_values() ->
  106:     [1, -1, 0, 42, 2147483647, null].
  107: 
  108: int64_values() ->
  109:     [9223372036854775807,
  110:      null].
  111: 
  112: ascii_string_values() ->
  113:     [<<>>, <<"1">>, <<"test">>,
  114:      <<"\\">>, <<"\\\\">>,
  115:      <<"'">>, <<"''">>, <<"'''">>,
  116:      <<"\"">>, <<"\"\"">>,
  117:      <<"\r\n">>, <<"\r">>, <<"\n">>,
  118:      binary:copy(<<"a">>, 250), null].
  119: 
  120: unicode_values() ->
  121:     ascii_string_values() ++
  122:     [<<"юникод"/utf8>>, <<"😁"/utf8>>]
  123:     ++
  124:     %% Would fail with binary_data_8k and mssql.
  125:     %% For some reason mssql returns string "7878...." of length 4000.
  126:     %% What is 78? 16#78 = 120 = $x.
  127:     %% i.e. half of 8000 bytes for data.
  128:     %% Probably 2 bytes encoding is used for this.
  129: %   [binary:copy(<<$x>>, 4001),
  130:     %% Helps to debug if we don't consume all data from a buffer.
  131:     %% Than there would be a gap of missing numbers in the middle.
  132:     %% 1000 of 1-es, 1000 of 2-s, ..., 1000 of 10-s.
  133:     %%
  134:     %% In one version of eodbc, it returns 5,5,5,5... instead of 1,1,1,1...
  135:     %%
  136:     %% Also,
  137:     %% eodbc:sql_query(Conn, "SELECT convert(varbinary(max), binary_data_8k) FROM test_types") = gives correct result.
  138:     %% but
  139:     %% eodbc:sql_query(Conn, "SELECT binary_data_8k FROM test_types") = gives not correct result.
  140:     %%
  141:     %% eodbc:sql_query(Conn, "SELECT convert(varbinary(1000), binary_data_8k) FROM test_types") = gives correct result.
  142:     %% gives 010101.... as expected
  143:     [iolist_to_binary([lists:duplicate(1000, X) || X <- lists:seq(1, 10)]),
  144:      binary:copy(<<$a>>, 10000),
  145:     %% There is a bug with 8001 chars limit in upstream odbc
  146:     %% We use a fork arcusfelis/eodbc, that has the bug fixed
  147:     %% https://bugs.erlang.org/browse/ERL-421
  148:      binary:copy(<<10>>, 10000), null].
  149: 
  150: binary_values() ->
  151:     [<<0>>, <<"255">>,
  152:      <<240,159>>, %% Incomplete unicode
  153:      <<240,159,0>>, %% Invalid unicode
  154:     iolist_to_binary(lists:seq(0, 255)),
  155:     %% one kilobyte
  156:     binary:copy(<<1>>, 1024),
  157:     %% two kilobytes
  158:     binary:copy(<<2>>, 2048),
  159:     binary:copy(<<5>>, 1024*5),
  160:     %% There is a bug with 8001 chars limit in upstream odbc
  161:     %% We use a fork arcusfelis/eodbc, that has the bug fixed
  162:     %% https://bugs.erlang.org/browse/ERL-421
  163:     binary:copy(<<8>>, 8002),
  164:     binary:copy(<<0>>, 100000),
  165:     null
  166:     ] ++
  167:     case is_odbc() orelse is_pgsql() of
  168:         true ->
  169:             [];
  170:         false ->
  171:             %% FIXME long data causes timeout with mssql
  172:             %%
  173:             %% FIXME %% epgsql_sock:handle_info/2 is not optimized
  174:             %% The query takes 30 seconds on Postgres
  175:             %% mongoose_rdbms:sql_query(<<"localhost">>, <<"SELECT binary_data_16m FROM test_types">>).
  176:             [binary:copy(<<16>>, 16777215)]
  177:     end.
  178: 
  179: binary_8k_values() ->
  180:     truncate_binaries(8000, unicode_values() ++ binary_values()).
  181: 
  182: binary_65k_values() ->
  183:     truncate_binaries(65535, unicode_values() ++ binary_values()).
  184: 
  185: binary_16m_values() ->
  186:     truncate_binaries(16777215, unicode_values() ++ binary_values()).
  187: 
  188: ascii_char_values() ->
  189:     [<<"a">>, <<"b">>].
  190: 
  191: enum_char_values() ->
  192:     [<<"A">>, <<"B">>, <<"C">>].
  193: 
  194: simple_like_texts() ->
  195:     [#{text => <<"hello user!">>,
  196:        not_matching => [<<"hi">>, <<"help">>],
  197:        matching => [<<"hello">>, <<"user">>, <<"hell">>]},
  198:      #{text => <<60,79,67,32,59,48,63,58,48>>,
  199:        not_matching => [<<62,66,64,48,65,66,53,66>>],
  200:        matching => [<<60,79,67>>]}].
  201: 
  202: like_texts() ->
  203:     simple_like_texts() ++
  204:         [#{text => <<"abc%">>,
  205:            not_matching => [<<"ab%">>, <<"%bc%">>],
  206:            matching => [<<"abc%">>, <<"abc">>]},
  207:          #{text => <<"żółć_"/utf8>>,
  208:            not_matching => [<<"_ółć_"/utf8>>],
  209:            matching => [<<"żół"/utf8>>, <<"ółć_"/utf8>>]}].
  210: 
  211: %%--------------------------------------------------------------------
  212: %% Test cases
  213: %%--------------------------------------------------------------------
  214: 
  215: %% Checks, that we at least can connect to db
  216: select_one_works_case(Config) ->
  217:     ?assert_equal({selected, [{<<"1">>}]},
  218:                   %% Postgres direct driver returns <<"1">>
  219:                   %% Everyone else return 1
  220:                   selected_to_binary(sql_query(Config, <<"SELECT 1">>))).
  221: 
  222: %% Should be binary.
  223: %% Unicode is another case.
  224: select_ascii_string_works_case(Config) ->
  225:     ?assert_equal({selected, [{<<"ascii">>}]},
  226:                   sql_query(Config, <<"SELECT 'ascii'">>)).
  227: 
  228: %% Writes and reads values of this datatype back
  229: read_int32_case(Config) ->
  230:     Values = int32_values(),
  231:     [check_int32(Config, Value) || Value <- Values].
  232: 
  233: read_int64_case(Config) ->
  234:     Values = int32_values() ++ int64_values(),
  235:     [check_int64(Config, Value) || Value <- Values].
  236: 
  237: read_unicode_case(Config) ->
  238:     [check_unicode(Config, Value) || Value <- unicode_values()].
  239: 
  240: read_unicode250_case(Config) ->
  241:     [check_unicode250(Config, Value) || Value <- unicode_values(), byte_size(Value) < 250].
  242: 
  243: read_ascii_char_case(Config) ->
  244:     [check_ascii_char(Config, Value) || Value <- ascii_char_values()].
  245: 
  246: read_ascii_string_case(Config) ->
  247:     [check_ascii_string(Config, Value)
  248:      || Value <- ascii_char_values() ++ ascii_string_values()].
  249: 
  250: read_binary_8k_case(Config) ->
  251:     [check_binary_8k(Config, Value) || Value <- binary_8k_values()].
  252: 
  253: read_binary_65k_case(Config) ->
  254:     [check_binary_65k(Config, Value) || Value <- binary_65k_values()].
  255: 
  256: read_binary_16m_case(Config) ->
  257:     [check_binary_16m(Config, Value) || Value <- binary_16m_values()].
  258: 
  259: read_enum_char_case(Config) ->
  260:     [check_enum_char(Config, Value) || Value <- enum_char_values()].
  261: 
  262: read_boolean_case(Config) ->
  263:     [check_boolean(Config, Value) || Value <- [true, false]].
  264: 
  265: %%--------------------------------------------------------------------
  266: %% Prepared cases
  267: %%--------------------------------------------------------------------
  268: 
  269: read_prep_int32_case(Config) ->
  270:     Values = int32_values(),
  271:     [check_prep_int32(Config, Value) || Value <- Values].
  272: 
  273: read_prep_int64_case(Config) ->
  274:     Values = int32_values() ++ int64_values(),
  275:     [check_prep_int64(Config, Value) || Value <- Values].
  276: 
  277: read_prep_unicode_case(Config) ->
  278:     [check_prep_unicode(Config, Value) || Value <- unicode_values()].
  279: 
  280: read_prep_unicode250_case(Config) ->
  281:     [check_prep_unicode250(Config, Value) || Value <- unicode_values(), byte_size(Value) < 250].
  282: 
  283: read_prep_ascii_char_case(Config) ->
  284:     [check_prep_ascii_char(Config, Value) || Value <- ascii_char_values()].
  285: 
  286: read_prep_ascii_string_case(Config) ->
  287:     [check_prep_ascii_string(Config, Value)
  288:      || Value <- ascii_char_values() ++ ascii_string_values()].
  289: 
  290: read_prep_binary_8k_case(Config) ->
  291:     [check_prep_binary_8k(Config, Value) || Value <- binary_8k_values()].
  292: 
  293: read_prep_binary_65k_case(Config) ->
  294:     [check_prep_binary_65k(Config, Value) || Value <- binary_65k_values()].
  295: 
  296: read_prep_binary_16m_case(Config) ->
  297:     [check_prep_binary_16m(Config, Value) || Value <- binary_16m_values()].
  298: 
  299: read_prep_enum_char_case(Config) ->
  300:     [check_prep_enum_char(Config, Value) || Value <- enum_char_values()].
  301: 
  302: read_prep_boolean_case(Config) ->
  303:     [check_prep_boolean(Config, Value) || Value <- [0, 1]].
  304: 
  305: truncate_binaries(Len, List) ->
  306:     [truncate_binary(Len, Bin) || Bin <- List].
  307: 
  308: truncate_binary(Len, Bin) when byte_size(Bin) > Len ->
  309:     binary:part(Bin, {0,Len});
  310: truncate_binary(_Len, Bin) ->
  311:     Bin.
  312: 
  313: safe_binary(Len, Bin) when byte_size(Bin) > Len ->
  314:     #{what => truncated_safe_binary,
  315:       truncated_length => Len,
  316:       total_length => byte_size(Bin),
  317:       truncated_binary => binary:part(Bin, {0,Len})};
  318: safe_binary(_Len, Bin) ->
  319:     Bin.
  320: 
  321: arguments_from_two_tables(Config) ->
  322:     erase_users(Config),
  323:     sql_prepare(Config, select_multi_args, users, [password, 'last.seconds'],
  324:                 <<"SELECT users.username from users "
  325:                   " LEFT JOIN last ON (last.username = users.username) "
  326:                   " WHERE users.password = ? AND last.seconds > ?">>),
  327:     UserInsert = "INSERT INTO users (username, server, password) VALUES ",
  328:     sql_query(Config, UserInsert ++ "('alice', 'domain', 'secret')"),
  329:     sql_query(Config, UserInsert ++ "('bob', 'domain', 'billy')"),
  330:     LastInsert = "INSERT INTO last (username, server, seconds, state) VALUES ",
  331:     sql_query(Config, LastInsert ++ "('alice', 'domain', 1615368268, 'ok')"),
  332:     sql_query(Config, LastInsert ++ "('bob', 'domain', 1610000000, 'cool')"),
  333:     SelectResult = sql_execute(Config, select_multi_args, [<<"secret">>, 1611000000]),
  334:     ?assert_equal({selected, [{<<"alice">>}]}, SelectResult),
  335:     erase_users(Config),
  336:     ok.
  337: 
  338: %% Ensures that ODBC uses a correct type when encoding NULL
  339: %% and it does not interfere with non-null values
  340: insert_batch_with_null_case(Config) ->
  341:     erase_table(Config),
  342:     sql_prepare(Config, insert_batch, test_types, [unicode, unicode],
  343:                 <<"INSERT INTO test_types(unicode) VALUES (?), (?)">>),
  344:     sql_execute(Config, insert_batch, [null, <<"check1">>]),
  345:     sql_execute(Config, insert_batch, [<<"check2">>, null]),
  346:     SelectResult = sql_query(Config, "SELECT unicode FROM test_types"),
  347:     %% Sorting with null values is DB specific, so sort it with Erlang
  348:     ?assert_equal({selected, [{null}, {null}, {<<"check1">>}, {<<"check2">>}]},
  349:                   selected_to_sorted(SelectResult)).
  350: 
  351: test_cast_insert(Config) ->
  352:     erase_table(Config),
  353:     sql_prepare(Config, insert_one, test_types, [unicode],
  354:                 <<"INSERT INTO test_types(unicode) VALUES (?)">>),
  355:     sql_execute_cast(Config, insert_one, [<<"check1">>]),
  356:     sql_query_cast(Config, <<"INSERT INTO test_types(unicode) VALUES ('check2')">>),
  357:     mongoose_helper:wait_until(
  358:       fun() ->
  359:               SelectResult = sql_query(Config, "SELECT unicode FROM test_types"),
  360:               ?assertEqual({selected, [{<<"check1">>}, {<<"check2">>}]},
  361:                            selected_to_sorted(SelectResult))
  362:       end, ok, #{name => cast_queries}).
  363: 
  364: test_request_insert(Config) ->
  365:     erase_table(Config),
  366:     sql_prepare(Config, insert_one, test_types, [unicode],
  367:                 <<"INSERT INTO test_types(unicode) VALUES (?)">>),
  368:     sql_execute_request(Config, insert_one, [<<"check1">>]),
  369:     sql_query_request(Config, <<"INSERT INTO test_types(unicode) VALUES ('check2')">>),
  370:     mongoose_helper:wait_until(
  371:       fun() ->
  372:               SelectResult = sql_query(Config, "SELECT unicode FROM test_types"),
  373:               ?assertEqual({selected, [{<<"check1">>}, {<<"check2">>}]},
  374:                            selected_to_sorted(SelectResult))
  375:       end, ok, #{name => request_queries}).
  376: 
  377: test_incremental_upsert(Config) ->
  378:     case is_odbc() of
  379:         true ->
  380:             ok;
  381:         false ->
  382:             do_test_incremental_upsert(Config)
  383:     end.
  384: 
  385: do_test_incremental_upsert(Config) ->
  386:     KeyFields = [<<"luser">>, <<"lserver">>, <<"remote_bare_jid">>],
  387:     InsertFields = KeyFields ++ [<<"msg_id">>, <<"content">>, <<"unread_count">>, <<"timestamp">>],
  388: 
  389:     Key = [<<"alice">>, <<"localhost">>, <<"bob@localhost">>],
  390:     Insert = [<<"alice">>, <<"localhost">>, <<"bob@localhost">>, <<"msg_id">>, <<"content">>, 1],
  391:     sql_prepare_upsert(Config, upsert_incr, inbox,
  392:                        InsertFields, [<<"timestamp">>], KeyFields, <<"timestamp">>),
  393:     sql_execute_upsert(Config, upsert_incr, Insert ++ [42], [42], Key),
  394:     sql_execute_upsert(Config, upsert_incr, Insert ++ [43], [43], Key),
  395:     sql_execute_upsert(Config, upsert_incr, Insert ++ [0], [0], Key),
  396:     SelectResult = sql_query(Config, <<"SELECT timestamp FROM inbox">>),
  397:     ?assertEqual({selected, [{<<"43">>}]}, selected_to_binary(SelectResult)).
  398: 
  399: %%--------------------------------------------------------------------
  400: %% Text searching
  401: %%--------------------------------------------------------------------
  402: 
  403: select_like_case(Config) ->
  404:     %% Non-prepared queries don't support proper LIKE escaping
  405:     [check_like(Config, TextMap) || TextMap <- simple_like_texts()].
  406: 
  407: select_like_prep_case(Config) ->
  408:     [check_like_prep(Config, TextMap) || TextMap <- like_texts()].
  409: 
  410: %%--------------------------------------------------------------------
  411: %% Helpers
  412: %%--------------------------------------------------------------------
  413: 
  414: sql_query(_Config, Query) ->
  415:     slow_rpc(mongoose_rdbms, sql_query, [host_type(), Query]).
  416: 
  417: sql_prepare(_Config, Name, Table, Fields, Query) ->
  418:     escalus_ejabberd:rpc(mongoose_rdbms, prepare, [Name, Table, Fields, Query]).
  419: 
  420: sql_prepare_upsert(_Config, Name, Table, Insert, Update, Unique, Incr) ->
  421:     escalus_ejabberd:rpc(rdbms_queries, prepare_upsert, [host_type(), Name, Table, Insert, Update, Unique, Incr]).
  422: 
  423: sql_execute(_Config, Name, Parameters) ->
  424:     slow_rpc(mongoose_rdbms, execute, [host_type(), Name, Parameters]).
  425: 
  426: sql_execute_cast(_Config, Name, Parameters) ->
  427:     slow_rpc(mongoose_rdbms, execute_cast, [host_type(), Name, Parameters]).
  428: 
  429: sql_query_cast(_Config, Query) ->
  430:     slow_rpc(mongoose_rdbms, sql_query_cast, [host_type(), Query]).
  431: 
  432: sql_execute_request(_Config, Name, Parameters) ->
  433:     slow_rpc(mongoose_rdbms, execute_request, [host_type(), Name, Parameters]).
  434: 
  435: sql_execute_upsert(_Config, Name, Insert, Update, Unique) ->
  436:     slow_rpc(rdbms_queries, execute_upsert, [host_type(), Name, Insert, Update, Unique]).
  437: 
  438: sql_query_request(_Config, Query) ->
  439:     slow_rpc(mongoose_rdbms, sql_query_request, [host_type(), Query]).
  440: 
  441: escape_null(_Config) ->
  442:     escalus_ejabberd:rpc(mongoose_rdbms, escape_null, []).
  443: 
  444: escape_string(_Config, Value) ->
  445:     escalus_ejabberd:rpc(mongoose_rdbms, escape_string, [Value]).
  446: 
  447: escape_binary(_Config, Value) ->
  448:     slow_rpc(mongoose_rdbms, escape_binary, [host_type(), Value]).
  449: 
  450: escape_boolean(_Config, Value) ->
  451:     escalus_ejabberd:rpc(mongoose_rdbms, escape_boolean, [Value]).
  452: 
  453: escape_like(_Config, Value) ->
  454:     escalus_ejabberd:rpc(mongoose_rdbms, escape_like, [Value]).
  455: 
  456: escape_prepared_like(_Config, Value) ->
  457:     escalus_ejabberd:rpc(mongoose_rdbms, escape_prepared_like, [Value]).
  458: 
  459: unescape_binary(_Config, Value) ->
  460:     escalus_ejabberd:rpc(mongoose_rdbms, unescape_binary, [host_type(), Value]).
  461: 
  462: use_escaped(_Config, Value) ->
  463:     escalus_ejabberd:rpc(mongoose_rdbms, use_escaped, [Value]).
  464: 
  465: use_escaped_like(_Config, Value) ->
  466:     escalus_ejabberd:rpc(mongoose_rdbms, use_escaped_like, [Value]).
  467: 
  468: escape_string_or_null(Config, null) ->
  469:     escape_null(Config);
  470: escape_string_or_null(Config, TextValue) ->
  471:     escape_string(Config, TextValue).
  472: 
  473: escape_binary_or_null(Config, null) ->
  474:     escape_null(Config);
  475: escape_binary_or_null(Config, Value) ->
  476:     escape_binary(Config, Value).
  477: 
  478: decode_boolean(_Config, Value) ->
  479:     escalus_ejabberd:rpc(mongoose_rdbms, to_bool, [Value]).
  480: 
  481: erase_table(Config) ->
  482:     sql_query(Config, <<"TRUNCATE TABLE test_types">>).
  483: 
  484: erase_users(Config) ->
  485:     sql_query(Config, <<"TRUNCATE TABLE users">>),
  486:     sql_query(Config, <<"TRUNCATE TABLE last">>).
  487: 
  488: check_int32(Config, Value) ->
  489:     check_generic_integer(Config, Value, <<"int32">>).
  490: 
  491: check_int64(Config, Value) ->
  492:     check_generic_integer(Config, Value, <<"int64">>).
  493: 
  494: check_generic_integer(Config, Value, Column) ->
  495:     EraseResult = erase_table(Config),
  496:     InsertQuery = <<"INSERT INTO test_types (", Column/binary, ") "
  497:                         "VALUES (", (integer_or_null_to_binary(Value))/binary, ")">>,
  498:     SelectQuery = <<"SELECT ", Column/binary, " FROM test_types">>,
  499:     InsertResult = sql_query(Config, InsertQuery),
  500:     SelectResult = sql_query(Config, SelectQuery),
  501:     %% Compare as binaries
  502:     ?assert_equal_extra({selected, [{integer_to_binary_or_null(Value)}]},
  503:                         selected_to_binary(SelectResult),
  504:                         #{column => Column,
  505:                           erase_result => EraseResult,
  506:                           test_value => Value,
  507:                           insert_query => InsertQuery,
  508:                           select_query => SelectQuery,
  509:                           select_result => SelectResult,
  510:                           insert_result => InsertResult}).
  511: 
  512: integer_or_null_to_binary(null) -> <<"NULL">>;
  513: integer_or_null_to_binary(X) -> integer_to_binary(X).
  514: 
  515: integer_to_binary_or_null(null) -> null;
  516: integer_to_binary_or_null(X) -> integer_to_binary(X).
  517: 
  518: %% Helper function to transform values to an uniform format.
  519: %% Single tuple, single element case.
  520: %% In ODBC int32 is integer, but int64 is binary.
  521: selected_to_binary({selected, [{Value}]}) when is_integer(Value) ->
  522:     {selected, [{integer_to_binary(Value)}]};
  523: selected_to_binary(Other) ->
  524:     Other.
  525: 
  526: selected_to_sorted({selected, Rows}) ->
  527:     {selected, lists:sort(Rows)};
  528: selected_to_sorted(Other) ->
  529:     Other.
  530: 
  531: value_to_binary(Value) when is_integer(Value) ->
  532:     integer_to_binary(Value);
  533: value_to_binary(Value) ->
  534:     Value.
  535: 
  536: check_unicode250(Config, Value) ->
  537:     check_unicode_generic(Config, Value, <<"unicode250">>).
  538: 
  539: check_unicode(Config, Value) ->
  540:     check_unicode_generic(Config, Value, <<"unicode">>).
  541: 
  542: check_unicode_generic(Config, Value, Column) ->
  543:     SValue = escape_string_or_null(Config, Value),
  544:     EraseResult = erase_table(Config),
  545:     InsertQuery = ["INSERT INTO test_types (", Column, ") "
  546:                         "VALUES (", use_escaped(Config, SValue), ")"],
  547:     SelectQuery = <<"SELECT ", Column/binary, " FROM test_types">>,
  548:     InsertResult = sql_query(Config, InsertQuery),
  549:     SelectResult = sql_query(Config, SelectQuery),
  550:     %% Compare as binaries
  551:     ?assert_equal_extra({selected, [{Value}]},
  552:                         SelectResult,
  553:                         #{column => Column,
  554:                           erase_result => EraseResult,
  555:                           expected_length => byte_size_or_null(Value),
  556:                           selected_length => maybe_selected_length(Config, SelectResult),
  557:                           compare_selected => compare_selected(Config, SelectResult, Value),
  558:                           test_value => Value,
  559:                           insert_query => InsertQuery,
  560:                           insert_query_binary => iolist_to_binary(InsertQuery),
  561:                           select_query => SelectQuery,
  562:                           select_result => SelectResult,
  563:                           insert_result => InsertResult}).
  564: 
  565: check_ascii_char(Config, Value) ->
  566:     SValue = escape_string_or_null(Config, Value),
  567:     EraseResult = erase_table(Config),
  568:     InsertQuery = ["INSERT INTO test_types (ascii_char) "
  569:                         "VALUES (", use_escaped(Config, SValue), ")"],
  570:     SelectQuery = <<"SELECT ascii_char FROM test_types">>,
  571:     InsertResult = sql_query(Config, InsertQuery),
  572:     SelectResult = sql_query(Config, SelectQuery),
  573:     %% Compare as binaries
  574:     ?assert_equal_extra({selected, [{Value}]},
  575:                         SelectResult,
  576:                         #{erase_result => EraseResult,
  577:                           test_value => Value,
  578:                           insert_query => InsertQuery,
  579:                           insert_query_binary => iolist_to_binary(InsertQuery),
  580:                           select_query => SelectQuery,
  581:                           select_result => SelectResult,
  582:                           insert_result => InsertResult}).
  583: 
  584: check_ascii_string(Config, Value) ->
  585:     SValue = escape_string_or_null(Config, Value),
  586:     EraseResult = erase_table(Config),
  587:     InsertQuery = ["INSERT INTO test_types (ascii_string) "
  588:                         "VALUES (", use_escaped(Config, SValue), ")"],
  589:     SelectQuery = <<"SELECT ascii_string FROM test_types">>,
  590:     InsertResult = sql_query(Config, InsertQuery),
  591:     SelectResult = sql_query(Config, SelectQuery),
  592:     %% Compare as binaries
  593:     ?assert_equal_extra({selected, [{Value}]},
  594:                         SelectResult,
  595:                         #{erase_result => EraseResult,
  596:                           test_value => Value,
  597:                           insert_query => InsertQuery,
  598:                           insert_query_binary => iolist_to_binary(InsertQuery),
  599:                           select_query => SelectQuery,
  600:                           select_result => SelectResult,
  601:                           insert_result => InsertResult}).
  602: 
  603: check_binary_8k(Config, Value) ->
  604:     check_binary(Config, Value, <<"binary_data_8k">>).
  605: 
  606: check_binary_65k(Config, Value) ->
  607:     check_binary(Config, Value, <<"binary_data_65k">>).
  608: 
  609: check_binary_16m(Config, Value) ->
  610:     check_binary(Config, Value, <<"binary_data_16m">>).
  611: 
  612: check_binary(Config, Value, Column) ->
  613:     SValue = escape_binary_or_null(Config, Value),
  614:     EraseResult = erase_table(Config),
  615:     InsertQuery = ["INSERT INTO test_types (", Column, ") "
  616:                         "VALUES (", use_escaped(Config, SValue), ")"],
  617:     SelectQuery = <<"SELECT ", Column/binary, " FROM test_types">>,
  618:     InsertResult = sql_query(Config, InsertQuery),
  619:     SelectResult = sql_query(Config, SelectQuery),
  620:     %% Compare as binaries
  621:     ?assert_equal_extra({selected, [{Value}]},
  622:                         selected_unescape(Config, SelectResult),
  623:                         #{erase_result => EraseResult,
  624:                           inserted_length => byte_size_or_null(Value),
  625:                           %% pgsql+odbc can truncate binaries
  626:                           maybe_selected_length => maybe_selected_length(Config, SelectResult),
  627:                           maybe_selected_tail => maybe_selected_tail(Config, SelectResult),
  628:                           compare_selected => compare_selected(Config, selected_unescape(Config, SelectResult), Value),
  629:                           test_value => Value,
  630:                           insert_query_binary => iolist_to_binary(InsertQuery),
  631:                           select_query => SelectQuery,
  632:                           select_result => SelectResult,
  633:                           insert_result => InsertResult}).
  634: 
  635: byte_size_or_null(null) ->
  636:     null;
  637: byte_size_or_null(Value) ->
  638:     byte_size(Value).
  639: 
  640: check_enum_char(Config, Value) when is_binary(Value) ->
  641:     SValue = escape_string_or_null(Config, Value),
  642:     EraseResult = erase_table(Config),
  643:     InsertQuery = ["INSERT INTO test_types (enum_char) "
  644:                         "VALUES (", use_escaped(Config, SValue), ")"],
  645:     SelectQuery = <<"SELECT enum_char FROM test_types">>,
  646:     InsertResult = sql_query(Config, InsertQuery),
  647:     SelectResult = sql_query(Config, SelectQuery),
  648:     %% Compare as binaries
  649:     ?assert_equal_extra({selected, [{Value}]},
  650:                         SelectResult,
  651:                         #{erase_result => EraseResult,
  652:                           test_value => Value,
  653:                           insert_query => InsertQuery,
  654:                           insert_query_binary => iolist_to_binary(InsertQuery),
  655:                           select_query => SelectQuery,
  656:                           select_result => SelectResult,
  657:                           insert_result => InsertResult}).
  658: 
  659: check_boolean(Config, Value) when is_boolean(Value) ->
  660:     SValue = escape_boolean(Config, Value),
  661:     EraseResult = erase_table(Config),
  662:     InsertQuery = ["INSERT INTO test_types (bool_flag) "
  663:                         "VALUES (", use_escaped(Config, SValue), ")"],
  664:     SelectQuery = <<"SELECT bool_flag FROM test_types">>,
  665:     InsertResult = sql_query(Config, InsertQuery),
  666:     SelectResult = sql_query(Config, SelectQuery),
  667:     %% Compare as binaries
  668:     ?assert_equal_extra({selected, [{Value}]},
  669:                         selected_decode_boolean(Config, SelectResult),
  670:                         #{erase_result => EraseResult,
  671:                           test_value => Value,
  672:                           insert_query => InsertQuery,
  673:                           insert_query_binary => iolist_to_binary(InsertQuery),
  674:                           select_query => SelectQuery,
  675:                           select_result => SelectResult,
  676:                           insert_result => InsertResult}).
  677: 
  678: selected_unescape(_Config, {selected, [{null}]}) ->
  679:     {selected, [{null}]};
  680: selected_unescape(Config, {selected, [{Value}]}) ->
  681:     {selected, [{unescape_binary(Config, Value)}]};
  682: selected_unescape(_Config, Other) ->
  683:     Other.
  684: 
  685: selected_decode_boolean(Config, {selected, [{Value}]}) ->
  686:     {selected, [{decode_boolean(Config, Value)}]};
  687: selected_decode_boolean(_Config, Other) ->
  688:     Other.
  689: 
  690: selected_boolean_to_binary_int(Config, {selected, [{Value}]}) ->
  691:     {selected, [{boolean_to_binary_int(decode_boolean(Config, Value))}]};
  692: selected_boolean_to_binary_int(_Config, Other) ->
  693:     Other.
  694: 
  695: boolean_to_binary_int(true) -> <<"1">>;
  696: boolean_to_binary_int(false) -> <<"0">>.
  697: 
  698: maybe_selected_length(_Config, {selected, [{Value}]}) when is_binary(Value) ->
  699:     byte_size(Value);
  700: maybe_selected_length(_Config, _Other) ->
  701:     unknown.
  702: 
  703: maybe_selected_tail(Config, Selected) ->
  704:     maybe_selected_tail(Config, Selected, 100).
  705: 
  706: maybe_selected_tail(_Config, {selected, [{Value}]}, TailLen)
  707:   when is_binary(Value), byte_size(Value) > TailLen ->
  708:     binary:part(Value, {byte_size(Value), -TailLen});
  709: maybe_selected_tail(_Config, {selected, [{Value}]}, _TailLen) ->
  710:     Value;
  711: maybe_selected_tail(_Config, _Other, _TailLen) ->
  712:     unknown.
  713: 
  714: check_prep_int32(Config, Value) ->
  715:     check_generic_prep_integer(Config, Value, <<"int32">>).
  716: 
  717: check_prep_int64(Config, Value) ->
  718:     check_generic_prep_integer(Config, Value, <<"int64">>).
  719: 
  720: check_prep_unicode(Config, Value) ->
  721:     check_generic_prep(Config, Value, <<"unicode">>).
  722: 
  723: check_prep_unicode250(Config, Value) ->
  724:     check_generic_prep(Config, Value, <<"unicode250">>).
  725: 
  726: %% Char is ascii string of length one
  727: check_prep_ascii_char(Config, Value) ->
  728:     check_generic_prep(Config, Value, <<"unicode">>).
  729: 
  730: %% Varchar
  731: check_prep_ascii_string(Config, Value) ->
  732:     check_generic_prep(Config, Value, <<"ascii_string">>).
  733: 
  734: check_prep_binary_65k(Config, Value) ->
  735:     %% MSSQL returns binaries in HEX encoding
  736:     check_generic_prep(Config, Value, <<"binary_data_65k">>, unescape_binary).
  737: 
  738: check_prep_binary_8k(Config, Value) ->
  739:     %% MSSQL returns binaries in HEX encoding
  740:     check_generic_prep(Config, Value, <<"binary_data_8k">>, unescape_binary).
  741: 
  742: check_prep_binary_16m(Config, Value) ->
  743:     %% MSSQL returns binaries in HEX encoding
  744:     check_generic_prep(Config, Value, <<"binary_data_16m">>, unescape_binary).
  745: 
  746: check_generic_prep_integer(Config, Value, Column) ->
  747:     check_generic_prep(Config, Value, Column).
  748: 
  749: check_prep_enum_char(Config, Value) ->
  750:     check_generic_prep(Config, Value, <<"enum_char">>).
  751: 
  752: check_prep_boolean(Config, Value) ->
  753:     check_generic_prep(Config, Value, <<"bool_flag">>, boolean_to_binary_int).
  754: 
  755: %% Data types
  756: %% {ok, Conn} = odbc:connect("DSN=mongoose-mssql;UID=sa;PWD=mongooseim_secret+ESL123", []).
  757: %% odbc:describe_table(Conn, "test_types").
  758: %% [{"unicode",{sql_wvarchar,536870911}},
  759: %%  {"binary_data_65k",'SQL_VARBINARY'},
  760: %%  {"ascii_char",{sql_char,1}},
  761: %%  {"ascii_string",{sql_varchar,250}},
  762: %%  {"int32",sql_integer},
  763: %%  {"int64",'SQL_BIGINT'},
  764: %%  {"int8",sql_tinyint}]
  765: 
  766: check_generic_prep(Config, Value, Column) ->
  767:     check_generic_prep(Config, Value, Column, to_binary).
  768: 
  769: check_generic_prep(Config, Value, Column, TransformResult) ->
  770:     EraseResult = erase_table(Config),
  771: 
  772:     InsertQuery = <<"INSERT INTO test_types (", Column/binary, ") "
  773:                         "VALUES (?)">>,
  774:     SelectQuery = <<"SELECT ", Column/binary, " FROM test_types">>,
  775:     Name = list_to_atom("insert_" ++ binary_to_list(Column)),
  776:     Table = test_types,
  777:     Fields = [binary_to_atom(Column, utf8)],
  778:     PrepareResult = sql_prepare(Config, Name, Table, Fields, InsertQuery),
  779:     Parameters = [Value],
  780:     InsertResult = sql_execute(Config, Name, Parameters),
  781:     SelectResult = sql_query(Config, SelectQuery),
  782:     %% Compare as binaries
  783:     ?assert_equal_extra({selected, [{value_to_binary(Value)}]},
  784:                         transform_selected(TransformResult, Config, SelectResult),
  785:                         #{column => Column,
  786:                           erase_result => EraseResult,
  787:                           test_value => Value,
  788:                           insert_query => InsertQuery,
  789:                           prepare_result => PrepareResult,
  790:                           select_query => SelectQuery,
  791:                           select_result => SelectResult,
  792:                           insert_result => InsertResult}),
  793:     check_generic_filtered_prep(Config, Value, Column, TransformResult),
  794:     case is_odbc() of
  795:         true ->
  796:             %% TOP is mssql feature, all other databases use LIMIT.
  797:             check_generic_filtered_top_prep(Config, Value, Column, TransformResult);
  798:         false ->
  799:             ok
  800:     end.
  801: 
  802: %% We want to ensure that variable substitution works in SELECTS too.
  803: %% We also want to check the result value is encoded correctly.
  804: check_generic_filtered_prep(_Config, null, _Column, _TransformResult) ->
  805:     skip_null_test;
  806: check_generic_filtered_prep(Config, Value, Column, TransformResult) ->
  807:     SelectQuery = <<"SELECT ", Column/binary,
  808:             " FROM test_types WHERE ", Column/binary, " = ?">>,
  809:     Name = list_to_atom("select_filtered_" ++ binary_to_list(Column)),
  810:     Table = test_types,
  811:     Fields = [binary_to_atom(Column, utf8)],
  812:     PrepareResult = sql_prepare(Config, Name, Table, Fields, SelectQuery),
  813:     Parameters = [Value],
  814:     SelectResult = sql_execute(Config, Name, Parameters),
  815:     %% Compare as binaries
  816:     ?assert_equal_extra({selected, [{value_to_binary(Value)}]},
  817:                         transform_selected(TransformResult, Config, SelectResult),
  818:                         #{column => Column,
  819:                           test_value => Value,
  820:                           prepare_result => PrepareResult,
  821:                           select_query => SelectQuery,
  822:                           select_result => SelectResult}).
  823: 
  824: check_generic_filtered_top_prep(_Config, null, _Column, _TransformResult) ->
  825:     skip_null_test;
  826: check_generic_filtered_top_prep(Config, Value, Column, TransformResult) ->
  827:     %% SQL Server requires you to place parenthesis around the argument to top if you pass in a variable:
  828:     %% https://stackoverflow.com/questions/7038818/ms-sql-exception-incorrect-syntax-near-p0
  829:     SelectQuery = <<"SELECT TOP (?) ", Column/binary,
  830:             " FROM test_types WHERE ", Column/binary, " = ?">>,
  831:     Name = list_to_atom("select_filtered_top_" ++ binary_to_list(Column)),
  832:     Table = test_types,
  833:     Fields = [limit, binary_to_atom(Column, utf8)],
  834:     PrepareResult = sql_prepare(Config, Name, Table, Fields, SelectQuery),
  835:     Parameters = [30, Value],
  836:     SelectResult = sql_execute(Config, Name, Parameters),
  837:     %% Compare as binaries
  838:     ?assert_equal_extra({selected, [{value_to_binary(Value)}]},
  839:                         transform_selected(TransformResult, Config, SelectResult),
  840:                         #{column => Column,
  841:                           test_value => Value,
  842:                           prepare_result => PrepareResult,
  843:                           select_query => SelectQuery,
  844:                           select_result => SelectResult}).
  845: 
  846: 
  847: 
  848: transform_selected(to_binary, _Config, SelectResult) ->
  849:     selected_to_binary(SelectResult);
  850: transform_selected(unescape_binary, Config, SelectResult) ->
  851:     selected_unescape(Config, SelectResult);
  852: transform_selected(boolean_to_binary_int, Config, SelectResult) ->
  853:     selected_boolean_to_binary_int(Config, SelectResult).
  854: 
  855: %% To KISS, we just test on a table with one row.
  856: check_like(Config, TextMap = #{text := TextValue,
  857:                                matching := MatchingList,
  858:                                not_matching := NotMatchingList}) ->
  859:     SValue = escape_string_or_null(Config, TextValue),
  860:     EraseResult = erase_table(Config),
  861:     InsertQuery = ["INSERT INTO test_types (unicode) "
  862:                         "VALUES (", use_escaped(Config, SValue), ")"],
  863:     InsertResult = sql_query(Config, InsertQuery),
  864:     Info = #{erase_result => EraseResult,
  865:              insert_query => InsertQuery,
  866:              insert_query_binary => iolist_to_binary(InsertQuery),
  867:              insert_result => InsertResult,
  868:              text_map => TextMap},
  869:     [check_like_matching(Config, TextValue, Matching, Info)
  870:      || Matching <- MatchingList],
  871:     [check_like_not_matching(Config, TextValue, NotMatching, Info)
  872:      || NotMatching <- NotMatchingList].
  873: 
  874: check_like_matching(Config, TextValue, Matching, Info) ->
  875:     SLike = escape_like(Config, Matching),
  876:     SelectQuery = ["SELECT unicode FROM test_types "
  877:                     "WHERE unicode LIKE ", use_escaped_like(Config, SLike)],
  878:     SelectResult = sql_query(Config, SelectQuery),
  879:     %% Compare as binaries
  880:     ?assert_equal_extra({selected, [{TextValue}]},
  881:                         SelectResult,
  882:                         Info#{pattern => Matching,
  883:                               select_query => SelectQuery,
  884:                               select_result => SelectResult}).
  885: 
  886: check_like_not_matching(Config, _TextValue, NotMatching, Info) ->
  887:     SLike = escape_like(Config, NotMatching),
  888:     SelectQuery = ["SELECT unicode FROM test_types "
  889:                     "WHERE unicode LIKE ", use_escaped_like(Config, SLike)],
  890:     SelectResult = sql_query(Config, SelectQuery),
  891:     %% Compare as binaries
  892:     ?assert_equal_extra({selected, []},
  893:                         SelectResult,
  894:                         Info#{pattern => NotMatching,
  895:                               select_query => SelectQuery,
  896:                               select_result => SelectResult}).
  897: 
  898: compare_selected(_Config, {selected, [{SelValue}]}, Value) ->
  899:     drop_common_prefix(0, SelValue, Value);
  900: compare_selected(_Config, _, _Value) ->
  901:     nomatch.
  902: 
  903: drop_common_prefix(Pos, <<X, SelValue/binary>>, <<X, Value/binary>>) ->
  904:     drop_common_prefix(Pos+1, SelValue, Value);
  905: drop_common_prefix(Pos, SelValue, Value) ->
  906:     #{pos => Pos,
  907:       selected_suffix => safe_binary(100, SelValue),
  908:       expected_suffix => safe_binary(100, Value)}.
  909: 
  910: is_odbc() ->
  911:     escalus_ejabberd:rpc(mongoose_rdbms, db_engine, [host_type()]) == odbc.
  912: 
  913: is_pgsql() ->
  914:     escalus_ejabberd:rpc(mongoose_rdbms, db_engine, [host_type()]) == pgsql.
  915: 
  916: slow_rpc(M, F, A) ->
  917:     Node = ct:get_config({hosts, mim, node}),
  918:     Cookie = escalus_ct:get_config(ejabberd_cookie),
  919:     Res = escalus_rpc:call(Node, M, F, A, timer:seconds(30), Cookie),
  920:     case Res of
  921:         {badrpc, timeout} ->
  922:             {badrpc, {timeout, M, F}};
  923:         _ ->
  924:             Res
  925:     end.
  926: 
  927: check_like_prep(Config, TextMap = #{text := TextValue,
  928:                                matching := MatchingList,
  929:                                not_matching := NotMatchingList}) ->
  930:     EraseResult = erase_table(Config),
  931:     Name = insert_unicode_prep,
  932:     SelName = select_unicode_prep,
  933:     Table = test_types,
  934:     Fields = [<<"unicode">>],
  935:     InsertQuery = <<"INSERT INTO test_types (unicode) VALUES (?)">>,
  936:     SelectQuery = <<"SELECT unicode FROM test_types WHERE unicode LIKE ? ESCAPE '$'">>,
  937:     PrepareResult = sql_prepare(Config, Name, Table, Fields, InsertQuery),
  938:     PrepareSelResult = sql_prepare(Config, SelName, Table, Fields, SelectQuery),
  939:     Parameters = [TextValue],
  940:     InsertResult = sql_execute(Config, Name, Parameters),
  941:     Info = #{erase_result => EraseResult,
  942:              insert_query => InsertQuery,
  943:              prepare_result => PrepareResult,
  944:              insert_result => InsertResult,
  945:              prepare_select_result => PrepareSelResult,
  946:              text_map => TextMap},
  947:     [check_like_matching_prep(SelName, Config, TextValue, Matching, Info)
  948:      || Matching <- MatchingList],
  949:     [check_like_not_matching_prep(SelName, Config, TextValue, NotMatching, Info)
  950:      || NotMatching <- NotMatchingList].
  951: 
  952: check_like_matching_prep(SelName, Config, TextValue, Matching, Info) ->
  953:     SMatching = escape_prepared_like(Config, Matching),
  954:     Parameters = [<<"%", SMatching/binary, "%">>],
  955:     SelectResult = sql_execute(Config, SelName, Parameters),
  956:     %% Compare as binaries
  957:     ?assert_equal_extra({selected, [{TextValue}]},
  958:                         SelectResult,
  959:                         Info#{pattern => Matching,
  960:                               select_result => SelectResult}).
  961: 
  962: check_like_not_matching_prep(SelName, Config, _TextValue, NotMatching, Info) ->
  963:     SNotMatching = escape_prepared_like(Config, NotMatching),
  964:     Parameters = [<<"%", SNotMatching/binary, "%">>],
  965:     SelectResult = sql_execute(Config, SelName, Parameters),
  966:     %% Compare as binaries
  967:     ?assert_equal_extra({selected, []},
  968:                         SelectResult,
  969:                         Info#{pattern => NotMatching,
  970:                               select_result => SelectResult}).