From 8c8e3469bcd1f8653ec9c3edce5227bdf44cf3d7 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Mon, 13 Dec 2004 23:00:12 +0000 Subject: [PATCH] * src/odbc/pg.sql: DB creation script for postgres * src/odbc/ejabberd_odbc.erl: Experimental support for ODBC * src/mod_last_odbc.erl: Likewise * src/mod_offline_odbc.erl: Likewise * src/ejabberd_auth_odbc.erl: Likewise * src/ejabberd_auth.erl: Likewise SVN Revision: 292 --- ChangeLog | 10 ++ src/ejabberd_auth.erl | 2 + src/ejabberd_auth_odbc.erl | 210 ++++++++++++++++++++++++++++++++ src/mod_last_odbc.erl | 135 +++++++++++++++++++++ src/mod_offline_odbc.erl | 242 +++++++++++++++++++++++++++++++++++++ src/odbc/ejabberd_odbc.erl | 123 +++++++++++++++++++ src/odbc/pg.sql | 89 ++++++++++++++ 7 files changed, 811 insertions(+) create mode 100644 src/ejabberd_auth_odbc.erl create mode 100644 src/mod_last_odbc.erl create mode 100644 src/mod_offline_odbc.erl create mode 100644 src/odbc/ejabberd_odbc.erl create mode 100644 src/odbc/pg.sql diff --git a/ChangeLog b/ChangeLog index 997126e95..c388cd053 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +2004-12-13 Alexey Shchepin + + * src/odbc/pg.sql: DB creation script for postgres + + * src/odbc/ejabberd_odbc.erl: Experimental support for ODBC + * src/mod_last_odbc.erl: Likewise + * src/mod_offline_odbc.erl: Likewise + * src/ejabberd_auth_odbc.erl: Likewise + * src/ejabberd_auth.erl: Likewise + 2004-12-12 Alexey Shchepin * src/mod_stats.erl: Minor optimizations diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 2c548a33c..8abd2ac6b 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -74,6 +74,8 @@ auth_module() -> ejabberd_auth_external; ldap -> ejabberd_auth_ldap; + odbc -> + ejabberd_auth_odbc; _ -> ejabberd_auth_internal end. diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl new file mode 100644 index 000000000..8003338f1 --- /dev/null +++ b/src/ejabberd_auth_odbc.erl @@ -0,0 +1,210 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_auth_odbc.erl +%%% Author : Alexey Shchepin +%%% Purpose : Authentification via ODBC +%%% Created : 12 Dec 2004 by Alexey Shchepin +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +-module(ejabberd_auth_odbc). +-author('alexey@sevcom.net'). +-vsn('$Revision$ '). + +%% External exports +-export([start/0, + set_password/2, + check_password/2, + check_password/4, + try_register/2, + dirty_get_registered_users/0, + get_password/1, + get_password_s/1, + is_user_exists/1, + remove_user/1, + remove_user/2, + plain_password_required/0 + ]). + +-record(passwd, {user, password}). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start() -> + ok. + +plain_password_required() -> + false. + +check_password(User, Password) -> + case jlib:nodeprep(User) of + error -> + false; + LUser -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + ["select password from users " + "where username='", Username, "'"]) of + {selected, ["password"], [{Password}]} -> + true; + _ -> + false + end + end. + +check_password(User, Password, StreamID, Digest) -> + case jlib:nodeprep(User) of + error -> + false; + LUser -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + ["select password from users " + "where username='", Username, "'"]) of + {selected, ["password"], [{Passwd}]} -> + DigRes = if + Digest /= "" -> + Digest == sha:sha(StreamID ++ Passwd); + true -> + false + end, + if DigRes -> + true; + true -> + (Passwd == Password) and (Password /= "") + end; + _ -> + false + end + end. + +set_password(User, Password) -> + case jlib:nodeprep(User) of + error -> + {error, invalid_jid}; + LUser -> + Username = ejabberd_odbc:escape(LUser), + Pass = ejabberd_odbc:escape(Password), + catch ejabberd_odbc:sql_query( + ["begin;" + "delete from users where username='", Username ,"';" + "insert into users(username, password) " + "values ('", Username, "', '", Pass, "'); commit"]) + end. + + +try_register(User, Password) -> + case jlib:nodeprep(User) of + error -> + {error, invalid_jid}; + LUser -> + Username = ejabberd_odbc:escape(LUser), + Pass = ejabberd_odbc:escape(Password), + case catch ejabberd_odbc:sql_query( + ["insert into users(username, password) " + "values ('", Username, "', '", Pass, "')"]) of + {updated, _} -> + {atomic, ok}; + _ -> + {atomic, exists} + end + end. + +dirty_get_registered_users() -> + case catch ejabberd_odbc:sql_query("select username from users") of + {selected, ["username"], Res} -> + [U || {U} <- Res]; + _ -> + [] + end. + +get_password(User) -> + case jlib:nodeprep(User) of + error -> + false; + LUser -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + ["select password from users " + "where username='", Username, "'"]) of + {selected, ["password"], [{Password}]} -> + Password; + _ -> + false + end + end. + +get_password_s(User) -> + case jlib:nodeprep(User) of + error -> + ""; + LUser -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + ["select password from users " + "where username='", Username, "'"]) of + {selected, ["password"], [{Password}]} -> + Password; + _ -> + "" + end + end. + +is_user_exists(User) -> + case jlib:nodeprep(User) of + error -> + false; + LUser -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + ["select password from users " + "where username='", Username, "'"]) of + {selected, ["password"], [{_Password}]} -> + true; + _ -> + false + end + end. + +remove_user(User) -> + case jlib:nodeprep(User) of + error -> + error; + LUser -> + Username = ejabberd_odbc:escape(LUser), + catch ejabberd_odbc:sql_query( + ["delete from users where username='", Username ,"'"]), + catch mod_roster:remove_user(User), + catch mod_offline:remove_user(User), + catch mod_last:remove_user(User), + catch mod_vcard:remove_user(User), + catch mod_private:remove_user(User) + end. + +remove_user(User, Password) -> + case jlib:nodeprep(User) of + error -> + error; + LUser -> + Username = ejabberd_odbc:escape(LUser), + Pass = ejabberd_odbc:escape(Password), + case catch + ejabberd_odbc:sql_query( + ["begin;" + "select password from users where username='", Username, "';" + "delete from users " + "where username='", Username, "' and password='", Pass, "';" + "commit"]) of + {selected, ["password"], [{Password}]} -> + catch mod_roster:remove_user(User), + catch mod_offline:remove_user(User), + catch mod_last:remove_user(User), + catch mod_vcard:remove_user(User), + catch mod_private:remove_user(User), + ok; + {selected, ["password"], []} -> + not_exists; + _ -> + not_allowed + end + end. diff --git a/src/mod_last_odbc.erl b/src/mod_last_odbc.erl new file mode 100644 index 000000000..4937778db --- /dev/null +++ b/src/mod_last_odbc.erl @@ -0,0 +1,135 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_last_odbc.erl +%%% Author : Alexey Shchepin +%%% Purpose : jabber:iq:last support (JEP-0012) +%%% Created : 24 Oct 2003 by Alexey Shchepin +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +-module(mod_last_odbc). +-author('alexey@sevcom.net'). +-vsn('$Revision$ '). + +-behaviour(gen_mod). + +-export([start/1, + stop/0, + process_local_iq/3, + process_sm_iq/3, + on_presence_update/3, + remove_user/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + + +start(Opts) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + gen_iq_handler:add_iq_handler(ejabberd_local, ?NS_LAST, + ?MODULE, process_local_iq, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, ?NS_LAST, + ?MODULE, process_sm_iq, IQDisc), + ejabberd_hooks:add(unset_presence_hook, + ?MODULE, on_presence_update, 50). + +stop() -> + gen_iq_handler:remove_iq_handler(ejabberd_local, ?NS_LAST), + gen_iq_handler:remove_iq_handler(ejabberd_sm, ?NS_LAST). + +process_local_iq(_From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + Sec = trunc(element(1, erlang:statistics(wall_clock))/1000), + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", ?NS_LAST}, + {"seconds", integer_to_list(Sec)}], + []}]} + end. + + +process_sm_iq(From, To, #iq{type = Type, sub_el = SubEl} = IQ) -> + case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + User = To#jid.luser, + {Subscription, _Groups} = + mod_roster:get_jid_info(User, From), + if + (Subscription == both) or (Subscription == from) -> + case catch mod_privacy:get_user_list(User) of + {'EXIT', _Reason} -> + get_last(IQ, SubEl, User); + List -> + case catch mod_privacy:check_packet( + User, List, + {From, To, + {xmlelement, "presence", [], []}}, + out) of + {'EXIT', _Reason} -> + get_last(IQ, SubEl, User); + allow -> + get_last(IQ, SubEl, User); + deny -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + end + end; + true -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + end + end. + +get_last(IQ, SubEl, LUser) -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + ["select seconds, state from last " + "where username='", Username, "'"]) of + {'EXIT', _Reason} -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + {selected, ["seconds","state"], []} -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; + {selected, ["seconds","state"], [{STimeStamp, Status}]} -> + case catch list_to_integer(STimeStamp) of + TimeStamp when is_integer(TimeStamp) -> + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp2 = MegaSecs * 1000000 + Secs, + Sec = TimeStamp2 - TimeStamp, + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", ?NS_LAST}, + {"seconds", integer_to_list(Sec)}], + [{xmlcdata, Status}]}]}; + _ -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end + end. + + + +on_presence_update(User, _Resource, Status) -> + LUser = jlib:nodeprep(User), + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp = MegaSecs * 1000000 + Secs, + Username = ejabberd_odbc:escape(LUser), + Seconds = ejabberd_odbc:escape(integer_to_list(TimeStamp)), + State = ejabberd_odbc:escape(Status), + ejabberd_odbc:sql_query( + ["begin;" + "delete from last where username='", Username, "';" + "insert into last(username, seconds, state) " + "values ('", Username, "', '", Seconds, "', '", State, "');", + "commit"]). + + +remove_user(User) -> + LUser = jlib:nodeprep(User), + Username = ejabberd_odbc:escape(LUser), + ejabberd_odbc:sql_query( + ["delete from last where username='", Username, "'"]). + diff --git a/src/mod_offline_odbc.erl b/src/mod_offline_odbc.erl new file mode 100644 index 000000000..abc06b471 --- /dev/null +++ b/src/mod_offline_odbc.erl @@ -0,0 +1,242 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_offline_odbc.erl +%%% Author : Alexey Shchepin +%%% Purpose : +%%% Created : 5 Jan 2003 by Alexey Shchepin +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +-module(mod_offline_odbc). +-author('alexey@sevcom.net'). + +-behaviour(gen_mod). + +-export([start/1, + init/0, + stop/0, + store_packet/3, + pop_offline_messages/2, + remove_user/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(offline_msg, {user, timestamp, expire, from, to, packet}). + +-define(PROCNAME, ejabberd_offline). +-define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). + +start(_) -> + % TODO: remove + ejabberd_odbc:start(), + ejabberd_hooks:add(offline_message_hook, + ?MODULE, store_packet, 50), + ejabberd_hooks:add(offline_subscription_hook, + ?MODULE, store_packet, 50), + ejabberd_hooks:add(resend_offline_messages_hook, + ?MODULE, pop_offline_messages, 50), + register(?PROCNAME, spawn(?MODULE, init, [])). + +init() -> + loop(). + +loop() -> + receive + #offline_msg{} = Msg -> + Msgs = receive_all([Msg]), + % TODO + Query = lists:map( + fun(M) -> + Username = + ejabberd_odbc:escape( + (M#offline_msg.to)#jid.luser), + From = M#offline_msg.from, + To = M#offline_msg.to, + {xmlelement, Name, Attrs, Els} = + M#offline_msg.packet, + Attrs2 = jlib:replace_from_to_attrs( + jlib:jid_to_string(From), + jlib:jid_to_string(To), + Attrs), + Packet = {xmlelement, Name, Attrs2, + Els ++ + [jlib:timestamp_to_xml( + calendar:now_to_universal_time( + M#offline_msg.timestamp))]}, + XML = + ejabberd_odbc:escape( + lists:flatten( + xml:element_to_string(Packet))), + ["insert into spool(username, xml) " + "values ('", Username, "', '", + XML, + "');"] + end, Msgs), + case catch ejabberd_odbc:sql_query( + ["begin; ", Query, " commit"]) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~n", [Reason]); + _ -> + ok + end, + loop(); + _ -> + loop() + end. + +receive_all(Msgs) -> + receive + #offline_msg{} = Msg -> + receive_all([Msg | Msgs]) + after 0 -> + Msgs + end. + + +stop() -> + ejabberd_hooks:delete(offline_message_hook, + ?MODULE, store_packet, 50), + ejabberd_hooks:delete(offline_subscription_hook, + ?MODULE, store_packet, 50), + ejabberd_hooks:delete(resend_offline_messages_hook, + ?MODULE, pop_offline_messages, 50), + exit(whereis(?PROCNAME), stop), + ok. + +store_packet(From, To, Packet) -> + Type = xml:get_tag_attr_s("type", Packet), + if + (Type /= "error") and (Type /= "groupchat") -> + case check_event(From, To, Packet) of + true -> + #jid{luser = LUser} = To, + TimeStamp = now(), + {xmlelement, _Name, _Attrs, Els} = Packet, + Expire = find_x_expire(TimeStamp, Els), + ?PROCNAME ! #offline_msg{user = LUser, + timestamp = TimeStamp, + expire = Expire, + from = From, + to = To, + packet = Packet}, + stop; + _ -> + ok + end; + true -> + ok + end. + +check_event(From, To, Packet) -> + {xmlelement, Name, Attrs, Els} = Packet, + case find_x_event(Els) of + false -> + true; + El -> + case xml:get_subtag(El, "id") of + false -> + case xml:get_subtag(El, "offline") of + false -> + true; + _ -> + ID = case xml:get_tag_attr_s("id", Packet) of + "" -> + {xmlelement, "id", [], []}; + S -> + {xmlelement, "id", [], + [{xmlcdata, S}]} + end, + ejabberd_router:route( + To, From, {xmlelement, Name, Attrs, + [{xmlelement, "x", + [{"xmlns", ?NS_EVENT}], + [ID, + {xmlelement, "offline", [], []}]}] + }), + true + end; + _ -> + false + end + end. + +find_x_event([]) -> + false; +find_x_event([{xmlcdata, _} | Els]) -> + find_x_event(Els); +find_x_event([El | Els]) -> + case xml:get_tag_attr_s("xmlns", El) of + ?NS_EVENT -> + El; + _ -> + find_x_event(Els) + end. + +find_x_expire(_, []) -> + never; +find_x_expire(TimeStamp, [{xmlcdata, _} | Els]) -> + find_x_expire(TimeStamp, Els); +find_x_expire(TimeStamp, [El | Els]) -> + case xml:get_tag_attr_s("xmlns", El) of + ?NS_EXPIRE -> + case xml:get_tag_attr_s("seconds", El) of + Val -> + case catch list_to_integer(Val) of + {'EXIT', _} -> + never; + Int when Int > 0 -> + {MegaSecs, Secs, MicroSecs} = TimeStamp, + S = MegaSecs * 1000000 + Secs + Int, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + {MegaSecs1, Secs1, MicroSecs}; + _ -> + never + end; + _ -> + never + end; + _ -> + find_x_expire(TimeStamp, Els) + end. + + +pop_offline_messages(Ls, User) -> + LUser = jlib:nodeprep(User), + EUser = ejabberd_odbc:escape(LUser), + case ejabberd_odbc:sql_query( + ["begin;" + "select * from spool where username='", EUser, "';" + "delete from spool where username='", EUser, "';" + "commit"]) of + {selected, ["username","xml"], Rs} -> + Ls ++ lists:flatmap( + fun({_, XML}) -> + case xml_stream:parse_element(XML) of + {error, _Reason} -> + []; + El -> + To = jlib:string_to_jid( + xml:get_tag_attr_s("to", El)), + From = jlib:string_to_jid( + xml:get_tag_attr_s("from", El)), + if + (To /= error) and + (From /= error) -> + [{route, From, To, El}]; + true -> + [] + end + end + end, Rs); + _ -> + Ls + end. + + +remove_user(User) -> + LUser = jlib:nodeprep(User), + Username = ejabberd_odbc:escape(LUser), + ejabberd_odbc:sql_query( + ["delete from spool where username='", Username, "'"]). + diff --git a/src/odbc/ejabberd_odbc.erl b/src/odbc/ejabberd_odbc.erl new file mode 100644 index 000000000..c496bf2be --- /dev/null +++ b/src/odbc/ejabberd_odbc.erl @@ -0,0 +1,123 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_odbc.erl +%%% Author : Alexey Shchepin +%%% Purpose : Serve ODBC connection +%%% Created : 8 Dec 2004 by Alexey Shchepin +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +-module(ejabberd_odbc). +-author('alexey@sevcom.net'). +-vsn('$Revision$ '). + +-behaviour(gen_server). + +%% External exports +-export([start/0, start_link/0, + sql_query/1, + escape/1]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + code_change/3, + handle_info/2, + terminate/2]). + +-record(state, {odbc_ref}). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start() -> + gen_server:start({local, ejabberd_odbc}, ejabberd_odbc, [], []). + +start_link() -> + gen_server:start_link({local, ejabberd_odbc}, ejabberd_odbc, [], []). + +sql_query(Query) -> + gen_server:call(ejabberd_odbc, {sql_query, Query}, 60000). + +escape(S) -> + [case C of + $\0 -> "\\0"; + $\n -> "\\n"; + $\t -> "\\t"; + $\b -> "\\b"; + $\r -> "\\r"; + $' -> "\\'"; + $" -> "\\\""; + $% -> "\\%"; + $_ -> "\\_"; + $\\ -> "\\\\"; + _ -> C + end || C <- S]. + + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_server +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%---------------------------------------------------------------------- +init([]) -> + {ok, Ref} = odbc:connect("DSN=ejabberd;UID=ejabberd;PWD=ejabberd", + [{scrollable_cursors, off}]), + {ok, #state{odbc_ref = Ref}}. + +%%---------------------------------------------------------------------- +%% Func: handle_call/3 +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- +handle_call({sql_query, Query}, _From, State) -> + Reply = odbc:sql_query(State#state.odbc_ref, Query), + {reply, Reply, State}; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +%%---------------------------------------------------------------------- +%% Func: handle_cast/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- +handle_info(_Info, State) -> + {noreply, State}. + +%%---------------------------------------------------------------------- +%% Func: terminate/2 +%% Purpose: Shutdown the server +%% Returns: any (ignored by gen_server) +%%---------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + diff --git a/src/odbc/pg.sql b/src/odbc/pg.sql new file mode 100644 index 000000000..9e0c154d9 --- /dev/null +++ b/src/odbc/pg.sql @@ -0,0 +1,89 @@ + + +CREATE TABLE users ( + username text NOT NULL, + "password" text NOT NULL +); + + +CREATE TABLE last ( + username text NOT NULL, + seconds text NOT NULL, + state text +); + + +CREATE TABLE rosterusers ( + username text NOT NULL, + jid text NOT NULL, + nick text, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + server character(1) NOT NULL, + subscribe text, + "type" text +); + + + +CREATE TABLE rostergroups ( + username text NOT NULL, + jid text NOT NULL, + grp text NOT NULL +); + + +CREATE TABLE spool ( + username text NOT NULL, + xml text +); + + + +CREATE TABLE vcard ( + username text NOT NULL, + full_name text, + first_name text, + last_name text, + nick_name text, + url text, + address1 text, + address2 text, + locality text, + region text, + pcode text, + country text, + telephone text, + email text, + orgname text, + orgunit text, + title text, + role text, + b_day date, + descr text +); + + + + +CREATE INDEX i_users_login ON users USING btree (username, "password"); + +CREATE INDEX i_rosteru_user_jid ON rosterusers USING btree (username, jid); + +CREATE INDEX i_rosteru_username ON rosterusers USING btree (username); + +CREATE INDEX pk_rosterg_user_jid ON rostergroups USING btree (username, jid); + +CREATE INDEX i_despool ON spool USING btree (username); + +CREATE INDEX i_rosteru_jid ON rosterusers USING btree (jid); + +ALTER TABLE ONLY users + ADD CONSTRAINT users_pkey PRIMARY KEY (username); + +ALTER TABLE ONLY last + ADD CONSTRAINT last_pkey PRIMARY KEY (username); + +ALTER TABLE ONLY vcard + ADD CONSTRAINT vcard_pkey PRIMARY KEY (username); +