Skip to content

refactor(api)!: build on climate-ref's query package with explicit filters - #147

Open
lewisjared wants to merge 27 commits into
mainfrom
refactor/query-package
Open

lewisjared wants to merge 27 commits into
mainfrom
refactor/query-package

Conversation

@lewisjared

@lewisjared lewisjared commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Moves the backend onto climate-ref's results readers and the climate_ref.query package, so every filter states which version, retraction and latest-execution scope it reads.

This builds against climate-ref 0.20.0 from PyPI, which ships the query package (Climate-REF/climate-ref#1011) and the reader fixes in Climate-REF/climate-ref#1018. The fixture database is migrated to match. With 0.20.0, scalar value pages with outlier detection take about 590 ms against 230 ms before at a tenth of production scale, because detection now runs in SQL.

Breaking API changes, with the frontend and generated client updated to match:

  • mip_era accepts only CMIP6 or CMIP7, so a lowercase, unknown or empty value is now a 422. On the diagnostics, executions and values routes, an era now keeps the groups that draw on that era or on no model data (such as observation-only groups), from the source types climate-ref records per group, instead of the execution's datasets. /models/ and /models/{source_id} still pick the era from the latest execution's datasets.
  • Diagnostics and /executions/statistics report groups (total, successful, failed, running, not started, dirty) in place of the separate count fields. groups_by_mip_era, with a groups object per era, replaces execution_groups_by_mip_era.
  • Diagnostics no longer report the per-attempt execution_count and successful_execution_count.
  • Group counts, diagnostic /values, the value flags and /executions/ now leave out retracted executions. On main, diagnostic /values and the group counts included them.
  • /diagnostics/{provider}/{diagnostic}/execution_groups lists only the promoted version, and now fills total_count.
  • /executions/{group}/values now returns the values of a group from a superseded diagnostic version, instead of nothing. The execution page now enables its value tabs from the group's own values, so these groups can open them.
  • source_id filters and /datasets/{id}/executions look at each group's latest execution only, where before any execution in the group matched. The value flags are the exception: with_values still matches source_id against every execution. This only differs when a retry changes a group's model, which its selectors normally prevent.
  • Resource usage on the dashboard and diagnostic pages covers only the promoted version of each diagnostic. Retracted executions still count.
  • /executions/ returns a 422 for an unknown failure_reason (before an empty list) and for a negative limit or offset.
  • /datasets/ is now ordered by slug.
  • Removes /diagnostics/facets and /diagnostics/{provider}/{diagnostic}/executions, which the frontend never called.

make perf-compare BASE=origin/feat/query-perf (#145, before it merged) at a tenth of production scale showed every endpoint the same or faster. For example diagnostics.values.scalar went from 3283 ms to 256 ms, executions.statistics from 512 ms to 22 ms and diagnostics.list from 221 ms to 44 ms. This can't run against main yet, because main's climate-ref 0.19 has no synthetic database generator. A head-only run after the rebase gives similar numbers (295 ms, 19 ms and 34 ms). The group counts read every row of execution_group, which the perf suite now allows on the three pages that count every group. That lookup finds each group's latest execution with a sorted subquery per group, which is worth improving upstream.

Adds /diagnostics/{provider}/{diagnostic}/values/facets and /executions/{group}/values/facets, which return the facets /values returns for the same filters. The values tables now fetch facets once per filter and pages with with_facets=false, so a series page drops from 63 ms to 14 ms. The explorer filter controls used to fetch facets through /values?limit=1, which ran outlier detection over every value, and now use the facets endpoint (85 ms against 269 ms). /values still returns facets by default.

The commits in the middle of the branch were written against unreleased climate-ref builds, so only the tip type-checks and passes the tests against 0.20.0.

Summary by CodeRabbit

  • New Features

    • Added separate metric-value facet endpoints for diagnostics and executions. Values requests can now skip facet loading with with_facets=false.
    • Execution statistics and diagnostic summaries now show group totals by status, including breakdowns by MIP era.
    • Added and clarified filters for MIP era, source model and execution failure reason.
  • Improvements

    • Diagnostic, execution and metric-value endpoints are faster, with facets loaded separately from paged values.
  • Breaking Changes

    • Removed the diagnostic executions list and global facets endpoints. Dataset ordering, execution statistics and filter validation have changed.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8025ffbe-85cf-4ea2-96b0-3cc5de28a895
Commits

Reviewing files that changed from the base of the PR and between 7988232 and c59a4bb.

Files ignored due to path filters: 2
  • backend/tests/test-data/tests.integration.test_cmip7_aft/test_solve_cmip7_aft/db/climate_ref.db is excluded by !**/*.db
  • backend/uv.lock is excluded by !**/*.lock
Files selected for processing: 59
  • backend/pyproject.toml
  • backend/src/ref_backend/api/routes/datasets.py
  • backend/src/ref_backend/api/routes/diagnostics.py
  • backend/src/ref_backend/api/routes/executions.py
  • backend/src/ref_backend/api/routes/models.py
  • backend/src/ref_backend/api/routes/results.py
  • backend/src/ref_backend/core/execution_groups.py
  • backend/src/ref_backend/core/filter_utils.py
  • backend/src/ref_backend/core/metric_values.py
  • backend/src/ref_backend/core/mip_eras.py
  • backend/src/ref_backend/core/model_ensemble.py
  • backend/src/ref_backend/core/model_runs.py
  • backend/src/ref_backend/core/reader_values.py
  • backend/src/ref_backend/core/resource_usage.py
  • backend/src/ref_backend/models/__init__.py
  • backend/src/ref_backend/models/common.py
  • backend/src/ref_backend/models/datasets.py
  • backend/src/ref_backend/models/diagnostics.py
  • backend/src/ref_backend/models/executions.py
  • backend/src/ref_backend/models/values.py
  • backend/src/ref_backend/testing.py
  • backend/tests/conftest.py
  • backend/tests/perf/test_endpoints.py
  • backend/tests/test_api/test_routes/test_datasets.py
  • backend/tests/test_api/test_routes/test_diagnostics.py
  • backend/tests/test_api/test_routes/test_executions.py
  • backend/tests/test_api/test_routes/test_mip_era_params.py
  • backend/tests/test_core/conftest.py
  • backend/tests/test_core/test_filter_utils.py
  • backend/tests/test_core/test_metric_values.py
  • backend/tests/test_core/test_mip_era_edges.py
  • backend/tests/test_core/test_model_runs.py
  • backend/tests/test_core/test_resource_usage.py
  • changelog/147.breaking.md
  • changelog/147.feature.md
  • changelog/147.improvement.md
  • frontend/src/client/@tanstack/react-query.gen.ts
  • frontend/src/client/index.ts
  • frontend/src/client/sdk.gen.ts
  • frontend/src/client/types.gen.ts
  • frontend/src/components/dashboard/executionStats.test.tsx
  • frontend/src/components/dashboard/executionStats.tsx
  • frontend/src/components/datasets/diagnosticSummaryTable.tsx
  • frontend/src/components/diagnostics/diagnosticCard.tsx
  • frontend/src/components/execution/values/valuesPanel.test.tsx
  • frontend/src/components/execution/values/valuesPanel.tsx
  • frontend/src/components/explorer/chartValues.test.ts
  • frontend/src/components/explorer/chartValues.ts
  • frontend/src/components/explorer/content/filterControls.tsx
  • frontend/src/components/explorer/content/useExplorerMetricValues.ts
  • frontend/src/hooks/useDiagnosticMetricValues.ts
  • frontend/src/hooks/useExecutionMetricValues.ts
  • frontend/src/hooks/useMetricValues.test.ts
  • frontend/src/hooks/useMetricValues.ts
  • frontend/src/lib/failures.test.ts
  • frontend/src/lib/failures.ts
  • frontend/src/lib/resourceUsageCsv.test.ts
  • frontend/src/routes/_app/diagnostics.$providerSlug.$diagnosticSlug/route.tsx
  • frontend/src/routes/_app/executions.$groupId/route.tsx
Walkthrough

The API now uses shared reader and query helpers for diagnostic summaries, execution groups, resource usage, and metric values. It adds filtered facet endpoints, formalises MIP-era and group-count types, and updates generated clients and frontend queries to use these contracts.

Changes: API query and statistics updates

Layer / File(s) Summary
Diagnostic and execution statistics
backend/src/ref_backend/models/*, backend/src/ref_backend/core/resource_usage.py, backend/src/ref_backend/api/routes/diagnostics.py, backend/src/ref_backend/api/routes/executions.py
Statistics now use nested group counts. Diagnostic summaries and resource usage use reader and query helpers with explicit filters for superseded and retracted executions.
Dataset and execution-group routes
backend/src/ref_backend/core/execution_groups.py, backend/src/ref_backend/api/routes/datasets.py, backend/src/ref_backend/api/routes/executions.py, backend/src/ref_backend/api/routes/diagnostics.py
Routes use a shared execution-group loader. Dataset lists use the reader, and execution-group routes apply route-specific filters, ordering, and pagination.
MIP-era filtering
backend/src/ref_backend/core/mip_eras.py, backend/src/ref_backend/core/model_runs.py, backend/src/ref_backend/core/model_ensemble.py, backend/src/ref_backend/api/routes/models.py, backend/src/ref_backend/models/datasets.py
MIP-era values are typed as CMIP6 or CMIP7. Shared helpers map eras to source types and support era filtering across diagnostic, execution, and model queries.
Metric-value facets
backend/src/ref_backend/api/routes/diagnostics.py, backend/src/ref_backend/api/routes/executions.py, backend/src/ref_backend/core/reader_values.py, backend/src/ref_backend/models/values.py
Diagnostic and execution value routes add facet endpoints and a with_facets option. Shared filters support value and facet requests, and reader helpers can retrieve facets without loading values.
Generated client and frontend updates
frontend/src/client/*, frontend/src/hooks/*, frontend/src/components/explorer/*, frontend/src/components/dashboard/*
The client exposes the new facet operations and updated response types. Frontend hooks fetch facets separately from values and use the value response as a fallback if a facet request fails. Components read group counts from the nested response shape.

Sequence Diagram(s)

sequenceDiagram
  participant ValuesPage
  participant MetricValuesHook
  participant FacetsRoute
  participant Reader
  ValuesPage->>MetricValuesHook: Request values and facets
  MetricValuesHook->>FacetsRoute: Request facets with shared filters
  FacetsRoute->>Reader: Read facets without loading values
  Reader-->>FacetsRoute: Return facets
  FacetsRoute-->>MetricValuesHook: Return facets
  MetricValuesHook-->>ValuesPage: Return values and facets
Loading

Possibly related issues

Possibly related PRs

Priority: Normal

Change: Feature

Merge Risk: Moderate · up to 79882

Retracted runs can still appear in model counts and failures and affect ensemble results. Correct both queries before merging.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 56.49% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 154 functions across 50 files. (6 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the main change: migrating the API to climate-ref's query package with explicit filters. The breaking-change marker is appropriate for the stated API changes.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 56.49% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 154 functions across 50 files. (6 skipped: 4 unsupported, 2 over the file limit.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

lewisjared added a commit that referenced this pull request Oct 23, 2026
lewisjared added a commit that referenced this pull request Oct 23, 2026
@lewisjared
lewisjared force-pushed the refactor/query-package branch from 0db729c to 8c4668c Compare October 23, 2026 12:08
Base automatically changed from feat/query-perf to main October 24, 2026 00:00
Migrates the fixture database for the new migrations.
Drops the deprecated MetricValueFilter(promoted_only=True), which is now the default.
Handles the reader's total_count now being optional.
Removes the app's own latest-execution subquery.
climate-ref now picks the latest execution with the same created_at then id rule.
Uses ResourcesReader.summary for the overall and per-diagnostic resource usage.
Passes include_retracted=True, so retracted executions still count as they did before.
The roll-ups now keep the promoted version only.
Moves the per-era dataset matching onto climate-ref's InputDatasetFilter.
Removes filter_utils, whose only user was the era filter.
Adds tests for an unknown era, an unknown facet, a facet only one era carries and a comma list.
The no-facet case trips an empty and_() deprecation upstream, which a test pins.
Moves the failure_reason, source_id and mip_era filters into the reader query.
Skips the reader's count, because the route counts after its own sort.
An unknown failure_reason or mip_era still returns an empty list.
Counts the groups per MIP era with one reader aggregate per era.
Adds a test that the CMIP6 count matches the executions list for that era.
Takes the catalog and detail group counts from the reader instead of two hand-written queries each.
The latest execution of a group is now the newest by created_at, not the highest id.
This matches the executions statistics and the CLI.
Adds tests for a backfilled failure and for an unknown era in the catalog.
Takes has_scalar_values and has_series_values from the reader for the list, catalog flags and detail.
Hides retracted executions from both values endpoints, so the flags and the values follow one rule.
The detail flags now count the promoted version only, like the list.
Removes metric_value_type_is, which the reader replaces.
The flag test now checks only listed diagnostics, so a local DIAGNOSTIC_EXCLUDE no longer breaks it.
Uses the reader for the dataset list, with slug_contains for name_contains.
The list is now ordered by slug rather than by most recently updated, so paging is stable.
Adds tests for the order, paging and a case-insensitive name match.
Replaces the legacy Query.get, which SQLAlchemy 2 warns about.
Adds eras_in_scope, which turns a mip_era query value into the eras it selects, in any case.
The era filters and the model pages now share it instead of comparing upper-cased text.
An empty mip_era on the model routes now means no filter, as it already did elsewhere.
Pins the parameter on every route that takes it: an unknown era matches nothing and case is ignored.
Covers an execution mixing CMIP6 and CMIP7 inputs, one with no CMIP input and the mip_id fallback that labels them.
These cases are absent from the fixture, so the tests build them in a copy of the database.
Uses InputDatasetFilter(or_no_model_input=True) for the "this era or no model input" rule.
This replaces executions_in_mip_era and the hand-built OR in the ensemble comparison.
Fixes an unknown mip_era on the diagnostic values and ensemble routes returning observation-only runs.
It now matches nothing, like every other route.
Moves the writable_client fixture to the shared conftest so the era tests can use it.
Removes the hand-written resource aggregates from the diagnostic catalog.
The roll-up keeps the promoted version only and still counts retracted executions.
The mip_era and source_id scope now applies to each execution rather than to its group.
climate-ref now reads an empty id list as matching nothing.
So `?isolate_ids=,` would have hidden every value instead of isolating none.
parse_id_list now returns None for a missing or blank list.
An unknown era on /models/{source_id} now gives the model with no runs rather than a 404.
This matches the empty lists every other route returns for an unknown era.
A missing model, or a known era the model has no runs in, still gives a 404,
because the model page uses it to say there are no runs in that era.
Test modules import test_settings and test_ref_config by name, so pytest ran them as tests.
Each one returned a value and raised PytestReturnNotNoneWarning.
Removes execution_count and successful_execution_count from the diagnostic list and detail responses.
Nothing in the frontend read them, and the group counts come from the reader.
Nothing in the frontend called it, and it read every value without the version or retraction rules /values applies.
…lters

Every reader call now names its version, retraction and latest-execution scope.

- `mip_era` takes only `CMIP6` or `CMIP7` and rejects anything else with a 422.
  It keeps the groups that draw on that era, from their recorded source types.
- Diagnostics report `groups` (total, successful, failed, running, not_started, dirty)
  in place of `execution_group_count` and `successful_execution_group_count`.
- `/executions/statistics` reports `groups` and `groups_by_mip_era` in place of the flat counts.
- `/diagnostics/{provider}/{diagnostic}/execution_groups` keeps only the promoted version
  and leaves out groups whose latest execution is retracted.
- `/datasets/{id}/executions` lists the groups whose latest execution used the dataset.
- Removes `/diagnostics/{provider}/{diagnostic}/executions`, which the frontend did not call.
- An unknown `failure_reason` on `/executions/` is a 422 rather than an empty list.
@lewisjared
lewisjared force-pushed the refactor/query-package branch from 89a7eb6 to 475968d Compare October 24, 2026 00:28
lewisjared added a commit that referenced this pull request Oct 24, 2026
@lewisjared
lewisjared force-pushed the refactor/query-package branch from 475968d to 7988232 Compare October 24, 2026 11:05
lewisjared added a commit that referenced this pull request Oct 24, 2026
@lewisjared
lewisjared marked this pull request as ready for review October 24, 2026 11:12

@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: 2


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 687f1a8c-0022-469b-8771-aabede5bf7ae
Commits

Reviewing files that changed from the base of the PR and between f7eb639 and 7988232.

Files ignored due to path filters: 2
  • backend/tests/test-data/tests.integration.test_cmip7_aft/test_solve_cmip7_aft/db/climate_ref.db is excluded by !**/*.db
  • backend/uv.lock is excluded by !**/*.lock
Files selected for processing: 58
  • backend/pyproject.toml
  • backend/src/ref_backend/api/routes/datasets.py
  • backend/src/ref_backend/api/routes/diagnostics.py
  • backend/src/ref_backend/api/routes/executions.py
  • backend/src/ref_backend/api/routes/models.py
  • backend/src/ref_backend/api/routes/results.py
  • backend/src/ref_backend/core/execution_groups.py
  • backend/src/ref_backend/core/filter_utils.py
  • backend/src/ref_backend/core/metric_values.py
  • backend/src/ref_backend/core/mip_eras.py
  • backend/src/ref_backend/core/model_ensemble.py
  • backend/src/ref_backend/core/model_runs.py
  • backend/src/ref_backend/core/reader_values.py
  • backend/src/ref_backend/core/resource_usage.py
  • backend/src/ref_backend/models/__init__.py
  • backend/src/ref_backend/models/common.py
  • backend/src/ref_backend/models/datasets.py
  • backend/src/ref_backend/models/diagnostics.py
  • backend/src/ref_backend/models/executions.py
  • backend/src/ref_backend/models/values.py
  • backend/src/ref_backend/testing.py
  • backend/tests/conftest.py
  • backend/tests/perf/test_endpoints.py
  • backend/tests/test_api/test_routes/test_datasets.py
  • backend/tests/test_api/test_routes/test_diagnostics.py
  • backend/tests/test_api/test_routes/test_executions.py
  • backend/tests/test_api/test_routes/test_mip_era_params.py
  • backend/tests/test_core/conftest.py
  • backend/tests/test_core/test_filter_utils.py
  • backend/tests/test_core/test_metric_values.py
  • backend/tests/test_core/test_mip_era_edges.py
  • backend/tests/test_core/test_model_runs.py
  • backend/tests/test_core/test_resource_usage.py
  • changelog/147.breaking.md
  • changelog/147.feature.md
  • changelog/147.improvement.md
  • frontend/src/client/@tanstack/react-query.gen.ts
  • frontend/src/client/index.ts
  • frontend/src/client/sdk.gen.ts
  • frontend/src/client/types.gen.ts
  • frontend/src/components/dashboard/executionStats.test.tsx
  • frontend/src/components/dashboard/executionStats.tsx
  • frontend/src/components/datasets/diagnosticSummaryTable.tsx
  • frontend/src/components/diagnostics/diagnosticCard.tsx
  • frontend/src/components/execution/values/valuesPanel.test.tsx
  • frontend/src/components/execution/values/valuesPanel.tsx
  • frontend/src/components/explorer/chartValues.test.ts
  • frontend/src/components/explorer/chartValues.ts
  • frontend/src/components/explorer/content/filterControls.tsx
  • frontend/src/components/explorer/content/useExplorerMetricValues.ts
  • frontend/src/hooks/useDiagnosticMetricValues.ts
  • frontend/src/hooks/useExecutionMetricValues.ts
  • frontend/src/hooks/useMetricValues.test.ts
  • frontend/src/hooks/useMetricValues.ts
  • frontend/src/lib/failures.test.ts
  • frontend/src/lib/failures.ts
  • frontend/src/lib/resourceUsageCsv.test.ts
  • frontend/src/routes/_app/diagnostics.$providerSlug.$diagnosticSlug/route.tsx
Files with no reviewable changes: 2
  • backend/tests/test_core/test_filter_utils.py
  • backend/src/ref_backend/core/filter_utils.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

columns = [cv_column(value, name) for name in dimensions]
# A retried group holds a value per attempt, so only the execution that decided it counts.
latest = latest_executions()
latest = select_latest_executions().subquery()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

Exclude retracted executions from ensemble comparisons.

If a group’s latest execution is retracted, select_latest_executions() still selects it. _scalar_rows then includes its scalar values in the model average and ensemble statistics. Filter the selected execution with models.Execution.retracted.is_(False) after choosing the latest execution. Do not filter before selection, as that would admit an older run. (raw.githubusercontent.com)

institutions still yields one row and is counted once.
"""
latest = latest_executions()
latest = select_latest_executions().subquery()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Major · Functional Correctness · Quick win

Exclude retracted latest executions from model runs.

If a group’s latest execution is retracted, this selection still passes it to model_run_rows and failed_runs. The model list and detail then count the retracted run, and detail can display it as a failure. Exclude retracted executions after selecting each group’s latest execution. Filtering before selection would incorrectly count an older run instead. (raw.githubusercontent.com)

An era now keeps the groups that draw on no model data in the counts, value flags,
execution group lists, statistics and resource usage, as it already did for values.
The diagnostic's value flags only cover its promoted version, so they disabled the tabs of older groups.
@lewisjared
lewisjared force-pushed the refactor/query-package branch from 7988232 to c59a4bb Compare October 24, 2026 11:21
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