From 8623b524de71b23a58df62022f4dc097ab33ce51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Wed, 22 Nov 2023 16:00:26 +0100 Subject: [PATCH] Add DB initialization code. Fix DB schema --- constants.py | 18 ++++++++++++++++++ database.py | 35 +++++++++++++++++++++++++++++++++-- install/database_schema.sql | 35 ++++++++++++++++++++--------------- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/constants.py b/constants.py index b9cb263..e4bbadb 100644 --- a/constants.py +++ b/constants.py @@ -7,3 +7,21 @@ DB_CONFIG_FILE_NAME = 'config.yaml' DEFAULT_DB_NAME = 'hanabi-league' DEFAULT_DB_USER = 'hanabi-league' DEFAULT_DB_PASS = 'hanabi-league' + +DB_TABLE_NAMES = [ + "users" + , "user_accounts" + , "downloads" + , "variants" + , "games" + , "game_participants" + , "game_actions" + , "seeds" + , "variant_base_ratings" + , "variant_ratings" + , "user_base_ratings" + , "user_ratings" + , "statistics" +] + +DATABASE_SCHEMA_PATH = 'install/database_schema.sql' diff --git a/database.py b/database.py index da01e81..a7eeb94 100644 --- a/database.py +++ b/database.py @@ -1,5 +1,7 @@ import psycopg2 +import psycopg2.extensions +import constants from config import read_db_config from log_setup import logger @@ -16,7 +18,7 @@ class DBConnectionManager: )) logger.debug("Established database connection.") - def get_connection(self): + def get_connection(self) -> psycopg2.extensions.connection: """ Get the database connection. If not already connected, this reads the database config file and connects to the DB. @@ -28,4 +30,33 @@ class DBConnectionManager: # Global instance that will hold our DB connection -conn_manager = DBConnectionManager() \ No newline at end of file +conn_manager = DBConnectionManager() + + +def get_existing_tables(): + conn = conn_manager.get_connection() + cur = conn.cursor() + table_names = ", ".join("'{}'".format(tablename) for tablename in constants.DB_TABLE_NAMES) + cur.execute( + "SELECT tablename FROM pg_tables WHERE" + " schemaname = 'public' AND" + " tablename IN ({})".format( + table_names + ) + ) + return [table for (table,) in cur.fetchall()] + + +def init_database(erase: bool = False): + tables = get_existing_tables() + + if not erase and len(tables) > 0: + logger.error("Aborting database initialization: Tables {} already exist".format(", ".join(tables))) + return + conn = conn_manager.get_connection() + cur = conn.cursor() + + with open(constants.DATABASE_SCHEMA_PATH, "r") as f: + cur.execute(f.read()) + conn.commit() + logger.verbose("Initialized DB tables.") diff --git a/install/database_schema.sql b/install/database_schema.sql index 60ede03..2be3642 100644 --- a/install/database_schema.sql +++ b/install/database_schema.sql @@ -65,25 +65,25 @@ * To be honest, I'd also like to apply all of the code to some non-league related games where I'd need this :D. */ -DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( id SERIAL PRIMARY KEY, - name TEXT NOT NULL - discord_tag TEXT, + name TEXT NOT NULL, + discord_tag TEXT ); -DROP TABLE IF EXISTS user_accounts; +DROP TABLE IF EXISTS user_accounts CASCADE; CREATE TABLE user_accounts ( /* The username from hanab.live. This will be used a) for associating games to one of our user_ids and b) show up in the player profile. */ - username TEXT NOT NULL, + username TEXT PRIMARY KEY, /** * Hanab.live stores a normalized username for each account to ensure that usernames are case-insensitive and don't have weird unicodes that resemble each other. * It uses go-unidecode to generate these from the actual usernames, and I suggest we do the same. * The corresponding python package is 'unidecode', so we should just use that here. * This will then ensure that no two sign-ups correspond to the same hanab.live account. */ - normalized_username TEXT NOT NULL UNIQUE + normalized_username TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); @@ -99,19 +99,18 @@ CREATE TABLE user_accounts ( DROP TABLE IF EXISTS downloads; CREATE TABLE downloads ( /** Notice this has to be a hanab.live username, not a user_id */ - username INTEGER NOT NULL, - latest_game_id INTEGER NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) + username TEXT PRIMARY KEY REFERENCES user_accounts, + latest_game_id INTEGER NOT NULL ); /** * For completeness, I suggest we store the hanab.live variants here together with their names. * So this will just be a static table that we populate once. */ -DROP TABLE IF EXISTS variants; +DROP TABLE IF EXISTS variants CASCADE; CREATE TABLE variants ( id INTEGER PRIMARY KEY, - name TEXT NOT NULL + name TEXT NOT NULL, num_suits INTEGER NOT NULL, clue_starved BOOLEAN NOT NULL, max_score SMALLINT NOT NULL @@ -122,7 +121,7 @@ CREATE TABLE variants ( * - Only download detailed data for each game once (The actions taken and the actual deck) * - We can evaluate statistics (also complicated ones) whenever we want, independent of the server */ -DROP TABLE IF EXISTS games; +DROP TABLE IF EXISTS games CASCADE; CREATE TABLE games ( /** Here, we should use the same ids as the game ids from hanab.live */ id INTEGER PRIMARY KEY, @@ -141,6 +140,7 @@ CREATE TABLE games ( FOREIGN KEY (variant_id) REFERENCES variants (id) ); +DROP TABLE IF EXISTS game_participants CASCADE; CREATE TABLE game_participants ( game_id INTEGER NOT NULL, /** @@ -223,7 +223,7 @@ CREATE TABLE seeds ( suit_index SMALLINT NOT NULL, rank SMALLINT NOT NULL, CONSTRAINT cards_unique UNIQUE (seed, card_index) -) +); /** @@ -242,14 +242,16 @@ CREATE TABLE seeds ( /** * Records the initial ratings of variants */ +DROP TABLE IF EXISTS variant_base_ratings CASCADE; CREATE TABLE variant_base_ratings ( variant_id SMALLINT PRIMARY KEY, - rating REAL NOT NULL, + rating REAL NOT NULL ); /** * Store a separate row for each elo update to one of the variants */ +DROP TABLE IF EXISTS variant_ratings CASCADE; CREATE TABLE variant_ratings ( /** This should reference the game that triggered the elo update */ league_id INTEGER PRIMARY KEY REFERENCES games, @@ -258,9 +260,10 @@ CREATE TABLE variant_ratings ( change REAL NOT NULL, value_after REAL NOT NULL, - FOREIGN KEY (variant_id) REFERENCES variants (id), + FOREIGN KEY (variant_id) REFERENCES variants (id) ); +DROP TABLE IF EXISTS user_base_ratings CASCADE; CREATE TABLE user_base_ratings ( user_id SMALLINT, /** @@ -276,6 +279,7 @@ CREATE TABLE user_base_ratings ( FOREIGN KEY (user_id) REFERENCES users (id) ); +DROP TABLE IF EXISTS user_ratings CASCADE; CREATE TABLE user_ratings ( /** This should reference the game that triggered the elo update. I would use the league_id here for proper ordering. */ league_id INTEGER PRIMARY KEY REFERENCES games, @@ -298,6 +302,7 @@ CREATE TABLE user_ratings ( /* TABLES RELATED TO STATISTICS */ /** This is a rough outline, not really happy with it right now. */ +DROP TABLE IF EXISTS statistics CASCADE; CREATE TABLE statistics ( game_id INTEGER PRIMARY KEY, /** I'd say all of the following can just be null in case we have not evaluated them yet. */