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