New files for GS (thanks to Stephan Maka)
This commit is contained in:
parent
bb77c39553
commit
a7d9fa7301
|
@ -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}]).
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
Loading…
Reference in New Issue