|
|
@ -3,18 +3,43 @@
|
|
|
|
-behaviour(gen_server).
|
|
|
|
-behaviour(gen_server).
|
|
|
|
|
|
|
|
|
|
|
|
-export([start_link/1, init/1, handle_cast/2, handle_call/3]).
|
|
|
|
-export([start_link/1, init/1, handle_cast/2, handle_call/3]).
|
|
|
|
|
|
|
|
-export([get_tick/0]).
|
|
|
|
|
|
|
|
|
|
|
|
-record(state, {database_file = none, database = none}).
|
|
|
|
-record(state, {database_file = none, database = none, tick_start = none}).
|
|
|
|
|
|
|
|
|
|
|
|
start_link(DatabaseFile) ->
|
|
|
|
start_link(DatabaseFile) ->
|
|
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [DatabaseFile], []).
|
|
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [DatabaseFile], []).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open_db(DatabaseFile) ->
|
|
|
|
|
|
|
|
{ok, DB} = sqlite3:open(event_db, [{file, DatabaseFile}]),
|
|
|
|
|
|
|
|
ok = sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"),
|
|
|
|
|
|
|
|
HT = case sqlite3:sql_exec(DB, "SELECT tick FROM match_states;") of
|
|
|
|
|
|
|
|
[{columns, C1}, {rows, R1}] ->
|
|
|
|
|
|
|
|
if length(R1) == 0 -> 0;
|
|
|
|
|
|
|
|
true ->
|
|
|
|
|
|
|
|
T1 = get_column("tick", C1, R1),
|
|
|
|
|
|
|
|
lists:max(T1)
|
|
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
_ -> 0
|
|
|
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
HFT = case sqlite3:sql_exec(DB, "SELECT tick FROM finals_states;") of
|
|
|
|
|
|
|
|
[{columns, C2}, {rows, R2}] ->
|
|
|
|
|
|
|
|
if length(R2) == 0 -> 0;
|
|
|
|
|
|
|
|
true ->
|
|
|
|
|
|
|
|
T2 = get_column("tick", C2, R2),
|
|
|
|
|
|
|
|
lists:max(T2)
|
|
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
_ -> 0
|
|
|
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
HighestTick = lists:max([HT, HFT]),
|
|
|
|
|
|
|
|
TickStart = erlang:monotonic_time(millisecond) - HighestTick,
|
|
|
|
|
|
|
|
{TickStart, DB}.
|
|
|
|
|
|
|
|
|
|
|
|
init([DatabaseFile]) ->
|
|
|
|
init([DatabaseFile]) ->
|
|
|
|
Exists = filelib:is_regular(DatabaseFile),
|
|
|
|
Exists = filelib:is_regular(DatabaseFile),
|
|
|
|
if Exists =:= true ->
|
|
|
|
if Exists =:= true ->
|
|
|
|
{ok, DB} = sqlite3:open(schedule_db, [{file, DatabaseFile}]),
|
|
|
|
{TickStart, DB} = open_db(DatabaseFile),
|
|
|
|
ok = sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"),
|
|
|
|
{ok, #state{database_file = DatabaseFile, database = DB, tick_start = TickStart}};
|
|
|
|
{ok, #state{database_file = DatabaseFile, database = DB}};
|
|
|
|
|
|
|
|
true ->
|
|
|
|
true ->
|
|
|
|
io:format("Failed to load database ~s~n", [DatabaseFile]),
|
|
|
|
io:format("Failed to load database ~s~n", [DatabaseFile]),
|
|
|
|
{ok, #state{database_file = none, database = none}}
|
|
|
|
{ok, #state{database_file = none, database = none}}
|
|
|
@ -53,6 +78,8 @@ init_db(Database) ->
|
|
|
|
{type, text, [not_null]},
|
|
|
|
{type, text, [not_null]},
|
|
|
|
{number, integer, [not_null]},
|
|
|
|
{number, integer, [not_null]},
|
|
|
|
{instance, integer, [not_null]},
|
|
|
|
{instance, integer, [not_null]},
|
|
|
|
|
|
|
|
{red, integer, [not_null]},
|
|
|
|
|
|
|
|
{blue, integer, [not_null]},
|
|
|
|
{score, blob, [not_null]}],
|
|
|
|
{score, blob, [not_null]}],
|
|
|
|
[{primary_key, [division, type, number, instance]},
|
|
|
|
[{primary_key, [division, type, number, instance]},
|
|
|
|
{foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]),
|
|
|
|
{foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]),
|
|
|
@ -60,9 +87,9 @@ init_db(Database) ->
|
|
|
|
ok = sqlite3:create_table(Database, match_states, [{division, integer, [not_null]},
|
|
|
|
ok = sqlite3:create_table(Database, match_states, [{division, integer, [not_null]},
|
|
|
|
{type, text, [not_null]},
|
|
|
|
{type, text, [not_null]},
|
|
|
|
{number, integer, [not_null]},
|
|
|
|
{number, integer, [not_null]},
|
|
|
|
{time, integer, [not_null]},
|
|
|
|
{tick, integer, [not_null]},
|
|
|
|
{state, blob, [not_null]}],
|
|
|
|
{state, text, [not_null]}],
|
|
|
|
[{primary_key, [division, type, number, time]},
|
|
|
|
[{primary_key, [division, type, number, tick]},
|
|
|
|
{foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]),
|
|
|
|
{foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]),
|
|
|
|
|
|
|
|
|
|
|
|
ok = sqlite3:create_table(Database, finals, [{number, integer, [not_null]},
|
|
|
|
ok = sqlite3:create_table(Database, finals, [{number, integer, [not_null]},
|
|
|
@ -79,14 +106,16 @@ init_db(Database) ->
|
|
|
|
|
|
|
|
|
|
|
|
ok = sqlite3:create_table(Database, finals_scores, [{number, integer, [not_null]},
|
|
|
|
ok = sqlite3:create_table(Database, finals_scores, [{number, integer, [not_null]},
|
|
|
|
{instance, integer, [not_null]},
|
|
|
|
{instance, integer, [not_null]},
|
|
|
|
|
|
|
|
{red, integer, [not_null]},
|
|
|
|
|
|
|
|
{blue, integer, [not_null]},
|
|
|
|
{score, blob, [not_null]}],
|
|
|
|
{score, blob, [not_null]}],
|
|
|
|
[{primary_key, [number, instance]},
|
|
|
|
[{primary_key, [number, instance]},
|
|
|
|
{foreign_key, {[number], finals, [number], ""}}]),
|
|
|
|
{foreign_key, {[number], finals, [number], ""}}]),
|
|
|
|
|
|
|
|
|
|
|
|
ok = sqlite3:create_table(Database, finals_states, [{number, integer, [not_null]},
|
|
|
|
ok = sqlite3:create_table(Database, finals_states, [{number, integer, [not_null]},
|
|
|
|
{time, integer, [not_null]},
|
|
|
|
{tick, integer, [not_null]},
|
|
|
|
{state, blob, [not_null]}],
|
|
|
|
{state, blob, [not_null]}],
|
|
|
|
[{primary_key, [number, time]},
|
|
|
|
[{primary_key, [number, tick]},
|
|
|
|
{foreign_key, {[number], finals, [number], ""}}]),
|
|
|
|
{foreign_key, {[number], finals, [number], ""}}]),
|
|
|
|
|
|
|
|
|
|
|
|
ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]},
|
|
|
|
ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]},
|
|
|
@ -147,6 +176,8 @@ get_column(Name, Columns, Rows) ->
|
|
|
|
Index = index_of(Name, Columns),
|
|
|
|
Index = index_of(Name, Columns),
|
|
|
|
[element(Index, Row) || Row <- Rows].
|
|
|
|
[element(Index, Row) || Row <- Rows].
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handle_call(get_tick_start, _, State) ->
|
|
|
|
|
|
|
|
{reply, State#state.tick_start, State};
|
|
|
|
handle_call({new_db, DatabaseFile}, _, State) ->
|
|
|
|
handle_call({new_db, DatabaseFile}, _, State) ->
|
|
|
|
ok = if State#state.database =:= none -> ok;
|
|
|
|
ok = if State#state.database =:= none -> ok;
|
|
|
|
true -> sqlite3:close(State#state.database)
|
|
|
|
true -> sqlite3:close(State#state.database)
|
|
|
@ -155,21 +186,19 @@ handle_call({new_db, DatabaseFile}, _, State) ->
|
|
|
|
ok = if Exists -> file:delete(DatabaseFile);
|
|
|
|
ok = if Exists -> file:delete(DatabaseFile);
|
|
|
|
true -> ok
|
|
|
|
true -> ok
|
|
|
|
end,
|
|
|
|
end,
|
|
|
|
{ok, Database} = sqlite3:open(schedule_db, [{file, DatabaseFile}]),
|
|
|
|
{TickStart, Database} = open_db(DatabaseFile),
|
|
|
|
ok = sqlite3:sql_exec(Database, "PRAGMA foreign_keys = ON;"),
|
|
|
|
{reply, init_db(Database), State#state{database_file = DatabaseFile, database = Database, tick_start = TickStart}};
|
|
|
|
{reply, init_db(Database), State#state{database_file = DatabaseFile, database = Database}};
|
|
|
|
|
|
|
|
handle_call({load_db, DatabaseFile}, _, State) ->
|
|
|
|
handle_call({load_db, DatabaseFile}, _, State) ->
|
|
|
|
true = filelib:is_regular(DatabaseFile),
|
|
|
|
true = filelib:is_regular(DatabaseFile),
|
|
|
|
{ok, Database} = if DatabaseFile == State#state.database_file ->
|
|
|
|
{TickStart, Database} = if DatabaseFile == State#state.database_file ->
|
|
|
|
{ok, State#state.database};
|
|
|
|
{State#state.tick_start, State#state.database};
|
|
|
|
true ->
|
|
|
|
true ->
|
|
|
|
ok = if State#state.database =:= none -> ok;
|
|
|
|
ok = if State#state.database =:= none -> ok;
|
|
|
|
true -> sqlite3:close(State#state.database)
|
|
|
|
true -> sqlite3:close(State#state.database)
|
|
|
|
end,
|
|
|
|
end,
|
|
|
|
{ok, DB} = sqlite3:open(schedule_db, [{file, DatabaseFile}]),
|
|
|
|
open_db(DatabaseFile)
|
|
|
|
{sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), DB}
|
|
|
|
|
|
|
|
end,
|
|
|
|
end,
|
|
|
|
{reply, ok, State#state{database_file = DatabaseFile, database = Database}};
|
|
|
|
{reply, ok, State#state{database_file = DatabaseFile, database = Database, tick_start = TickStart}};
|
|
|
|
handle_call(close_db, _, State) ->
|
|
|
|
handle_call(close_db, _, State) ->
|
|
|
|
if State#state.database =:= none -> {reply, ok, State};
|
|
|
|
if State#state.database =:= none -> {reply, ok, State};
|
|
|
|
true ->
|
|
|
|
true ->
|
|
|
@ -193,6 +222,25 @@ handle_call({delete_teams, Removed}, _, State) ->
|
|
|
|
ok = delete_teams(State#state.database, Removed),
|
|
|
|
ok = delete_teams(State#state.database, Removed),
|
|
|
|
{reply, ok, State};
|
|
|
|
{reply, ok, State};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handle_call({match_score, Division, Round, Number, Red, Blue, Score}, _, State) ->
|
|
|
|
|
|
|
|
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(State#state.database,
|
|
|
|
|
|
|
|
io_lib:format(
|
|
|
|
|
|
|
|
"SELECT instance FROM match_scores WHERE Division = ~p AND type = '~s' AND number = ~p;",
|
|
|
|
|
|
|
|
[Division, atom_to_list(Round), Number])),
|
|
|
|
|
|
|
|
Instances = get_column("instance", Columns, Rows),
|
|
|
|
|
|
|
|
NextInstance = if length(Instances) == 0 -> 1;
|
|
|
|
|
|
|
|
true -> lists:max(Instances) + 1
|
|
|
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
ok = first_error(sqlite3:sql_exec_script(State#state.database, lists:append(
|
|
|
|
|
|
|
|
[io_lib:format("INSERT INTO match_scores(division, type, number, instance, red, blue, score) VALUES(~p, '~s', ~p, ~p, ~p, ~p, '~s');", [Division, atom_to_list(Round), Number, NextInstance, Red, Blue, Score]),
|
|
|
|
|
|
|
|
io_lib:format("INSERT INTO match_states(division, type, number, tick, state) VALUES(~p, '~s', ~p, ~p, '~s');", [Division, atom_to_list(Round), Number, get_tick(State), "scored"])]))),
|
|
|
|
|
|
|
|
{reply, ok, State};
|
|
|
|
|
|
|
|
handle_call({match_state, Division, Round, Number, MatchState}, _, State) ->
|
|
|
|
|
|
|
|
{rowid, _} = sqlite3:sql_exec(State#state.database,
|
|
|
|
|
|
|
|
io_lib:format("INSERT INTO match_states(division, type, number, tick, state) VALUES(~p, '~s', ~p, ~p, '~s');",
|
|
|
|
|
|
|
|
[Division, atom_to_list(Round), Number, get_tick(State), MatchState])),
|
|
|
|
|
|
|
|
{reply, ok, State};
|
|
|
|
|
|
|
|
|
|
|
|
handle_call({add_division, PSize, QSize, ESize}, _, State) ->
|
|
|
|
handle_call({add_division, PSize, QSize, ESize}, _, State) ->
|
|
|
|
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(State#state.database, "SELECT division FROM divisions;"),
|
|
|
|
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(State#state.database, "SELECT division FROM divisions;"),
|
|
|
|
Divisions = lists:sort(get_column("division", Columns, Rows)),
|
|
|
|
Divisions = lists:sort(get_column("division", Columns, Rows)),
|
|
|
@ -236,3 +284,7 @@ get_teams(Database) ->
|
|
|
|
|
|
|
|
|
|
|
|
handle_cast(_, State) ->
|
|
|
|
handle_cast(_, State) ->
|
|
|
|
{noreply, State}.
|
|
|
|
{noreply, State}.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get_tick(State) -> erlang:monotonic_time(millisecond) - State#state.tick_start.
|
|
|
|
|
|
|
|
get_tick() ->
|
|
|
|
|
|
|
|
erlang:monotonic_time(millisecond) - gen_server:call(?MODULE, get_tick_start).
|
|
|
|