diff --git a/include/ejabberd_sql.hrl b/include/ejabberd_sql.hrl index 1734b1cb195..6fe86f7599c 100644 --- a/include/ejabberd_sql.hrl +++ b/include/ejabberd_sql.hrl @@ -50,3 +50,20 @@ boolean :: fun((boolean()) -> binary()), in_array_string :: fun((binary()) -> binary()), like_escape :: fun(() -> binary())}). + + +-record(sql_index, {columns, + unique = false :: boolean()}). +-record(sql_column, {name :: binary(), + type, + default = false, + opts = []}). +-record(sql_table, {name :: binary(), + columns :: [#sql_column{}], + indices = [] :: [#sql_index{}], + post_create}). +-record(sql_schema, {version :: integer(), + tables :: [#sql_table{}], + update = []}). +-record(sql_references, {table :: binary(), + column :: binary()}). diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index 1871d3a71e6..a1cda8cab37 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -45,7 +45,31 @@ %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -start(_Host) -> ok. +start(Host) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), + ok. + +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"password">>, type = text}, + #sql_column{name = <<"serverkey">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"salt">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"iterationcount">>, type = integer, + default = true}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. stop(_Host) -> ok. diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl index ecb1625c7bf..da95815751d 100644 --- a/src/ejabberd_oauth_sql.erl +++ b/src/ejabberd_oauth_sql.erl @@ -41,8 +41,35 @@ -include("logger.hrl"). init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"oauth_token">>, + columns = + [#sql_column{name = <<"token">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"scope">>, type = text}, + #sql_column{name = <<"expire">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"token">>], + unique = true}]}, + #sql_table{ + name = <<"oauth_client">>, + columns = + [#sql_column{name = <<"client_id">>, type = text}, + #sql_column{name = <<"client_name">>, type = text}, + #sql_column{name = <<"grant_type">>, type = text}, + #sql_column{name = <<"options">>, type = text}], + indices = [#sql_index{ + columns = [<<"client_id">>], + unique = true}]}]}]. + store(R) -> Token = R#oauth_token.token, {User, Server} = R#oauth_token.us, diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl index 173e412af04..cc706d14e91 100644 --- a/src/ejabberd_option.erl +++ b/src/ejabberd_option.erl @@ -162,6 +162,7 @@ -export([sql_type/0, sql_type/1]). -export([sql_username/0, sql_username/1]). -export([trusted_proxies/0]). +-export([update_sql_schema/0]). -export([use_cache/0, use_cache/1]). -export([validate_stream/0]). -export([version/0]). @@ -1096,6 +1097,10 @@ sql_username(Host) -> trusted_proxies() -> ejabberd_config:get_option({trusted_proxies, global}). +-spec update_sql_schema() -> boolean(). +update_sql_schema() -> + ejabberd_config:get_option({update_sql_schema, global}). + -spec use_cache() -> boolean(). use_cache() -> use_cache(global). diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl index 95b3d92c3f3..3a65daaae73 100644 --- a/src/ejabberd_options.erl +++ b/src/ejabberd_options.erl @@ -260,6 +260,8 @@ opt_type(net_ticktime) -> econf:timeout(second); opt_type(new_sql_schema) -> econf:bool(); +opt_type(update_sql_schema) -> + econf:bool(); opt_type(oauth_access) -> econf:acl(); opt_type(oauth_cache_life_time) -> @@ -607,6 +609,7 @@ options() -> {negotiation_timeout, timer:seconds(30)}, {net_ticktime, timer:seconds(60)}, {new_sql_schema, ?USE_NEW_SQL_SCHEMA_DEFAULT}, + {update_sql_schema, true}, {oauth_access, none}, {oauth_cache_life_time, fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, @@ -756,6 +759,7 @@ globals() -> negotiation_timeout, net_ticktime, new_sql_schema, + update_sql_schema, node_start, oauth_cache_life_time, oauth_cache_missed, diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl index a4938528b0c..fb06ab510c1 100644 --- a/src/ejabberd_options_doc.erl +++ b/src/ejabberd_options_doc.erl @@ -911,6 +911,11 @@ doc() -> "configuration flag '--enable-new-sql-schema' which is set " "at compile time."), [binary:part(ejabberd_config:version(), {0,5})]}}}, + {update_sql_schema, + #{value => "true | false", + desc => + ?T("Allow ejabberd to update SQL schema. " + "The default value is 'true'.")}}, {oauth_access, #{value => ?T("AccessName"), desc => ?T("By default creating OAuth tokens is not allowed. " diff --git a/src/ejabberd_router_sql.erl b/src/ejabberd_router_sql.erl index 0647ddefe38..d2e3693171c 100644 --- a/src/ejabberd_router_sql.erl +++ b/src/ejabberd_router_sql.erl @@ -37,6 +37,8 @@ %%% API %%%=================================================================== init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, schemas()), Node = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'route' table...", []), case ejabberd_sql:sql_query( @@ -48,6 +50,23 @@ init() -> Err end. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"route">>, + columns = + [#sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}, + #sql_column{name = <<"local_hint">>, type = text}], + indices = [#sql_index{ + columns = [<<"domain">>, <<"server_host">>, + <<"node">>, <<"pid">>], + unique = true}]}]}]. + register_route(Domain, ServerHost, LocalHint, _, Pid) -> PidS = misc:encode_pid(Pid), LocalHintS = enc_local_hint(LocalHint), diff --git a/src/ejabberd_sm_sql.erl b/src/ejabberd_sm_sql.erl index d4fd72a451d..ff06d3ddac7 100644 --- a/src/ejabberd_sm_sql.erl +++ b/src/ejabberd_sm_sql.erl @@ -48,6 +48,7 @@ init() -> ?DEBUG("Cleaning SQL SM table...", []), lists:foldl( fun(Host, ok) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), case ejabberd_sql:sql_query( Host, ?SQL("delete from sm where node=%(Node)s")) of {updated, _} -> @@ -60,6 +61,29 @@ init() -> Err end, ok, ejabberd_sm:get_vh_by_backend(?MODULE)). +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"sm">>, + columns = + [#sql_column{name = <<"usec">>, type = bigint}, + #sql_column{name = <<"pid">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"priority">>, type = text}, + #sql_column{name = <<"info">>, type = text}], + indices = [#sql_index{ + columns = [<<"usec">>, <<"pid">>], + unique = true}, + #sql_index{ + columns = [<<"node">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>]}]}]}]. + set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, priority = Priority, info = Info}) -> InfoS = misc:term_to_expr(Info), diff --git a/src/ejabberd_sql_schema.erl b/src/ejabberd_sql_schema.erl new file mode 100644 index 00000000000..172bc41e46a --- /dev/null +++ b/src/ejabberd_sql_schema.erl @@ -0,0 +1,936 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_sql.erl +%%% Author : Alexey Shchepin +%%% Purpose : SQL schema versioning +%%% Created : 15 Aug 2023 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2023 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., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(ejabberd_sql_schema). + +-author('alexey@process-one.net'). + +-export([start/1, update_schema/3, + get_table_schema/2, get_table_indices/2, test/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +start(Host) -> + case should_update_schema(Host) of + true -> + case table_exists(Host, <<"schema_version">>) of + true -> + ok; + false -> + Table = filter_table_sh(schema_table()), + Res = create_table(Host, Table), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to create table ~s: ~p", + [Table#sql_table.name, Error]), + {error, Error}; + _ -> + ok + end + end; + false -> + ok + end. + +schema_table() -> + #sql_table{ + name = <<"schema_version">>, + columns = [#sql_column{name = <<"module">>, type = text}, + #sql_column{name = <<"version">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"module">>], + unique = true}]}. + +get_table_schema(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(pgsql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select " + " @(a.attname)s, " + " @(pg_catalog.format_type(a.atttypid, a.atttypmod))s " + " from " + " pg_class t, " + " pg_attribute a " + " where " + " a.attrelid = t.oid and " + " a.attnum > 0 and " + " a.atttypid > 0 and " + " t.relkind = 'r' and " + " t.relname=%(Table)s")) + of + {selected, Cols} -> + [{Col, string_to_type(SType)} || {Col, SType} <- Cols] + end; + (sqlite, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(i.name)s, @(i.type)s" + " from pragma_table_info(%(Table)s) as i")) + of + {selected, Cols} -> + [{Col, string_to_type(SType)} || {Col, SType} <- Cols] + end; + (mysql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(column_name)s, @(column_type)s" + " from information_schema.columns" + " where table_name=%(Table)s and" + " table_schema=schema()" + " order by ordinal_position")) + of + {selected, Cols} -> + [{Col, string_to_type(SType)} || {Col, SType} <- Cols] + end + end). + +get_table_indices(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(pgsql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select " + " @(i.relname)s, " + " @(a.attname)s " + " from " + " pg_class t, " + " pg_class i, " + " pg_index ix, " + " pg_attribute a " + " where " + " t.oid = ix.indrelid and " + " i.oid = ix.indexrelid and " + " a.attrelid = t.oid and " + " a.attnum = ANY(ix.indkey) and " + " t.relkind = 'r' and " + " t.relname=%(Table)s " + " order by " + " i.relname, " + " array_position(ix.indkey, a.attnum)")) + of + {selected, Cols} -> + Indices = + lists:foldr( + fun({IdxName, ColName}, Acc) -> + maps:update_with( + IdxName, + fun(Cs) -> [ColName | Cs] end, + [ColName], + Acc) + end, #{}, Cols), + maps:to_list(Indices) + end; + (sqlite, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(i.name)s, @(c.name)s " + " from pragma_index_list(%(Table)s) as i," + " pragma_index_xinfo(i.name) as c" + " where c.cid >= 0" + " order by i.name, c.seqno")) + of + {selected, Cols} -> + Indices = + lists:foldr( + fun({IdxName, ColName}, Acc) -> + maps:update_with( + IdxName, + fun(Cs) -> [ColName | Cs] end, + [ColName], + Acc) + end, #{}, Cols), + maps:to_list(Indices) + end; + (mysql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(index_name)s, @(column_name)s" + " from information_schema.statistics" + " where table_name=%(Table)s and" + " table_schema=schema()" + " order by index_name, seq_in_index")) + of + {selected, Cols} -> + Indices = + lists:foldr( + fun({IdxName, ColName}, Acc) -> + maps:update_with( + IdxName, + fun(Cs) -> [ColName | Cs] end, + [ColName], + Acc) + end, #{}, Cols), + maps:to_list(Indices) + end + end). + +find_index_name(Host, Table, Columns) -> + Indices = get_table_indices(Host, Table), + case lists:keyfind(Columns, 2, Indices) of + false -> + false; + {Name, _} -> + {ok, Name} + end. + +get_version(Host, Module) -> + SModule = misc:atom_to_binary(Module), + ejabberd_sql:sql_query( + Host, + ?SQL("select @(version)d" + " from schema_version" + " where module=%(SModule)s")). + +store_version(Host, Module, Version) -> + SModule = misc:atom_to_binary(Module), + ?SQL_UPSERT( + Host, + "schema_version", + ["!module=%(SModule)s", + "version=%(Version)d"]). + +table_exists(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(pgsql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @()b exists (select * from pg_tables " + " where tablename=%(Table)s)")) + of + {selected, [{Res}]} -> + Res + end; + (sqlite, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @()b exists" + " (select 0 from pragma_table_info(%(Table)s))")) + of + {selected, [{Res}]} -> + Res + end; + (mysql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @()b exists" + " (select 0 from information_schema.tables" + " where table_name=%(Table)s and" + " table_schema=schema())")) + of + {selected, [{Res}]} -> + Res + end + end). + +filter_table_sh(Table) -> + case {ejabberd_sql:use_new_schema(), Table#sql_table.name} of + {true, _} -> + Table; + {_, <<"route">>} -> + Table; + {false, _} -> + Table#sql_table{ + columns = + lists:keydelete( + <<"server_host">>, #sql_column.name, Table#sql_table.columns), + indices = + lists:map( + fun(Idx) -> + Idx#sql_index{ + columns = + lists:delete( + <<"server_host">>, Idx#sql_index.columns) + } + end, Table#sql_table.indices) + } + end. + +string_to_type(SType) -> + case string:lowercase(SType) of + <<"text">> -> text; + <<"mediumtext">> -> text; + <<"bigint">> -> bigint; + <<"bigint ", _/binary>> -> bigint; + <<"bigint(", _/binary>> -> bigint; + <<"integer">> -> integer; + <<"int">> -> integer; + <<"int(", _/binary>> -> integer; + <<"smallint">> -> smallint; + <<"smallint(", _/binary>> -> smallint; + <<"numeric">> -> numeric; + <<"decimal", _/binary>> -> numeric; + <<"bigserial">> -> bigserial; + <<"boolean">> -> boolean; + <<"tinyint(1)">> -> boolean; + <<"bytea">> -> blob; + <<"blob">> -> blob; + <<"timestamp", _/binary>> -> timestamp; + <<"character(", R/binary>> -> + {ok, [N], []} = io_lib:fread("~d)", binary_to_list(R)), + {char, N}; + <<"char(", R/binary>> -> + {ok, [N], []} = io_lib:fread("~d)", binary_to_list(R)), + {char, N}; + <<"varchar(", _/binary>> -> text; + <<"character varying(", _/binary>> -> text; + T -> + ?ERROR_MSG("Unknown SQL type '~s'", [T]), + {undefined, T} + end. + +check_columns_compatibility(RequiredColumns, Columns) -> + lists:all( + fun(#sql_column{name = Name, type = Type}) -> + %io:format("col ~p~n", [{Name, Type}]), + case lists:keyfind(Name, 1, Columns) of + false -> + false; + {_, Type2} -> + %io:format("tt ~p~n", [{Type, Type2}]), + case {Type, Type2} of + {T, T} -> true; + {text, blob} -> true; + {{text, _}, blob} -> true; + {{text, _}, text} -> true; + {{text, _}, {varchar, _}} -> true; + {text, {varchar, _}} -> true; + {{char, _}, text} -> true; + {{varchar, _}, text} -> true; + {smallint, integer} -> true; + {smallint, bigint} -> true; + {smallint, numeric} -> true; + {integer, bigint} -> true; + {integer, numeric} -> true; + {bigint, numeric} -> true; + {bigserial, integer} -> true; + {bigserial, bigint} -> true; + {bigserial, numeric} -> true; + _ -> false + end + end + end, RequiredColumns). + +guess_version(Host, Schemas) -> + LastSchema = lists:max(Schemas), + SomeTablesExist = + lists:any( + fun(Table) -> + table_exists(Host, Table#sql_table.name) + end, LastSchema#sql_schema.tables), + if + SomeTablesExist -> + CompatibleSchemas = + lists:filter( + fun(Schema) -> + lists:all( + fun(Table) -> + Table2 = filter_table_sh(Table), + CurrentColumns = + get_table_schema( + Host, Table2#sql_table.name), + check_columns_compatibility( + Table2#sql_table.columns, + CurrentColumns) + end, Schema#sql_schema.tables) + end, Schemas), + case CompatibleSchemas of + [] -> -1; + _ -> + (lists:max(CompatibleSchemas))#sql_schema.version + end; + true -> + 0 + end. + +get_current_version(Host, Module, Schemas) -> + case get_version(Host, Module) of + {selected, [{Version}]} -> + Version; + {selected, []} -> + Version = guess_version(Host, Schemas), + if + Version > 0 -> + store_version(Host, Module, Version); + true -> + ok + end, + Version + end. + +format_type(pgsql, _DBVersion, Column) -> + case Column#sql_column.type of + text -> <<"text">>; + {text, _} -> <<"text">>; + bigint -> <<"bigint">>; + integer -> <<"integer">>; + smallint -> <<"smallint">>; + numeric -> <<"numeric">>; + boolean -> <<"boolean">>; + blob -> <<"bytea">>; + timestamp -> <<"timestamp">>; + {char, N} -> [<<"character(">>, integer_to_binary(N), <<")">>]; + bigserial -> <<"bigserial">> + end; +format_type(sqlite, _DBVersion, Column) -> + case Column#sql_column.type of + text -> <<"text">>; + {text, _} -> <<"text">>; + bigint -> <<"bigint">>; + integer -> <<"integer">>; + smallint -> <<"smallint">>; + numeric -> <<"numeric">>; + boolean -> <<"boolean">>; + blob -> <<"blob">>; + timestamp -> <<"timestamp">>; + {char, N} -> [<<"character(">>, integer_to_binary(N), <<")">>]; + bigserial -> <<"integer primary key autoincrement">> + end; +format_type(mysql, _DBVersion, Column) -> + case Column#sql_column.type of + text -> <<"text">>; + {text, big} -> <<"mediumtext">>; + {text, N} when is_integer(N), N < 191 -> + [<<"varchar(">>, integer_to_binary(N), <<")">>]; + {text, _} -> <<"text">>; + bigint -> <<"bigint">>; + integer -> <<"integer">>; + smallint -> <<"smallint">>; + numeric -> <<"numeric">>; + boolean -> <<"boolean">>; + blob -> <<"blob">>; + timestamp -> <<"timestamp">>; + {char, N} -> [<<"character(">>, integer_to_binary(N), <<")">>]; + bigserial -> <<"bigint auto_increment primary key">> + end. + +format_default(pgsql, _DBVersion, Column) -> + case Column#sql_column.type of + text -> <<"''">>; + {text, _} -> <<"''">>; + bigint -> <<"0">>; + integer -> <<"0">>; + smallint -> <<"0">>; + numeric -> <<"0">>; + boolean -> <<"false">>; + blob -> <<"''">>; + timestamp -> <<"now()">> + %{char, N} -> <<"''">>; + %bigserial -> <<"0">> + end; +format_default(sqlite, _DBVersion, Column) -> + case Column#sql_column.type of + text -> <<"''">>; + {text, _} -> <<"''">>; + bigint -> <<"0">>; + integer -> <<"0">>; + smallint -> <<"0">>; + numeric -> <<"0">>; + boolean -> <<"false">>; + blob -> <<"''">>; + timestamp -> <<"CURRENT_TIMESTAMP">> + %{char, N} -> <<"''">>; + %bigserial -> <<"0">> + end; +format_default(mysql, _DBVersion, Column) -> + case Column#sql_column.type of + text -> <<"('')">>; + {text, _} -> <<"('')">>; + bigint -> <<"0">>; + integer -> <<"0">>; + smallint -> <<"0">>; + numeric -> <<"0">>; + boolean -> <<"false">>; + blob -> <<"('')">>; + timestamp -> <<"CURRENT_TIMESTAMP">> + %{char, N} -> <<"''">>; + %bigserial -> <<"0">> + end. + +escape_name(pgsql, _DBVersion, <<"type">>) -> + <<"\"type\"">>; +escape_name(_DBType, _DBVersion, ColumnName) -> + ColumnName. + +format_column_def(DBType, DBVersion, Column) -> + [<<" ">>, + escape_name(DBType, DBVersion, Column#sql_column.name), <<" ">>, + format_type(DBType, DBVersion, Column), + <<" NOT NULL">>, + case Column#sql_column.default of + false -> []; + true -> + [<<" DEFAULT ">>, format_default(DBType, DBVersion, Column)] + end, + case lists:keyfind(sql_references, 1, Column#sql_column.opts) of + false -> []; + #sql_references{table = T, column = C} -> + [<<" REFERENCES ">>, T, <<"(">>, C, <<") ON DELETE CASCADE">>] + end]. + +format_mysql_index_column(Table, ColumnName) -> + {value, Column} = + lists:keysearch( + ColumnName, #sql_column.name, Table#sql_table.columns), + NeedsSizeLimit = + case Column#sql_column.type of + {text, N} when is_integer(N), N < 191 -> false; + {text, _} -> true; + text -> true; + _ -> false + end, + if + NeedsSizeLimit -> + [ColumnName, <<"(191)">>]; + true -> + ColumnName + end. + +format_create_index(pgsql, _DBVersion, Table, Index) -> + TableName = Table#sql_table.name, + Unique = + case Index#sql_index.unique of + true -> <<"UNIQUE ">>; + false -> <<"">> + end, + Name = [<<"i_">>, TableName, <<"_">>, + lists:join( + <<"_">>, + Index#sql_index.columns)], + [<<"CREATE ">>, Unique, <<"INDEX ">>, Name, <<" ON ">>, TableName, + <<" USING btree (">>, + lists:join( + <<", ">>, + Index#sql_index.columns), + <<");">>]; +format_create_index(sqlite, _DBVersion, Table, Index) -> + TableName = Table#sql_table.name, + Unique = + case Index#sql_index.unique of + true -> <<"UNIQUE ">>; + false -> <<"">> + end, + Name = [<<"i_">>, TableName, <<"_">>, + lists:join( + <<"_">>, + Index#sql_index.columns)], + [<<"CREATE ">>, Unique, <<"INDEX ">>, Name, <<" ON ">>, TableName, + <<"(">>, + lists:join( + <<", ">>, + Index#sql_index.columns), + <<");">>]; +format_create_index(mysql, _DBVersion, Table, Index) -> + TableName = Table#sql_table.name, + Unique = + case Index#sql_index.unique of + true -> <<"UNIQUE ">>; + false -> <<"">> + end, + Name = [<<"i_">>, TableName, <<"_">>, + lists:join( + <<"_">>, + Index#sql_index.columns)], + [<<"CREATE ">>, Unique, <<"INDEX ">>, Name, + <<" USING BTREE ON ">>, TableName, + <<"(">>, + lists:join( + <<", ">>, + lists:map( + fun(Col) -> + format_mysql_index_column(Table, Col) + end, Index#sql_index.columns)), + <<");">>]. + +format_create_table(pgsql = DBType, DBVersion, Table) -> + TableName = Table#sql_table.name, + [iolist_to_binary( + [<<"CREATE TABLE ">>, TableName, <<" (\n">>, + lists:join( + <<",\n">>, + lists:map( + fun(C) -> format_column_def(DBType, DBVersion, C) end, + Table#sql_table.columns)), + <<"\n);\n">>])] ++ + lists:map( + fun(I) -> + iolist_to_binary( + [format_create_index(DBType, DBVersion, Table, I), + <<"\n">>]) + end, + Table#sql_table.indices); +format_create_table(sqlite = DBType, DBVersion, Table) -> + TableName = Table#sql_table.name, + [iolist_to_binary( + [<<"CREATE TABLE ">>, TableName, <<" (\n">>, + lists:join( + <<",\n">>, + lists:map( + fun(C) -> format_column_def(DBType, DBVersion, C) end, + Table#sql_table.columns)), + <<"\n);\n">>])] ++ + lists:map( + fun(I) -> + iolist_to_binary( + [format_create_index(DBType, DBVersion, Table, I), + <<"\n">>]) + end, + Table#sql_table.indices); +format_create_table(mysql = DBType, DBVersion, Table) -> + TableName = Table#sql_table.name, + [iolist_to_binary( + [<<"CREATE TABLE ">>, TableName, <<" (\n">>, + lists:join( + <<",\n">>, + lists:map( + fun(C) -> format_column_def(DBType, DBVersion, C) end, + Table#sql_table.columns)), + <<"\n) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n">>])] ++ + lists:map( + fun(I) -> + iolist_to_binary( + [format_create_index(DBType, DBVersion, Table, I), + <<"\n">>]) + end, + Table#sql_table.indices). +%format_create_table(DBType, _DBVersion, Table) -> +% ?ERROR_MSG("Can't create SQL table ~p on ~p", +% [Table#sql_table.name, DBType]), +% error. + +create_table(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(DBType, DBVersion) -> + SQLs = format_create_table(DBType, DBVersion, Table), + ?INFO_MSG("Creating table ~s:~n~s~n", + [Table#sql_table.name, SQLs]), + lists:foreach( + fun(SQL) -> ejabberd_sql:sql_query_t(SQL) end, SQLs), + case Table#sql_table.post_create of + undefined -> + ok; + F -> + F(DBType, DBVersion) + end + end). + +create_tables(Host, Module, Schema) -> + lists:foreach( + fun(Table) -> + Table2 = filter_table_sh(Table), + Res = create_table(Host, Table2), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to create table ~s: ~p", + [Table2#sql_table.name, Error]), + error(Error); + _ -> + ok + end + end, Schema#sql_schema.tables), + store_version(Host, Module, Schema#sql_schema.version). + +should_update_schema(Host) -> + SupportedDB = + case ejabberd_option:sql_type(Host) of + pgsql -> true; + sqlite -> true; + mysql -> true; + _ -> false + end, + case ejabberd_option:update_sql_schema() andalso SupportedDB of + true -> + case ejabberd_sql:use_new_schema() of + true -> + Host == ejabberd_config:get_myname(); + false -> + true + end; + false -> + false + end. + +update_schema(Host, Module, Schemas) -> + case should_update_schema(Host) of + true -> + Version = get_current_version(Host, Module, Schemas), + LastSchema = lists:max(Schemas), + LastVersion = LastSchema#sql_schema.version, + case Version of + _ when Version < 0 -> + ?ERROR_MSG("Can't update SQL schema for module ~p, please do it manually", [Module]); + 0 -> + create_tables(Host, Module, LastSchema); + LastVersion -> + ok; + _ when LastVersion < Version -> + ?ERROR_MSG("The current SQL schema for module ~p is ~p, but the latest known schema in the module is ~p", [Module, Version, LastVersion]); + _ -> + lists:foreach( + fun(Schema) -> + if + Schema#sql_schema.version > Version -> + do_update_schema(Host, Module, Schema); + true -> + ok + end + end, lists:sort(Schemas)) + end; + false -> + ok + end. + +do_update_schema(Host, Module, Schema) -> + lists:foreach( + fun({add_column, TableName, ColumnName}) -> + {value, Table} = + lists:keysearch( + TableName, #sql_table.name, Schema#sql_schema.tables), + {value, Column} = + lists:keysearch( + ColumnName, #sql_column.name, Table#sql_table.columns), + Res = + ejabberd_sql:sql_query( + Host, + fun(DBType, DBVersion) -> + Def = format_column_def(DBType, DBVersion, Column), + Default = format_default(DBType, DBVersion, Column), + SQLs = + [[<<"ALTER TABLE ">>, + TableName, + <<" ADD COLUMN\n">>, + Def, + <<" DEFAULT ">>, + Default, <<";\n">>]] ++ + case Column#sql_column.default of + false -> + [[<<"ALTER TABLE ">>, + TableName, + <<" ALTER COLUMN ">>, + ColumnName, + <<" DROP DEFAULT;">>]]; + _ -> + [] + end, + ?INFO_MSG("Add column ~s/~s:~n~s~n", + [TableName, + ColumnName, + SQLs]), + lists:foreach( + fun(SQL) -> ejabberd_sql:sql_query_t(SQL) end, + SQLs) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end; + ({drop_column, TableName, ColumnName}) -> + Res = + ejabberd_sql:sql_query( + Host, + fun(_DBType, _DBVersion) -> + SQL = [<<"ALTER TABLE ">>, + TableName, + <<" DROP COLUMN ">>, + ColumnName, + <<";">>], + ?INFO_MSG("Drop column ~s/~s:~n~s~n", + [TableName, + ColumnName, + SQL]), + ejabberd_sql:sql_query_t(SQL) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end; + ({create_index, TableName, Columns}) -> + {value, Table1} = + lists:keysearch( + TableName, #sql_table.name, Schema#sql_schema.tables), + {value, Index1} = + lists:keysearch( + Columns, #sql_index.columns, Table1#sql_table.indices), + Table = filter_table_sh(Table1), + Index = + case ejabberd_sql:use_new_schema() of + true -> + Index1; + false -> + Index1#sql_index{ + columns = + lists:delete( + <<"server_host">>, Index1#sql_index.columns) + } + end, + Res = + ejabberd_sql:sql_query( + Host, + fun(DBType, DBVersion) -> + SQL1 = format_create_index( + DBType, DBVersion, Table, Index), + SQL = iolist_to_binary(SQL1), + ?INFO_MSG("Create index ~s/~p:~n~s~n", + [Table#sql_table.name, + Index#sql_index.columns, + SQL]), + ejabberd_sql:sql_query_t(SQL) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end; + ({drop_index, TableName, Columns1}) -> + Columns = + case ejabberd_sql:use_new_schema() of + true -> + Columns1; + false -> + lists:delete( + <<"server_host">>, Columns1) + end, + case find_index_name(Host, TableName, Columns) of + false -> + ?ERROR_MSG("Can't find an index to drop for ~s/~p", + [TableName, Columns]); + {ok, IndexName} -> + Res = + ejabberd_sql:sql_query( + Host, + fun(DBType, _DBVersion) -> + SQL = + case DBType of + mysql -> + [<<"DROP INDEX ">>, + IndexName, + <<" ON ">>, + TableName, + <<";">>]; + _ -> + [<<"DROP INDEX ">>, + IndexName, <<";">>] + end, + ?INFO_MSG("Drop index ~s/~p:~n~s~n", + [TableName, + Columns, + SQL]), + ejabberd_sql:sql_query_t(SQL) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end + end + end, Schema#sql_schema.update), + store_version(Host, Module, Schema#sql_schema.version). + +test() -> + Schemas = + [#sql_schema{ + version = 2, + tables = + [#sql_table{ + name = <<"archive2">>, + columns = [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"origin_id">>, type = text}, + #sql_column{name = <<"type">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"origin_id">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>]} + ]}], + update = + [{add_column, <<"archive2">>, <<"origin_id">>}, + {create_index, <<"archive2">>, + [<<"server_host">>, <<"origin_id">>]}, + {drop_index, <<"archive2">>, + [<<"server_host">>, <<"origin_id">>]}, + {drop_column, <<"archive2">>, <<"origin_id">>} + ]}, + #sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"archive2">>, + columns = [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = {text, 10}}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>]} + ]}]}], + update_schema(<<"localhost">>, mod_foo, Schemas). diff --git a/src/ejabberd_sql_sup.erl b/src/ejabberd_sql_sup.erl index 54bc6ca8713..7320174e618 100644 --- a/src/ejabberd_sql_sup.erl +++ b/src/ejabberd_sql_sup.erl @@ -51,7 +51,9 @@ start(Host) -> type => supervisor, modules => [?MODULE]}, case supervisor:start_child(ejabberd_db_sup, Spec) of - {ok, _} -> ok; + {ok, _} -> + ejabberd_sql_schema:start(Host), + ok; {error, {already_started, Pid}} -> %% Wait for the supervisor to fully start _ = supervisor:count_children(Pid), diff --git a/src/mod_announce_sql.erl b/src/mod_announce_sql.erl index 20ec78295ea..f0d949fa19f 100644 --- a/src/mod_announce_sql.erl +++ b/src/mod_announce_sql.erl @@ -40,9 +40,26 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"motd">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"xml">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + set_motd_users(LServer, USRs) -> F = fun() -> lists:foreach( diff --git a/src/mod_bosh_sql.erl b/src/mod_bosh_sql.erl index 2b849a364ee..321f37db32b 100644 --- a/src/mod_bosh_sql.erl +++ b/src/mod_bosh_sql.erl @@ -37,6 +37,8 @@ %%% API %%%=================================================================== init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, schemas()), Node = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'bosh' table...", []), case ejabberd_sql:sql_query( @@ -48,6 +50,20 @@ init() -> Err end. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"bosh">>, + columns = + [#sql_column{name = <<"sid">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}], + indices = [#sql_index{ + columns = [<<"sid">>], + unique = true}]}]}]. + open_session(SID, Pid) -> PidS = misc:encode_pid(Pid), Node = erlang:atom_to_binary(node(Pid), latin1), diff --git a/src/mod_caps_sql.erl b/src/mod_caps_sql.erl index 5a545e6e54a..f6ea562366a 100644 --- a/src/mod_caps_sql.erl +++ b/src/mod_caps_sql.erl @@ -37,9 +37,25 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"caps_features">>, + columns = + [#sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"subnode">>, type = text}, + #sql_column{name = <<"feature">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"node">>, <<"subnode">>]}]}]}]. + caps_read(LServer, {Node, SubNode}) -> case ejabberd_sql:sql_query( LServer, diff --git a/src/mod_last_sql.erl b/src/mod_last_sql.erl index 98e65ee938d..3d305af66f3 100644 --- a/src/mod_last_sql.erl +++ b/src/mod_last_sql.erl @@ -38,9 +38,25 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"last">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"seconds">>, type = text}, + #sql_column{name = <<"state">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + get_last(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl index b21e84a7b8f..4898944eca7 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -43,9 +43,59 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"archive">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = {text, 10}}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>]} + ], + post_create = + fun(mysql, _) -> + ejabberd_sql:sql_query_t( + <<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>); + (_, _) -> + ok + end}, + #sql_table{ + name = <<"archive_prefs">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"def">>, type = text}, + #sql_column{name = <<"always">>, type = text}, + #sql_column{name = <<"never">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, diff --git a/src/mod_mix_pam_sql.erl b/src/mod_mix_pam_sql.erl index 7fc26b9e673..606d306ce15 100644 --- a/src/mod_mix_pam_sql.erl +++ b/src/mod_mix_pam_sql.erl @@ -33,10 +33,29 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> - %% TODO +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"mix_pam">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"id">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"username">>, <<"server_host">>, + <<"channel">>, <<"service">>], + unique = true}]}]}]. + add_channel(User, Channel, ID) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), diff --git a/src/mod_mix_sql.erl b/src/mod_mix_sql.erl index 753fd9fd007..74f341fb804 100644 --- a/src/mod_mix_sql.erl +++ b/src/mod_mix_sql.erl @@ -34,10 +34,65 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> - %% TODO +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"mix_channel">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"hidden">>, type = boolean}, + #sql_column{name = <<"hmac_key">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>], + unique = true}, + #sql_index{ + columns = [<<"service">>]}]}, + #sql_table{ + name = <<"mix_participant">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"id">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>, + <<"username">>, <<"domain">>], + unique = true}]}, + #sql_table{ + name = <<"mix_subscription">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = {text, 75}}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = {text, 75}}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"jid">>, type = text}], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>, + <<"username">>, <<"domain">>, + <<"node">>], + unique = true}, + #sql_index{ + columns = [<<"channel">>, <<"service">>, + <<"node">>]}]}]}]. + set_channel(LServer, Channel, Service, CreatorJID, Hidden, Key) -> {User, Domain, _} = jid:tolower(CreatorJID), RawJID = jid:encode(jid:remove_resource(CreatorJID)), diff --git a/src/mod_mqtt_sql.erl b/src/mod_mqtt_sql.erl index 8db044355cb..a75554e1bf5 100644 --- a/src/mod_mqtt_sql.erl +++ b/src/mod_mqtt_sql.erl @@ -36,9 +36,33 @@ init() -> ?ERROR_MSG("Backend 'sql' is only supported for db_type", []), {error, db_failure}. -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"mqtt_pub">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"topic">>, type = text}, + #sql_column{name = <<"qos">>, type = smallint}, + #sql_column{name = <<"payload">>, type = blob}, + #sql_column{name = <<"payload_format">>, type = smallint}, + #sql_column{name = <<"content_type">>, type = text}, + #sql_column{name = <<"response_topic">>, type = text}, + #sql_column{name = <<"correlation_data">>, type = blob}, + #sql_column{name = <<"user_property">>, type = blob}, + #sql_column{name = <<"expiry">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"topic">>, <<"server_host">>], + unique = true}]}]}]. + publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> PayloadFormat = encode_pfi(maps:get(payload_format_indicator, Props, binary)), ResponseTopic = maps:get(response_topic, Props, <<"">>), diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl index 51a42c32a40..e7eec9c0f62 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -52,6 +52,7 @@ %%% API %%%=================================================================== init(Host, Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), case gen_mod:ram_db_mod(Opts, mod_muc) of ?MODULE -> clean_tables(Host); @@ -59,6 +60,82 @@ init(Host, Opts) -> ok end. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"muc_room">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"opts">>, type = {text, big}}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"name">>, <<"host">>], + unique = true}, + #sql_index{ + columns = [<<"host">>, <<"created_at">>]}]}, + #sql_table{ + name = <<"muc_registered">>, + columns = + [#sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"jid">>, <<"host">>], + unique = true}, + #sql_index{ + columns = [<<"nick">>]}]}, + #sql_table{ + name = <<"muc_online_room">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}], + indices = [#sql_index{ + columns = [<<"name">>, <<"host">>], + unique = true}]}, + #sql_table{ + name = <<"muc_online_users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server">>, type = {text, 75}}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = {text, 75}}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}], + indices = [#sql_index{ + columns = [<<"username">>, <<"server">>, + <<"resource">>, <<"name">>, + <<"host">>], + unique = true}]}, + #sql_table{ + name = <<"muc_room_subscribers">>, + columns = + [#sql_column{name = <<"room">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"nodes">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"host">>, <<"room">>, <<"jid">>], + unique = true}, + #sql_index{ + columns = [<<"host">>, <<"jid">>]}, + #sql_index{ + columns = [<<"jid">>]}]}]}]. + store_room(LServer, Host, Name, Opts, ChangesHints) -> {Subs, Opts2} = case lists:keytake(subscribers, 1, Opts) of {value, {subscribers, S}, OptN} -> {S, OptN}; diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index a3b9d054533..933da38daab 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -40,9 +40,28 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"spool">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"seq">>, type = bigserial}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>]}, + #sql_index{ + columns = [<<"created_at">>]}]}]}]. + store_message(#offline_msg{us = {LUser, LServer}} = M) -> From = M#offline_msg.from, To = M#offline_msg.to, diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl index ade3bf1ad7a..eb53b72edde 100644 --- a/src/mod_privacy_sql.erl +++ b/src/mod_privacy_sql.erl @@ -42,9 +42,57 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"privacy_default_list">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"name">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}, + #sql_table{ + name = <<"privacy_list">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"id">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"name">>], + unique = true}]}, + #sql_table{ + name = <<"privacy_list_data">>, + columns = + [#sql_column{name = <<"id">>, type = bigint, + opts = [#sql_references{ + table = <<"privacy_list">>, + column = <<"id">>}]}, + #sql_column{name = <<"t">>, type = {char, 1}}, + #sql_column{name = <<"value">>, type = text}, + #sql_column{name = <<"action">>, type = {char, 1}}, + #sql_column{name = <<"ord">>, type = numeric}, + #sql_column{name = <<"match_all">>, type = boolean}, + #sql_column{name = <<"match_iq">>, type = boolean}, + #sql_column{name = <<"match_message">>, type = boolean}, + #sql_column{name = <<"match_presence_in">>, type = boolean}, + #sql_column{name = <<"match_presence_out">>, type = boolean}], + indices = [#sql_index{columns = [<<"id">>]}]}]}]. + unset_default(LUser, LServer) -> case unset_default_privacy_list(LUser, LServer) of ok -> diff --git a/src/mod_private_sql.erl b/src/mod_private_sql.erl index a1bb38c822b..1b9584afed7 100644 --- a/src/mod_private_sql.erl +++ b/src/mod_private_sql.erl @@ -37,9 +37,28 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"private_storage">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"namespace">>, type = text}, + #sql_column{name = <<"data">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"namespace">>], + unique = true}]}]}]. + set_data(LUser, LServer, Data) -> F = fun() -> lists:foreach( diff --git a/src/mod_proxy65_sql.erl b/src/mod_proxy65_sql.erl index 1b47e517d8d..ffe9c3557dd 100644 --- a/src/mod_proxy65_sql.erl +++ b/src/mod_proxy65_sql.erl @@ -34,6 +34,8 @@ %%% API %%%=================================================================== init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, schemas()), NodeS = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'proxy65' table...", []), case ejabberd_sql:sql_query( @@ -47,6 +49,25 @@ init() -> Err end. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"proxy65">>, + columns = + [#sql_column{name = <<"sid">>, type = text}, + #sql_column{name = <<"pid_t">>, type = text}, + #sql_column{name = <<"pid_i">>, type = text}, + #sql_column{name = <<"node_t">>, type = text}, + #sql_column{name = <<"node_i">>, type = text}, + #sql_column{name = <<"jid_i">>, type = text}], + indices = [#sql_index{ + columns = [<<"sid">>], + unique = true}, + #sql_index{ + columns = [<<"jid_i">>]}]}]}]. + register_stream(SID, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), diff --git a/src/mod_pubsub_sql.erl b/src/mod_pubsub_sql.erl index 0a59d093fe2..2bc14b97208 100644 --- a/src/mod_pubsub_sql.erl +++ b/src/mod_pubsub_sql.erl @@ -21,12 +21,96 @@ %% API -export([init/3]). +-include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _ServerHost, _Opts) -> +init(_Host, ServerHost, _Opts) -> + ejabberd_sql_schema:update_schema(ServerHost, ?MODULE, schemas()), ok. %%%=================================================================== %%% Internal functions %%%=================================================================== +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"pubsub_node">>, + columns = + [#sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"parent">>, type = text, + default = true}, + #sql_column{name = <<"plugin">>, type = text}, + #sql_column{name = <<"nodeid">>, type = bigserial}], + indices = [#sql_index{ + columns = [<<"nodeid">>], + unique = true}, + #sql_index{ + columns = [<<"parent">>]}, + #sql_index{ + columns = [<<"host">>, <<"node">>], + unique = true}]}, + #sql_table{ + name = <<"pubsub_node_option">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"val">>, type = text}], + indices = [#sql_index{columns = [<<"nodeid">>]}]}, + #sql_table{ + name = <<"pubsub_node_owner">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"owner">>, type = text}], + indices = [#sql_index{columns = [<<"nodeid">>]}]}, + #sql_table{ + name = <<"pubsub_state">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"affiliation">>, type = {char, 1}}, + #sql_column{name = <<"subscriptions">>, type = text, + default = true}, + #sql_column{name = <<"stateid">>, type = bigserial}], + indices = [#sql_index{columns = [<<"stateid">>], + unique = true}, + #sql_index{columns = [<<"jid">>]}, + #sql_index{columns = [<<"nodeid">>, <<"jid">>], + unique = true}]}, + #sql_table{ + name = <<"pubsub_item">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"itemid">>, type = text}, + #sql_column{name = <<"publisher">>, type = text}, + #sql_column{name = <<"creation">>, type = {text, 32}}, + #sql_column{name = <<"modification">>, type = {text, 32}}, + #sql_column{name = <<"payload">>, type = {text, big}, + default = true}], + indices = [#sql_index{columns = [<<"nodeid">>, <<"itemid">>], + unique = true}, + #sql_index{columns = [<<"itemid">>]}]}, + #sql_table{ + name = <<"pubsub_subscription_opt">>, + columns = + [#sql_column{name = <<"subid">>, type = text}, + #sql_column{name = <<"opt_name">>, type = {text, 32}}, + #sql_column{name = <<"opt_value">>, type = text}], + indices = [#sql_index{columns = [<<"subid">>, <<"opt_name">>], + unique = true}]}]}]. diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl index 7a433a57d06..eb4f498e55d 100644 --- a/src/mod_push_sql.erl +++ b/src/mod_push_sql.erl @@ -39,9 +39,32 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"push_session">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"xml">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"timestamp">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"service">>, <<"node">>], + unique = true}]}]}]. + store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> XML = encode_xdata(XData), TS = misc:now_to_usec(NowTS), diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index 67d9697f774..12caba6451e 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -43,9 +43,55 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"rosterusers">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"subscription">>, type = {char, 1}}, + #sql_column{name = <<"ask">>, type = {char, 1}}, + #sql_column{name = <<"askmessage">>, type = text}, + #sql_column{name = <<"server">>, type = {char, 1}}, + #sql_column{name = <<"subscribe">>, type = text}, + #sql_column{name = <<"type">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"jid">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"jid">>]}]}, + #sql_table{ + name = <<"rostergroups">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"grp">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"jid">>]}]}, + #sql_table{ + name = <<"roster_version">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"version">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + read_roster_version(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, diff --git a/src/mod_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl index d921e5474a3..485b76b137a 100644 --- a/src/mod_shared_roster_sql.erl +++ b/src/mod_shared_roster_sql.erl @@ -43,9 +43,40 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"sr_group">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"opts">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"name">>], + unique = true}]}, + #sql_table{ + name = <<"sr_user">>, + columns = + [#sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"grp">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"jid">>, <<"grp">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"grp">>]}]}]}]. + list_groups(Host) -> case ejabberd_sql:sql_query( Host, diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl index 7d1c00e11f1..b663a4b00ac 100644 --- a/src/mod_vcard_sql.erl +++ b/src/mod_vcard_sql.erl @@ -41,9 +41,79 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, schemas()), ok. +schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"vcard">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"vcard">>, type = {text, big}}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}, + #sql_table{ + name = <<"vcard_search">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"lusername">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"fn">>, type = text}, + #sql_column{name = <<"lfn">>, type = text}, + #sql_column{name = <<"family">>, type = text}, + #sql_column{name = <<"lfamily">>, type = text}, + #sql_column{name = <<"given">>, type = text}, + #sql_column{name = <<"lgiven">>, type = text}, + #sql_column{name = <<"middle">>, type = text}, + #sql_column{name = <<"lmiddle">>, type = text}, + #sql_column{name = <<"nickname">>, type = text}, + #sql_column{name = <<"lnickname">>, type = text}, + #sql_column{name = <<"bday">>, type = text}, + #sql_column{name = <<"lbday">>, type = text}, + #sql_column{name = <<"ctry">>, type = text}, + #sql_column{name = <<"lctry">>, type = text}, + #sql_column{name = <<"locality">>, type = text}, + #sql_column{name = <<"llocality">>, type = text}, + #sql_column{name = <<"email">>, type = text}, + #sql_column{name = <<"lemail">>, type = text}, + #sql_column{name = <<"orgname">>, type = text}, + #sql_column{name = <<"lorgname">>, type = text}, + #sql_column{name = <<"orgunit">>, type = text}, + #sql_column{name = <<"lorgunit">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"lusername">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"lfn">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lfamily">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lgiven">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lmiddle">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lnickname">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lbday">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lctry">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"llocality">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lemail">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lorgname">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lorgunit">>]}]}]}]. + stop(_Host) -> ok.