diff --git a/src/database.erl b/src/database.erl index 9e83b16..3c09edc 100644 --- a/src/database.erl +++ b/src/database.erl @@ -3,18 +3,43 @@ -behaviour(gen_server). -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) -> 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]) -> Exists = filelib:is_regular(DatabaseFile), if Exists =:= true -> - {ok, DB} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), - ok = sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), - {ok, #state{database_file = DatabaseFile, database = DB}}; + {TickStart, DB} = open_db(DatabaseFile), + {ok, #state{database_file = DatabaseFile, database = DB, tick_start = TickStart}}; true -> io:format("Failed to load database ~s~n", [DatabaseFile]), {ok, #state{database_file = none, database = none}} @@ -53,6 +78,8 @@ init_db(Database) -> {type, text, [not_null]}, {number, integer, [not_null]}, {instance, integer, [not_null]}, + {red, integer, [not_null]}, + {blue, integer, [not_null]}, {score, blob, [not_null]}], [{primary_key, [division, type, number, instance]}, {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]}, {type, text, [not_null]}, {number, integer, [not_null]}, - {time, integer, [not_null]}, - {state, blob, [not_null]}], - [{primary_key, [division, type, number, time]}, + {tick, integer, [not_null]}, + {state, text, [not_null]}], + [{primary_key, [division, type, number, tick]}, {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]), 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]}, {instance, integer, [not_null]}, + {red, integer, [not_null]}, + {blue, integer, [not_null]}, {score, blob, [not_null]}], [{primary_key, [number, instance]}, {foreign_key, {[number], finals, [number], ""}}]), ok = sqlite3:create_table(Database, finals_states, [{number, integer, [not_null]}, - {time, integer, [not_null]}, + {tick, integer, [not_null]}, {state, blob, [not_null]}], - [{primary_key, [number, time]}, + [{primary_key, [number, tick]}, {foreign_key, {[number], finals, [number], ""}}]), ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]}, @@ -129,8 +158,8 @@ write_division_matches(Database, Division, Type, Matches) -> [Division, atom_to_list(Type)]), lists:append([io_lib:format( "INSERT INTO matches(division, type, number, blue_1, blue_2, red_1, red_2) VALUES (~p, '~s', ~p, '~s', '~s', '~s', '~s'); ", - [Division, atom_to_list(Type), N, B1, B2, R1, R2]) - || {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]), + [Division, atom_to_list(Type), N, B1, B2, R1, R2]) + || {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]), "COMMIT;"]), first_error(sqlite3:sql_exec_script(Database, SQL)). @@ -147,6 +176,8 @@ get_column(Name, Columns, Rows) -> Index = index_of(Name, Columns), [element(Index, Row) || Row <- Rows]. +handle_call(get_tick_start, _, State) -> + {reply, State#state.tick_start, State}; handle_call({new_db, DatabaseFile}, _, State) -> ok = if State#state.database =:= none -> ok; true -> sqlite3:close(State#state.database) @@ -155,21 +186,19 @@ handle_call({new_db, DatabaseFile}, _, State) -> ok = if Exists -> file:delete(DatabaseFile); true -> ok end, - {ok, Database} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), - ok = sqlite3:sql_exec(Database, "PRAGMA foreign_keys = ON;"), - {reply, init_db(Database), State#state{database_file = DatabaseFile, database = Database}}; + {TickStart, Database} = open_db(DatabaseFile), + {reply, init_db(Database), State#state{database_file = DatabaseFile, database = Database, tick_start = TickStart}}; handle_call({load_db, DatabaseFile}, _, State) -> true = filelib:is_regular(DatabaseFile), - {ok, Database} = if DatabaseFile == State#state.database_file -> - {ok, State#state.database}; - true -> - ok = if State#state.database =:= none -> ok; - true -> sqlite3:close(State#state.database) - end, - {ok, DB} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), - {sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), DB} - end, - {reply, ok, State#state{database_file = DatabaseFile, database = Database}}; + {TickStart, Database} = if DatabaseFile == State#state.database_file -> + {State#state.tick_start, State#state.database}; + true -> + ok = if State#state.database =:= none -> ok; + true -> sqlite3:close(State#state.database) + end, + open_db(DatabaseFile) + end, + {reply, ok, State#state{database_file = DatabaseFile, database = Database, tick_start = TickStart}}; handle_call(close_db, _, State) -> if State#state.database =:= none -> {reply, ok, State}; true -> @@ -193,19 +222,38 @@ handle_call({delete_teams, Removed}, _, State) -> ok = delete_teams(State#state.database, Removed), {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) -> [{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(State#state.database, "SELECT division FROM divisions;"), Divisions = lists:sort(get_column("division", Columns, Rows)), NextDivision = first_empty([0 | Divisions]), {rowid, _} = sqlite3:sql_exec(State#state.database, - io_lib:format("INSERT INTO divisions(division, psize, qsize, esize) VALUES(~p, ~p, ~p, ~p);", - [NextDivision, PSize, QSize, ESize])), + io_lib:format("INSERT INTO divisions(division, psize, qsize, esize) VALUES(~p, ~p, ~p, ~p);", + [NextDivision, PSize, QSize, ESize])), {reply, ok, State}; handle_call({edit_division, Division, PSize, QSize, ESize}, _, State) -> ok = sqlite3:sql_exec(State#state.database, - io_lib:format( - "UPDATE divisions SET psize = ~p, qsize = ~p, esize = ~p WHERE division = ~p;", - [PSize, QSize, ESize, Division])), + io_lib:format( + "UPDATE divisions SET psize = ~p, qsize = ~p, esize = ~p WHERE division = ~p;", + [PSize, QSize, ESize, Division])), {reply, ok, State}; handle_call({delete_division, Division}, _, State) -> ok = first_error(sqlite3:sql_exec_script(State#state.database, io_lib:format("UPDATE teams SET division = NULL WHERE division = ~p; DELETE FROM divisions WHERE division = ~p;", [Division, Division]))), @@ -236,3 +284,7 @@ get_teams(Database) -> handle_cast(_, 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).