🎉🤖 Declare datasets to the Admin API instead of upserting them - #6700
Draft
Marigold wants to merge 6 commits into
Draft
🎉🤖 Declare datasets to the Admin API instead of upserting them#6700Marigold wants to merge 6 commits into
Marigold wants to merge 6 commits into
Conversation
Grapher owns the tables that hang off `variables` and the chart configs a variable leaves behind in MySQL and R2, so it now owns the delete. ETL asks what would be deleted, applies its own environment-dependent policy to anything still used by a chart, and only then lets Grapher delete. Set SKIP_GHOST_VARIABLE_CLEANUP=1 to work locally without a running admin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the SKIP_GHOST_VARIABLE_CLEANUP env var: a refused connection means no admin server is running, which locally is normal, so warn instead of failing. Cleanup still reports as unsuccessful, so the checksum stays unset and a later run against a reachable admin does the sweep. Other admin API errors still propagate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the dry-run round trip and the _cleanup_is_allowed wrapper. Grapher deletes the ghosts no chart uses and hands back the ones it didn't touch, with variable names and chart slugs; ETL prints them and applies its existing raise-or-warn rule inline, the way it did before this moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two Codex findings. The cleanup call went through the plain session, so a staging admin restart returning 502 would fail every grapher step, including ones with nothing to clean — it now uses requests_with_retry(), which needed POST added to allowed_methods. And the unreachable-admin fallback caught deployed environments too, quietly skipping cleanup while the deploy reported success. It's now local-only; staging and production raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
upsert_metadata used to write the variable row, commit, then issue four SELECTs to read back what it had just written — because the ids, timestamps and resolved tag/FAQ links only exist once MySQL has seen them. The Admin API now does both halves, so it can assemble <id>.metadata.json from the rows in hand. Variables go in chunks sized by payload bytes rather than count, since metadata size varies by an order of magnitude. ETL keeps what needs the values: the checksums, the inferred type, the timespan, and the distinct entity ids and years the JSON lists — and it still uploads both R2 files. Deletes the 219 lines of data_metadata.py that existed only to read the metadata back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
Quick links (staging server):
Login: chart-diff: ✅No charts for review.data-diff: ✅ no differencesNo differences found. Automatically updated datasets matching excess_mortality|covid|fluid|flunet|country_profile|garden/ihme_gbd/2019/gbd_risk are not included. Run locally with Edited: 2026-08-18 11:54:30 UTC |
ETL now states what a dataset should contain and lets Grapher reconcile it, rather than upserting rows and inferring deletions from what it didn't mention. Three calls, all by catalogPath — no variable ids cross the boundary any more. Metadata is always sent: Grapher compares it against what it last published, which is where the previous version actually lives. That removes calculate_checksum_metadata and _dataset_metadata_fields, and with them the keep-list ETL used to carry across every chunk — if nothing is ever skipped, every live indicator is named in every run. ETL keeps what needs the values: the data checksum, the inferred type, the timespan, the distinct entities and years, and the data.json upload. It reports the data checksums only once those uploads have gone through. SUBSET and INSTANT runs never declare — a partial list would read as "this dataset should contain less" and delete real indicators. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #6694 — base is
ghost-variables, notmaster. Needs owid/owid-grapher#7001 (same branch name, one staging server).ETL now declares what a dataset should contain and lets Grapher reconcile it, instead of upserting rows and inferring deletions from what it didn't mention. Three calls, all addressed by catalogPath — no variable ids cross the boundary any more.
Why declaring changes so much
A ghost indicator used to be identified by absence — nobody says "delete this", the signal is that nobody mentioned it. That's why ETL had to accumulate every variable id across ~95 chunk calls and hand the list over at the end.
Sending metadata for all indicators every run removes that: if nothing is ever skipped, every live indicator is named in every run, so the first call's list is the complete membership and deletions follow from it directly. No keep-list, no run id, no session state.
That also deletes
calculate_checksum_metadata— the most bug-prone function in the file, carrying scar tissue from two real incidents — along with_dataset_metadata_fieldsand its hand-maintained "kept in sync with" invariant. Grapher compares metadata against what it last published, which is where the previous version actually lives.What ETL keeps
Everything that needs the values, which Grapher never receives: the data checksum, the inferred
type, thetimespan, the distinct entity ids and years, and thedata.jsonupload itself. Entity names and codes are resolved Grapher-side, so the payload carries ids.Nothing is written before it's true
Grapher publishes
metadata.jsonitself now, so the row, the R2 object andmetadataChecksumland together inside one call.dataChecksumis written only by the third call, after ETL's uploads have gone through. Whoever publishes a file records its checksum — which is why there's no confirmation call and no in-progress marker.A run that dies mid-way leaves those files looking stale and gets them re-uploaded next time. Wasteful, never wrong.
Cost we're accepting
A one-line
.meta.ymledit on a huge dataset now sends all its metadata — ~470MB onwho/2021-07-01/ghe/estimates(47,160 indicators in a single table). The bill lands on a handful of datasets and buys the removal of the run-state machinery.Still open
SUBSET/INSTANTruns never declare and never record a checksum — correct, but it means a filtered run no longer cleans up anything. Worth a second opinion on whether that's the behaviour we want.Details
Files
etl/grapher/to_db.py—prepare_indicator(payload + data checksum, no skipping) andflush_indicator_batch(send chunk, upload the data files Grapher asks for, return their checksums).upsert_dataset,upsert_metadata,upload_metadata,upsert_origins,calculate_checksum_metadata,_dataset_metadata_fieldsandcleanup_ghost_variablesare all gone.blocked_indicators_allow_runkeeps the raise-or-warn policy, which stays here because it depends on the environment and a chart-diff against production.apps/chart_sync/admin_api.py—put_dataset,put_indicators,put_dataset_checksum, all throughrequests_with_retry().etl/steps/__init__.py— prepares every indicator, declares, then chunks by payload bytes._filter_indicatorshandles the SUBSET/INSTANT paths.etl/grapher/helpers.py—_dataset_metadata_payload.apps/backport/datasync/data_metadata.py— 295 lines → 77.lib/owl/owl/grapher.py— same flow.Checked
make checkandmake unittest(679 passed).