From 12a2ebbfffaba882b2f4250bcdf5fd268cc98550 Mon Sep 17 00:00:00 2001 From: James Raspass Date: Thu, 14 Nov 2024 06:18:56 +0000 Subject: [PATCH] WIP save experimental solutions too --- main.go | 50 ++++++++++++++++++++++++++ routes/golfer_cheevos.go | 25 +++++++------ routes/golfer_profile.go | 4 ++- routes/legacy_api.go | 17 ++++++++- routes/rankings_holes.go | 4 +-- sql/a-schema.sql | 76 +++++++++++++++++++++++++-------------- sql/b-earn-cheevos.sql | 13 ++++--- sql/c-other-functions.sql | 6 +++- 8 files changed, 144 insertions(+), 51 deletions(-) diff --git a/main.go b/main.go index dcc3936f1b..b193703a01 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,12 @@ import ( "os" "time" + "github.com/code-golf/code-golf/config" "github.com/code-golf/code-golf/db" "github.com/code-golf/code-golf/discord" "github.com/code-golf/code-golf/github" "github.com/code-golf/code-golf/routes" + "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) @@ -18,6 +20,24 @@ func main() { db := db.Open() + // Attempt to populate the holes table every second until we succeed. + // This handles the site starting before the DB. + go func() { + if err := populateHolesTable(db); err != nil { + log.Println(err) + } else { + return + } + + for range time.Tick(time.Second) { + if err := populateHolesTable(db); err != nil { + log.Println(err) + } else { + break + } + } + }() + // Every 10 seconds. go func() { // Refreshing the mat views every 10 seconds is overkill on dev. @@ -146,3 +166,33 @@ func main() { // Live only listens on HTTP, TLS is handled by Caddy. panic(http.ListenAndServe(":80", routes.Router(db))) } + +func populateHolesTable(db *sqlx.DB) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + insertHole, err := tx.Prepare("INSERT INTO holes (id, experiment) VALUES ($1, $2)") + if err != nil { + return err + } + + if _, err := tx.Exec("TRUNCATE holes"); err != nil { + return err + } + + for _, hole := range config.AllHoleList { + if _, err := insertHole.Exec(hole.ID, hole.Experiment); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + log.Println("Populated holes table.") + return nil +} diff --git a/routes/golfer_cheevos.go b/routes/golfer_cheevos.go index d8c8484f44..633b1767c0 100644 --- a/routes/golfer_cheevos.go +++ b/routes/golfer_cheevos.go @@ -124,9 +124,8 @@ func golferCheevosGET(w http.ResponseWriter, r *http.Request) { } { cheevoProgress( `SELECT COUNT(DISTINCT(hole, lang)) - FROM solutions - WHERE NOT failing - AND hole = ANY($1) + FROM stable_passing_solutions + WHERE hole = ANY($1) AND lang = ANY($2) AND user_id = $3`, []string{cheevo.cheevo}, @@ -137,23 +136,23 @@ func golferCheevosGET(w http.ResponseWriter, r *http.Request) { cheevoProgress( `SELECT pangramglot(array_agg(DISTINCT lang)) - FROM solutions - WHERE NOT failing AND hole = 'pangram-grep' AND user_id = $1`, + FROM stable_passing_solutions + WHERE hole = 'pangram-grep' AND user_id = $1`, []string{"pangramglot"}, ) cheevoProgress( `SELECT COALESCE(EXTRACT(days FROM TIMEZONE('UTC', NOW()) - MIN(submitted)), 0) - FROM solutions - WHERE NOT FAILING AND user_id = $1`, + FROM stable_passing_solutions + WHERE user_id = $1`, []string{"aged-like-fine-wine"}, ) cheevoProgress( `WITH langs AS ( SELECT COUNT(DISTINCT lang) - FROM solutions - WHERE NOT failing AND user_id = $1 + FROM stable_passing_solutions + WHERE user_id = $1 GROUP BY hole ) SELECT COALESCE(MAX(count), 0) FROM langs`, []string{"polyglot", "polyglutton", "omniglot", "omniglutton"}, @@ -162,8 +161,8 @@ func golferCheevosGET(w http.ResponseWriter, r *http.Request) { cheevoProgress( `WITH distinct_holes AS ( SELECT DISTINCT hole - FROM solutions - WHERE NOT failing AND user_id = $2 + FROM stable_passing_solutions + WHERE user_id = $2 ) SELECT COUNT(DISTINCT $1::hstore->hole::text) FROM distinct_holes`, []string{"smörgåsbord"}, config.HoleCategoryHstore, @@ -171,8 +170,8 @@ func golferCheevosGET(w http.ResponseWriter, r *http.Request) { cheevoProgress( `SELECT COUNT(DISTINCT hole) - FROM solutions - WHERE NOT failing AND user_id = $1`, + FROM stable_passing_solutions + WHERE user_id = $1`, cheevoIDs(config.CheevoTree["Total Holes"]), ) diff --git a/routes/golfer_profile.go b/routes/golfer_profile.go index 65cbec12d5..7f63610a4b 100644 --- a/routes/golfer_profile.go +++ b/routes/golfer_profile.go @@ -85,7 +85,9 @@ func golferGET(w http.ResponseWriter, r *http.Request) { lang lang, user_id FROM solutions - WHERE CASE WHEN $1 THEN user_id = ANY(following($2, $3)) + JOIN holes ON id = hole + WHERE experiment = 0 -- TODO This is probably temporary + AND CASE WHEN $1 THEN user_id = ANY(following($2, $3)) ELSE user_id = $2 END GROUP BY user_id, hole, lang diff --git a/routes/legacy_api.go b/routes/legacy_api.go index 7776029326..ef9addbc0b 100644 --- a/routes/legacy_api.go +++ b/routes/legacy_api.go @@ -134,6 +134,21 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) { out.Cheevos = append(out.Cheevos, *c) } } + + if _, err := db.ExecContext( + r.Context(), + `SELECT save_solution( + bytes := octet_length($1), + chars := char_length($1), + code := $1, + hole := $2, + lang := $3, + user_id := $4 + )`, + in.Code, in.Hole, in.Lang, golfer.ID, + ); err != nil { + panic(err) + } } else if pass && golfer != nil && !experimental { if err := db.QueryRowContext( r.Context(), @@ -337,7 +352,7 @@ func apiMiniRankingsGET(w http.ResponseWriter, r *http.Request) { if hole == nil || lang == nil { w.WriteHeader(http.StatusNotFound) return - } else if hole.Experiment != 0 || lang.Experiment != 0 { + } else if hole.Experiment != 0 || lang.Experiment != 0 { // TODO hole.Exp != 0 w.Write([]byte("[]")) return } diff --git a/routes/rankings_holes.go b/routes/rankings_holes.go index a9c4942dc7..e293604402 100644 --- a/routes/rankings_holes.go +++ b/routes/rankings_holes.go @@ -53,14 +53,14 @@ func rankingsHolesGET(w http.ResponseWriter, r *http.Request) { holeWhere = pq.Array(config.RecentHoles) } - if data.HoleID != "all" && config.HoleByID[data.HoleID] == nil || + if data.HoleID != "all" && config.AllHoleByID[data.HoleID] == nil || data.LangID != "all" && config.LangByID[data.LangID] == nil || data.Scoring != "chars" && data.Scoring != "bytes" { w.WriteHeader(http.StatusNotFound) return } - if data.Hole = config.HoleByID[data.HoleID]; data.Hole != nil { + if data.Hole = config.AllHoleByID[data.HoleID]; data.Hole != nil { data.PrevHole, data.NextHole = getPrevNextHole(r, data.Hole) } diff --git a/sql/a-schema.sql b/sql/a-schema.sql index 44d747f0f7..41df734e0e 100644 --- a/sql/a-schema.sql +++ b/sql/a-schema.sql @@ -29,34 +29,40 @@ CREATE TYPE connection AS ENUM ( CREATE TYPE hole AS ENUM ( '12-days-of-christmas', '24-game', '99-bottles-of-beer', - 'abundant-numbers', 'abundant-numbers-long', 'arabic-to-roman', - 'arithmetic-numbers', 'arrows', 'ascending-primes', 'ascii-table', - 'brainfuck', 'card-number-validation', 'catalan-numbers', - 'catalans-constant', 'christmas-trees', 'collatz', 'css-colors', 'cubes', - 'day-of-week', 'dfa-simulator', 'diamonds', 'divisors', 'emirp-numbers', + 'abundant-numbers', 'abundant-numbers-long', 'apérys-constant', + 'arabic-to-roman', 'arithmetic-numbers', 'arrows', 'ascending-primes', + 'ascii-table', 'billiards', 'brainfuck', 'card-number-validation', + 'catalan-numbers', 'catalans-constant', 'christmas-trees', 'collatz', + 'css-colors', 'css-grid', 'cubes', 'day-of-week', 'dfa-simulator', + 'diamonds', 'divisors', 'ellipse-perimeters', 'emirp-numbers', 'emirp-numbers-long', 'emojify', 'evil-numbers', 'evil-numbers-long', 'factorial-factorisation', 'farey-sequence', 'fibonacci', 'fizz-buzz', - 'foo-fizz-buzz-bar', 'forsyth-edwards-notation', 'fractions', - 'game-of-life', 'gijswijts-sequence', 'happy-numbers', - 'happy-numbers-long', 'hexdump', 'intersection', 'inventory-sequence', - 'isbn', 'jacobi-symbol', 'kaprekar-numbers', 'kolakoski-constant', + 'floyd-steinberg-dithering', 'foo-fizz-buzz-bar', + 'forsyth-edwards-notation', 'fractions', 'game-of-life', + 'gijswijts-sequence', 'gray-code-decoder', 'gray-code-encoder', + 'happy-numbers', 'happy-numbers-long', 'hexagonal-spiral', 'hexdump', + 'hilbert-curve', 'intersection', 'inventory-sequence', 'isbn', + 'jacobi-symbol', 'kaprekar-numbers', 'kolakoski-constant', 'kolakoski-sequence', 'leap-years', 'levenshtein-distance', 'leyland-numbers', 'ln-2', 'look-and-say', 'lucky-numbers', - 'lucky-tickets', 'mahjong', 'maze', 'medal-tally', 'morse-decoder', - 'morse-encoder', 'musical-chords', 'n-queens', 'niven-numbers', - 'niven-numbers-long', 'number-spiral', 'odious-numbers', - 'odious-numbers-long', 'ordinal-numbers', 'palindromemordnilap', - 'pangram-grep', 'pascals-triangle', 'pernicious-numbers', - 'pernicious-numbers-long', 'poker', 'polyominoes', 'prime-numbers', - 'prime-numbers-long', 'proximity-grid', 'qr-decoder', 'quine', 'recamán', + 'lucky-tickets', 'mahjong', 'mandelbrot', 'maze', 'medal-tally', + 'morse-decoder', 'morse-encoder', 'musical-chords', 'n-queens', + 'nfa-simulator', 'niven-numbers', 'niven-numbers-long', 'number-spiral', + 'odd-polyomino-tiling', 'odious-numbers', 'odious-numbers-long', + 'ordinal-numbers', 'p-adic-expansion', 'palindromemordnilap', + 'pangram-grep', 'partition-numbers', 'pascals-triangle', + 'pernicious-numbers', 'pernicious-numbers-long', 'placeholder', 'poker', + 'polyominoes', 'prime-numbers', 'prime-numbers-long', 'proximity-grid', + 'qr-decoder', 'qr-encoder', 'quadratic-formula', 'quine', 'recamán', 'repeating-decimals', 'reverse-polish-notation', 'reversi', 'rijndael-s-box', 'rock-paper-scissors-spock-lizard', 'roman-to-arabic', - 'rule-110', 'seven-segment', 'si-units', 'sierpiński-triangle', - 'smith-numbers', 'spelling-numbers', 'star-wars-opening-crawl', 'sudoku', + 'rule-110', 'semiprime-numbers', 'seven-segment', 'si-units', + 'sierpiński-triangle', 'smith-numbers', 'spelling-numbers', + 'sphenic-numbers', 'star-wars-gpt', 'star-wars-opening-crawl', 'sudoku', 'sudoku-fill-in', 'ten-pin-bowling', 'time-distance', 'tongue-twisters', - 'transpose-sentence', 'united-states', 'vampire-numbers', - 'van-eck-sequence', 'zeckendorf-representation', 'zodiac-signs', 'γ', 'λ', - 'π', 'τ', 'φ', '√2', '𝑒' + 'transpose-sentence', 'trinomial-triangle', 'turtle', 'united-states', + 'vampire-numbers', 'van-eck-sequence', 'zeckendorf-representation', + 'zodiac-signs', 'γ', 'λ', 'π', 'τ', 'φ', '√2', '𝑒' ); CREATE TYPE idea_category AS ENUM ('cheevo', 'hole', 'lang', 'other'); @@ -152,6 +158,14 @@ CREATE TABLE follows ( CHECK (follower_id != followee_id) -- Can't follow yourself! ); +-- config/data/holes.toml is the canonical source of truth for hole data. +-- This table is a shadow copy, updated on startup, used in DB queries. +-- TODO Move category here, remove config.HoleCategoryHstore. +CREATE UNLOGGED TABLE holes ( + id hole NOT NULL PRIMARY KEY, + experiment int NOT NULL +); + CREATE TABLE notes ( user_id int NOT NULL REFERENCES users(id) ON DELETE CASCADE, hole hole NOT NULL, @@ -211,6 +225,13 @@ CREATE TABLE trophies ( PRIMARY KEY (user_id, trophy) ); +-- Hole isn't experimental and solution isn't failing. +CREATE VIEW stable_passing_solutions AS + SELECT solutions.* + FROM solutions + JOIN holes ON id = hole + WHERE experiment = 0 AND NOT failing; + CREATE MATERIALIZED VIEW medals AS WITH ranks AS ( SELECT user_id, hole, lang, scoring, submitted, COUNT(*) OVER (PARTITION BY hole, lang, scoring), @@ -219,8 +240,7 @@ CREATE MATERIALIZED VIEW medals AS WITH ranks AS ( ORDER BY CASE WHEN scoring = 'bytes' THEN bytes ELSE chars END ) - FROM solutions - WHERE NOT failing + FROM stable_passing_solutions ) SELECT user_id, hole, lang, scoring, submitted, (enum_range(NULL::medal))[rank + 2] medal FROM ranks @@ -240,8 +260,7 @@ CREATE MATERIALIZED VIEW rankings AS WITH strokes AS ( select hole, lang, scoring, user_id, submitted, case when scoring = 'bytes' then bytes else chars end strokes, case when scoring = 'bytes' then chars else bytes end other_strokes - from solutions - where not failing + from stable_passing_solutions ), min as ( select hole, scoring, min(strokes)::numeric Sa from strokes @@ -299,16 +318,21 @@ CREATE INDEX solutions_lang_key ON solutions(lang, user_id) WHERE NOT failing; CREATE ROLE "code-golf" WITH LOGIN; --- Only owners can refresh. +-- Materialized views. Only owners can refresh. ALTER MATERIALIZED VIEW medals OWNER TO "code-golf"; ALTER MATERIALIZED VIEW points OWNER TO "code-golf"; ALTER MATERIALIZED VIEW rankings OWNER TO "code-golf"; +-- Views. +GRANT SELECT ON stable_passing_solutions TO "code-golf"; + +-- Tables. GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE authors TO "code-golf"; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE connections TO "code-golf"; GRANT SELECT, INSERT, UPDATE ON TABLE discord_records TO "code-golf"; GRANT SELECT, INSERT, UPDATE ON TABLE discord_state TO "code-golf"; GRANT SELECT, INSERT, DELETE ON TABLE follows TO "code-golf"; +GRANT SELECT, INSERT, TRUNCATE ON TABLE holes TO "code-golf"; GRANT SELECT, INSERT, TRUNCATE ON TABLE ideas TO "code-golf"; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE notes TO "code-golf"; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE sessions TO "code-golf"; diff --git a/sql/b-earn-cheevos.sql b/sql/b-earn-cheevos.sql index c9e73b7044..379341653c 100644 --- a/sql/b-earn-cheevos.sql +++ b/sql/b-earn-cheevos.sql @@ -24,18 +24,17 @@ BEGIN ----------- SELECT COUNT(DISTINCT solutions.hole) INTO holes - FROM solutions WHERE NOT failing AND solutions.user_id = user_id; + FROM stable_passing_solutions + WHERE solutions.user_id = user_id; SELECT array_agg(DISTINCT solutions.hole) INTO holes_for_lang - FROM solutions - WHERE NOT failing - AND solutions.lang = lang + FROM stable_passing_solutions + WHERE solutions.lang = lang AND solutions.user_id = user_id; SELECT array_agg(DISTINCT solutions.lang) INTO langs_for_hole - FROM solutions - WHERE NOT failing - AND solutions.hole = hole + FROM stable_passing_solutions + WHERE solutions.hole = hole AND solutions.user_id = user_id; ------------------------ diff --git a/sql/c-other-functions.sql b/sql/c-other-functions.sql index a0021986e1..dd21f86839 100644 --- a/sql/c-other-functions.sql +++ b/sql/c-other-functions.sql @@ -255,7 +255,11 @@ BEGIN AND solutions.scoring = 'chars'; END IF; - ret.earned := earn_cheevos(hole, lang, user_id); + -- Only earn cheevos if the hole isn't experimental. + SELECT experiment = 0 INTO found FROM holes WHERE id = hole; + IF found THEN + ret.earned := earn_cheevos(hole, lang, user_id); + END IF; RETURN ret; END;