1 |
|
-module(mongoose_domain_sql). |
2 |
|
|
3 |
|
-export([start/1]). |
4 |
|
|
5 |
|
-export([insert_domain/2, |
6 |
|
delete_domain/2, |
7 |
|
set_domain_for_deletion/2, |
8 |
|
set_status/2]). |
9 |
|
|
10 |
|
-export([select_domain_admin/1, |
11 |
|
set_domain_admin/2, |
12 |
|
delete_domain_admin/1]). |
13 |
|
|
14 |
|
-export([select_domain/1, |
15 |
|
get_minmax_event_id/0, |
16 |
|
count_events_between_ids/2, |
17 |
|
get_event_ids_between/2, |
18 |
|
select_from/2, |
19 |
|
select_updates_between/2, |
20 |
|
get_enabled_dynamic/0, |
21 |
|
delete_events_older_than/1, |
22 |
|
insert_dummy_event/1]). |
23 |
|
|
24 |
|
%% interfaces only for integration tests |
25 |
|
-export([prepare_test_queries/0, |
26 |
|
erase_database/1, |
27 |
|
insert_full_event/2, |
28 |
|
insert_domain_settings_without_event/2]). |
29 |
|
|
30 |
|
-ignore_xref([erase_database/1, prepare_test_queries/0, get_enabled_dynamic/0, |
31 |
|
insert_full_event/2, insert_domain_settings_without_event/2]). |
32 |
|
|
33 |
|
-import(mongoose_rdbms, [prepare/4, execute_successfully/3]). |
34 |
|
|
35 |
|
-type event_id() :: non_neg_integer(). |
36 |
|
-type domain() :: jid:lserver(). |
37 |
|
-type row() :: {event_id(), domain(), mongooseim:host_type() | null}. |
38 |
|
-export_type([row/0]). |
39 |
|
|
40 |
|
start(_) -> |
41 |
127 |
{LimitSQL, LimitMSSQL} = rdbms_queries:get_db_specific_limits_binaries(), |
42 |
127 |
Enabled = integer_to_binary(status_to_int(enabled)), |
43 |
|
%% Settings |
44 |
127 |
prepare(domain_insert_settings, domain_settings, [domain, host_type], |
45 |
|
<<"INSERT INTO domain_settings (domain, host_type) " |
46 |
|
"VALUES (?, ?)">>), |
47 |
127 |
prepare(domain_update_settings_status, domain_settings, |
48 |
|
[status, domain], |
49 |
|
<<"UPDATE domain_settings " |
50 |
|
"SET status = ? " |
51 |
|
"WHERE domain = ?">>), |
52 |
127 |
prepare(domain_delete_settings, domain_settings, [domain], |
53 |
|
<<"DELETE FROM domain_settings WHERE domain = ?">>), |
54 |
127 |
prepare(domain_select, domain_settings, [domain], |
55 |
|
<<"SELECT host_type, status " |
56 |
|
"FROM domain_settings WHERE domain = ?">>), |
57 |
127 |
prepare(domain_select_from, domain_settings, |
58 |
|
rdbms_queries:add_limit_arg(limit, [id]), |
59 |
|
<<"SELECT ", LimitMSSQL/binary, |
60 |
|
" id, domain, host_type " |
61 |
|
" FROM domain_settings " |
62 |
|
" WHERE id > ? AND status = ", Enabled/binary, " " |
63 |
|
" ORDER BY id ", |
64 |
|
LimitSQL/binary>>), |
65 |
|
%% Events |
66 |
127 |
prepare(domain_insert_event, domain_events, [domain], |
67 |
|
<<"INSERT INTO domain_events (domain) VALUES (?)">>), |
68 |
127 |
prepare(domain_insert_full_event, domain_events, [id, domain], |
69 |
|
<<"INSERT INTO domain_events (id, domain) VALUES (?, ?)">>), |
70 |
127 |
prepare(domain_events_minmax, domain_events, [], |
71 |
|
<<"SELECT MIN(id), MAX(id) FROM domain_events">>), |
72 |
127 |
prepare(domain_count_event_between, domain_events, [id, id], |
73 |
|
<<"SELECT COUNT(*) FROM domain_events WHERE id >= ? AND id <= ?">>), |
74 |
127 |
prepare(domain_event_ids_between, domain_events, [id, id], |
75 |
|
<<"SELECT id FROM domain_events WHERE id >= ? AND id <= ? ORDER BY id">>), |
76 |
127 |
prepare(domain_events_delete_older_than, domain_events, [id], |
77 |
|
<<"DELETE FROM domain_events WHERE id < ?">>), |
78 |
127 |
prepare(domain_select_events_between, domain_events, [id, id], |
79 |
|
<<"SELECT " |
80 |
|
" domain_events.id, domain_events.domain, domain_settings.host_type " |
81 |
|
" FROM domain_events " |
82 |
|
" LEFT JOIN domain_settings ON " |
83 |
|
"(domain_settings.domain = domain_events.domain AND " |
84 |
|
"domain_settings.status = ", Enabled/binary, ") " |
85 |
|
" WHERE domain_events.id >= ? AND domain_events.id <= ? " |
86 |
|
" ORDER BY domain_events.id ">>), |
87 |
|
%% Admins |
88 |
127 |
prepare(domain_insert_admin, domain_admins, [domain, pass_details], |
89 |
|
<<"INSERT INTO domain_admins (domain, pass_details) VALUES (?, ?)">>), |
90 |
127 |
prepare(domain_update_admin, domain_admins, [pass_details, domain], |
91 |
|
<<"UPDATE domain_admins" |
92 |
|
" SET pass_details = ? " |
93 |
|
" WHERE domain = ?">>), |
94 |
127 |
prepare(domain_delete_admin, domain_admins, [domain], |
95 |
|
<<"DELETE FROM domain_admins WHERE domain = ?">>), |
96 |
127 |
prepare(domain_select_admin, domain_admins, [domain], |
97 |
|
<<"SELECT domain, pass_details" |
98 |
|
" FROM domain_admins WHERE domain = ?">>), |
99 |
127 |
ok. |
100 |
|
|
101 |
|
prepare_test_queries() -> |
102 |
12 |
Enabled = integer_to_binary(status_to_int(enabled)), |
103 |
12 |
prepare(domain_erase_admins, domain_admins, [], |
104 |
|
<<"DELETE FROM domain_admins">>), |
105 |
12 |
prepare(domain_erase_settings, domain_settings, [], |
106 |
|
<<"DELETE FROM domain_settings">>), |
107 |
12 |
prepare(domain_erase_events, domain_events, [], |
108 |
|
<<"DELETE FROM domain_events">>), |
109 |
12 |
prepare(domain_get_status_dynamic, domain_settings, [], |
110 |
|
<<"SELECT " |
111 |
|
" domain, host_type " |
112 |
|
" FROM domain_settings " |
113 |
|
" WHERE status = ", Enabled/binary, " " |
114 |
|
" ORDER BY id">>), |
115 |
12 |
prepare(domain_events_get_all, domain_events, [], |
116 |
|
<<"SELECT id, domain FROM domain_events ORDER BY id">>). |
117 |
|
|
118 |
|
%% ---------------------------------------------------------------------------- |
119 |
|
%% API |
120 |
|
insert_domain(Domain, HostType) -> |
121 |
53 |
transaction(fun(Pool) -> |
122 |
53 |
case select_domain(Domain) of |
123 |
|
{error, not_found} -> |
124 |
49 |
insert_domain_settings(Pool, Domain, HostType), |
125 |
49 |
insert_domain_event(Pool, Domain), |
126 |
49 |
ok; |
127 |
|
{ok, _} -> |
128 |
4 |
{error, duplicate} |
129 |
|
end |
130 |
|
end). |
131 |
|
|
132 |
|
select_domain(Domain) -> |
133 |
124 |
Pool = get_db_pool(), |
134 |
124 |
case execute_successfully(Pool, domain_select, [Domain]) of |
135 |
|
{selected, []} -> |
136 |
62 |
{error, not_found}; |
137 |
|
{selected, [Row]} -> |
138 |
62 |
{ok, row_to_map(Row)} |
139 |
|
end. |
140 |
|
|
141 |
|
delete_domain(Domain, HostType) -> |
142 |
11 |
transaction(fun(Pool) -> |
143 |
11 |
case select_domain(Domain) of |
144 |
|
{ok, #{host_type := HT}} when HT =:= HostType -> |
145 |
11 |
{updated, 1} = delete_domain_settings(Pool, Domain), |
146 |
11 |
insert_domain_event(Pool, Domain), |
147 |
11 |
ok; |
148 |
|
{ok, _} -> |
149 |
:-( |
{error, wrong_host_type}; |
150 |
|
{error, not_found} -> |
151 |
:-( |
ok |
152 |
|
end |
153 |
|
end). |
154 |
|
|
155 |
|
set_domain_for_deletion(Domain, HostType) -> |
156 |
18 |
transaction(fun(Pool) -> |
157 |
26 |
case select_domain(Domain) of |
158 |
|
{ok, #{host_type := HT}} when HT =:= HostType -> |
159 |
19 |
{updated, 1} = set_domain_for_deletion_settings(Pool, Domain), |
160 |
19 |
insert_domain_event(Pool, Domain), |
161 |
11 |
ok; |
162 |
|
{ok, _} -> |
163 |
3 |
{error, wrong_host_type}; |
164 |
|
{error, not_found} -> |
165 |
4 |
{error, not_found} |
166 |
|
end |
167 |
|
end). |
168 |
|
|
169 |
|
select_domain_admin(Domain) -> |
170 |
516 |
Pool = get_db_pool(), |
171 |
516 |
case execute_successfully(Pool, domain_select_admin, [Domain]) of |
172 |
|
{selected, []} -> |
173 |
24 |
{error, not_found}; |
174 |
|
{selected, [Row]} -> |
175 |
492 |
{ok, Row} |
176 |
|
end. |
177 |
|
|
178 |
|
set_domain_admin(Domain, Password) -> |
179 |
29 |
Iterations = mongoose_scram:iterations(), |
180 |
29 |
HashMap = mongoose_scram:password_to_scram_sha(Password, Iterations, sha512), |
181 |
29 |
PassDetails = mongoose_scram:serialize(HashMap), |
182 |
29 |
transaction(fun(Pool) -> |
183 |
29 |
case select_domain_admin(Domain) of |
184 |
|
{ok, _} -> |
185 |
21 |
update_domain_admin(Pool, Domain, PassDetails), |
186 |
21 |
ok; |
187 |
|
{error, not_found} -> |
188 |
8 |
insert_domain_admin(Pool, Domain, PassDetails), |
189 |
8 |
ok |
190 |
|
end |
191 |
|
end). |
192 |
|
|
193 |
|
delete_domain_admin(Domain) -> |
194 |
19 |
transaction(fun(Pool) -> |
195 |
19 |
case select_domain_admin(Domain) of |
196 |
|
{ok, _} -> |
197 |
6 |
{updated, 1} = delete_domain_admin(Pool, Domain), |
198 |
6 |
ok; |
199 |
|
{error, not_found} -> |
200 |
13 |
{error, not_found} |
201 |
|
end |
202 |
|
end). |
203 |
|
|
204 |
|
insert_domain_admin(Pool, Domain, PassDetails) -> |
205 |
8 |
execute_successfully(Pool, domain_insert_admin, [Domain, PassDetails]). |
206 |
|
|
207 |
|
update_domain_admin(Pool, Domain, PassDetails) -> |
208 |
21 |
execute_successfully(Pool, domain_update_admin, [PassDetails, Domain]). |
209 |
|
|
210 |
|
delete_domain_admin(Pool, Domain) -> |
211 |
6 |
execute_successfully(Pool, domain_delete_admin, [Domain]). |
212 |
|
|
213 |
|
%% Returns smallest id first |
214 |
|
select_from(FromId, Limit) -> |
215 |
219 |
Pool = get_db_pool(), |
216 |
219 |
Args = rdbms_queries:add_limit_arg(Limit, [FromId]), |
217 |
219 |
{selected, Rows} = execute_successfully(Pool, domain_select_from, Args), |
218 |
219 |
Rows. |
219 |
|
|
220 |
|
get_enabled_dynamic() -> |
221 |
:-( |
Pool = get_db_pool(), |
222 |
:-( |
prepare_test_queries(), |
223 |
:-( |
{selected, Rows} = execute_successfully(Pool, domain_get_status_dynamic, []), |
224 |
:-( |
Rows. |
225 |
|
|
226 |
|
%% FromId, ToId are included into the result |
227 |
|
-spec select_updates_between(event_id(), event_id()) -> [row()]. |
228 |
|
select_updates_between(FromId, ToId) -> |
229 |
187 |
Pool = get_db_pool(), |
230 |
187 |
Args = [FromId, ToId], |
231 |
187 |
{selected, Rows} = execute_successfully(Pool, domain_select_events_between, Args), |
232 |
187 |
Rows. |
233 |
|
|
234 |
|
get_minmax_event_id() -> |
235 |
717 |
Pool = get_db_pool(), |
236 |
717 |
{selected, [{Min, Max}]} = execute_successfully(Pool, domain_events_minmax, []), |
237 |
715 |
case Min of |
238 |
|
null -> |
239 |
96 |
{null, null}; |
240 |
|
_ -> |
241 |
619 |
{mongoose_rdbms:result_to_integer(Min), |
242 |
|
mongoose_rdbms:result_to_integer(Max)} |
243 |
|
end. |
244 |
|
|
245 |
|
count_events_between_ids(Min, Max) -> |
246 |
4 |
Pool = get_db_pool(), |
247 |
4 |
Selected = execute_successfully(Pool, domain_count_event_between, [Min, Max]), |
248 |
4 |
mongoose_rdbms:selected_to_integer(Selected). |
249 |
|
|
250 |
|
%% Min and Max are included in the result set |
251 |
|
get_event_ids_between(Min, Max) -> |
252 |
102 |
Pool = get_db_pool(), |
253 |
102 |
{selected, Rows} = execute_successfully(Pool, domain_event_ids_between, [Min, Max]), |
254 |
102 |
[mongoose_rdbms:result_to_integer(Id) || {Id} <- Rows]. |
255 |
|
|
256 |
|
delete_events_older_than(Id) -> |
257 |
3 |
transaction(fun(Pool) -> |
258 |
3 |
execute_successfully(Pool, domain_events_delete_older_than, [Id]) |
259 |
|
end). |
260 |
|
|
261 |
|
insert_dummy_event(EventId) -> |
262 |
26 |
insert_full_event(EventId, <<>>). |
263 |
|
|
264 |
|
insert_full_event(EventId, Domain) -> |
265 |
35 |
case mongoose_rdbms:db_type() of |
266 |
|
mssql -> |
267 |
:-( |
insert_full_event_mssql(EventId, Domain); |
268 |
|
_ -> |
269 |
35 |
Pool = get_db_pool(), |
270 |
35 |
execute_successfully(Pool, domain_insert_full_event, [EventId, Domain]) |
271 |
|
end. |
272 |
|
|
273 |
|
insert_full_event_mssql(EventId, Domain) -> |
274 |
|
%% MSSQL does not allow to specify ids, |
275 |
|
%% that are supposed to be autoincremented, easily |
276 |
|
%% https://docs.microsoft.com/pl-pl/sql/t-sql/statements/set-identity-insert-transact-sql |
277 |
:-( |
transaction(fun(Pool) -> |
278 |
|
%% This query could not be a prepared query |
279 |
|
%% You will get an error: |
280 |
|
%% "No SQL-driver information available." |
281 |
|
%% when trying to execute |
282 |
:-( |
mongoose_rdbms:sql_query(Pool, <<"SET IDENTITY_INSERT domain_events ON">>), |
283 |
:-( |
try |
284 |
:-( |
execute_successfully(Pool, domain_insert_full_event, [EventId, Domain]) |
285 |
|
after |
286 |
:-( |
mongoose_rdbms:sql_query(Pool, <<"SET IDENTITY_INSERT domain_events OFF">>) |
287 |
|
end |
288 |
|
end). |
289 |
|
|
290 |
|
%% ---------------------------------------------------------------------------- |
291 |
|
%% For testing |
292 |
|
|
293 |
|
erase_database(Pool) -> |
294 |
10 |
execute_successfully(Pool, domain_erase_events, []), |
295 |
10 |
execute_successfully(Pool, domain_erase_settings, []), |
296 |
10 |
execute_successfully(Pool, domain_erase_admins, []). |
297 |
|
|
298 |
|
insert_domain_settings_without_event(Domain, HostType) -> |
299 |
:-( |
Pool = get_db_pool(), |
300 |
:-( |
execute_successfully(Pool, domain_insert_settings, [Domain, HostType]). |
301 |
|
|
302 |
|
%% ---------------------------------------------------------------------------- |
303 |
|
|
304 |
|
%% Inserts a new event with an autoincremented EventId. |
305 |
|
%% Rows would not appear in the EventId order, RDBMS likes to rearrange them. |
306 |
|
%% Example of rearranging: |
307 |
|
%% Events with ids [1, 2, 3] could appear as: |
308 |
|
%% 1. [1] |
309 |
|
%% 2. [1, 3] - at this step record with EventId=2 is not visible yet. |
310 |
|
%% 3. [1, 2, 3] - and finally everything is fine. |
311 |
|
insert_domain_event(Pool, Domain) -> |
312 |
90 |
execute_successfully(Pool, domain_insert_event, [Domain]). |
313 |
|
|
314 |
|
insert_domain_settings(Pool, Domain, HostType) -> |
315 |
49 |
execute_successfully(Pool, domain_insert_settings, [Domain, HostType]). |
316 |
|
|
317 |
|
delete_domain_settings(Pool, Domain) -> |
318 |
11 |
execute_successfully(Pool, domain_delete_settings, [Domain]). |
319 |
|
|
320 |
|
set_domain_for_deletion_settings(Pool, Domain) -> |
321 |
19 |
ExtStatus = status_to_int(deleting), |
322 |
19 |
execute_successfully(Pool, domain_update_settings_status, [ExtStatus, Domain]). |
323 |
|
|
324 |
|
-spec set_status(domain(), enabled | disabled) -> ok | {error, term()}. |
325 |
|
set_status(Domain, Status) -> |
326 |
17 |
transaction(fun(Pool) -> |
327 |
17 |
case select_domain(Domain) of |
328 |
|
{error, Reason} -> |
329 |
4 |
{error, Reason}; |
330 |
|
{ok, #{status := CurrentStatus, host_type := HostType}} -> |
331 |
13 |
case mongoose_domain_core:is_host_type_allowed(HostType) of |
332 |
|
false -> |
333 |
2 |
{error, unknown_host_type}; |
334 |
|
true when deleting =:= CurrentStatus -> |
335 |
:-( |
{error, domain_deleted}; |
336 |
|
true when Status =:= CurrentStatus -> |
337 |
:-( |
ok; |
338 |
|
true -> |
339 |
11 |
update_domain_enabled(Pool, Domain, Status), |
340 |
11 |
insert_domain_event(Pool, Domain), |
341 |
11 |
ok |
342 |
|
end |
343 |
|
end |
344 |
|
end). |
345 |
|
|
346 |
|
update_domain_enabled(Pool, Domain, Status) -> |
347 |
11 |
ExtStatus = status_to_int(Status), |
348 |
11 |
execute_successfully(Pool, domain_update_settings_status, [ExtStatus, Domain]). |
349 |
|
|
350 |
|
row_to_map({HostType, Status}) -> |
351 |
62 |
IntStatus = mongoose_rdbms:result_to_integer(Status), |
352 |
62 |
#{host_type => HostType, status => int_to_status(IntStatus)}. |
353 |
|
|
354 |
|
-spec int_to_status(0..2) -> mongoose_domain_api:status(). |
355 |
8 |
int_to_status(0) -> disabled; |
356 |
43 |
int_to_status(1) -> enabled; |
357 |
11 |
int_to_status(2) -> deleting. |
358 |
|
|
359 |
|
-spec status_to_int(mongoose_domain_api:status()) -> 0..2. |
360 |
7 |
status_to_int(disabled) -> 0; |
361 |
143 |
status_to_int(enabled) -> 1; |
362 |
19 |
status_to_int(deleting) -> 2. |
363 |
|
|
364 |
|
get_db_pool() -> |
365 |
2054 |
mongoose_config:get_opt([services, service_domain_db, db_pool]). |
366 |
|
|
367 |
|
transaction(F) -> |
368 |
150 |
transaction(F, 3, []). |
369 |
|
|
370 |
|
%% MSSQL especially likes to kill a connection deadlocked by tablock connections. |
371 |
|
%% But that's fine, we could just restart |
372 |
|
%% (there is no logic, that would suffer by a restart of a transaction). |
373 |
|
transaction(_F, 0, Errors) -> |
374 |
:-( |
{error, {db_error, Errors}}; |
375 |
|
transaction(F, Retries, Errors) when Retries > 0 -> |
376 |
150 |
Pool = get_db_pool(), |
377 |
150 |
Result = rdbms_queries:sql_transaction(Pool, fun() -> F(Pool) end), |
378 |
150 |
case Result of |
379 |
|
{aborted, _} -> %% Restart any rolled back transaction |
380 |
:-( |
timer:sleep(100), %% Small break before retry |
381 |
:-( |
transaction(F, Retries - 1, [Result|Errors]); |
382 |
|
_ -> |
383 |
150 |
simple_result(Result) |
384 |
|
end. |
385 |
|
|
386 |
150 |
simple_result({atomic, Result}) -> Result; |
387 |
:-( |
simple_result(Other) -> {error, {db_error, Other}}. |