diff --git a/src/ejabberd_auth_storage.erl b/src/ejabberd_auth_storage.erl new file mode 100644 index 000000000..f69356432 --- /dev/null +++ b/src/ejabberd_auth_storage.erl @@ -0,0 +1,303 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_auth_storage.erl +%%% Author : Alexey Shchepin , Stephan Maka +%%% Purpose : Authentification via gen_storage +%%% Created : 16 Sep 2008 Stephan Maka +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 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(ejabberd_auth_storage). +-author('alexey@process-one.net'). + +%% External exports +-export([start/1, + set_password/3, + check_password/3, + check_password/5, + try_register/3, + dirty_get_registered_users/0, + get_vh_registered_users/1, + get_vh_registered_users/2, + get_vh_registered_users_number/1, + get_vh_registered_users_number/2, + get_password/2, + get_password_s/2, + is_user_exists/2, + remove_user/2, + remove_user/3, + plain_password_required/0 + ]). + +-include("ejabberd.hrl"). + +-record(passwd, {us, password}). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(Host) -> + Backend = + case ejabberd_config:get_local_option({auth_storage, Host}) of + undefined -> mnesia; + B -> B + end, + gen_storage:create_table(Backend, Host, passwd, + [{odbc_host, Host}, + {disc_copies, [node()]}, + {attributes, record_info(fields, passwd)}, + {types, [{us, {text, text}}]} + ]), + update_table(Host), + ok. + +plain_password_required() -> + false. + +check_password(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch gen_storage:dirty_read(LServer, {passwd, US}) of + [#passwd{password = Password}] -> + Password /= ""; + _ -> + false + end. + +check_password(User, Server, Password, StreamID, Digest) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch gen_storage:dirty_read(LServer, {passwd, US}) of + [#passwd{password = Passwd}] -> + DigRes = if + Digest /= "" -> + Digest == sha:sha(StreamID ++ Passwd); + true -> + false + end, + if DigRes -> + true; + true -> + (Passwd == Password) and (Password /= "") + end; + _ -> + false + end. + +%% @spec (User::string(), Server::string(), Password::string()) -> +%% ok | {error, invalid_jid} +set_password(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + if + (LUser == error) or (LServer == error) -> + {error, invalid_jid}; + true -> + %% TODO: why is this a transaction? + F = fun() -> + gen_storage:write(LServer, + #passwd{us = US, + password = Password}) + end, + {atomic, ok} = gen_storage:transaction(LServer, passwd, F), + ok + end. + +try_register(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + if + (LUser == error) or (LServer == error) -> + {error, invalid_jid}; + true -> + F = fun() -> + case gen_storage:read(LServer, {passwd, US}) of + [] -> + gen_storage:write(LServer, + #passwd{us = US, + password = Password}), + ok; + [_E] -> + exists + end + end, + %% TODO: transaction retval? + gen_storage:transaction(LServer, passwd, F) + end. + +%% Get all registered users in Mnesia +dirty_get_registered_users() -> + %% TODO: + exit(not_implemented). + +get_vh_registered_users(Server) -> + LServer = jlib:nameprep(Server), + lists:map(fun(#passwd{us = US}) -> + US + end, + gen_storage:dirty_select(LServer, passwd, + [{'=', us, {'_', LServer}}])). + +get_vh_registered_users(Server, [{from, Start}, {to, End}]) + when is_integer(Start) and is_integer(End) -> + get_vh_registered_users(Server, [{limit, End-Start+1}, {offset, Start}]); + +get_vh_registered_users(Server, [{limit, Limit}, {offset, Offset}]) + when is_integer(Limit) and is_integer(Offset) -> + case get_vh_registered_users(Server) of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) + end; + +get_vh_registered_users(Server, [{prefix, Prefix}]) + when is_list(Prefix) -> + Set = [{U,S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)], + lists:keysort(1, Set); + +get_vh_registered_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}]) + when is_list(Prefix) and is_integer(Start) and is_integer(End) -> + get_vh_registered_users(Server, [{prefix, Prefix}, {limit, End-Start+1}, {offset, Start}]); + +get_vh_registered_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) + when is_list(Prefix) and is_integer(Limit) and is_integer(Offset) -> + case [{U,S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)] of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) + end; + +get_vh_registered_users(Server, _) -> + get_vh_registered_users(Server). + +get_vh_registered_users_number(Server) -> + Set = get_vh_registered_users(Server), + length(Set). + +get_vh_registered_users_number(Server, [{prefix, Prefix}]) when is_list(Prefix) -> + Set = [{U, S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)], + length(Set); + +get_vh_registered_users_number(Server, _) -> + get_vh_registered_users_number(Server). + +get_password(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch gen_storage:dirty_read(LServer, passwd, US) of + [#passwd{password = Password}] -> + Password; + _ -> + false + end. + +get_password_s(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch gen_storage:dirty_read(LServer, passwd, US) of + [#passwd{password = Password}] -> + Password; + _ -> + [] + end. + +is_user_exists(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch gen_storage:dirty_read(LServer, {passwd, US}) of + [] -> + false; + [_] -> + true; + _ -> + false + end. + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + F = fun() -> + gen_storage:delete(LServer, {passwd, US}) + end, + gen_storage:transaction(LServer, passwd, F), + ejabberd_hooks:run(remove_user, LServer, [User, Server]). + +remove_user(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + F = fun() -> + case gen_storage:read(LServer, {passwd, US}) of + [#passwd{password = Password}] -> + gen_storage:delete(LServer, {passwd, US}), + ok; + [_] -> + not_allowed; + _ -> + not_exists + end + end, + case gen_storage:transaction(LServer, passwd, F) of + {atomic, ok} -> + ejabberd_hooks:run(remove_user, LServer, [User, Server]), + ok; + {atomic, Res} -> + Res; + _ -> + bad_request + end. + +update_table(Host) -> + gen_storage_migration:migrate_mnesia( + Host, passwd, + [{passwd, [user, password], + fun({passwd, User, Password}) -> + #passwd{us = {User, Host}, + password = Password} + end}]), + gen_storage_migration:migrate_odbc( + Host, [passwd], + [{"users", ["username", "password"], + fun(_, User, Password) -> + #passwd{us = {User, Host}, + password = Password} + end}]). diff --git a/src/gen_storage.erl b/src/gen_storage.erl new file mode 100644 index 000000000..87e8eef9c --- /dev/null +++ b/src/gen_storage.erl @@ -0,0 +1,579 @@ +-module(gen_storage). +-author('stephan@spaceboyz.net'). + +-export([behaviour_info/1]). + +-export([all_table_hosts/1, + table_info/3, + create_table/4, delete_table/2, + add_table_copy/4, add_table_index/3, + read/2, write/2, delete/2, delete_object/2, + read/4, write/4, delete/4, delete_object/4, + select/2, select/3, select/4, select/5, + count_records/2, count_records/3, delete_where/3, + dirty_read/2, dirty_write/2, dirty_delete/2, dirty_delete_object/2, + dirty_read/3, dirty_write/3, dirty_delete/3, dirty_delete_object/3, + dirty_select/3, + dirty_count_records/2, dirty_count_records/3, dirty_delete_where/3, + async_dirty/3, + transaction/3, + write_lock_table/2]). + +behaviour_info(callbacks) -> + [{table_info, 1}, + {prepare_tabdef, 2}, + {create_table, 1}, + {delete_table, 1}, + {add_table_copy, 3}, + {add_table_index, 2}, + {dirty_read, 2}, + {read, 3}, + {dirty_select, 2}, + {select, 3}, + {dirty_count_records, 2}, + {count_records, 2}, + {dirty_write, 2}, + {write, 3}, + {dirty_delete, 2}, + {delete, 3}, + {dirty_delete_object, 2}, + {delete_object, 3}, + {delete_where, 2}, + {dirty_delete_where, 2}, + {async_dirty, 2}, + {transaction, 2}]; +behaviour_info(_) -> + undefined. + +-type storage_host() :: string(). +-type storage_table() :: atom(). +-type lock_kind() :: read | write | sticky_write. +-record(table, {host_name :: {storage_host(), storage_table()}, + backend :: atom(), + def :: any()}). +-record(mnesia_def, {table :: atom(), + tabdef :: list()}). + + +%% Returns all hosts where the table Tab is defined +-spec all_table_hosts(atom()) -> + [storage_host()]. +all_table_hosts(Tab) -> + mnesia:dirty_select(table, [{#table{host_name = '$1', + _ = '_'}, + [{'=:=', {element, 2, '$1'}, {const, Tab}}], + [{element, 1, '$1'}]}]). + +-spec table_info(storage_host, storage_table, atom()) -> + any(). +table_info(Host, Tab, InfoKey) -> + Info = + case get_table(Host, Tab) of + #table{backend = mnesia, + def = #mnesia_def{tabdef = Def}} -> + [{backend, mnesia} | Def]; + #table{backend = Backend, + def = Def} -> + Info1 = Backend:table_info(Def), + BackendName = case Backend of + gen_storage_odbc -> odbc + end, + [{backend, BackendName} | Info1] + end, + case InfoKey of + all -> Info; + _ -> + case lists:keysearch(InfoKey, 1, Info) of + {value, {_, Value}} -> + Value; + false when InfoKey =:= record_name -> + Tab + end + end. + + +%% @spec create_table(backend(), Host::string(), Name::atom(), options()) -> {atomic, ok} | {aborted, Reason} +%% @type options() = [option()] +%% @type option() = {odbc_host, string()} +%% | {Table::atom(), [tabdef()]} +%% @type tabdef() = {attributes, AtomList} +%% | {record_name, atom()} +%% | {types, attributedef()} +%% @type attributedef() = [{Column::atom(), columndef()}] +%% @type columndef() = text +%% | int +%% | tuple() with an arbitrary number of columndef() +%% option() is any mnesia option +%% columndef() defaults to text for all unspecified attributes + +-spec create_table(atom(), storage_host(), storage_table(), #table{}) -> + {atomic, ok}. + +create_table(mnesia, Host, Tab, Def) -> + MDef = filter_mnesia_tabdef(Def), + define_table(mnesia, Host, Tab, #mnesia_def{table = Tab, + tabdef = MDef}), + mnesia:create_table(Tab, MDef); + +create_table(odbc, Host, Tab, Def) -> + ODef = gen_storage_odbc:prepare_tabdef(Tab, Def), + define_table(gen_storage_odbc, Host, Tab, ODef), + gen_storage_odbc:create_table(ODef). + +-spec define_table(atom(), storage_host(), storage_table(), #table{}) -> + ok. +define_table(Backend, Host, Name, Def) -> + mnesia:create_table(table, [{attributes, record_info(fields, table)}]), + mnesia:dirty_write(#table{host_name = {Host, Name}, + backend = Backend, + def = Def}). + +-spec filter_mnesia_tabdef(#table{}) -> + [{atom(), any()}]. + +filter_mnesia_tabdef(TabDef) -> + lists:filter(fun filter_mnesia_tabdef_/1, TabDef). + +filter_mnesia_tabdef_({access_mode, _}) -> true; +filter_mnesia_tabdef_({attributes, _}) -> true; +filter_mnesia_tabdef_({disc_copies, _}) -> true; +filter_mnesia_tabdef_({disc_only_copies, _}) -> true; +filter_mnesia_tabdef_({index, _}) -> true; +filter_mnesia_tabdef_({load_order, _}) -> true; +filter_mnesia_tabdef_({ram_copies, _}) -> true; +filter_mnesia_tabdef_({record_name, _}) -> true; +filter_mnesia_tabdef_({snmp, _}) -> true; +filter_mnesia_tabdef_({type, _}) -> true; +filter_mnesia_tabdef_({local_content, _}) -> true; +filter_mnesia_tabdef_(_) -> false. + + +-spec delete_table(storage_host(), storage_table()) -> + {atomic, ok}. +delete_table(Host, Tab) -> + backend_apply(delete_table, Host, Tab). + + +-spec add_table_copy(storage_host(), storage_table(), node(), atom()) -> + {atomic, ok}. +add_table_copy(Host, Tab, Node, Type) -> + backend_apply(add_table_copy, Host, Tab, [Node, Type]). + +-spec add_table_index(storage_host(), storage_table(), atom()) -> + {atomic, ok}. +add_table_index(Host, Tab, Attribute) -> + backend_apply(add_table_index, Host, Tab, [Attribute]). + + +-spec read(storage_host(), {storage_table(), any()}) -> + [tuple()]. +read(Host, {Tab, Key}) -> + backend_apply(read, Host, Tab, [Key, read]). + +-spec read(storage_host(), storage_table(), any(), lock_kind()) -> + [tuple()]. +read(Host, Tab, Key, LockKind) -> + backend_apply(read, Host, Tab, [Key, LockKind]). + + +-spec dirty_read(storage_host(), {storage_table(), any()}) -> + [tuple()]. +dirty_read(Host, {Tab, Key}) -> + backend_apply(dirty_read, Host, Tab, [Key]). + +-spec dirty_read(storage_host(), storage_table(), any()) -> + [tuple()]. +dirty_read(Host, Tab, Key) -> + backend_apply(dirty_read, Host, Tab, [Key]). + + +%% select/3 + +-type matchvalue() :: '_' + | integer() + | string(). +%% | {matchvalue(), matchrule()}. +-type matchrule() :: {'and', matchrule(), matchrule()} + | {'andalso', matchrule(), matchrule()} + | {'or', matchrule(), matchrule()} + | {'orelse', matchrule(), matchrule()} + | {'=', Attribute::atom(), matchvalue()} + | {'=/=', Attribute::atom(), matchvalue()} + | {like, Attribute::atom(), matchvalue()}. + +%% For the like operator the last element (not the tail as in +%% matchspecs) may be '_'. +-spec select(storage_host(), storage_table(), [matchrule()]) -> + [record()]. +select(Host, Tab, MatchRules) -> + select(Host, Tab, MatchRules, read). + +-spec select(storage_host(), storage_table(), [matchrule()], lock_kind()) -> + [record()]. +select(Host, Tab, MatchRules, Lock) -> + case get_table(Host, Tab) of + #table{backend = mnesia}-> + MatchSpec = matchrules_to_mnesia_matchspec(Tab, MatchRules), + mnesia:select(Tab, MatchSpec, Lock); + #table{backend = Backend, + def = Def}-> + Backend:select(Def, MatchRules, undefined) + end. + +-spec select(storage_host(), storage_table(), [matchrule()], integer(), lock_kind()) -> + {[record()], any()} | '$end_of_table'. +select(Host, Tab, MatchRules, N, Lock) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + MatchSpec = matchrules_to_mnesia_matchspec(Tab, MatchRules), + mnesia:select(Tab, MatchSpec, N, Lock); + #table{backend = Backend, + def = Def} -> + Backend:select(Def, MatchRules, N) + end. + +-spec select({storage_host(), storage_table()}, any()) -> + {[record()], any()} | '$end_of_table'. +select({Host, Tab}, Cont) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + mnesia:select(Cont); + #table{backend = Backend} -> + Backend:select(Cont) + end. + +-spec dirty_select(storage_host(), storage_table(), [matchrule()]) -> + [record()]. +dirty_select(Host, Tab, MatchRules) -> + case get_table(Host, Tab) of + #table{backend = mnesia}-> + MatchSpec = matchrules_to_mnesia_matchspec(Tab, MatchRules), + mnesia:dirty_select(Tab, MatchSpec); + #table{backend = Backend, + def = Def}-> + Backend:dirty_select(Def, MatchRules) + end. + + +matchrules_to_mnesia_matchspec(Tab, MatchRules) -> + RecordName = mnesia:table_info(Tab, record_name), + Attributes = mnesia:table_info(Tab, attributes), + %% Build up {record_name, '$1', '$2', ...} + MatchHead = list_to_tuple([RecordName | + lists:reverse( + lists:foldl( + fun(_, L) -> + A = list_to_atom( + [$$ | integer_to_list( + length(L) + 1)]), + [A | L] + end, [], Attributes))]), + %% Transform conditions + MatchConditions = + [matchrules_transform_conditions(Attributes, Rule) || + Rule <- MatchRules], + %% Always full records + MatchBody = ['$_'], + + [{MatchHead, + MatchConditions, + MatchBody}]. + + +%% TODO: special handling for '=='? +matchrules_transform_conditions(Attributes, {Op, Attribute, Value}) + when Op =:= '='; Op =:= '=='; Op =:= '=:='; Op =:= like; + Op =:= '<'; Op =:= '>'; Op =:= '>='; Op =:= '=<' -> + Var = case list_find(Attribute, Attributes) of + false -> exit(unknown_attribute); + N -> list_to_atom([$$ | integer_to_list(N)]) + end, + if + is_tuple(Value) -> + {Expr, _} = + lists:foldl( + fun('_', {R, N}) -> + {R, N + 1}; + (V, {R, N}) -> + {[matchrules_transform_column_op(Op, {element, N, Var}, {const, V}) | R], + N + 1} + end, {[], 1}, tuple_to_list(Value)), + case Expr of + [E] -> E; + _ -> list_to_tuple(['andalso' | Expr]) + end; + true -> + matchrules_transform_column_op(Op, Var, Value) + end; + +matchrules_transform_conditions(Attributes, T) when is_tuple(T) -> + L = tuple_to_list(T), + L2 = [matchrules_transform_conditions(Attributes, E) || E <- L], + list_to_tuple(L2). + + +matchrules_transform_column_op(like, Expression, Pattern) -> + case lists:foldl(fun('_', {R, E1}) -> + {R, E1}; + (P, {R, E1}) -> + Comparision = {'=:=', {hd, E1}, {const, P}}, + {[Comparision | R], {tl, E1}} + end, + {[], Expression}, Pattern) of + {[Comparision], _} -> + Comparision; + {Comparisions, _} -> + list_to_tuple(['andalso' | lists:reverse(Comparisions)]) + end; + +matchrules_transform_column_op(Op, Expression, Pattern) + when Op =:= '='; Op =:= '=:=' -> + {'=:=', Expression, Pattern}; + +matchrules_transform_column_op(Op, Expression, Pattern) -> + {Op, Expression, Pattern}. + + +%% Finds the first occurence of an element in a list +list_find(E, L) -> + list_find(E, L, 1). + +list_find(_, [], _) -> + false; +list_find(E, [E | _], N) -> + N; +list_find(E, [_ | L], N) -> + list_find(E, L, N + 1). + + +-spec dirty_count_records(storage_host(), storage_table()) -> + integer(). +dirty_count_records(Host, Tab) -> + dirty_count_records(Host, Tab, []). + +-spec dirty_count_records(storage_host(), storage_table(), [matchrule()]) -> + integer(). +dirty_count_records(Host, Tab, MatchRules) -> + case get_table(Host, Tab) of + #table{backend = mnesia}-> + [{MatchHead, MatchConditions, _}] = matchrules_to_mnesia_matchspec(Tab, MatchRules), + MatchSpec = [{MatchHead, MatchConditions, [[]]}], + length(mnesia:dirty_select(Tab, MatchSpec)); + #table{backend = Backend, + def = Def}-> + Backend:dirty_count_records(Def, MatchRules) + end. + + +-define(COUNT_RECORDS_BATCHSIZE, 100). +-spec count_records(storage_host(), storage_table()) -> + integer(). +count_records(Host, Tab) -> + count_records(Host, Tab, []). + +-spec count_records(storage_host(), storage_table(), [matchrule()]) -> + integer(). +count_records(Host, Tab, MatchRules) -> + case get_table(Host, Tab) of + #table{backend = mnesia}-> + [{MatchHead, MatchConditions, _}] = matchrules_to_mnesia_matchspec(Tab, MatchRules), + MatchSpec = [{MatchHead, MatchConditions, [[]]}], + case mnesia:select(Tab, MatchSpec, + ?COUNT_RECORDS_BATCHSIZE, read) of + {Result, Cont} -> + Count = length(Result), + mnesia_count_records_cont(Cont, Count); + '$end_of_table' -> + 0 + end; + #table{backend = Backend, + def = Def}-> + Backend:count_records(Def, MatchRules) + end. + +mnesia_count_records_cont(Cont, Count) -> + case mnesia:select(Cont) of + {Result, Cont} -> + NewCount = Count + length(Result), + mnesia_count_records_cont(Cont, NewCount); + '$end_of_table' -> + Count + end. + + +-spec write(storage_host(), tuple()) -> + ok. +write(Host, Rec) -> + Tab = element(1, Rec), + backend_apply(write, Host, Tab, [Rec, write]). + +-spec write(storage_host(), storage_table(), tuple(), lock_kind()) -> + ok. +write(Host, Tab, Rec, LockKind) -> + backend_apply(write, Host, Tab, [Rec, LockKind]). + + +-spec dirty_write(storage_host(), tuple()) -> + ok. +dirty_write(Host, Rec) -> + Tab = element(1, Rec), + backend_apply(dirty_write, Host, Tab, [Rec]). + +-spec dirty_write(storage_host(), storage_table(), tuple()) -> + ok. +dirty_write(Host, Tab, Rec) -> + backend_apply(dirty_write, Host, Tab, [Rec]). + + +-spec delete(storage_host(), {storage_table(), any()}) -> + ok. +delete(Host, {Tab, Key}) -> + backend_apply(delete, Host, Tab, [Key, write]). + +-spec delete(storage_host(), storage_table(), any(), lock_kind()) -> + ok. +delete(Host, Tab, Key, LockKind) -> + backend_apply(delete, Host, Tab, [Key, LockKind]). + + +-spec dirty_delete(storage_host(), {storage_table(), any()}) -> + ok. +dirty_delete(Host, {Tab, Key}) -> + backend_apply(dirty_delete, Host, Tab, [Key]). + +-spec dirty_delete(storage_host(), storage_table(), any()) -> + ok. +dirty_delete(Host, Tab, Key) -> + backend_apply(dirty_delete, Host, Tab, [Key]). + + +-spec delete_object(storage_host(), tuple()) -> + ok. +delete_object(Host, Rec) -> + Tab = element(1, Rec), + backend_apply(delete_object, Host, Tab, [Rec, write]). + +-spec delete_object(storage_host(), storage_table(), tuple(), lock_kind()) -> + ok. +delete_object(Host, Tab, Rec, LockKind) -> + backend_apply(delete_object, Host, Tab, [Rec, LockKind]). + + +-spec dirty_delete_object(storage_host(), tuple()) -> + ok. +dirty_delete_object(Host, Rec) -> + Tab = element(1, Rec), + backend_apply(delete_object, Host, Tab, [Rec]). + +-spec dirty_delete_object(storage_host(), storage_table(), tuple()) -> + ok. +dirty_delete_object(Host, Tab, Rec) -> + backend_apply(delete_object, Host, Tab, [Rec]). + + +-define(DELETE_WHERE_BATCH_SIZE, 100). + +-spec delete_where(storage_host(), storage_table(), [matchrule()]) -> + ok. +delete_where(Host, Tab, MatchRules) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + MatchSpec = matchrules_to_mnesia_matchspec(Tab, MatchRules), + mnesia:write_lock_table(Tab), + SR = mnesia:select(Tab, MatchSpec, ?DELETE_WHERE_BATCH_SIZE, write), + delete_where_mnesia1(SR); + #table{backend = Backend, + def = Def} -> + Backend:delete_where(Def, MatchRules) + end. + +delete_where_mnesia1('$end_of_table') -> + ok; +delete_where_mnesia1({Objects, Cont}) -> + lists:foreach(fun(Object) -> + mnesia:delete_object(Object) + end, Objects), + delete_where_mnesia1(mnesia:select(Cont)). + + +-spec dirty_delete_where(storage_host(), storage_table(), [matchrule()]) -> + ok. +dirty_delete_where(Host, Tab, MatchRules) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + MatchSpec = matchrules_to_mnesia_matchspec(Tab, MatchRules), + F = fun() -> + mnesia:write_lock_table(Tab), + Objects = mnesia:select(Tab, MatchSpec, write), + lists:foreach(fun(Object) -> + mnesia:delete_object(Object) + end, Objects) + end, + {atomic, _} = mnesia:transaction(F); + #table{backend = Backend, + def = Def} -> + Backend:dirty_delete_where(Def, MatchRules) + end. + + +-spec write_lock_table(storage_host(), storage_table()) -> + ok. +write_lock_table(Host, Tab) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + mnesia:write_lock_table(Tab); + _ -> + ignored + end. + + +-spec transaction(storage_host(), storage_table(), fun()) -> + {atomic, any()}. +%% Warning: all tabs touched by the transaction must use the same +%% storage backend! +transaction(Host, Tab, Fun) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + mnesia:transaction(Fun); + #table{backend = Backend, + def = Def} -> + Backend:transaction(Def, Fun) + end. + + +-spec async_dirty(storage_host(), storage_table(), fun()) -> + {atomic, any()}. +%% Warning: all tabs touched by the async_dirty must use the same +%% storage backend! +async_dirty(Host, Tab, Fun) -> + case get_table(Host, Tab) of + #table{backend = mnesia} -> + mnesia:async_dirty(Fun); + #table{backend = Backend, + def = Def} -> + Backend:async_dirty(Def, Fun) + end. + + +get_table(Host, Tab) -> + case mnesia:dirty_read(table, {Host, Tab}) of + [T] -> + T; + _ -> + error_logger:error_msg("gen_storage: Table ~p not found on ~p~n", [Tab, Host]), + exit(table_not_found) + end. + +backend_apply(F, Host, Tab) -> + backend_apply(F, Host, Tab, []). + +backend_apply(F, Host, Tab, A) -> + #table{backend = Backend, + def = Def} = get_table(Host, Tab), + case Def of + #mnesia_def{table = Tab} -> + apply(Backend, F, [Tab | A]); + _ -> + apply(Backend, F, [Def | A]) + end. + diff --git a/src/gen_storage_migration.erl b/src/gen_storage_migration.erl new file mode 100644 index 000000000..55ea45509 --- /dev/null +++ b/src/gen_storage_migration.erl @@ -0,0 +1,223 @@ +-module(gen_storage_migration). + +-export([migrate_mnesia/3, migrate_odbc/3]). + +-include("ejabberd.hrl"). + +%% @type Migrations = [{OldTable, OldAttributes, MigrateFun}] +migrate_mnesia(Host, Table, Migrations) -> + SameTableName = [Migration + || {OldTable, _, _} = Migration <- Migrations, + OldTable =:= Table], + lists:foreach(fun(Migration) -> + case (catch migrate_mnesia1(Host, Table, Migration)) of + ok -> ok; + ignored -> ok; + R -> + ?ERROR_MSG("Error performing migration ~p:~n~p", + [Migration, R]) + end + end, SameTableName), + DifferentTableName = [Migration + || {OldTable, _, _} = Migration <- Migrations, + OldTable =/= Table], + lists:foreach(fun(Migration) -> + case (catch migrate_mnesia1(Host, Table, Migration)) of + ok -> ok; + ignored -> ok; + R -> + ?ERROR_MSG("Error performing migration ~p:~n~p", + [Migration, R]) + end + end, DifferentTableName). + +migrate_mnesia1(Host, Table, {OldTable, OldAttributes, MigrateFun}) -> + case (catch mnesia:table_info(OldTable, attributes)) of + OldAttributes -> + if + Table =:= OldTable -> + %% TODO: move into transaction + TmpTable = list_to_atom(atom_to_list(Table) ++ "_tmp"), + NewRecordName = gen_storage:table_info(Host, Table, record_name), + NewAttributes = gen_storage:table_info(Host, Table, attributes), + ?INFO_MSG("Migrating mnesia table ~p via ~p~nfrom ~p~nto ~p", + [Table, TmpTable, OldAttributes, NewAttributes]), + + {atomic, ok} = mnesia:create_table( + TmpTable, + [{disc_only_copies, [node()]}, + {type, bag}, + {local_content, true}, + {record_name, NewRecordName}, + {attributes, NewAttributes}]), + F1 = fun() -> + mnesia:write_lock_table(TmpTable), + mnesia:foldl( + fun(OldRecord, _) -> + NewRecord = MigrateFun(OldRecord), + ?DEBUG("~p-~p: ~p -> ~p~n",[OldTable, Table, OldRecord, NewRecord]), + if + is_tuple(NewRecord) -> + mnesia:write(TmpTable, NewRecord, write); + true -> + ignored + end + end, ok, OldTable) + end, + {atomic, ok} = mnesia:transaction(F1), + mnesia:delete_table(OldTable), + TableInfo = gen_storage:table_info(Host, Table, all), + {value, {_, Backend}} = lists:keysearch(backend, 1, TableInfo), + gen_storage:create_table(Backend, Host, Table, TableInfo), + F2 = fun() -> + mnesia:write_lock_table(Table), + mnesia:foldl( + fun(NewRecord, _) -> + ?DEBUG("~p-~p: ~p~n",[OldTable, Table, NewRecord]), + gen_storage:write(Host, Table, NewRecord, write) + end, ok, TmpTable) + end, + {atomic, ok} = mnesia:transaction(F2), + mnesia:delete_table(TmpTable), + ?INFO_MSG("Migration of mnesia table ~p successfully finished", [Table]); + + Table =/= OldTable -> + ?INFO_MSG("Migrating mnesia table ~p to ~p~nfrom ~p", + [OldTable, Table, OldAttributes]), + F1 = fun() -> + mnesia:write_lock_table(Table), + mnesia:foldl( + fun(OldRecord, _) -> + NewRecord = MigrateFun(OldRecord), + ?DEBUG("~p-~p: ~p -> ~p~n",[OldTable, Table, OldRecord, NewRecord]), + if + is_tuple(NewRecord) -> + gen_storage:write(Host, Table, NewRecord, write); + true -> + ignored + end + end, ok, OldTable) + end, + {atomic, ok} = mnesia:transaction(F1), + mnesia:delete_table(OldTable), + ?INFO_MSG("Migration of mnesia table ~p successfully finished", [Table]), + ok + end; + _ -> + ignored + end. + + +migrate_odbc(Host, Tables, Migrations) -> + try ejabberd_odbc:sql_transaction( + Host, + fun() -> + lists:foreach( + fun(Migration) -> + case (catch migrate_odbc1(Host, Tables, Migration)) of + ok -> ok; + ignored -> ok; + R -> + ?ERROR_MSG("Error performing migration ~p:~n~p", + [Migration, R]) + end + end, Migrations) + end) + catch exit:{noproc, _Where} -> + ?INFO_MSG("Not migrating ODBC on host ~p because no ODBC was configured.", [Host]), + ok + end. + +migrate_odbc1(Host, Tables, {OldTable, OldColumns, MigrateFun}) -> + migrate_odbc1(Host, Tables, {[{OldTable, OldColumns}], MigrateFun}); + +migrate_odbc1(Host, Tables, {OldTablesColumns, MigrateFun}) -> + {[OldTable | _] = OldTables, + [OldColumns | _] = OldColumnsAll} = lists:unzip(OldTablesColumns), + OldTablesA = [list_to_atom(Table) || Table <- OldTables], + case [odbc_table_columns_t(OldTable1) + || OldTable1 <- OldTables] of + OldColumnsAll -> + ?INFO_MSG("Migrating ODBC table ~p to gen_storage tables ~p", [OldTable, Tables]), + + %% rename old tables to *_old + lists:foreach(fun(OldTable1) -> + {updated, _} = + ejabberd_odbc:sql_query_t("alter table " ++ OldTable1 ++ + " rename to " ++ OldTable1 ++ "_old") + end, OldTables), + %% recreate new tables + lists:foreach(fun(NewTable) -> + case lists:member(NewTable, OldTablesA) of + true -> + TableInfo = + gen_storage:table_info(Host, NewTable, all), + {value, {_, Backend}} = + lists:keysearch(backend, 1, TableInfo), + gen_storage:create_table(Backend, Host, + NewTable, TableInfo); + false -> ignored + end + end, Tables), + + SELECT = + fun(Columns, Table, Keys) -> + Table1 = case lists:member(Table, OldTables) of + true -> Table ++ "_old"; + false -> Table + end, + WherePart = case Keys of + [] -> ""; + _ -> " WHERE " ++ + string:join([K ++ "=" ++ + if + is_list(V) -> + "\"" ++ ejabberd_odbc:escape(V) ++ "\""; + is_integer(V) -> + integer_to_list(V) + end + || {K, V} <- Keys], + " AND ") + end, + {selected, _, Rows} = + ejabberd_odbc:sql_query_t("SELECT " ++ string:join(Columns, ", ") ++ + " FROM " ++ Table1 ++ + WherePart), + [tuple_to_list(Row) || Row <- Rows] + end, + + %% TODO: this will need lots of RAM, make it batched + OldRows = SELECT(OldColumns, OldTable, []), + NRows = + lists:foldl(fun(OldRow, NRow) -> + NewRecords = apply(MigrateFun, [SELECT | OldRow]), + if + is_list(NewRecords) -> + lists:foreach( + fun(NewRecord) -> + %% TODO: gen_storage transaction? + gen_storage:dirty_write(Host, NewRecord) + end, NewRecords); + is_tuple(NewRecords) -> + gen_storage:dirty_write(Host, NewRecords) + end, + NRow + 1 + end, 0, OldRows), + + lists:foreach(fun(OldTable1) -> + {updated, _} = ejabberd_odbc:sql_query_t("drop table " ++ OldTable1 ++ "_old") + end, OldTables), + + ?INFO_MSG("Migrated ODBC table ~p to gen_storage tables ~p (~p rows)", [OldTable, Tables, NRows]); + _ -> + ignored + end. + + +odbc_table_columns_t(Table) -> + case ejabberd_odbc:sql_query_t("select column_name from information_schema.columns where table_name='" ++ Table ++ "'") of + {selected, _, Columns1} -> + Columns2 = lists:map(fun({C}) -> C end, Columns1), + Columns2 + end. + diff --git a/src/gen_storage_odbc.erl b/src/gen_storage_odbc.erl new file mode 100644 index 000000000..e3901c438 --- /dev/null +++ b/src/gen_storage_odbc.erl @@ -0,0 +1,674 @@ +-module(gen_storage_odbc). +-author('stephan@spaceboyz.net'). +-behaviour(gen_storage). + + +-export([table_info/1, prepare_tabdef/2, + create_table/1, delete_table/1, + add_table_copy/3, add_table_index/2, + read/3, select/3, count_records/2, write/3, + delete/3, delete_object/3, + dirty_read/2, dirty_select/2, dirty_count_records/2, dirty_write/2, + dirty_delete/2, dirty_delete_object/2, + delete_where/2, dirty_delete_where/2, + async_dirty/2, + transaction/2]). + +%% TODO: append 's' to table names in SQL? + +-record(tabdef, {name :: atom(), % Table name + record_name :: atom(), % Record name + table_type :: 'set' | 'bag', % atom() = set | bag + attributes :: [string()], % Columns + columns :: string(), % "\"col1\", \"col2\" ,..." + column_names :: [{string(), [string()]}], % [{string(), [string()]}] (already quoted) + types :: [{string(), atom()}], + host :: string() + }). +-record(odbc_cont, {tabdef, sql, offset = 0, limit}). + +-include_lib("exmpp/include/exmpp.hrl"). % for #jid{} + + +table_info(#tabdef{record_name = RecordName, + table_type = TableType, + attributes = Attributes, + types = Types, + host = Host}) -> + [{record_name, RecordName}, + {table_type, TableType}, + {attributes, lists:map(fun erlang:list_to_atom/1, Attributes)}, + {types, [{list_to_atom(A), T} + || {A, T} <- Types]}, + {odbc_host, Host}]. + +%%% Def preparation %%% + +prepare_tabdef(Name, TabOpts) -> + {value, {_, Host}} = lists:keysearch(odbc_host, 1, TabOpts), + RecordName = + case lists:keysearch(record_name, 1, TabOpts) of + {value, {_, RecordName1}} -> RecordName1; + false -> Name + end, + TableType = + case lists:keysearch(type, 1, TabOpts) of + {value, {_, TableType1}} -> TableType1; + false -> set + end, + Types = + case lists:keysearch(types, 1, TabOpts) of + {value, {_, Types1}} -> + [{atom_to_list(A), T} || {A, T} <- Types1]; + false -> + [] + end, + {value, {_, Attributes1}} = lists:keysearch(attributes, 1, TabOpts), + Attributes = lists:map(fun atom_to_list/1, Attributes1), + ColumnQuote = case ejabberd_odbc:db_type(Host) of + mysql -> "`"; + _ -> "\"" + end, + ColumnNames = + lists:map( + fun(Attribute) -> + case lists:keysearch(Attribute, 1, Types) of + {value, {_, Type}} when is_tuple(Type) -> + Tokens = string:tokens(Attribute, "_"), + if + length(Tokens) == size(Type) -> + TokensQuoted = [ColumnQuote ++ Token ++ ColumnQuote || + Token <- Tokens], + {Attribute, TokensQuoted}; + true -> + {Attribute, + fold_decrementing( + fun(N, A) -> + [ColumnQuote ++ + Attribute ++ integer_to_list(N) ++ + ColumnQuote | A] + end, [], size(Type))} + end; + _ -> + {Attribute, [ColumnQuote ++ Attribute ++ ColumnQuote]} + end + end, Attributes), + AttributesFull = + lists:foldr( + fun(Attribute, A) -> + T = tabdef_column_names(#tabdef{column_names = ColumnNames}, + Attribute), + T ++ A + end, [], Attributes), + Columns = string:join(AttributesFull, ", "), + #tabdef{name = Name, + record_name = RecordName, + table_type = TableType, + attributes = Attributes, + columns = Columns, + column_names = ColumnNames, + types = Types, + host = Host}. + + +create_table(#tabdef{name = Tab, + host = Host, + table_type = TableType, + attributes = Attributes = [KeyName | _], + types = Types} = TabDef) -> + {A, K} = + lists:foldr( + fun(Attribute, {Q, K}) -> + IsKey = TableType =:= bag orelse + Attribute =:= KeyName, + NoTextKeys = IsKey andalso + ejabberd_odbc:db_type(Host) =:= mysql, + KN = tabdef_column_names(TabDef, Attribute), + case lists:keysearch(Attribute, 1, Types) of + {value, {_, Tuple}} when is_tuple(Tuple) -> + {0, [], T} = + lists:foldr( + fun(TT, {N, [Name | Names], T}) -> + A = Name ++ " " ++ + type_to_sql_type(TT, NoTextKeys) ++ + case T of + "" -> ""; + _ -> ", " ++ T + end, + {N - 1, Names, A} + end, {size(Tuple), lists:reverse(KN), ""}, tuple_to_list(Tuple)); + {value, {_, T1}} -> + [Column] = KN, + T = [Column, $ , type_to_sql_type(T1, NoTextKeys)]; + false -> + [Column] = KN, + T = [Column, $ , type_to_sql_type(text, NoTextKeys)] + end, + K2 = if + IsKey -> + KN ++ K; + true -> + K + end, + Q2 = case Q of + "" -> T; + _ -> [T, ", ", Q] + end, + {Q2, K2} + end, {"", []}, Attributes), + TabS = atom_to_list(Tab), + PKey = case TableType of + set -> [", PRIMARY KEY (", string:join(K, ", "), ")"]; + bag -> [] + end, + case odbc_command(Host, + ["CREATE TABLE ", TabS, + " (", A, PKey, ")"]) of + ok -> + case TableType of + bag -> + KeyColumns = tabdef_column_names(TabDef, KeyName), + Q = ["CREATE INDEX ", TabS, "_bag ON ", + TabS, " USING (", string:join(KeyColumns, ", "), $)], + case odbc_command(Host, Q) of + ok -> + {atomic, ok}; + {error, Reason} -> + {aborted, Reason} + end; + _ -> + {atomic, ok} + end; + {error, Reason} -> + {aborted, Reason} + end. + +type_to_sql_type(Type, false = _NoTextKeys) -> + type_to_sql_type(Type); +type_to_sql_type(Type, true = _NoTextKeys) -> + case type_to_sql_type(Type) of + "TEXT" -> "VARCHAR(255)"; + "text" -> "VARCHAR(255)"; + R -> R + end. + +type_to_sql_type(pid) -> "TEXT"; +type_to_sql_type(jid) -> "TEXT"; +type_to_sql_type(ljid) -> "TEXT"; +type_to_sql_type(atom) -> "TEXT"; +type_to_sql_type(A) when is_atom(A) -> atom_to_list(A). + + +delete_table(#tabdef{name = Tab, host = Host}) -> + case odbc_command(Host, ["DROP TABLE ", atom_to_list(Tab)]) of + ok -> + {atomic, ok}; + {error, Reason} -> + {aborted, Reason} + end. + +add_table_copy(_, _, _) -> + ignored. + +add_table_index(#tabdef{name = Tab, host = Host} = TabDef, Attribute) -> + TabS = atom_to_list(Tab), + AttributeS = atom_to_list(Attribute), + A = tabdef_column_names(TabDef, AttributeS), + Q = ["CREATE INDEX ", TabS, $_, AttributeS, + " ON ", TabS, " USING (", string:join(A, ", "), ")"], + case odbc_command(Host, Q) of + ok -> + {atomic, ok}; + {error, Reason} -> + {aborted, Reason} + end. + +dirty_read(#tabdef{host = Host} = TabDef, + Key) -> + Q = prepare_select_query(TabDef, Key), + rows_to_result(TabDef, odbc_query(Host, Q)). + +read(TabDef, Key, _LockKind) -> + Q = prepare_select_query(TabDef, Key), + rows_to_result(TabDef, odbc_query_t(Q)). + +prepare_select_query(#tabdef{name = Tab, + columns = Columns} = TabDef, + Key) -> + ["SELECT ", Columns, + " FROM ", atom_to_list(Tab), + " WHERE ", prepare_where_rule(TabDef, Key)]. + +prepare_where_rule(#tabdef{attributes = [KeyAttr | _]} = TabDef, + Key) -> + prepare_where_rule(TabDef, KeyAttr, Key). + +prepare_where_rule(#tabdef{name = Tab} = TabDef, + Attribute, Value) -> + Columns = tabdef_column_names(TabDef, Attribute), + TabS = atom_to_list(Tab), + if + is_tuple(Value) andalso + length(Columns) > 1 -> + string:join( + lists:zipwith( + fun(C, V) -> + [TabS, $., C, " = ", format(V)] + end, Columns, tuple_to_list(Value)), + " AND "); + true -> + [Column] = Columns, + [TabS, $., Column, " = ", format(Value)] + end. + +select(#odbc_cont{tabdef = TabDef, + sql = SQL, + limit = Limit, + offset = Offset} = Cont) -> + Q = [SQL, " LIMIT ", integer_to_list(Limit), " OFFSET ", integer_to_list(Offset)], + Results = rows_to_result(TabDef, odbc_query_t(Q)), + if + length(Results) == 0 -> + '$end_of_table'; + true -> + {Results, Cont#odbc_cont{offset = Offset + length(Results)}} + end. + +select(TabDef, MatchRules, N) -> + Q = prepare_select_rules_query(TabDef, MatchRules), + case N of + undefined -> + rows_to_result(TabDef, odbc_query_t(Q)); + _ when is_integer(N) -> + #tabdef{attributes = [KeyAttr | _]} = TabDef, + KeyColumns = tabdef_column_names(TabDef, KeyAttr), + %% Use ordering! + Q2 = [Q, " ORDER BY ", string:join(KeyColumns, ",")], + %% TODO: correct for bag tables + Cont = #odbc_cont{tabdef = TabDef, + sql = Q2, limit = N}, + select(Cont) + end. + + +dirty_select(#tabdef{host = Host} = TabDef, MatchRules) -> + Q = prepare_select_rules_query(TabDef, MatchRules), + rows_to_result(TabDef, odbc_query(Host, Q)). + + +prepare_select_rules_query(#tabdef{name = Tab, + columns = Columns} = TabDef, + MatchRules) -> + WherePart = prepare_where_match_rules(TabDef, MatchRules), + ["SELECT ", Columns, + " FROM ", atom_to_list(Tab), + WherePart]. + + +prepare_where_match_rules(TabDef, MatchRules) -> + W1 = [prepare_match_rule(TabDef, Rule) || Rule <- MatchRules], + W2 = remove_omits(W1), + case W2 of + [] -> ""; + _ -> [" WHERE ", string:join(W2, " AND ")] + end. + + +%% TODO: {not, R} + +prepare_match_rule(TabDef, T) + when element(1, T) =:= 'and'; element(1, T) =:= 'andalso'; + element(1, T) =:= 'or'; element(1, T) =:= 'orelse' -> + [Op | Rules] = tuple_to_list(T), + W1 = lists:map( + fun(Rule) -> + prepare_match_rule(TabDef, Rule) + end, Rules), + if + Op =:= 'and' orelse Op =:= 'andalso' -> + W2 = remove_omits(W1), + string:join(W2, " AND "); + Op =:= 'or' orelse Op =:= 'orelse' -> + AlwaysTrue = lists:member(omit, W1), + if + AlwaysTrue -> omit; + true -> string:join(W1, " OR ") + end + end; + +prepare_match_rule(#tabdef{name = Tab} = TabDef, + {Op, Attribute, Value}) -> + case tabdef_column_names(TabDef, Attribute) of + [Column] -> + prepare_match_op(Tab, Op, Column, Value); + Columns -> + W1 = lists:zipwith( + fun(C, V) -> + prepare_match_op(Tab, Op, C, V) + end, Columns, tuple_to_list(Value)), + W2 = remove_omits(W1), + string:join(W2, " AND ") + end. + +prepare_match_op(_Tab, _Op, _Column, '_') -> + omit; +prepare_match_op(Tab, Op, Column, Value) + when Op =:= '=='; Op =:= '=:=' -> + prepare_match_op(Tab, '=', Column, Value); +prepare_match_op(Tab, '=<', Column, Value) -> + prepare_match_op(Tab, '<=', Column, Value); +prepare_match_op(Tab, '=/=', Column, Value) -> + prepare_match_op(Tab, '!=', Column, Value); +prepare_match_op(Tab, like, Column, Value) -> + io_lib:format("~s.~s LIKE ~s", [Tab, Column, format(make_pattern(Value))]); +prepare_match_op(Tab, Op, Column, Value) -> + io_lib:format("~s.~s ~s ~s", [Tab, Column, Op, format(Value)]). + +make_pattern(S) -> + R = make_pattern(S, []), + lists:reverse(R). +make_pattern(['_' | S], R) -> + make_pattern(S, [$% | R]); +make_pattern([C | S], R) -> + make_pattern(S, [C | R]). + +remove_omits(L) -> + lists:filter(fun(E) -> + E =/= omit + end, L). + +rows_to_result(#tabdef{record_name = RecordName, + attributes = Attributes, + types = Types}, Rows) -> + %% TODO: this can be cached per-table in the tabdef + TypesList = + lists:map( + fun(Attribute) -> + case lists:keysearch(Attribute, 1, Types) of + {value, {_, T}} -> T; + false -> text + end + end, Attributes), + lists:map( + fun(RowTuple) -> + {_, Row} = + row_to_result(tuple_to_list(RowTuple), + TypesList, []), + list_to_tuple([RecordName | Row]) + end, Rows). + + +row_to_result(Row, [], Result) -> + {Row, lists:reverse(Result)}; +row_to_result([Field | Row], [Type | Types], Result) -> + case Type of + int -> + Row2 = Row, + R = list_to_integer(Field); + bigint -> + Row2 = Row, + R = list_to_integer(Field); + text -> + Row2 = Row, + R = Field; + pid -> + Row2 = Row, + R = list_to_pid(Field); + jid -> + Row2 = Row, + R = jlib:string_to_jid(Field); + ljid -> + Row2 = Row, + R = jlib:jid_tolower(jlib:string_to_jid(Field)); + atom -> + Row2 = Row, + R = list_to_atom(Field); + _ when is_tuple(Type) -> + {Row2, R1} = row_to_result([Field | Row], tuple_to_list(Type), []), + R = list_to_tuple(R1) + end, + row_to_result(Row2, Types, [R | Result]). + + +dirty_count_records(#tabdef{host = Host, + attributes = [KeyAttr | _], + name = Tab} = TabDef, MatchRules) -> + WherePart = prepare_where_match_rules(TabDef, MatchRules), + [Column | _] = tabdef_column_names(TabDef, KeyAttr), + Q = ["SELECT count(", Column, ") FROM ", atom_to_list(Tab), + WherePart], + {selected, [_], [{Count}]} = odbc_query(Host, Q), + list_to_integer(Count). + + +count_records(#tabdef{attributes = [KeyAttr | _], + name = Tab} = TabDef, MatchRules) -> + WherePart = prepare_where_match_rules(TabDef, MatchRules), + [Column | _] = tabdef_column_names(TabDef, KeyAttr), + Q = ["SELECT count(", Column, ") FROM ", atom_to_list(Tab), + WherePart], + {selected, [_], [{Count}]} = odbc_query_t(Q), + list_to_integer(Count). + + +dirty_write(#tabdef{table_type = TableType} = TabDef, Rec) -> + case TableType of + bag -> + F = fun() -> + delete_object(TabDef, Rec), + insert(TabDef, Rec) + end; + set -> + Key = element(2, Rec), + F = fun() -> + delete(TabDef, Key), + insert(TabDef, Rec) + end + end, + case transaction(TabDef, F) of + {atomic, ok} -> ok; + {aborted, Reason} -> exit(Reason) + end. + +write(#tabdef{table_type = TableType} = TabDef, Rec, _LockKind) -> + case TableType of + bag -> + delete_object(TabDef, Rec); + set -> + Key = element(2, Rec), + delete(TabDef, Key) + end, + insert(TabDef, Rec). + + +dirty_delete(#tabdef{host = Host} = TabDef, Key) -> + Q = prepare_delete_command(TabDef, Key), + case odbc_command(Host, Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + +delete(TabDef, Key, _LockKind) -> + delete(TabDef, Key). + +delete(TabDef, Key) -> + Q = prepare_delete_command(TabDef, Key), + case odbc_command_t(Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + +prepare_delete_command(#tabdef{name = Tab} = TabDef, + Key) -> + ["DELETE FROM ", atom_to_list(Tab), + " WHERE ", prepare_where_rule(TabDef, Key)]. + +%% TODO: branch to delete if table_type == set (less overhead) +dirty_delete_object(#tabdef{host = Host} = TabDef, Rec) -> + Q = prepare_delete_object_command(TabDef, Rec), + case odbc_command(Host, Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + +delete_object(TabDef, Rec, _LockKind) -> + delete_object(TabDef, Rec). + +delete_object(TabDef, Rec) -> + Q = prepare_delete_object_command(TabDef, Rec), + case odbc_command_t(Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + +prepare_delete_object_command(#tabdef{name = Tab, + attributes = Attributes} = TabDef, + Rec) -> + [_ | Values] = tuple_to_list(Rec), + W = lists:zipwith( + fun(Attribute, Value) -> + prepare_where_rule(TabDef, Attribute, Value) + end, Attributes, Values), + io_lib:format("DELETE FROM ~s WHERE ~s", + [Tab, string:join(W, " AND ")]). + + +delete_where(#tabdef{name = Tab} = TabDef, MatchRules) -> + WherePart = prepare_where_match_rules(TabDef, MatchRules), + Q = io_lib:format("DELETE FROM ~s ~s", + [Tab, WherePart]), + case odbc_command_t(Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + +dirty_delete_where(#tabdef{host = Host,name = Tab} = TabDef, MatchRules) -> + WherePart = prepare_where_match_rules(TabDef, MatchRules), + Q = io_lib:format("DELETE FROM ~s ~s", + [Tab, WherePart]), + case odbc_command(Host, Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + + +insert(TabDef, Rec) -> + [_ | Values] = tuple_to_list(Rec), + Q = prepare_insert_command(TabDef, Values), + case odbc_command_t(Q) of + ok -> ok; + {error, Reason} -> exit(Reason) + end. + +prepare_insert_command(#tabdef{name = Tab, + columns = Columns, + attributes = Attributes, + types = Types}, + Values) -> + {V, []} = + lists:foldl( + fun(Attribute, {V, [Value | Values1]}) -> + case lists:keysearch(Attribute, 1, Types) of + {value, {_, Type}} when is_tuple(Type) -> + io:format("Type for ~p: ~p = ~p~n",[Attribute, Type, Value]), + ValueL = tuple_to_list(Value), + if + length(ValueL) == size(Type) -> + ok; + true -> + exit(tuple_malformed) + end, + F = lists:reverse( + lists:map( + fun format/1, + ValueL)), + {F ++ V, Values1}; + _ -> + {[format(Value) | V], Values1} + end + end, {[], Values}, Attributes), + ["INSERT INTO ", atom_to_list(Tab), + " (", Columns, ") VALUES (", + string:join(lists:reverse(V), ","), $)]. + + +transaction(#tabdef{host = Host}, Fun) -> + %% ejabberd_odbc already returns mnesia-style tuples + ejabberd_odbc:sql_transaction(Host, Fun). + +%% Mnesia has async_dirty, maybe ODBC has something similar: +%% "Call the Fun in a context which is not protected by a transaction." +async_dirty(Tab, Fun) -> + transaction(Tab, Fun). + +tabdef_column_names(TabDef, Attribute) when is_atom(Attribute) -> + tabdef_column_names(TabDef, atom_to_list(Attribute)); +tabdef_column_names(#tabdef{column_names = ColumnNames}, Attribute) -> + {value, {_, AttributeColumnNames}} = lists:keysearch(Attribute, 1, ColumnNames), + AttributeColumnNames. + + +format(I) when is_integer(I) -> + %% escaping not needed + integer_to_list(I); + +format(A) when is_atom(A) -> + %% escaping usually not needed, watch atom() usage + "'" ++ atom_to_list(A) ++ "'"; + +format(P) when is_pid(P) -> + %% escaping not needed + "'" ++ pid_to_list(P) ++ "'"; + +format({jid, _, _, _, _} = JID) -> + format(exmpp_jid:to_list(JID)); + +format({_, _, _} = LJID) -> + format(exmpp_jid:to_list(LJID)); + +format(B) when is_binary(B) -> + format(binary_to_list(B)); + +format(S) when is_list(S) -> + "'" ++ lists:flatten(lists:map(fun odbc_queries:escape/1, S)) ++ "'". + + +odbc_command(Host, Q) -> + case ejabberd_odbc:sql_query(Host, Q) of + {error, Reason} -> + {error, Reason}; + {updated, _} -> + ok + end. + +odbc_command_t(Q) -> + case ejabberd_odbc:sql_query_t(Q) of + {error, Reason} -> + {error, Reason}; + {updated, _} -> + ok + end. + + +odbc_query(Host, Q) -> + case ejabberd_odbc:sql_query(Host, Q) of + {selected, _Cols, Res} -> + Res; + {error, Reason} -> + exit(Reason) + end. + +odbc_query_t(Q) -> + case ejabberd_odbc:sql_query_t(Q) of + {selected, _Cols, Res} -> + Res; + {error, Reason} -> + exit(Reason) + end. + + +fold_decrementing(_, Arg, N) when N =< 0 -> + Arg; +fold_decrementing(Fun, Arg, N) -> + Arg2 = Fun(N, Arg), + fold_decrementing(Fun, Arg2, N - 1).