From 73e316a80f69686181d0e087128438f36c8e7eb5 Mon Sep 17 00:00:00 2001 From: albdert Date: Fri, 16 Oct 2026 11:08:13 +0200 Subject: [PATCH 1/2] sorting --- backend/src/resolvers.ts | 30 ++++++++++++++++++------ backend/src/schema.ts | 14 +++++++++++- db/grants.sql | 3 ++- db/indexes.sql | 5 ++-- db/load.py | 49 ++++++++++++++++++++++++---------------- 5 files changed, 70 insertions(+), 31 deletions(-) diff --git a/backend/src/resolvers.ts b/backend/src/resolvers.ts index 757dc29..5208819 100644 --- a/backend/src/resolvers.ts +++ b/backend/src/resolvers.ts @@ -4,6 +4,7 @@ import { pool } from './db.js'; type GamesArgs = { search?: string | null; filter?: GameFilter | null; + sort: GameSort; first: number; }; @@ -72,6 +73,16 @@ const GAMES_WHERE = ` AND ${WHERE_RELEASED_FROM} AND ${WHERE_RELEASED_TO} `; +// SQL sorting med order by +// denne variablen mapper en enum til en gitt order +// som kan brukes i SQL-spørringen +const ORDER_BY = { + POPULARITY: 'added DESC, id DESC', + NEWEST: 'released DESC NULLS LAST, id DESC', + METACRITIC: 'metacritic DESC NULLS LAST, id DESC', + NAME: 'name ASC, id ASC', +} as const; +type GameSort = keyof typeof ORDER_BY; // SQL queries const SQL_COUNT = ` @@ -90,12 +101,17 @@ const SQL_GAME = ` SELECT ${GAME_COLS} FROM games WHERE id = $1 `; -const SQL_GAMES = ` - SELECT ${GAME_COLS} FROM games - ${GAMES_WHERE} - ORDER BY added DESC, id DESC - LIMIT $6 -`; + +// denne må være en funksjon for å kunne ta inn en variabel sort +// finnes sikkert en bedre måte... +function sqlGames(sort: GameSort) { + return ` + SELECT ${GAME_COLS} FROM games + ${GAMES_WHERE} + ORDER BY ${ORDER_BY[sort]} + LIMIT $6 + `; +} // hjelpefunksjoner function toIds(ids?: string[] | null) { @@ -141,7 +157,7 @@ async function games(_parent: unknown, args: GamesArgs) { const values = filterValues(args); const first = Math.min(Math.max(args.first, 1), MAX_PAGE_SIZE); - const res = await pool.query(SQL_GAMES, [...values, first]); + const res = await pool.query(sqlGames(args.sort), [...values, first]); return { edges: res.rows.map((row) => ({ node: row, cursor: '' })), pageInfo: { hasNextPage: false, endCursor: null }, diff --git a/backend/src/schema.ts b/backend/src/schema.ts index 9d2621e..9e0a90f 100644 --- a/backend/src/schema.ts +++ b/backend/src/schema.ts @@ -1,4 +1,16 @@ export const typeDefs = `#graphql + + """ + Definerer en enum for å holde sorteringsverdiene våre. + Disse mappes til en spesifikk ORDER BY setning i resolveren. + """ + enum GameSort { + POPULARITY, + NEWEST, + METACRITIC, + NAME + } + """ Definerer input argumentene som er gyldige mot serveren. Brukes for å filtrere visningen av spill. @@ -78,7 +90,7 @@ export const typeDefs = `#graphql """ type Query { game(id: ID!): Game - games(search: String, filter: GameFilter, first: Int=20): GameConnection! + games(search: String, filter: GameFilter, sort: GameSort!=POPULARITY first: Int=20): GameConnection! genres: [Facet!]! platforms: [Facet!]! } diff --git a/db/grants.sql b/db/grants.sql index 733b740..1de3387 100644 --- a/db/grants.sql +++ b/db/grants.sql @@ -9,5 +9,6 @@ GRANT SELECT ON ALL TABLES IN SCHEMA public TO webapp; GRANT INSERT ON users TO webapp; GRANT USAGE ON SEQUENCE users_id_seq TO webapp; -GRANT INSERT ON reviews TO webapp; +GRANT INSERT, DELETE ON reviews TO webapp; +GRANT UPDATE (rating, body) ON reviews TO webapp; GRANT USAGE ON SEQUENCE reviews_id_seq TO webapp; diff --git a/db/indexes.sql b/db/indexes.sql index 929a704..d89c1a4 100644 --- a/db/indexes.sql +++ b/db/indexes.sql @@ -1,9 +1,8 @@ CREATE INDEX games_search_trgm ON games USING gin (search_names gin_trgm_ops); CREATE INDEX games_added_id ON games (added DESC, id DESC); -CREATE INDEX games_rating_id ON games (rating DESC NULLS LAST, id DESC); -CREATE INDEX games_released_id ON games (released DESC NULLS LAST, id DESC); -CREATE INDEX games_meta_id ON games (metacritic DESC NULLS LAST, id DESC); +CREATE INDEX games_released_id ON games ((COALESCE(released, '-infinity'::date)) DESC, id DESC); +CREATE INDEX games_meta_id ON games ((COALESCE(metacritic, -1)) DESC, id DESC); CREATE INDEX games_name_id ON games (name, id); CREATE INDEX game_genres_rev ON game_genres (genre_id, game_id); diff --git a/db/load.py b/db/load.py index 3e6f825..5b65b74 100644 --- a/db/load.py +++ b/db/load.py @@ -10,6 +10,7 @@ PARQUET = os.environ.get("PARQUET_PATH", str(Path.home() / "data" / "videogames_data.parquet")) DATABASE_URL = os.environ["DATABASE_URL"] FORCE = "--force" in sys.argv +WIPE_USERS = "--wipe-user-data" in sys.argv # (parquet column, lookup table, junction table, junction fk column) RELATIONS = [ @@ -125,18 +126,30 @@ def copy_csv(cur: psycopg.Cursor, table: str, columns: str, path: Path) -> None: while chunk := f.read(1 << 20): cp.write(chunk) +def has_user_data(cur) -> bool: + for table in ("users", "reviews"): + cur.execute("SELECT to_regclass(%s)", (f"public.{table}",)) + if cur.fetchone()[0] is None: + continue + cur.execute(f"SELECT EXISTS (SELECT 1 FROM {table})") + if cur.fetchone()[0]: + return True + return False def main() -> None: - with tempfile.TemporaryDirectory() as tmp: - out = Path(tmp) - print("Exporting CSVs from Parquet...") - export_csvs(out) + with psycopg.connect(DATABASE_URL) as conn: + cur = conn.cursor() + cur.execute("SELECT to_regclass('public.games')") + if cur.fetchone()[0] is not None and not FORCE: + sys.exit("games table already exists. Rerun with --force to force reload.") - with psycopg.connect(DATABASE_URL) as conn: - cur = conn.cursor() - cur.execute("SELECT to_regclass('public.games')") - if cur.fetchone()[0] is not None and not FORCE: - sys.exit("games table already exists. Rerun with --force to wipe and reload (deletes reviews too).") + if has_user_data(cur) and not WIPE_USERS: + sys.exit("users/reviews contain data. Add --wipe-user-data to delete it.") + + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) + print("Exporting CSVs from Parquet...") + export_csvs(out) print("Creating tables...") cur.execute((HERE / "schema.sql").read_text()) @@ -147,17 +160,15 @@ def main() -> None: copy_csv(cur, table, "id, name", out / f"{table}.csv") copy_csv(cur, junction, f"game_id, {fk}", out / f"{junction}.csv") - print("Creating indexes...") - cur.execute((HERE / "indexes.sql").read_text()) - conn.commit() - - cur.execute("ANALYZE") - conn.commit() - - for table in ["games"] + [r[1] for r in RELATIONS] + [r[2] for r in RELATIONS]: - cur.execute(f"SELECT count(*) FROM {table}") - print(f"{table}: {cur.fetchone()[0]}") + print("Creating indexes...") + cur.execute((HERE / "indexes.sql").read_text()) + conn.commit() + cur.execute("ANALYZE") + conn.commit() + for table in ["games"] + [r[1] for r in RELATIONS] + [r[2] for r in RELATIONS]: + cur.execute(f"SELECT count(*) FROM {table}") + print(f"{table}: {cur.fetchone()[0]}") if __name__ == "__main__": main() From b1446594fcd7b1d04c29fac8f156b566999415df Mon Sep 17 00:00:00 2001 From: albdert Date: Fri, 16 Oct 2026 13:37:38 +0200 Subject: [PATCH 2/2] fungerende sortering + db endringer --- backend/src/schema.ts | 2 +- db/indexes.sql | 3 +++ db/schema.sql | 2 -- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/schema.ts b/backend/src/schema.ts index 9e0a90f..437122e 100644 --- a/backend/src/schema.ts +++ b/backend/src/schema.ts @@ -90,7 +90,7 @@ export const typeDefs = `#graphql """ type Query { game(id: ID!): Game - games(search: String, filter: GameFilter, sort: GameSort!=POPULARITY first: Int=20): GameConnection! + games(search: String, filter: GameFilter, sort: GameSort!=POPULARITY first: Int!=20): GameConnection! genres: [Facet!]! platforms: [Facet!]! } diff --git a/db/indexes.sql b/db/indexes.sql index d89c1a4..0343637 100644 --- a/db/indexes.sql +++ b/db/indexes.sql @@ -14,3 +14,6 @@ CREATE INDEX game_publishers_rev ON game_publishers (publisher_id, g CREATE INDEX game_tags_rev ON game_tags (tag_id, game_id); CREATE INDEX reviews_game_created ON reviews (game_id, created_at DESC); +CREATE INDEX reviews_user_created ON reviews (user_id, created_at DESC); + +CREATE UNIQUE INDEX users_username_lower ON users (lower(username)); \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index cbf51ed..558654d 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -76,7 +76,6 @@ CREATE TABLE users ( password_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE UNIQUE INDEX users_username_lower ON users (lower(username)); CREATE TABLE reviews ( id BIGSERIAL PRIMARY KEY, @@ -87,4 +86,3 @@ CREATE TABLE reviews ( created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (game_id, user_id) ); -CREATE INDEX reviews_user_created ON reviews (user_id, created_at DESC);