[blob-store 5/5] Retire the database: pipeline + /v3 + /v3/site db-less behind DISABLE_DB - #11
Closed
chondl wants to merge 7 commits into
Closed
[blob-store 5/5] Retire the database: pipeline + /v3 + /v3/site db-less behind DISABLE_DB#11chondl wants to merge 7 commits into
chondl wants to merge 7 commits into
Conversation
The noteworthy/upcoming DuckDB reads join matches and events, so they must resolve both from a single manifest generation. Capture base = _sync() once per query and pass it to _source, matching the base-threaded signature the atomic Parquet set introduced on duckdb-api.
…ndependent A4: when db-less mode seeds a cross-season EPA with no prior-year team_years (a partial/forgotten backfill), log a prominent warning and set a flag exposed via /info, instead of silently regressing every team to the rookie mean. Also move the historical parquet write out of the DISABLE_DB branch so db-less backfill still produces parquet.
The DuckDB read path for /v3/site noteworthy matches must match the SQLAlchemy path: offseason events (EventType.OFFSEASON) have modified rules, so their scores are excluded from the noteworthy-match leaderboards. Mirrors the exclusion in src/db/functions/noteworthy_matches.py. Harmless when no offseason events are present (the filter matches nothing to exclude).
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #10 (
duckdb-api). Baseduckdb-api, headdb-retirement, fork-only. Draft: the default backend is unchanged — withDISABLE_DBunset everything runs exactly as before. This is the last PR in the stack: the flip that runs the entire system — update pipeline,/v3, and/v3/site— with no relational database at all.It closes the "cutover gap" #10 named: the last DB reads move onto the snapshot (#9) and Parquet (#10) blobs,
/v3/site's two direct DB readers get the same facade/v3already has, and one flag turns the DB off end to end.Why each change
Add
DISABLE_DBflag; construct no engine or session db-less (src/constants.py,src/db/main.py).DISABLE_DB=True(env, mirrorsPROD). Under it,src/db/main.pysetsengine = Session = None, so no code path constructs a SQLAlchemy engine or session — importingsrc/dbstays harmless (models still register onBase.metadata), but nothing can open a connection.DISABLE_DBimplies the DuckDB serving backend everywhere.Serve
/v3and/v3/sitefrom DuckDB whenDISABLE_DB(src/api/backend.py,src/db_duckdb/,src/site/backend.py,src/site/match.py). #10'ssrc/api/backend.pyfacade already routes the six/v3routers tosrc/db_duckdb; this makesDISABLE_DBselect it too./v3/site's read functions call the/v3get_*_cachedwrappers, so they already ride that facade — exceptget_noteworthy_matches/get_upcoming_matches, which query the DB directly (Match join Event, top-N). Those get DuckDB implementations (parameterized SQL joins over the Parquet blobs, same signatures) behind asrc/site/backend.pyfacade mirroringsrc/api/backend.py; because these are cross-table joins (Match join Event), each one captures the read layer's base dir once and resolvesmatchesandeventsfrom a single manifest generation (#10's atomic Parquet set), so a top-N never pairs tables from different cycles. A missing year-file in the read layer now returns empty rather than raising, matching the DB's "no rows for this year" semantics.Move last pipeline DB reads to Parquet; skip DB writes db-less (
src/data/backend.py,src/data/main.py,src/data/router.py,src/google/storage.py). The pipeline's remaining DB reads:objs[5], [blob-store 3/5] Pipeline state as a snapshot blob; DB becomes a downstream consumer #9) and are consumed from that list inprocess_year_tba— verified nothing re-reads them from the DB. No change needed.team_years, feedingget_init_epa) andpost_process's norm_epa aggregates now read throughsrc/data/backend.py, which dispatches tosrc/db_duckdb(parquet/{year}/team_years.parquet) underDISABLE_DB. When a prior year's Parquet is absent the read returns empty -> rookie seeding. Unlike an empty DB (which is a deliberate first-ever run), a partial/forgotten backfill silently regressing every team to the rookie mean is a misconfiguration — so db-less mode now logs a prominent warning and setsDB_LESS_SEED_INCOMPLETE, surfaced via/info, when it seeds a cross-season year with no prior data. Backfill prior years to Parquet before running db-less, or EPA seeds will be wrong (parity verified to 0.0 only with the backfill present).DISABLE_DBbranch, so a db-less backfill still produces Parquet.db_orig = read_objs_db) and every DB upsert are skipped cleanly db-less; snapshot + blobs + Parquet remain the source of truth.storage.write_objspre-renders each cycle (events/all, team pages, noteworthy, upcoming) are generated through the same facades, so the bucket-first frontend stays fresh db-less.refresh_teamsis a DB-reconciliation maintenance op — no-op db-less, because team metadata already flows through the snapshot every cycle. The site update-trigger reads etags/events from the snapshot when db-less.Deliberate non-goals: no incremental-EPA checkpoint and no stale-while-revalidate refresh loop — full deterministic replay stays the pipeline's contract, so a cycle is always a clean recompute rather than an in-place mutation that could drift.
Evidence (local rig: fake-GCS + full 2026 season; CockroachDB toggled)
Default mode unchanged.
DISABLE_DBunset, DB up: shared smoke 10/10 incl.--run-update.Db-less cold start (the money shot). CockroachDB container stopped (
:26257connection-refused),DISABLE_DB=True, servers booted cold:Write DBstep, no session ever opened, no tracebacks. ~20-23s/cycle./v3(/v3/team/254-> 254 sane) and/v3/site(team_years/2026,noteworthy_matches/2026with all 6 score groups,upcoming_matches) all 200, served from DuckDB over Parquet.--run-update, consistency probe max EPA diff 0.0000. (Check-semantics note: the suite labels/v3/team/254and/v3/site/team_yearsas "db-backed reads"; db-less those are served from DuckDB/Parquet, so the label is historical — no check inherently requires CockroachDB, all pass.)Heal on the DB's return. Restarted CockroachDB, deleted 25
team_yearsrows to simulate drift (3724 -> 3699). One default-mode cycle (loading the snapshot state the db-less cycles accumulated) diff-upserted the missing rows back (3699 -> 3724); smoke 10/10. This is #9's honest-diff heal, preserved intact.Cross-year Parquet seed parity. Fabricated a prior year (2025) from real 2026
team_years, written identically to the DB and toparquet/2025/team_years.parquet.get_init_epa()computed from the Parquet seed vs the DB seed over 60 teams x all EPA components: max diff 0.00e+00.Diff: +252/-37, 11 files. Rig-only; staging never touched.
First-deploy / bootstrap order (db-less)
An empty bucket has no Parquet and no snapshot, so serving and the pipeline must be brought up in order:
reset_all_years, or a run with the DB still up that writes Parquet) so prior years exist before any db-less cross-season seed — otherwise every team rookie-seeds (now loudly flagged via/info, see above)./v3and/v3/sitereturn empty (200), not 500 — team endpoints included (_teams_sourceglob fallback). The site rides its bucket blobs meanwhile.Verified on the rig: a cold
get_team(254)against an empty bucket returnsNone(no crash); after one folded cycle publishes Parquet, DuckDB serves254 -> "The Cheesy Poofs",year 2026,team_year epafrom Parquet.Decommission checklist (maintainer's one-way step)
This PR leaves
src/db/and the CockroachDB dependency in place — withDISABLE_DBset the DB simply has no readers and no writers. When you are ready to make it final:DISABLE_DB=Truefor a soak period (pipeline +/v3+/v3/siteall db-less), watching the Parquet/snapshot blobs as the sole source of truth.src/db/and its now-unused imports; collapsesrc/data/backend.py/src/site/backend.py/src/api/backend.pyto the DuckDB arm only (theelsearms andDISABLE_DBguards become dead).sqlalchemy-cockroachdb/psycopg2from dependencies; deleteclean_db,read_objs/write_objs,src/db/functions.After that the relational DB is gone and Parquet + snapshot blobs are the whole system.