Skip to content

🎉 Charts as first-class citizens in ETL, addressed by chart config UUID - #6511

Open
danyx23 wants to merge 40 commits into
masterfrom
etl-charts
Open

🎉 Charts as first-class citizens in ETL, addressed by chart config UUID#6511
danyx23 wants to merge 40 commits into
masterfrom
etl-charts

Conversation

@danyx23

@danyx23 danyx23 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Written by Claude Opus 5 — @danyx23 at the wheel.

Summary

This is a slight modification of @pabloarosado's existing "charts as first-class citizens" work in #6174 — the collection/chart-upsert machinery and the layered etlConfig approach are his. This PR adds two commits on top that make the chart's config UUID (charts.configId) its identity, both for addressing charts and for matching them across environments, instead of the numeric auto-increment id. Companion grapher PR: owid/owid-grapher#6826 (stacked on owid/owid-grapher#6825).

New on top of #6174:

  • chart_upsert: replaces the bootstrap create + etlConfig PUT with the new idempotent PUT /charts/by-config/:uuid/etlConfig upsert. A new chart's admin patch starts out empty so the ETL layer owns dimensions etc. from birth (no more rediff workaround needed for fresh charts). Retries can no longer create duplicate charts.
  • The UUID is declared in the config YAML, not looked up. See below — this is the main departure from 🎉 Charts as first-class citizens in ETL: layered approach with grapher #6174's addressing model.
  • chart-diff: matches source/target charts by configId first (the column exists on production today, so no runtime column guard is needed), then catalogPath, keeping id+createdAt only as a legacy fallback. Catalog-path twins generalize to cross-env twins.
  • chart-sync: carries the source chart's configId when creating charts on the target (via the upsert endpoint for ETL-authored charts, and a new configId param on POST /charts for hand-authored ones), so future diffs match the rows directly. Narrative charts are now matched by their chartConfigId instead of the collision-prone numeric id.
  • model: Chart.load_chart supports config_id lookups; NarrativeChart.load_by_chart_config_id added.
  • etl chart-config-id: new CLI to write the UUID into a chart's config YAML — see below.

Requires the grapher endpoints from owid/owid-grapher#6826.

How chart identity differs from the base PR

In #6174 a single chart's ETL identity was resolved from the target database: chart_upsert looked the chart up by catalogPath, fell back to matching on slug, adopted whatever configId it found, and minted a fresh UUID only when nothing matched.

Here the identity is declared in the config YAML as a new required top-level field, and nothing is looked up:

chart_config_id: "0191b6c7-5595-70b2-8d30-fa03fccd7add"
topic_tags:
  - "Animal Welfare"
dimensions: []
views:
  - ...

Why the change: a lookup makes the identity environment-derived, so the same YAML can resolve to a different chart in each environment, or create a new chart wherever the lookup misses (e.g. a chart whose slug was changed in the admin). Declaring the UUID makes one YAML address one chart everywhere — local, staging and production — and makes "which chart does this step own?" answerable by reading the file.

Consequences:

  • chart_config_id is required for single-chart collections (dimensions: []) and rejected on mdims, which stay identified by their catalog path.
  • Bringing an existing chart into ETL means putting that chart's current charts.configId in the YAML. Creating a new chart means minting a UUIDv7 (new_chart_config_id(), formerly the private _uuid_v7).
  • Validation is deliberately strict, in Collection.validate_chart_config_id(): the value must be a canonical lower-case dashed UUID. uuid.UUID() on its own also accepts {braces}, a urn:uuid: prefix, undashed hex and upper case — none of which match grapher's CHAR(36) lookup key, so they would have missed the intended chart and silently created a new one. A non-v7 UUID is accepted but logged as a warning, since every chart config UUID grapher has generated so far is a v7. The check runs from Collection.save() (covering programmatically-built collections) and again from upsert_collection_as_chart() (so no entry point bypasses it), and the multidim schema carries the same canonical-form pattern for YAML configs.
  • Dropped guard: the previous slug-based lookup raised if a step's slug collided with a chart owned by a different ETL step. Duplicate slugs are now left to grapher to reject. If we want that check back it can be a build-time check over the config YAMLs, which needs no DB.

etl chart-config-id

Since the UUID now has to be in the file, there's a subcommand to put it there, rather than expecting people to invent a UUIDv7 or hand-query the DB:

etl chart-config-id new <config.yml>                                    # brand-new chart
etl chart-config-id lookup <config.yml> --slug banning-of-chick-culling # existing chart, by slug
etl chart-config-id lookup <config.yml> --chart-id 7118                 # existing chart, by numeric id

lookup queries the configured grapher DB (OWID_ENV, or --env <staging-branch> / --env <path/to/.env>). The chart is always named explicitly — by slug, or by the numeric id that the admin shows and that older automation carries — because guessing which chart is meant is precisely the failure this field exists to prevent.

Guards, since this field decides chart identity:

  • Only single-chart configs are accepted (dimensions: [] and exactly one view), so an mdim — including one that fills dimensions programmatically — can't be given a field that would then be rejected at build time.
  • An existing chart_config_id is never replaced without --force.
  • Re-writing the same value is a no-op, so the command is safe to re-run.
  • A slug matching several charts resolves to the published one, or errors telling you to pick with --chart-id.

The write goes through ruamel, so comments and formatting survive.

banning_of_chick_culling.config.yml — the one ETL-authored single chart so far — declares the UUID of the chart that already exists at /grapher/banning-of-chick-culling, so pushes keep landing on it rather than orphaning it.

Test plan

  • Extended chart-diff matching tests (config-UUID twins, catalogPath twins, legacy id+createdAt fallback).
  • chart_config_id validation tests: required for single charts, rejected on mdims, must be a string, must be canonical (parametrized over the four lenient forms uuid.UUID would otherwise wave through), non-v7 warning, and UUIDv7 format/time-ordering of the minted value.
  • etl chart-config-id tests: accepted/rejected config shapes, overwrite guard, no-op on re-write, comment preservation, and the exactly-one-identifier rule.
  • Both lookup modes exercised against staging-site-etl-charts: --slug banning-of-chick-culling and --chart-id 7118 both resolve to 0191b6c7-5595-70b2-8d30-fa03fccd7add, matching the UUID committed in the YAML.
  • make check and the affected test files pass.

🤖 Generated with Claude Code

@owidbot

owidbot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Quick links (staging server):

Site Dev Site Preview Admin Wizard Docs Docs Preview

Login: ssh owid@staging-site-etl-charts

chart-diff: ✅ No charts for review.
data-diff: ✅ no differences

No 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 etl diff REMOTE data/ --include <dataset> --verbose.

Edited: 2026-07-31 10:07:01 UTC
Execution time: 4.40 seconds

@danyx23

danyx23 commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

etl/apps/chart_sync/cli.py

Lines 203 to 206 in 5d174a2

select(gm.Variable.datasetId)
.distinct()
.join(gm.ChartDimensions, gm.ChartDimensions.variableId == gm.Variable.id)
.where(gm.ChartDimensions.chartId.in_(chart_ids), gm.Variable.datasetId.isnot(None))

P2 Badge Query archived datasets with target chart IDs

Written by Claude GPT-5.6 Sol — @danyx23 at the wheel.

With --archive, UUID/catalog-path twins can have different source and target numeric IDs, but this target-session query still filters with the source IDs collected above. Consequently it does not collect the datasets currently used by the actual target charts, so datasets orphaned by their synchronization are omitted from the later archive pass. Build this filter from each diff's target_chart.id when a target twin exists.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/wizard/app_pages/chart_diff/chart_diff.py
Comment thread etl/collection/model/core.py Outdated
Comment thread .claude/skills/chart-editing/SKILL.md Outdated
Comment thread .claude/skills/chart-editing/SKILL.md Outdated
@pabloarosado

pabloarosado commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Thanks @danyx23! While testing the scenario of creating a new chart from VSCode, I realised that the grapher chart editor was empty (showing a "No table loaded yet"). This looked like a bug, but it turned out to be an issue with the staging server. This was Fable's assessment:

The box serves the admin client from live-data/bakedSite/assets-admin/, and that bundle had been frozen since the server's creation (Jul 24, 06:38) even though the grapher checkout and server-side code were current and fresh builds kept landing in owid-grapher/dist/. So the editor was running pre-PR client code that doesn't know about the etlConfig layer. It manually copied dist/assets-admin/ into bakedSite/ yesterday, so the editor works now, but whatever re-bakes those assets on this box is still broken (staging-site-etl-chart-config re-bakes fine, most recently Jul 30 at 11:32). Until that's fixed, client-side changes you push won't reach the served admin bundle.

Apart from that staging server hiccup, etl chart creation and editing is working as expected, and the addition of UUID to the chart steps makes sense.

pabloarosado added a commit that referenced this pull request Jul 31, 2026
Codex review fixes on #6511: an mdim whose dimensions were all pruned
(single choice in use) was reclassified as a single chart and failed the
chart_config_id validation with a misleading error — chart mode is now
decided by the declared chart_config_id, with an actionable error for the
pruned case. The chart-editing skill no longer pins an outdated $schema
and no longer prescribes --force --only as the normal push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@pabloarosado pabloarosado left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this work, Daniel. I've tested it and works as expected (except for an issue I mentioned that seems to be temporary).
I've also created this other PR to handle how chart diff and chart sync should behave. But it shouldn't block the current PR.

@danyx23

danyx23 commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

Written by Claude Opus 5 — @danyx23 at the wheel.

Heads-up: the grapher side of this work (owid/owid-grapher#6826) removes PUT /charts/:chartId/etlConfig. ETL config pushes are now addressed only by the chart's config UUID, via PUT /charts/by-config/:chartConfigId/etlConfig.

This branch still calls the removed endpoint in one place: AdminAPI.put_chart_etl_config in apps/chart_sync/admin_api.py, used from apps/chart_sync/cli.py when syncing a chart that already exists in the target environment. etl/collection/chart_upsert.py is unaffected — it already uses upsert_chart_etl_config.

The fix is to drop put_chart_etl_config and call upsert_chart_etl_config instead, with the target chart's own config UUID, which is available right at that call site:

resp = target_api.upsert_chart_etl_config(
    chart_config_id=diff.target_chart.configId,
    grapher_config=migrated_etl_config,
    catalog_path=diff.source_chart.catalogPath,
    user_id=user_id,
)

It has to be target_chart.configId, not source_chart.configId: for a cross-environment twin matched on catalogPath the two can differ, and the by-config endpoint would then create a new chart in the target instead of updating the matched one.

Two related grapher-side changes worth knowing about:

  • catalogPath is no longer treated as a chart's identity. A push carrying a different path than the one on record now simply updates it, so a renamed or moved step is an ordinary update rather than a 409. Only assigning a path that already belongs to a different chart is still rejected.
  • Chart config UUIDs are normalised to lower case when stored. The case-insensitive comparison added in 9f278a9 stays correct, and should stop being needed for newly written rows.

DELETE /charts/:chartId/etlConfig is unchanged (and has no callers here — delete_chart_etl_config is defined but never used).

These two branches need to land together, so this one should be updated before the grapher PR merges.

pabloarosado and others added 18 commits August 25, 2026 16:21
Lets ETL push standalone Grapher charts as zero-dimension mdim
collections. A `.config.yml` with `dimensions: []` and exactly one
view is routed through `Collection.upsert_to_db` to a new endpoint
that writes the chart's ETL-authored grapher config into
`chart_configs.etlConfig` (added on the grapher side).

The endpoint lives on owid-grapher:
  PUT /admin/api/charts/:id/etlConfig

Admin patches in `chart_configs.patch` are preserved across ETL
re-pushes by construction — ETL and admin write to different
columns. No drift-check apparatus, no bot-user stamping, no
ETL_FORCE_CHART. The full rendered config is recomputed server-side
as merge(variableETL, etlConfig, patch).

Contents:
- `apps/chart_sync/admin_api.py`: new `put_chart_etl_config` and
  `delete_chart_etl_config` methods.
- `etl/collection/chart_upsert.py`: new module that translates a
  zero-dim collection view into a grapher chart config (resolving
  catalog paths to variable IDs) and pushes it via the new endpoint.
  Creates the chart with a minimal bootstrap if it doesn't exist yet.
- `etl/collection/model/core.py`: `Collection.upsert_to_db` short-
  circuits to `upsert_collection_as_chart` when `dimensions` is empty.
- `etl/steps/__init__.py`: `ExportStep` can run from a `.config.yml`
  with no Python script for `export://multidim/...` paths.
- `apps/step_update/cli.py` + `etl/version_tracker.py`: recognise
  YAML-only export steps in step discovery and version tracking.
- `schemas/multidim-schema.json`: documents that `dimensions` can be
  empty for single-chart collections (the schema already allowed it
  structurally — `minItems` was never set).
- `etl/steps/export/multidim/animal_welfare/latest/banning_of_chick_culling.config.yml`
  + DAG entry: chick-culling chart as the example/test artifact.

Depends on owid-grapher PR #6553 (the endpoint and the etlConfig
column don't exist on master yet). To test end-to-end on staging:
SSH into the staging server, check out `etl-chart-config` in the
owid-grapher folder, run the migration, restart admin, then run
this ETL step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `Collection.upsert_to_db`: when dimensions are empty, explicitly require
  exactly one view before dispatching to chart upsert. Previously the
  assertion lived only inside `upsert_collection_as_chart`; surfacing it at
  the dispatch point makes the implicit "zero-dim ⇒ single chart" behaviour
  switch concrete with a clear error for the malformed case.

- `dag/animal_welfare.yml`: rewrite the test-scaffolding-flavoured comment
  on the chick-culling export step to a normal one-liner.

Resolves codex findings 4 and 5 from the PR review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DAG entries for `export://multidim/...` steps backed by a `.config.yml`
(single-chart or mdim collections without a `.py`) were not clickable
because the resolver only looked for `.py`, `__init__.py`, or `.ipynb`.
Add `.config.yml` to the candidate list for `export://` URIs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-chart `.config.yml` files (zero-dim collection + no sibling `.py`)
push to the chart admin endpoint and are created unpublished by default.
The public `/grapher/{slug}` route doesn't render unpublished charts, so
clicking "Preview chart" was failing for newly-created ETL-authored
charts. Detect the single-chart case and route the preview through
`/admin/grapher/{slug}` instead, which renders drafts.

`parseExportMultidim` becomes async and returns `isChart` + `chartSlug`
based on (a) `dimensions: []` in the .config.yml AND (b) no sibling
`.py` file (multi-dim steps like air_pollution use `dimensions: []` as
a placeholder filled programmatically — those still go via the mdim
route).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`OWIDEnv.admin_site` already ends in `/admin`, so prefixing `/admin/charts/...`
produced URLs like `http://.../admin/admin/charts/9138/edit`. Drop the duplicated
segment; matches the convention used by `datasets_admin_site` etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `/grapher/<slug>.png` route only renders published charts. For drafts
it 404s — which is the common case while iterating on an ETL-authored
chart. owid-grapher already exposes `/grapher/by-uuid/<chart_configs.id>.png`
which serves any chart from R2's `byUUID/` directory, published or not.

Adds `get_chart_png_url.py`: takes a chart slug or numeric chart id,
queries the staging DB via `OWID_ENV.read_sql` to resolve it to the
`chart_configs.id` UUID, prints the by-uuid PNG URL. Accepts extra
grapher query params via `--key=value` flags (tab, time, country, …).

Updates SKILL.md to recommend this as the primary path; keeps the
agent-browser path as a fallback for MDIM data pages and interactive UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous content documented a `.chart.yml` system in `etl/steps/graph/`
that was an unmerged experiment and no longer exists in the repo. Rewrite
the skill to cover the actual Phase 1 workflow: single ETL-authored charts
as zero-dim mdim collections in `etl/steps/export/multidim/`, pushed via
`chart_configs.etlConfig`, previewed via the `by-uuid` PNG route.

Covers: file layout, minimum viable config, DAG entry, indicator catalog
paths + display.name overrides, common chart-config edits, push command,
preview via the check-chart-preview skill, and the admin co-editing model.

Adds a "when NOT to use" section pointing at create-multidim for charts
with dropdowns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hens-by-housing-system chart was an iteration experiment, not a
canonical reference. The chick-culling chart is the committed Phase 1
example in this repo, so cite only that. Replace the eggs-and-hens
indicator paths in the display-name example with placeholders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- chart_upsert: set topic_tags on newly created ETL-authored charts (admin-owned after creation)
- chart-preview ext: use fs.access for .py existence check instead of readFile
- clickable-dag-steps: backfill 0.0.4 changelog entry

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…quired

- Schema: drop `title`/`default_selection` from top-level required; drop `title_variant` from the title block's required.
- Model: `Collection.title` and `Collection.default_selection` default to None.
- Runtime: error when an mdim collection has no title; per-view warning when a non-map-only view has no entity selection fallback (neither collection-level `default_selection` nor per-view `selectedEntityNames`).
- Example YAML + chart-editing skill: drop top-level `title:` / `default_selection:` from single-chart configs (they were ignored in chart-mode anyway).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validate_title() raised for Explorers (Collections with dimensions) even
though explorers manage their own title, breaking test_explorer_migration.
Gate both validations on _collection_type == "multidim".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin etlConfig endpoint only checks the schema version, not structure, so a
typo'd field or wrong type would be stored and silently render wrong. Validate the
built config against the locally vendored grapher schema before pushing (no network),
surfacing a clear error pointing at the offending field. Skips if the config's schema
version isn't vendored locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🎉🤖 Key ETL charts by catalogPath (parallel to mdims)

Persist the collection's catalog_path on charts.catalogPath (via the etlConfig
endpoint) and look up the chart by it instead of by slug. This gives single
charts the same stable ETL identity mdims already have
(multi_dim_data_pages.catalogPath), with the slug left as the mutable public URL.

Behavior change: ETL no longer adopts a hand-authored chart that merely shares a
slug — it keys on catalogPath, so an accidental slug collision surfaces at publish
time instead of silently overwriting.

Pairs with owid-grapher PR (adds the charts.catalogPath column). Targets the
chart-config feature branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📜🤖 Fix put_chart_etl_config docstring for row-model storage

The ETL config is stored in its own chart_configs row (charts.configIdETL),
not in a chart_configs.etlConfig column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ETL-authored charts are created independently on staging and prod, so they
have different ids but a shared catalogPath. Match them across environments
by catalogPath (falling back to id+createdAt for hand-authored charts), sync
the etlConfig layer alongside the main config, and bootstrap a created chart
with its etlConfig + catalogPath so ETL can adopt it. Approvals/conflicts for
"catalog-path twins" (prod minted its own row) key on a NULL target timestamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ins)

set_status recorded a catalog-path twin's approval with the prod chart's
updatedAt, but _get_approvals looks twins up with a NULL target timestamp —
so re-approving an existing ETL chart would lose the approval. Record via
_target_updated_at_for_review so recording and lookup agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pabloarosado and others added 19 commits August 25, 2026 16:23
… columns

Production's charts table doesn't have catalogPath/configIdETL yet (owid-grapher
#6553 isn't merged to master), so chart-diff — which loads prod charts — failed
with "Unknown column", breaking the staging build. Added a
PROD_HAS_ETL_CHART_COLUMNS flag (default False): chart-diff falls back to
id+createdAt matching and defers those columns out of the prod query. No
practical loss yet (prod has no ETL/catalogPath charts). TODO: remove the flag +
defer() once the grapher schema is in prod.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the PROD_HAS_ETL_CHART_COLUMNS flag with a runtime check of the target
(production) DB's information_schema: chart-diff uses catalogPath matching only
when charts.catalogPath/configIdETL actually exist there, otherwise it defers
those columns out of the query and falls back to id+createdAt matching. The ETL
PR merges after owid-grapher #6553, so prod will already have the columns by
then and chart-diff switches over on its own — no flag to flip or code to touch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… run

test_chart_diff.py imported chart_diff at module top. pytest imports every test
module during collection (even ones deselected by -m integration), and importing
chart_diff applies its module-level @st_cache_data decorators, which call the
@cache'd is_running_in_streamlit(). At collection time there's no Streamlit
runtime, so it froze to False process-wide and routed producer_analytics'
cached _process_df onto the buggy non-Streamlit path, crashing an unrelated
integration test. Moving the import into the test bodies keeps collection clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ETL-authored charts now set isInheritanceEnabled=True explicitly when the chart
row is first created, so grapher_config defined on an indicator flows into every
chart built on it. Set only at creation, so existing charts (ETL or admin) are
never touched. No owid-grapher change needed; the admin create endpoint already
honors the inheritance flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charts that pre-date ETL authorship have no catalogPath, so the
catalogPath-only lookup failed against any DB where the step had never
run (rebuilt staging, prod at merge time) and crashed on the slug
collision with the existing chart. Fall back to the slug and adopt the
chart: put_chart_etl_config stamps the catalog path, so later runs find
it by catalogPath directly. Refuse to adopt a chart already owned by a
different ETL step.

Verified on staging: first run adopts chart 7118 with a byte-identical
rendered config (admin map override preserved, no version bump); second
run takes the regular catalogPath update path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use the chart's config UUID (charts.configId) as its stable identity
when talking to the grapher admin and when matching charts across
environments, instead of the numeric auto-increment id:

- chart_upsert: replace the bootstrap create + etlConfig PUT with the
  new idempotent PUT /charts/by-config/:uuid/etlConfig upsert. New
  charts get a client-generated UUIDv7; the admin patch of created
  charts stays empty so the ETL layer owns dimensions etc. from birth.
- chart-diff: match source/target by configId first (works on prod
  today, no new columns needed), then catalogPath, with id+createdAt
  as a legacy fallback; catalog-path twins generalize to cross-env
  twins.
- chart-sync: carry the source chart's configId when creating charts
  on the target (via the upsert endpoint for ETL-authored charts and
  a new configId param on POST /charts for hand-authored ones), so
  future diffs match directly. Narrative charts are now matched by
  their chartConfigId instead of the collision-prone numeric id.
- model: Chart.load_chart supports config_id lookups;
  NarrativeChart.load_by_chart_config_id added.

Requires the corresponding owid-grapher branch (by-config etlConfig
upsert + configId param on chart creation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chart config UUID is the chart's identity, so it belongs in the config
YAML rather than being resolved from whichever environment we happen to be
pushing to.

chart_upsert previously looked the chart up by catalogPath (falling back to
slug) and adopted the `configId` it found, minting a fresh UUIDv7 only when
nothing matched. That makes the identity environment-derived: the same YAML
could resolve to different charts in different environments, or create a new
one where the lookup missed.

- `Collection.chart_config_id`: new top-level field, required for — and only
  valid on — single-chart collections (`dimensions: []`). Mdims keep being
  identified by their catalog path.
- `validate_chart_config_id()`: runs from `save()` (so it covers
  programmatically-set values too) and from `upsert_collection_as_chart()` (so
  no entry point can bypass it). Requires a canonical lower-case dashed UUID —
  `uuid.UUID()` alone accepts braces, `urn:uuid:`, undashed hex and upper case,
  none of which would match grapher's CHAR(36) lookup key, so the upsert would
  silently create a new chart instead. Warns on non-v7, since every chart
  config UUID grapher has generated is a v7.
- chart_upsert: the DB lookup is gone; the UUID comes from the collection and
  goes straight to the by-config upsert endpoint.
- `_uuid_v7()` becomes the public `new_chart_config_id()` — it is now a
  config-authoring helper rather than runtime logic.
- multidim schema: `chart_config_id` with a canonical-form pattern.
- banning_of_chick_culling: declares the existing chart's `charts.configId`
  (0191b6c7-5595-70b2-8d30-fa03fccd7add), so pushes keep landing on the chart
  at /grapher/banning-of-chick-culling instead of orphaning it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writing `chart_config_id` by hand means either inventing a UUIDv7 or digging the
existing chart's `charts.configId` out of the DB, and a wrong value silently
abandons a chart or creates a duplicate. This adds a subcommand for both cases:

    etl chart-config-id new <config.yml>
    etl chart-config-id lookup <config.yml> --slug banning-of-chick-culling
    etl chart-config-id lookup <config.yml> --chart-id 7118

`lookup` reads the configured grapher DB (`OWID_ENV`, or `--env <staging-branch>`
/ `--env <path/to/.env>`). The chart is always named explicitly — by slug or by
the numeric id shown in the admin, which is what older automation carries — since
guessing which chart is meant is the failure the field exists to prevent.

Guards, because this field decides chart identity:

- Only single-chart configs are accepted (`dimensions: []` and exactly one view),
  so an mdim — including one that fills dimensions programmatically — can't get
  the field that `Collection.validate_chart_config_id()` would then reject.
- An existing `chart_config_id` is never replaced without `--force`.
- Writing the same value again is a no-op, so the command is safe to re-run.
- A slug matching several charts resolves to the published one, or errors telling
  you to pick with --chart-id.

The write goes through ruamel so comments and formatting survive; the UUID is
placed first, as the header of the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two leftovers from before the CLI existed:

- The "chart that exists only in the admin" case still said to write the
  `charts.configId` into the YAML by hand; point it at `chart-config-id lookup
  --chart-id` instead.
- "ETL addresses a chart by its config UUID, never by slug or numeric id" read
  as contradicting the `lookup --slug` / `--chart-id` examples right below it.
  Scope it to push time and note that slug/id are how you find the UUID once,
  while authoring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Master's test_every_admin_api_call_has_a_timeout (merged in 8a13943)
enforces a bounded timeout on every call in admin_api.py; the etlConfig
methods predate that rule on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The config pinned grapher-schema.009, two versions behind the current
default. Since only the current schema is vendored in schemas/,
_validate_chart_config found no local file and silently skipped
validation entirely.

Omitting the field lets chart_upsert's setdefault apply
DEFAULT_GRAPHER_SCHEMA (011 today), so the config validates locally again
and won't go stale on the next bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex review fixes on #6511: an mdim whose dimensions were all pruned
(single choice in use) was reclassified as a single chart and failed the
chart_config_id validation with a misleading error — chart mode is now
decided by the declared chart_config_id, with an actionable error for the
pruned case. The chart-editing skill no longer pins an outdated $schema
and no longer prescribes --force --only as the normal push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UUIDs are case-insensitive and the grapher admin accepts a
caller-supplied one in any case, so DB values can differ in case across
environments while naming the same chart. Normalize on the ETL side:
chart-diff and chart-sync twin-matching compare case-insensitively, and
`etl chart-config-id lookup` writes the canonical lower-case form into
the YAML (which is the only form `validate_chart_config_id` accepts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Master renamed `ChartConfig.full` to `ChartConfig.config` and added a test
requiring multidim configs to pin `grapher_schema`. Adapt this branch's
`load_etl_config` and the new single-chart config accordingly, and restore the
`title or {}` guard in `combine_collections` that this branch's optional
`Collection.title` needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 🐛 Follow grapher's chart column renames for the ETL layer

* 🔨🤖 Follow grapher's chart column renames for the ETL layer

owid/owid-grapher#6826 renamed charts.configIdETL to charts.patchConfigIdETL
(an authored layer, like charts.patchConfigId and variables.patchConfigIdETL)
and charts.catalogPath to charts.etlConfigCatalogPath (the step that owns the
ETL layer, not an identifier). Rename the ORM columns and every read of them
in chart-diff, chart-sync and the chart-config-id CLI, and check for the new
names when probing whether production has the columns yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pabloarosado and others added 2 commits August 27, 2026 14:51
The docs still named `chart_configs.patch`, `chart_configs.full` and a
`chart_configs.etlConfig` column. None of those exist: every layer is its own
`chart_configs` row holding a `config`, and which layer you mean comes from the
pointer on `charts` — `configId` for the rendered config, `patchConfigId` for
what an admin authored, `patchConfigIdETL` for what ETL pushed.

Doc-only. The chart-editing skill is the guide people follow, so it should
describe the columns that are actually there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every other extension here keeps its current .vsix in install/ and moves older
builds to install/archive/. chart-preview had 0.0.1, 0.0.2 and 0.0.3 sitting
side by side, so it was not obvious which one to install. package.json is on
0.0.3; the other two are archived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants