Skip to content

🐛 Layer-aware chart diff and sync for ETL-authored charts - #6581

Closed
pabloarosado wants to merge 4 commits into
etl-chartsfrom
chart-diff-layers
Closed

🐛 Layer-aware chart diff and sync for ETL-authored charts#6581
pabloarosado wants to merge 4 commits into
etl-chartsfrom
chart-diff-layers

Conversation

@pabloarosado

@pabloarosado pabloarosado commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Written by Claude Fable 5 — @pabloarosado at the wheel.

Stacked on #6511 "Charts as first-class citizens in ETL, addressed by chart config UUID". Fixes how chart-diff and chart-sync treat ETL-authored charts, so that production admin edits can never be silently overwritten and unrelated branches stop seeing phantom chart changes. ETL-only changes; no owid-grapher changes required.

The problem (two scenarios)

Phantom diffs on unrelated branches. An ETL-authored chart is rebuilt from its YAML by every staging server whose branch contains it. That rebuild carries no production admin edits, so the moment an admin hotfixes the chart in production, every staging server's copy differs from production and the chart shows up in every branch's chart-diff — as if the branch had changed it. The "diff" it shows is literally the hotfix being undone.

Silent overwrite of production hotfixes. For cross-environment twins (same config UUID, different numeric ids — the normal state of an ETL-authored chart), the edited-in-production conflict check was disabled unconditionally, and approvals were recorded with a NULL target timestamp that later production edits couldn't invalidate. Approving such a phantom diff and merging made chart-sync push the staging config over production, deleting the hotfix with no warning (flagged by Codex on #6511 "Charts as first-class citizens in ETL, addressed by chart config UUID" as P1).

The model

An ETL-authored chart is two layers with two owners and two delivery routes:

Layer Owner Reaches production via
etlConfig (code layer) git production's ETL run after merge
admin patch admins chart-diff approval + chart-sync

Chart-diff should list a chart only when the branch changed a layer; chart-sync should write only layers the branch changed.

What changed

  • chart-diff selection (_modified_chart_configs_on_staging): ETL-managed charts are compared layer by layer instead of full-vs-full. A chart is listed only if the code layers differ, or the staging patch is non-pristine and differs from production's. Production-only patch edits no longer surface on unrelated branches.
  • Pristine patch: a patch holding only the ETL bootstrap keys (slug, $schema, version, id) and unpublished counts as untouched. Publishing, or any config override, marks it as a deliberate admin edit.
  • chart-sync: for ETL-managed charts, update_chart (which the server re-diffs into the target's patch) runs only when the staging patch is non-pristine — in both the update and the create paths. A pristine patch can never clobber production's.
  • Conflicts: the twin override now applies only while the staging patch is pristine (when sync can't touch production's patch anyway). Once the staging patch carries edits, a production edit is flagged as a conflict again.
  • Approvals: _target_updated_at_for_review binds approvals to the production timestamp that was reviewed, twins included, so a later production edit invalidates a stale approval.
  • Chart.load_patch_config() added to the grapher model (mirrors load_etl_config). After the chart_configs refactor a chart's rendered config, admin patch and ETL layer are three chart_configs rows (configId, patchConfigId, patchConfigIdETL); the layer comparison reads each from its own row.
  • Follows the column renames from owid/owid-grapher#6826 "ETL-authored chart configs, addressed by chart config UUID" (charts.configIdETLpatchConfigIdETL, charts.catalogPathetlConfigCatalogPath) in the ORM, chart-diff, chart-sync and etl chart-config-id. These touch #6511 "Charts as first-class citizens in ETL, addressed by chart config UUID"'s code and can be moved down to that branch.

The staging-admin warning

For ETL-authored charts, the branch's medium is the YAML; edits made in the staging admin are legal but should be a conscious choice. When chart-diff finds an ETL chart with deliberate staging-admin edits that differ from production, it now shows a warning: the edits will sync as an admin override, and the reviewer is nudged to move them into the chart's .config.yml (versioned, reviewed with the code). Long-standing overrides on adopted charts that are identical on both sides don't trigger it.

Known limitations

  • A slug rename made in the staging admin is indistinguishable from the ETL bootstrap slug and reads as pristine (won't sync). Renames should be made in the YAML.
  • When the target DB doesn't yet have charts.patchConfigIdETL (production before owid/owid-grapher#6826 "ETL-authored chart configs, addressed by chart config UUID" deploys), the comparison falls back to today's full-config behavior.
  • The UI still shows code-layer changes as ordinary approvable diffs. Approving them is now harmless (sync writes nothing for them), but reclassifying them as review-only — like data changes — is a possible follow-up.

Test plan

  • New scenario tests in tests/apps/wizard/app_pages/chart_diff/test_chart_diff.py: pristine-patch detection, phantom hidden on unrelated branches, code-layer change listed, staging-admin edit listed, adopted chart with identical patches hidden, twin conflict gated on staging patch.
  • Two existing twin tests updated for the new approval binding.
  • Full non-integration suite passes (839 tests), make check clean.

🤖 Generated with Claude Code

@owidbot

owidbot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Quick links (staging server):

Site Dev Site Preview Admin Wizard Docs Docs Preview

Login: ssh owid@staging-site-chart-diff-layers

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-08-26 17:25:56 UTC
Execution time: 5.12 seconds

@pabloarosado
pabloarosado marked this pull request as ready for review July 31, 2026 12:14
@pabloarosado
pabloarosado requested a review from danyx23 July 31, 2026 12:14
@pabloarosado

pabloarosado commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Written by Claude Fable 5 — @pabloarosado at the wheel.

Four scenarios showing what this PR changes in practice. The cast: DS1 is a data scientist who created a chart from ETL (the chart's configuration lives in a YAML file in this repository, and the chart is live on the website). DS2 is another data scientist with an open pull request. Admin is a colleague who edits charts in the production chart admin.

Two background facts that all scenarios rely on:

  • Every pull request gets a staging server. The staging server runs the branch's ETL, and the ETL rebuilds every ETL-authored chart from its YAML file.
  • A chart rebuilt from a YAML file contains only what the YAML file says. Edits that a person made in the production chart admin live only in the production database; the staging server's copy of the chart does not have them.

Scenario 1: a typo is fixed in production while an unrelated pull request is open

Admin notices a typo in the live chart's subtitle and fixes the typo directly in the production chart admin. DS2 has an open pull request about something unrelated, say a tourism dataset. DS2's staging server rebuilt DS1's chart from the YAML file, so the staging copy still has the typo.

  • Today: chart-diff compares the staging copy with the production chart, sees the subtitles differ, and lists the chart in DS2's pull request as if DS2 had changed it. The difference on screen is, literally, the typo fix being removed. No warning appears (chart-diff's usual "this chart was edited in production" warning does not work here, because the staging server created its own copy of the chart instead of starting from production's copy). If DS2 approves the chart, which is a natural thing to do when wrapping up a review, then after the merge chart-sync copies the staging version to production. The typo fix is deleted, and nobody is told.
  • With this PR: chart-diff asks what DS2's branch actually changed about this chart. The YAML configuration is unchanged. Nobody edited the chart in the staging admin. So the chart does not appear in DS2's chart-diff at all, chart-sync does not touch it, and the typo fix stays in production.

Scenario 2: a data update rebuilds the chart while production carries a fix

DS2's pull request updates the dataset that DS1's chart plots. The staging server rebuilds the chart so that it points at the new version of the indicators. Production, meanwhile, carries Admin's subtitle fix.

  • Today: chart-diff correctly lists the chart, because the data behind it changed. But when DS2 approves, chart-sync copies the entire staging configuration to production, including the subtitle without the fix. The data update goes through, and the subtitle fix is deleted as a side effect.
  • With this PR: the chart is still listed, and it should be: DS2 must check that the chart looks right with the new data. But on approval, chart-sync writes only the part the branch changed, which is the configuration that comes from the YAML file. The subtitle fix lives in production's admin edits, and stays untouched.

Scenario 3: someone edits an ETL chart in the staging admin

During review, DS1 changes the chart's y-axis in the staging server's chart admin, instead of changing the YAML file.

  • Today: the edit shows up in chart-diff like any other change. Nothing tells the reviewer that this edit bypasses the YAML file. And if Admin edited the same chart in production in the meantime, no conflict warning appears, so approving would overwrite Admin's production edit.
  • With this PR: the edit shows up with a warning: these changes were made in the staging admin; if approved, they will be copied to production as admin overrides; consider moving them into the chart's YAML file instead, where they are versioned and reviewed like any code change. And if Admin edited the same chart in production in the meantime, chart-diff now shows a conflict and blocks approval until the reviewer resolves it.

Scenario 4: a chart is approved, then edited in production before the merge

On Monday, DS2 approves the chart in chart-diff. The pull request is not merged yet. On Wednesday, Admin edits the same chart in the production admin. On Friday, the pull request is merged.

  • Today: the Monday approval is still considered valid, because for ETL-authored charts the approval does not record which production version was on the reviewer's screen. Chart-sync proceeds and overwrites Wednesday's production edit under Monday's approval.
  • With this PR: the approval records which production version the reviewer saw. Wednesday's edit makes Monday's approval stale, the chart goes back to "pending", and someone has to look at it again before anything is synced.

🤖 Generated with Claude Code

@danyx23

danyx23 commented Aug 25, 2026

Copy link
Copy Markdown
Member

@pabloarosado I've refreshed the 3 PRs (2 in grapher, one here) with recent additions. Can you refresh this PR to adapt to the changes made on the PR below and the grapher api changes that might be relevant?

pabloarosado and others added 3 commits August 26, 2026 09:32
An ETL-authored chart is two layers with two owners: the etlConfig (owned
by git, delivered by the production ETL) and the admin patch (owned by
admins, delivered by chart-sync). Comparing and syncing whole configs made
production admin hotfixes look like branch changes on unrelated staging
servers, and let an approved sync silently overwrite them.

- chart-diff compares ETL-managed charts layer by layer: a chart is listed
  only when the branch changed a layer (code layer differs, or the staging
  patch was deliberately edited).
- chart-sync writes the admin patch only when the staging patch is
  non-pristine; a pristine patch never overwrites production's.
- Cross-env twin conflicts are re-enabled whenever the staging patch
  carries edits, and approvals now bind to the production timestamp they
  reviewed, so later production edits invalidate them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
For ETL-authored charts, the branch's medium is the config YAML; edits made
in the staging admin are legal but should be a conscious choice. Chart-diff
now shows a warning when an ETL chart carries deliberate staging admin edits
that differ from production, explaining they will sync as an admin override
and suggesting to move them into the YAML.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
pabloarosado added a commit to owid/owid-grapher that referenced this pull request Aug 26, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous staging build ran the ETL export step against the old admin
process while the grapher build was still restarting it, so the by-config
etlConfig route returned 404.

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

Copy link
Copy Markdown
Contributor Author

@pabloarosado I've refreshed the 3 PRs (2 in grapher, one here) with recent additions. Can you refresh this PR to adapt to the changes made on the PR below and the grapher api changes that might be relevant?

Thanks Daniel, done.

@pabloarosado

Copy link
Copy Markdown
Contributor 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

Here are some automated review suggestions for this pull request.

Reviewed commit: 6772a2ee72

ℹ️ 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 on lines +244 to +245
if patch.get("isPublished"):
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not treat existing publication as a staging edit

When an ETL-authored chart was already published before the staging server was created, its source patch legitimately contains isPublished: true; this makes every such patch non-pristine. If production then receives an admin-only hotfix, etl_chart_has_branch_changes interprets the resulting patch difference as a branch change, and chart-sync sets push_admin_patch and can replace the production patch with staging's version—the exact phantom-diff/overwrite scenario this change is intended to prevent. Track whether the patch was edited on this staging branch rather than treating a longstanding publication flag as evidence of a staging edit.

Useful? React with 👍 / 👎.

Comment on lines +1220 to +1222
target_df = read_sql(
base_q(target_session) + where, target_session, params={"chart_ids": tuple(source_df.chartId.unique())}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Match target layers using the stable chart identity

For cross-environment twins—the normal case described here where staging and production have different numeric IDs but share a config UUID—this query searches production using the staging numeric ID. The target row is therefore absent or unrelated, target_df["chartChecksum"] is null, and the new layer-aware comparison is never executed, so production-only hotfixes still appear as phantom changes. Resolve target rows by configId (or etlConfigCatalogPath) and align them back to the source chart IDs before comparing layers.

Useful? React with 👍 / 👎.

Comment on lines +1249 to +1251
if not etl_chart_has_branch_changes(
json.loads(source_etl_raw), json.loads(target_etl_raw), source_patch, target_patch
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve inheritance-flag changes in layer comparison

When an ETL-managed chart changes only isInheritanceEnabled, the checksum differs, but this branch compares only the ETL and patch JSON layers and marks the configs equal because the table-level flag is passed to neither side. The chart consequently disappears from chart-diff; moreover, a pristine patch makes chart-sync skip update_chart, so the changed flag would not reach production even if another change selected the chart. Include isInheritanceEnabled in the layer-aware decision and ensure such a change triggers the chart update.

Useful? React with 👍 / 👎.

@pabloarosado

Copy link
Copy Markdown
Contributor Author

Closing this PR. The four scenarios this PR was meant to fix work already on the base branch, after the column renames (done in #6765). The additional layer-aware logic is not needed.

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