Skip to content

feat: ingest ESMValTool reference datasets for provenance#790

Open
lewisjared wants to merge 3 commits into
mainfrom
feat/esmvaltool-reference-datasets
Open

feat: ingest ESMValTool reference datasets for provenance#790
lewisjared wants to merge 3 commits into
mainfrom
feat/esmvaltool-reference-datasets

Conversation

@lewisjared

@lewisjared lewisjared commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Description

Surfaces the reference (observational/reanalysis) datasets that ESMValTool diagnostics use into the database, so they can be recorded for provenance and shown in the frontend alongside the model datasets an execution ran against. This is the first step of the reference-dataset work; the API/frontend surfacing comes in a follow-up.

Ingestion. A dedicated esmvaltool-reference dataset type is ingested during ref providers setup. Because this data is not CMOR/obs4MIPs compliant, its metadata is parsed from the ESMValTool OBS/OBS6, native6 and obs4MIPs layout conventions (DRS path + filename) rather than from file global attributes. This is why it is a deliberately separate dataset type from obs4MIPs, rather than reusing that model.

Declarative reference datasets. Diagnostics now declare the reference datasets they compare against via a reference_datasets specification, instead of hardcoding them inside recipe construction. This gives a single source of truth for provenance (enso, regional historical changes and sea-ice diagnostics migrated).

Provenance linkage. Each execution now records the reference datasets it used, resolved from the ingested catalogue via the declared selectors, so they appear in the execution's dataset list next to the model datasets.

Model consolidation. The near-identical obs4MIPs and PMP climatology dataset models are consolidated onto a shared ReferenceDatasetMixin while remaining distinct dataset types with their own tables and version lineage.

Adds Alembic migration a7b8c9d0e1f2 (child of the add_version_key head) creating the esmvaltool_reference_dataset table.

Checklist

Please confirm that this pull request has done the following:

  • Tests added
  • Documentation added (where applicable)
  • Changelog item added to changelog/

Summary by CodeRabbit

  • New Features

    • Added ingestion and database support for ESMValTool reference (observational/reanalysis) datasets, including provenance tracking for diagnostic executions.
    • Diagnostics can now declare reference datasets declaratively; recipe generation uses these declarations consistently across supported diagnostics.
  • Bug Fixes

    • Improved parsing of reference dataset metadata using dataset layout conventions rather than relying on CMOR-style attributes.
    • Standardised recipe metadata ordering and ensured provenance linking is idempotent, with additional validation tests.

Reference (observational/reanalysis) datasets used by ESMValTool
diagnostics are ingested into the database under a dedicated
`esmvaltool-reference` dataset type during `ref providers setup`.
Because this data is not CMOR/obs4MIPs compliant, its metadata is
parsed from the ESMValTool OBS/OBS6, native6 and obs4MIPs layout
conventions rather than from file global attributes.

Diagnostics now declare the reference datasets they use via a
`reference_datasets` specification instead of hardcoding them in
recipe construction, and each execution records the reference
datasets it compared against so they appear alongside the model
datasets in the execution's dataset list.

The near-identical obs4MIPs and PMP climatology dataset models are
consolidated onto a shared ReferenceDatasetMixin while remaining
distinct dataset types.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@lewisjared, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 131e74f7-3b29-4e0d-a605-32610665a224

📥 Commits

Reviewing files that changed from the base of the PR and between 1eca0b2 and 2fee868.

📒 Files selected for processing (1)
  • packages/climate-ref-esmvaltool/tests/unit/test_provider.py
📝 Walkthrough

Walkthrough

This PR adds an esmvaltool-reference dataset type for ESMValTool observational and reanalysis data, introduces declarative reference dataset specs for diagnostics, adds dataset parsing/ingestion and database support, and records reference dataset provenance during execution setup.

Changes

ESMValTool reference dataset provenance

Layer / File(s) Summary
Core selector contract and source type
packages/climate-ref-core/src/climate_ref_core/diagnostics.py, packages/climate-ref-core/src/climate_ref_core/source_types.py
Adds ReferenceDatasetSelector, the default Diagnostic.reference_dataset_selectors() hook, and the ESMValToolReference source type.
Reference specs and diagnostic wiring
packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/reference.py, .../diagnostics/base.py, .../diagnostics/enso.py, .../diagnostics/regional_historical_changes.py, .../diagnostics/sea_ice_area_basic.py, .../tests/unit/diagnostics/*, changelog/+esmvaltool-reference-datasets.feature.md
Adds the declarative reference spec type, wires diagnostics to declare reference_datasets, updates recipe construction to use spec objects, and refreshes tests and changelog text.
Database model and migration
packages/climate-ref/src/climate_ref/models/dataset.py, packages/climate-ref/src/climate_ref/migrations/versions/2026-07-03T0000_a7b8c9d0e1f2_add_esmvaltool_reference_dataset.py
Adds the shared reference dataset mixin, the ESMValToolReferenceDataset model, and the migration for the new table, indexes, and enum value.
Parsing and ingestion adapter
packages/climate-ref/src/climate_ref/datasets/esmvaltool_reference.py, packages/climate-ref/src/climate_ref/datasets/__init__.py, packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/__init__.py, packages/climate-ref/tests/unit/datasets/*
Adds path-based parsing for OBS/native6/obs4MIPs layouts, registers the adapter, wires provider ingestion, and adds adapter and ingestion tests.
Provenance resolution and solver wiring
packages/climate-ref/src/climate_ref/reference_provenance.py, packages/climate-ref/src/climate_ref/solver.py, packages/climate-ref/tests/unit/test_reference_provenance.py
Adds reference dataset resolution/linking helpers, calls them from solver execution setup, and covers the link creation behaviour in tests.

Sequence Diagram(s)

sequenceDiagram
  participant ESMValToolProvider
  participant ESMValToolReferenceDatasetAdapter
  participant parse_esmvaltool_reference
  participant Database

  ESMValToolProvider->>ESMValToolReferenceDatasetAdapter: ingest_data(config, db)
  ESMValToolReferenceDatasetAdapter->>parse_esmvaltool_reference: parse file path
  parse_esmvaltool_reference-->>ESMValToolReferenceDatasetAdapter: metadata or INVALID_ASSET
  ESMValToolReferenceDatasetAdapter->>Database: store reference datasets
  ESMValToolReferenceDatasetAdapter-->>ESMValToolProvider: ingestion stats
Loading
sequenceDiagram
  participant Solver
  participant Diagnostic
  participant reference_provenance
  participant Database

  Solver->>Diagnostic: reference_dataset_selectors()
  Diagnostic-->>Solver: selectors
  Solver->>reference_provenance: link_reference_datasets(db, execution, diagnostic)
  reference_provenance->>reference_provenance: resolve_reference_dataset_ids(db, selectors)
  reference_provenance->>Database: query matching reference datasets
  Database-->>reference_provenance: dataset ids
  reference_provenance->>Database: insert execution_datasets links
  reference_provenance-->>Solver: new link count
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: ingesting ESMValTool reference datasets for provenance.
Description check ✅ Passed The description covers the required sections and checklist items, with enough detail on ingestion, provenance, and model changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/esmvaltool-reference-datasets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🧹 Nitpick comments (4)
packages/climate-ref/src/climate_ref/models/dataset.py (1)

316-323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the ESMValTool reference fields optional. data_type, tier, long_name, and units are nullable in the table, so annotate them as None-able to match what callers can actually receive.

packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/__init__.py (1)

68-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a TYPE_CHECKING-guarded Database hint instead of Any.

db: Any sacrifices type-checking on the ingestion call for a param that's always a Database at runtime. A TYPE_CHECKING-only import preserves the optional-dependency behaviour while giving static type coverage.

Proposed refactor
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from climate_ref.database import Database
...
-    def ingest_data(self, config: Config, db: Any) -> None:
+    def ingest_data(self, config: Config, db: "Database") -> None:
packages/climate-ref/src/climate_ref/reference_provenance.py (1)

24-59: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Selector resolution re-queries the DB per execution instead of caching per diagnostic.

reference_dataset_selectors() is fixed per diagnostic (see climate_ref_esmvaltool/diagnostics/base.py), but resolve_reference_dataset_ids is invoked fresh for every execution built in solve_required_executions's loop (per the solver.py snippet showing link_reference_datasets called right after register_datasets for each execution). For diagnostics with many executions, this repeats identical queries (and identical "no match" warnings) once per execution rather than once per diagnostic.

Consider resolving/caching selector → dataset-id results once per diagnostic (or per unique selector set) in the solver loop, rather than per execution.

packages/climate-ref-core/src/climate_ref_core/diagnostics.py (1)

417-439: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

ReferenceDatasetSelector is frozen but not truly hashable due to the facets dict field.

@frozen (attrs) auto-generates a __hash__ that includes all fields when eq=True. Since facets is typically a plain dict, hash(selector) will raise TypeError: unhashable type: 'dict' if any code ever needs to put these selectors in a set or use them as dict keys (e.g. dedup logic). This isn't exercised by the visible code today, but it's a common attrs footgun worth guarding against before this type is used more broadly for provenance dedup.

Confirmed via attrs docs: with frozen=True, attrs "will write a __hash__ function for you automatically" that hashes field values, and dict fields are inherently unhashable.

♻️ Option: make hashing explicit-safe or use a hashable facets representation
-@frozen
+@frozen(eq=False)  # or: keep eq for value equality but avoid hash generation issues
 class ReferenceDatasetSelector:
     ...
-    facets: Mapping[str, str]
+    facets: Mapping[str, str]  # consider tuple[tuple[str, str], ...] if hashability is ever needed

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d405ebf-2cfb-4052-95ee-5a6711047d3b

📥 Commits

Reviewing files that changed from the base of the PR and between 67f1580 and 629f2de.

📒 Files selected for processing (23)
  • changelog/+esmvaltool-reference-datasets.feature.md
  • packages/climate-ref-core/src/climate_ref_core/diagnostics.py
  • packages/climate-ref-core/src/climate_ref_core/source_types.py
  • packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/__init__.py
  • packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/base.py
  • packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/enso.py
  • packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/reference.py
  • packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
  • packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/sea_ice_area_basic.py
  • packages/climate-ref-esmvaltool/tests/unit/diagnostics/recipes/recipe_enso_characteristics_cmip6.yml
  • packages/climate-ref-esmvaltool/tests/unit/diagnostics/recipes/recipe_enso_characteristics_cmip7.yml
  • packages/climate-ref-esmvaltool/tests/unit/diagnostics/recipes/recipe_regional_historical_trend_obs4mips.yml
  • packages/climate-ref-esmvaltool/tests/unit/diagnostics/test_reference.py
  • packages/climate-ref-esmvaltool/tests/unit/test_provider.py
  • packages/climate-ref/src/climate_ref/datasets/__init__.py
  • packages/climate-ref/src/climate_ref/datasets/esmvaltool_reference.py
  • packages/climate-ref/src/climate_ref/migrations/versions/2026-07-03T0000_a7b8c9d0e1f2_add_esmvaltool_reference_dataset.py
  • packages/climate-ref/src/climate_ref/models/dataset.py
  • packages/climate-ref/src/climate_ref/reference_provenance.py
  • packages/climate-ref/src/climate_ref/solver.py
  • packages/climate-ref/tests/unit/datasets/test_datasets.py
  • packages/climate-ref/tests/unit/datasets/test_esmvaltool_reference.py
  • packages/climate-ref/tests/unit/test_reference_provenance.py

Comment thread packages/climate-ref/src/climate_ref/reference_provenance.py
Comment on lines +786 to +789
# Record reference (observational) datasets the diagnostic compares against.
# These are provenance only: they are not solver inputs and do not affect the
# dataset hash or the data passed to the diagnostic.
link_reference_datasets(db, execution, potential_execution.diagnostic)

@coderabbitai coderabbitai Bot Jul 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Backfill provenance for up-to-date executions too.

link_reference_datasets only runs when a fresh Execution is created. Existing up-to-date groups continue before this call, so running ref providers setup after an execution can leave the now-resolved reference datasets absent from provenance. Consider linking the latest existing execution before the skip path, or adding an explicit backfill during reference ingestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deferring — this is a deliberate scope decision. This PR records provenance for executions as they are created; backfilling reference datasets onto already up-to-date executions when ref providers setup is re-run afterwards is a larger behavioural change (a backfill pass over existing executions) that belongs in the follow-up alongside the read/API surface. Noting it there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@lewisjared, understood — deferring the backfill to the follow-up PR alongside the read/API surface makes sense given it's a distinct behavioural change (a pass over existing executions) rather than a natural extension of provenance-on-creation. Thanks for clarifying the scope boundary.


✏️ Learnings added
Learnt from: lewisjared
Repo: Climate-REF/climate-ref PR: 790
File: packages/climate-ref/src/climate_ref/solver.py:786-789
Timestamp: 2026-07-06T04:40:24.135Z
Learning: In climate-ref's `packages/climate-ref/src/climate_ref/solver.py`, `link_reference_datasets` is intentionally only called when a fresh `Execution` is created during `solve_required_executions`. Backfilling reference-dataset provenance onto already up-to-date execution groups (e.g., when `ref providers setup` is re-run after diagnostics were already executed) is a deliberate out-of-scope decision for the ESMValTool reference dataset provenance PR, to be addressed in a follow-up PR alongside the read/API surface for reference datasets.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread packages/climate-ref/tests/unit/test_reference_provenance.py
- Migration a7b8c9d0e1f2 now widens the SQLite dataset_type VARCHAR to fit
  the new ESMValToolReference enum member, fixing the migrations-up-to-date
  integration check (Postgres was already handled via ALTER TYPE).
- _parse_obs reads the timerange from the trailing token, matching
  _parse_obs4mips, so an unexpected extra segment does not drop the date range.
- _parse_obs4mips rejects an empty filename stem instead of ingesting an
  empty variable_id.
- Tighten the no-match provenance test to assert the emitted warning.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 55e4a0c2-b034-4e24-99f9-df167ff6be02

📥 Commits

Reviewing files that changed from the base of the PR and between 629f2de and 1eca0b2.

📒 Files selected for processing (4)
  • packages/climate-ref/src/climate_ref/datasets/esmvaltool_reference.py
  • packages/climate-ref/src/climate_ref/migrations/versions/2026-07-03T0000_a7b8c9d0e1f2_add_esmvaltool_reference_dataset.py
  • packages/climate-ref/tests/unit/datasets/test_esmvaltool_reference.py
  • packages/climate-ref/tests/unit/test_reference_provenance.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/climate-ref/tests/unit/test_reference_provenance.py
  • packages/climate-ref/tests/unit/datasets/test_esmvaltool_reference.py
  • packages/climate-ref/src/climate_ref/datasets/esmvaltool_reference.py

Comment on lines +86 to +92
op.drop_table("esmvaltool_reference_dataset")

# On SQLite, narrow ``dataset_type`` back to the previous enum member set. On PostgreSQL the
# ``ESMValToolReference`` value is intentionally left in the enum type: PostgreSQL does not
# support removing a value from an enum without recreating the type, and leaving it is harmless.
bind = op.get_bind()
if bind.dialect.name != "postgresql":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remove parent dataset rows before dropping the subtype table.

esmvaltool_reference_dataset is joined to dataset.id, so dropping only the child table can leave parent rows with dataset_type = 'ESMValToolReference'. On non-PostgreSQL, the later batch alter removes that enum member and can fail while copying existing rows; even where it succeeds, the downgraded app inherits unknown dataset rows.

🐛 Proposed downgrade fix
 def downgrade() -> None:
+    op.execute(
+        sa.text(
+            "DELETE FROM dataset "
+            "WHERE dataset_type = :dataset_type"
+        ).bindparams(dataset_type="ESMValToolReference")
+    )
+
     with op.batch_alter_table("esmvaltool_reference_dataset", schema=None) as batch_op:
         batch_op.drop_index(batch_op.f("ix_esmvaltool_reference_dataset_variable_id"))

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...f/src/climate_ref/datasets/esmvaltool_reference.py 86.44% 6 Missing and 2 partials ⚠️
...limate-ref/src/climate_ref/reference_provenance.py 91.42% 1 Missing and 2 partials ⚠️
...imate-ref-core/src/climate_ref_core/diagnostics.py 88.88% 1 Missing ⚠️
Flag Coverage Δ
core 92.41% <91.17%> (-0.06%) ⬇️
providers 87.33% <100.00%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...mate-ref-core/src/climate_ref_core/source_types.py 93.33% <100.00%> (+0.47%) ⬆️
...-esmvaltool/src/climate_ref_esmvaltool/__init__.py 100.00% <100.00%> (ø)
...ool/src/climate_ref_esmvaltool/diagnostics/base.py 89.75% <100.00%> (+0.12%) ⬆️
...ool/src/climate_ref_esmvaltool/diagnostics/enso.py 92.45% <100.00%> (+0.45%) ⬆️
...rc/climate_ref_esmvaltool/diagnostics/reference.py 100.00% <100.00%> (ø)
...valtool/diagnostics/regional_historical_changes.py 85.82% <100.00%> (+0.66%) ⬆️
...e_ref_esmvaltool/diagnostics/sea_ice_area_basic.py 100.00% <100.00%> (ø)
...s/climate-ref/src/climate_ref/datasets/__init__.py 98.13% <100.00%> (+0.05%) ⬆️
...ages/climate-ref/src/climate_ref/models/dataset.py 98.13% <100.00%> (+0.02%) ⬆️
packages/climate-ref/src/climate_ref/solver.py 98.94% <100.00%> (+0.01%) ⬆️
... and 3 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Mirror the PMP provider's ingest_data tests (skip-when-climate-ref-absent,
missing data directory, no valid datasets, and the success path) so the new
reference-data ingestion glue is exercised.
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.

1 participant