New files for GS (thanks to Stephan Maka)

This commit is contained in:
Badlop 2010-07-22 18:43:38 +02:00
parent bb77c39553
commit a7d9fa7301
4 changed files with 1779 additions and 0 deletions

View File

@ -0,0 +1,303 @@
%%%----------------------------------------------------------------------
%%% File : ejabberd_auth_storage.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>, 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}]).

579
src/gen_storage.erl Normal file
View File

@ -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.

View File

@ -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.

674
src/gen_storage_odbc.erl Normal file
View File

@ -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).