2010-07-22 18:43:38 +02:00
|
|
|
-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,
|
2010-07-22 18:47:36 +02:00
|
|
|
async_dirty/2, sync_dirty/2,
|
2010-07-22 18:43:38 +02:00
|
|
|
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)
|
2010-09-28 16:05:33 +02:00
|
|
|
types :: [{string(), atom() | tuple()}],
|
2010-07-22 18:43:38 +02:00
|
|
|
host :: string()
|
|
|
|
}).
|
|
|
|
-record(odbc_cont, {tabdef, sql, offset = 0, limit}).
|
|
|
|
|
2010-07-22 18:47:36 +02:00
|
|
|
-include("ejabberd.hrl"). % for ?DEBUG macro
|
|
|
|
-include_lib("exmpp/include/exmpp.hrl"). % for #jid{} and #xmlel{}
|
2010-07-22 18:43:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
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,
|
2010-07-22 18:47:36 +02:00
|
|
|
%% The "packet" column in the table offline_msg,
|
|
|
|
%% must be "text" in order to be large enough to
|
|
|
|
%% contain a full stanza, not limited to a small VARCHAR():
|
|
|
|
NoTextKeys = IsKey andalso ejabberd_odbc:db_type(Host) =:= mysql
|
|
|
|
andalso Attribute /= "packet",
|
2010-07-22 18:43:38 +02:00
|
|
|
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
|
2010-07-22 18:47:36 +02:00
|
|
|
%% This 105 limits the size of fields in the primary key.
|
|
|
|
%% That prevents MySQL from complaining when setting the
|
|
|
|
%% last_activity key (text, text) with this error:
|
|
|
|
%% #42000Specified key was too long; max key length is 1000bytes"
|
|
|
|
%% Similarly for rosteritem and other tables, maybe also PgSQL.
|
|
|
|
set -> [", PRIMARY KEY (", string:join(K, "(105), "), "(105))"];
|
2010-07-22 18:43:38 +02:00
|
|
|
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 ",
|
2010-07-22 18:47:36 +02:00
|
|
|
TabS, " (", string:join(KeyColumns, "(75), "), "(75))"],
|
2010-07-22 18:43:38 +02:00
|
|
|
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";
|
2010-07-22 18:47:36 +02:00
|
|
|
type_to_sql_type(xmlel) -> "TEXT";
|
2010-07-22 18:43:38 +02:00
|
|
|
type_to_sql_type(jid) -> "TEXT";
|
|
|
|
type_to_sql_type(ljid) -> "TEXT";
|
|
|
|
type_to_sql_type(atom) -> "TEXT";
|
2010-07-22 18:47:36 +02:00
|
|
|
type_to_sql_type(binary) -> "TEXT";
|
2010-07-22 18:43:38 +02:00
|
|
|
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,
|
2010-07-22 18:47:36 +02:00
|
|
|
" ON ", TabS, " (", string:join(A, "(75), "), "(75))"],
|
2010-07-22 18:43:38 +02:00
|
|
|
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) ->
|
2010-07-22 18:47:36 +02:00
|
|
|
make_pattern(S, []).
|
|
|
|
make_pattern([], R) ->
|
|
|
|
lists:reverse(R);
|
2010-07-22 18:43:38 +02:00
|
|
|
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;
|
2010-07-22 18:47:36 +02:00
|
|
|
binary ->
|
|
|
|
Row2 = Row,
|
|
|
|
R = list_to_binary(Field);
|
2010-07-22 18:43:38 +02:00
|
|
|
pid ->
|
|
|
|
Row2 = Row,
|
|
|
|
R = list_to_pid(Field);
|
2010-07-22 18:47:36 +02:00
|
|
|
xmlel ->
|
|
|
|
Row2 = Row,
|
|
|
|
[R] = exmpp_xml:parse_document(Field, [names_as_atom]);
|
2010-07-22 18:43:38 +02:00
|
|
|
jid ->
|
|
|
|
Row2 = Row,
|
2010-07-22 18:47:36 +02:00
|
|
|
R = exmpp_jid:parse(Field);
|
2010-07-22 18:43:38 +02:00
|
|
|
ljid ->
|
|
|
|
Row2 = Row,
|
2010-07-22 18:47:36 +02:00
|
|
|
R = jlib:short_prepd_jid(exmpp_jid:parse(Field));
|
2010-07-22 18:43:38 +02:00
|
|
|
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],
|
2010-07-22 18:47:36 +02:00
|
|
|
[{Count}] = odbc_query(Host, Q),
|
2010-07-22 18:43:38 +02:00
|
|
|
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],
|
2010-07-22 18:47:36 +02:00
|
|
|
[{Count}] = odbc_query_t(Q),
|
2010-07-22 18:43:38 +02:00
|
|
|
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) ->
|
2010-07-22 18:47:36 +02:00
|
|
|
?DEBUG("Type for ~p: ~p = ~p~n",[Attribute, Type, Value]),
|
2010-07-22 18:43:38 +02:00
|
|
|
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).
|
|
|
|
|
2010-07-22 18:47:36 +02:00
|
|
|
%% Mnesia has sync_dirty, maybe ODBC has something similar:
|
|
|
|
%% "Call the Fun in a context which is not protected by a transaction."
|
|
|
|
%% "The difference [with async_dirty] is that the operations are performed
|
|
|
|
%% synchronously. The caller waits for the updates to be performed on all
|
|
|
|
%% active replicas before the Fun returns."
|
|
|
|
sync_dirty(Tab, Fun) ->
|
|
|
|
transaction(Tab, Fun).
|
|
|
|
|
2010-07-22 18:43:38 +02:00
|
|
|
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));
|
|
|
|
|
2010-07-22 18:47:36 +02:00
|
|
|
format({N, D, R}) when (R==undefined) or (not is_atom(R)) ->
|
|
|
|
format(exmpp_jid:to_list(N, D, R));
|
|
|
|
|
|
|
|
format(Xmlel) when is_record(Xmlel, xmlel) ->
|
|
|
|
format(exmpp_xml:document_to_list(Xmlel));
|
2010-07-22 18:43:38 +02:00
|
|
|
|
|
|
|
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).
|