Store division info in database

main
noah metz 2024-01-28 22:03:16 -07:00
parent e96790b215
commit 3a1820bea7
2 changed files with 129 additions and 88 deletions

@ -2,13 +2,9 @@
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/1, init/1, handle_cast/2, handle_call/3, add_teams/1]). -export([start_link/1, init/1, handle_cast/2, handle_call/3]).
-export([set_test_config/0, new/1, close/0]). -record(state, {database_file = none, database = none}).
-record(division, {teams = [], practice = 0, qualification = 0}).
-record(config, {divisions = []}).
-record(state, {database_file = none, database = none, teams = [], config = none}).
start_link(DatabaseFile) -> start_link(DatabaseFile) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [DatabaseFile], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [DatabaseFile], []).
@ -25,57 +21,88 @@ init([DatabaseFile]) ->
end. end.
init_db(Database) -> init_db(Database) ->
ok = sqlite3:create_table(Database, genids, [{genid, text, [not_null]}],
[{primary_key, [genid]}]),
ok = sqlite3:create_table(Database, divisions, [{division, integer, [not_null]}, ok = sqlite3:create_table(Database, divisions, [{division, integer, [not_null]},
{prac_size, integer, [not_null]}, {psize, integer, [not_null]},
{prac_seed, integer, [not_null]}, {qsize, integer, [not_null]},
{qual_size, integer, [not_null]}, {esize, integer, [not_null]}],
{qual_seed, integer, [not_null]}],
[{primary_key, [division]}]), [{primary_key, [division]}]),
ok = sqlite3:create_table(Database, teams, [{team, text, [not_null]}, ok = sqlite3:create_table(Database, teams, [{team, text, [not_null]},
{division, integer}, {division, integer},
{alliance, integer},
{inspection, blob}], {inspection, blob}],
[{primary_key, [team]}, [{primary_key, [team]},
{foreign_key, {[division], divisions, [division], "ON DELETE RESTRICT"}}]), {foreign_key, {[division], divisions, [division], ""}}]),
ok = sqlite3:create_table(Database, matches, [{genid, text, [not_null]},
{round, text, [not_null]}, ok = sqlite3:create_table(Database, matches, [{division, integer, [not_null]},
{type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{red_1, text, [not_null]}, {red_1, text, [not_null]},
{red_2, text, [not_null]}, {red_2, text, [not_null]},
{blue_1, text, [not_null]}, {blue_1, text, [not_null]},
{blue_2, text, [not_null]}], {blue_2, text, [not_null]}],
[{primary_key, [genid, round, number]}, [{primary_key, [division, type, number]},
{check, "round IN ('practice', 'qualification', 'elimination', 'final')"}, {check, "type IN ('practice', 'qualification', 'elimination')"},
{foreign_key, {[genid], genids, [genid], "ON DELETE RESTRICT"}}, {foreign_key, {[division], divisions, [division], ""}},
{foreign_key, {[blue_1], teams, [team], "ON DELETE RESTRICT"}}, {foreign_key, {[blue_1], teams, [team], ""}},
{foreign_key, {[blue_2], teams, [team], "ON DELETE RESTRICT"}}, {foreign_key, {[blue_2], teams, [team], ""}},
{foreign_key, {[red_1], teams, [team], "ON DELETE RESTRICT"}}, {foreign_key, {[red_1], teams, [team], ""}},
{foreign_key, {[red_2], teams, [team], "ON DELETE RESTRICT"}}]), {foreign_key, {[red_2], teams, [team], ""}}]),
ok = sqlite3:create_table(Database, match_scores, [{genid, text, [not_null]},
{round, text, [not_null]}, ok = sqlite3:create_table(Database, match_scores, [{division, integer, [not_null]},
{type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{instance, integer, [not_null]}, {instance, integer, [not_null]},
{score, blob, [not_null]}], {score, blob, [not_null]}],
[{primary_key, [genid, round, number, instance]}, [{primary_key, [division, type, number, instance]},
{foreign_key, {[genid, round, number], matches, [genid, round, number], "ON DELETE CASCADE"}}]), {foreign_key, {[division, type, number], matches, [division, type, number], ""}}]),
ok = sqlite3:create_table(Database, match_states, [{genid, text, [not_null]},
{round, text, [not_null]}, ok = sqlite3:create_table(Database, match_states, [{division, integer, [not_null]},
{type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{time, integer, [not_null]}, {time, integer, [not_null]},
{state, blob, [not_null]}], {state, blob, [not_null]}],
[{primary_key, [genid, round, number, time]}, [{primary_key, [division, type, number, time]},
{foreign_key, {[genid, round, number], matches, [genid, round, number], "ON DELETE CASCADE"}}]), {foreign_key, {[division, type, number], matches, [division, type, number], ""}}]),
ok = sqlite3:create_table(Database, finals, [{number, integer, [not_null]},
{red_1, text, [not_null]},
{red_2, text, [not_null]},
{blue_1, text, [not_null]},
{blue_2, text, [not_null]}],
[{primary_key, [number]},
{foreign_key, {[blue_1], teams, [team], ""}},
{foreign_key, {[blue_2], teams, [team], ""}},
{foreign_key, {[red_1], teams, [team], ""}},
{foreign_key, {[red_2], teams, [team], ""}}]),
ok = sqlite3:create_table(Database, finals_scores, [{number, integer, [not_null]},
{instance, 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]},
{state, blob, [not_null]}],
[{primary_key, [number, time]},
{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]},
{type, text, [not_null]}, {type, text, [not_null]},
{attempt, integer, [not_null]}, {attempt, integer, [not_null]},
{score, blob}], {score, blob, [not_null]}],
[{primary_key, [team, type, attempt]}, [{primary_key, [team, type, attempt]},
{check, "type IN ('driver', 'autonomous')"}, {check, "type IN ('driver', 'autonomous')"},
{foreign_key, {[team], teams, [team], "ON DELETE CASCADE"}}]), {foreign_key, {[team], teams, [team], ""}}]),
ok. ok.
first_error([]) -> ok;
first_error([ok | Rest]) -> first_error(Rest);
first_error([{rowid, _} | Rest]) -> first_error(Rest);
first_error([Error | _]) -> Error.
delete_teams(Database, Teams) -> delete_teams(Database, Teams) ->
SQL = lists:append(["BEGIN; ", SQL = lists:append(["BEGIN; ",
lists:append([io_lib:format("DELETE FROM teams WHERE team = ~p; ", lists:append([io_lib:format("DELETE FROM teams WHERE team = ~p; ",
@ -90,50 +117,36 @@ add_teams(Database, Teams) ->
"COMMIT;"]), "COMMIT;"]),
first_error(sqlite3:sql_exec_script(Database, SQL)). first_error(sqlite3:sql_exec_script(Database, SQL)).
first_error([]) -> ok; assign_divisions(Database, Teams) ->
first_error([ok | Rest]) -> first_error(Rest); SQL = lists:append(["BEGIN; ",
first_error([Error | _]) -> Error. lists:append([io_lib:format("UPDATE teams SET division = ~p WHERE team = '~s'; ", [Division, Team]) || {Team, Division} <- Teams]),
"COMMIT;"]),
delete_matches(Database, GenID) ->
SQL = io_lib:format("DELETE FROM matches WHERE genid = ~p", [GenID]),
first_error(sqlite3:sql_exec_script(Database, SQL)). first_error(sqlite3:sql_exec_script(Database, SQL)).
write_matches(Database, Type, Teams, Seed, Size, Round, Matches) -> write_division_matches(Database, Division, Type, Matches) ->
GenID = schedule:make_genid(Type, Round, Teams, Seed, Size),
SQL = lists:append(["BEGIN; ", SQL = lists:append(["BEGIN; ",
io_lib:format("INSERT INTO genids(genid, type, size, seed) VALUES('~s', '~s', ~p, ~p); ", io_lib:format("DELETE FROM matches WHERE division = ~p AND type = '~s';",
[GenID, atom_to_list(Type), Size, Seed]), [Division, atom_to_list(Type)]),
lists:append([io_lib:format( lists:append([io_lib:format(
"INSERT INTO matches(genid, round, number, blue_1, blue_2, red_1, red_2) "INSERT INTO matches(division, type, number, blue_1, blue_2, red_1, red_2) VALUES (~p, '~s', ~p, '~s', '~s', '~s', '~s'); ",
VALUES ('~s', '~s', ~p, '~s', '~s', '~s', '~s'); ", [Division, atom_to_list(Type), N, B1, B2, R1, R2])
[GenID, atom_to_list(Round), N, B1, B2, R1, R2])
|| {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]), || {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]),
"COMMIT;"]), "COMMIT;"]),
{first_error(sqlite3:sql_exec_script(Database, SQL)), GenID}. first_error(sqlite3:sql_exec_script(Database, SQL)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-define(TEST_CONFIG, #config{divisions = [#division{teams = ["A", "B", "C", "D"], practice = 4}]}).
set_test_config() ->
gen_server:call(?MODULE, {set_config, ?TEST_CONFIG}).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
add_teams(Teams) -> index_of(Item, List) -> index_of(Item, List, 1).
gen_server:call(?MODULE, {add_teams, Teams}). index_of(Item, [Element | _], N) when Item == Element -> N;
index_of(Item, [_ | List], N) -> index_of(Item, List, N+1).
new(DatabaseFile) -> first_empty([Num1, Num2 | Nums]) when Num2 == (Num1 + 1) -> first_empty([Num2 | Nums]);
gen_server:call(?MODULE, {new_db, DatabaseFile}). first_empty([Num | _]) -> Num + 1.
close() -> get_column(_, [], _) -> [];
gen_server:call(?MODULE, close_db). get_column(_, _, []) -> [];
get_column(Name, Columns, Rows) ->
Index = index_of(Name, Columns),
[element(Index, Row) || Row <- Rows].
handle_call({set_config, Config}, _, State) when is_record(Config, config) ->
{reply, ok, State#state{config = Config}};
handle_call(close_db, _, State) ->
if State#state.database =:= none -> {reply, ok, State};
true ->
ok = sqlite3:close(State#state.database),
{reply, ok, State#state{database = none, database_file = none}}
end;
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)
@ -157,28 +170,59 @@ handle_call({load_db, DatabaseFile}, _, State) ->
{sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), DB} {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}};
handle_call({delete_teams, Removed}, _, State) -> handle_call(close_db, _, State) ->
Teams = lists:filter(fun(X) -> lists:member(X, Removed) =:= false end, State#state.teams), if State#state.database =:= none -> {reply, ok, State};
ok = delete_teams(State#state.database, Removed), true ->
{reply, ok, State#state{teams = Teams}}; ok = sqlite3:close(State#state.database),
{reply, ok, State#state{database = none, database_file = none}}
end;
handle_call({add_teams, Teams}, _, State) -> handle_call({add_teams, Teams}, _, State) ->
ok = add_teams(State#state.database, Teams), ok = add_teams(State#state.database, Teams),
{reply, ok, State#state{teams = Teams}}; {reply, ok, State};
handle_call({new_schedule, Division, Round, Seed}, _, State) -> handle_call({assign_divisions, Teams}, _, State) ->
DivInfo = lists:nth(Division, State#state.config#config.divisions), ok = assign_divisions(State#state.database, Teams),
Teams = DivInfo#division.teams, {reply, ok, State};
Size = case Round of handle_call({delete_teams, Removed}, _, State) ->
practice when DivInfo#division.practice =/= 0 -> ok = delete_teams(State#state.database, Removed),
DivInfo#division.practice; {reply, ok, State};
qualification when DivInfo#division.qualification =/= 0->
DivInfo#division.qualification handle_call({add_division, PSize, QSize, ESize}, _, State) ->
end, [{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])),
{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])),
{reply, ok, State};
handle_call({delete_division, Division}, _, State) ->
ok = sqlite3:sql_exec(State#state.database, io_lib:format("DELETE FROM divisions WHERE division = ~p;", [Division])),
{reply, ok, State};
handle_call({generate_division, Division, Round, Seed}, _, State) ->
Size = get_div_size(State#state.database, Division, Round),
Teams = get_div_teams(State#state.database, Division),
io:fwrite("Generating division ~p/~s with teams ~p, size ~p, and seed ~p~n", [Division, Round, Teams, Size, Seed]),
Matches = schedule:create(Seed, Size, Teams), Matches = schedule:create(Seed, Size, Teams),
{ok, GenID} = write_matches(State#state.database, division, Teams, Seed, Size, Round, Matches), ok = write_division_matches(State#state.database, Division, Round, Matches),
{reply, GenID, State};
handle_call({delete_schedule, GenID}, _, State) ->
ok = delete_matches(State#state.database, GenID),
{reply, ok, State}. {reply, ok, State}.
get_div_size(Database, Division, Round) ->
Column = if Round =:= practice -> "psize";
Round =:= qualification -> "qsize"
end,
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(Database, io_lib:format("SELECT ~s FROM divisions WHERE division = ~p;", [Column, Division])),
[Size] = get_column(Column, Columns, Rows),
Size.
get_div_teams(Database, Division) ->
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(Database, io_lib:format("SELECT team FROM teams WHERE division = ~p;", [Division])),
get_column("team", Columns, Rows).
handle_cast(_, State) -> handle_cast(_, State) ->
{noreply, State}. {noreply, State}.

@ -4,7 +4,7 @@
-export([start_link/0, init/1, handle_cast/2, handle_call/3]). -export([start_link/0, init/1, handle_cast/2, handle_call/3]).
-export([create/3, make_genid/5]). -export([create/3]).
create(Seed, Size, Teams) -> create(Seed, Size, Teams) ->
gen_server:call(?MODULE, {generate, Seed, Size, Teams}). gen_server:call(?MODULE, {generate, Seed, Size, Teams}).
@ -67,6 +67,3 @@ pick_matches(Teams, Matches) ->
pick_matches(NewTeams, [Match | Matches]). pick_matches(NewTeams, [Match | Matches]).
pick_matches(Teams) -> pick_matches(Teams, []). pick_matches(Teams) -> pick_matches(Teams, []).
make_genid(Type, Round, Teams, Seed, Size) ->
lists:sublist([ Y || <<X:4>> <= crypto:hash(sha, "TYPE:" ++ atom_to_list(Type) ++ "ROUND:" ++ atom_to_list(Round) ++ Teams ++ [Seed, Size]), Y <- integer_to_list(X, 16)], 4).