Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions backend/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { pool } from './db.js';
type GamesArgs = {
search?: string | null;
filter?: GameFilter | null;
sort: GameSort;
first: number;
};

Expand Down Expand Up @@ -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 = `
Expand All @@ -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) {
Expand Down Expand Up @@ -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 },
Expand Down
14 changes: 13 additions & 1 deletion backend/src/schema.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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!]!
}
Expand Down
3 changes: 2 additions & 1 deletion db/grants.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
8 changes: 5 additions & 3 deletions db/indexes.sql
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -15,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));
49 changes: 30 additions & 19 deletions db/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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())
Expand All @@ -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()
2 changes: 0 additions & 2 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);