Skip to content

feat(results): add the climate_ref.results read layer#780

Merged
lewisjared merged 23 commits into
mainfrom
feat/results-read-layer
Jul 6, 2026
Merged

feat(results): add the climate_ref.results read layer#780
lewisjared merged 23 commits into
mainfrom
feat/results-read-layer

Conversation

@lewisjared

@lewisjared lewisjared commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

Introduces climate_ref.results, a typed, ORM-free read layer that gives notebooks, the CLI, and (eventually) the ref-app API one shared definition of each results query instead of each consumer re-implementing SQLAlchemy. A single Reader entry point exposes focused per-domain sub-readers, each returning frozen, session-detached DTOs with a to_pandas() helper, so a DataFrame built inside a with Database(...) block stays valid afterwards.

  • reader.values — scalar/series metric-value DTOs, DB-wide and per-query facets, and outlier detection. kind (model vs reference) is surfaced as a first-class field and stripped from the dimensions mapping so it appears once. Backs ref executions values.
  • reader.executions — execution-group and execution listing plus per-(provider, diagnostic) statistics. The existing ref executions list-groups and stats commands now go through the reader.
  • reader.datasets — dataset listing with latest-version deduplication pushed into SQL via a new dataset.version_key column (kept in sync by a mapper event, with a backfill migration). DatasetAdapter.load_catalog and the reader share one query builder, so the two can no longer drift.
  • reader.diagnostics — new domain plus a new ref diagnostics list command that shows each diagnostic's provider, promoted version, all-version execution-group count, and promoted-version successful/inflight/total counts.
  • reader.artifacts — config-independent resolution of output, log, and bundle paths.

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 diagnostics list with provider/diagnostic filtering, selectable columns, table/JSON output, and pagination.
    • Added executions values to view scalar and series metric values with paging, dimension filtering, and outlier-aware display.
    • Introduced a session-safe results read layer for datasets, executions, diagnostics, values, and output artefacts.
  • Bug Fixes
    • Improved “latest version” dataset selection and ensured --limit applies to dataset file rows when --include-files is used.
    • More robust catalogue time coercion and safer results-path handling.
  • Tests
    • Added/expanded unit tests for the new CLI commands and results-layer behaviour.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a typed climate_ref.results read layer for diagnostics, executions, datasets, and metric values, wires new diagnostics and execution-values CLI commands into the app, and moves dataset “latest version” selection to SQL using a version_key column plus related catalog-loading changes.

Changes

Typed results readers and CLI integration

Layer / File(s) Summary
Results package scaffolding and metric query helpers
packages/climate-ref/src/climate_ref/results/__init__.py, packages/climate-ref/src/climate_ref/results/_converters.py, packages/climate-ref/src/climate_ref/results/_query.py, packages/climate-ref/src/climate_ref/results/frames.py, packages/climate-ref/src/climate_ref/results/outliers.py
climate_ref.results now re-exports the public reader/filter types, adds _as_str_tuple, defines the shared metric-value query builder and filter contract, provides frame conversion helpers, and adds scalar outlier detection.
Execution artefact path resolution
packages/climate-ref/src/climate_ref/results/artifacts.py, packages/climate-ref/tests/unit/results/test_artifacts.py
ArtifactsReader resolves output directories, logs, bundles, and files under a guarded results root, with path containment tests.
Diagnostics read surface
packages/climate-ref/src/climate_ref/results/diagnostics.py, packages/climate-ref/src/climate_ref/cli/diagnostics.py, packages/climate-ref/tests/unit/results/test_diagnostics.py, packages/climate-ref/tests/unit/cli/test_root.py
Diagnostics filters, DTOs, SQL selection, pagination, statistics merging, reader behaviour, and the diagnostics CLI command are added and covered by reader, query, and CLI tests.
Executions read surface
packages/climate-ref/src/climate_ref/results/executions.py, packages/climate-ref/src/climate_ref/models/execution.py, packages/climate-ref/tests/unit/results/test_executions.py
Execution group/result DTOs, statistics and outputs queries, reader methods, and eager-loading for execution groups are added and tested.
Values read surface and top-level Reader
packages/climate-ref/src/climate_ref/results/values.py, packages/climate-ref/tests/unit/results/test_results.py
Scalar/series value DTOs and collections, the values reader, and the top-level reader entry point are added with coverage for querying, outliers, facets, and detachment.
Executions CLI refactor and values command
packages/climate-ref/src/climate_ref/cli/executions.py, packages/climate-ref/tests/unit/cli/test_executions.py, packages/climate-ref/tests/unit/cli/test_executions/test_inspect.txt
executions list, inspect, and stats now use the results readers, an outputs panel is rendered in inspect, and executions values is added for scalar and series output.
Results-side dataset reader
packages/climate-ref/src/climate_ref/results/datasets.py, packages/climate-ref/tests/unit/results/test_datasets.py
The detached dataset reader, dataset DTOs, and collection/DataFrame rendering are added and tested alongside dataset filtering, pagination, and detachment.
Results-side diagnostics and executions reader coverage
packages/climate-ref/tests/unit/results/test_diagnostics.py, packages/climate-ref/tests/unit/results/test_executions.py, packages/climate-ref/tests/unit/results/test_results.py
The new reader APIs are exercised across diagnostics, executions, and metric values, including detached DTO behaviour, stats, outputs, and outlier handling.

SQL-level latest-version dataset deduplication

Layer / File(s) Summary
Dataset version key and backfill
packages/climate-ref/src/climate_ref/models/dataset.py, packages/climate-ref/src/climate_ref/migrations/versions/2026-07-02T0338_f6a7b8c9d0e1_add_version_key.py, packages/climate-ref/tests/unit/datasets/test_datasets.py, packages/climate-ref/tests/unit/datasets/test_migrations_version_key.py
Dataset.version_key is added, kept in sync from version, and backfilled by migration, with model and migration tests.
Dataset select builder and read layer
packages/climate-ref/src/climate_ref/models/dataset_query.py, packages/climate-ref/src/climate_ref/results/datasets.py, packages/climate-ref/tests/unit/datasets/test_dataset_query.py, packages/climate-ref/tests/unit/results/test_datasets.py
select_datasets now applies facet, finalisation, execution, and diagnostic filters and deduplicates latest rows using version_key ranking, and the results-layer dataset reader consumes that query and materialises detached DTOs.
Dataset adapter and catalog loading
packages/climate-ref/src/climate_ref/datasets/base.py, packages/climate-ref/src/climate_ref/datasets/utils.py, packages/climate-ref/src/climate_ref/solve_helpers.py, packages/climate-ref/tests/unit/datasets/test_cmip6.py, packages/climate-ref/tests/unit/datasets/test_datasets.py, packages/climate-ref/tests/unit/datasets/test_utils.py, packages/climate-ref/tests/unit/cli/test_datasets.py
Dataset loading now uses the shared SQL dedup query, applies file-row limits after explosion, coerces catalog time columns through a shared helper, and keeps the related dataset and CLI tests aligned.
Batch series-axis ingestion
packages/climate-ref/src/climate_ref/executor/result_handling.py, packages/climate-ref/src/climate_ref/models/metric_value.py, packages/climate-ref/tests/unit/executor/test_stress_results_layer.py
Series index deduplication is batched through SeriesIndex.bulk_get_or_create, and the stress tests pin the SQL statement count for ingestion paths.

Possibly related PRs

  • Climate-REF/climate-ref#772: The main PR’s typed results/CLI values layer and its tests use the same metric-value kind and reference-series contract introduced there.
  • Climate-REF/climate-ref#786: Both PRs touch execution-group “latest” selection and related execution-reader behaviour, so they overlap at the same query and eager-loading code paths.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as diagnostics list
  participant Reader
  participant DiagnosticsReader
  participant Database
  CLI->>Reader: Reader(database)
  CLI->>DiagnosticsReader: list(DiagnosticFilter, limit, offset)
  DiagnosticsReader->>Database: select_diagnostics()
  DiagnosticsReader->>Database: select_execution_statistics()
  DiagnosticsReader-->>CLI: DiagnosticCollection
  CLI-->>CLI: render_dataframe()
Loading
sequenceDiagram
  participant CLI as executions values
  participant Reader
  participant ValuesReader
  participant OutlierPolicy
  CLI->>Reader: Reader(database)
  CLI->>ValuesReader: scalar_values(...) or series_values(...)
  ValuesReader->>OutlierPolicy: detect_scalar_outliers() when scalar outliers enabled
  ValuesReader-->>CLI: ScalarValueCollection or SeriesValueCollection
  CLI-->>CLI: render table or JSON
Loading
flowchart TD
  DatasetFilter --> select_datasets
  select_datasets --> PolymorphicEntityResolution
  PolymorphicEntityResolution --> FacetAndJoinFilters
  FacetAndJoinFilters --> RankOverVersionKey
  RankOverVersionKey --> LatestRows
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarises the main change: adding the climate_ref.results read layer.
Description check ✅ Passed It follows the template with Description and Checklist sections and covers tests, docs, and changelog status.
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/results-read-layer

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.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.41341% with 60 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ages/climate-ref/src/climate_ref/results/_query.py 82.29% 10 Missing and 7 partials ⚠️
...ages/climate-ref/src/climate_ref/cli/executions.py 85.71% 9 Missing and 5 partials ⚠️
...es/climate-ref/src/climate_ref/results/outliers.py 87.95% 5 Missing and 5 partials ⚠️
...ages/climate-ref/src/climate_ref/results/values.py 95.85% 6 Missing and 3 partials ⚠️
...ages/climate-ref/src/climate_ref/results/frames.py 90.69% 2 Missing and 2 partials ⚠️
...kages/climate-ref/src/climate_ref/datasets/base.py 90.00% 1 Missing and 1 partial ⚠️
...es/climate-ref/src/climate_ref/results/datasets.py 97.87% 1 Missing and 1 partial ⚠️
...climate-ref/src/climate_ref/results/diagnostics.py 97.64% 1 Missing and 1 partial ⚠️
Flag Coverage Δ
core 92.48% <94.41%> (+0.13%) ⬆️
providers 86.58% <ø> (+0.02%) ⬆️

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

Files with missing lines Coverage Δ
...ckages/climate-ref/src/climate_ref/cli/__init__.py 92.24% <100.00%> (+0.06%) ⬆️
...ckages/climate-ref/src/climate_ref/cli/datasets.py 88.34% <100.00%> (+0.72%) ⬆️
...ges/climate-ref/src/climate_ref/cli/diagnostics.py 100.00% <100.00%> (ø)
...ages/climate-ref/src/climate_ref/datasets/utils.py 92.24% <100.00%> (+0.44%) ⬆️
...te-ref/src/climate_ref/executor/result_handling.py 96.00% <100.00%> (-0.04%) ⬇️
...ages/climate-ref/src/climate_ref/models/dataset.py 98.10% <100.00%> (+0.08%) ⬆️
...limate-ref/src/climate_ref/models/dataset_query.py 100.00% <100.00%> (ø)
...es/climate-ref/src/climate_ref/models/execution.py 98.72% <100.00%> (ø)
...climate-ref/src/climate_ref/models/metric_value.py 92.72% <100.00%> (-1.03%) ⬇️
...es/climate-ref/src/climate_ref/results/__init__.py 100.00% <100.00%> (ø)
... and 13 more

... and 3 files with indirect coverage changes

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

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

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

63-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a concrete type instead of Any for timestamps.

created_at/updated_at are typed Any, weakening the "typed DTO" contract the rest of the results layer establishes. A concrete type (e.g. datetime.datetime) would preserve type safety for to_pandas() consumers.

packages/climate-ref/tests/unit/cli/test_datasets.py (1)

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

Make the multi-file dataset deterministic for this regression test.

first() is unordered, so the extra file may be added to a dataset that the CLI would not select first. In that case, a regressed dataset-level --limit 1 path could still return one row and pass. Pick the dataset using the same ordering as the CLI query, or assert the unbounded --include-files output first and target that dataset.

packages/climate-ref/src/climate_ref/results/frames.py (1)

79-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reaching into entity._cv_dimensions (protected attribute) across module boundaries.

collect_facets accesses a private-by-convention attribute on the model classes from a different module. Consider exposing this as a public classmethod/property on MetricValue for a cleaner boundary.

packages/climate-ref/src/climate_ref/results/values.py (1)

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

Inconsistent empty-result schema vs. ExecutionGroupCollection.to_pandas().

Unlike ExecutionGroupCollection.to_pandas() (executions.py, Lines 246-279), which explicitly lists columns so callers get a stable empty table, ScalarValueCollection/SeriesValueCollection.to_pandas() return a DataFrame with zero columns when items is empty (dynamic columns derived from dict(v.dimensions), so nothing is emitted for an empty list). This is fine for the CLI (which special-cases len(collection) == 0), but direct Reader consumers (notebooks, future API) calling .to_pandas() on an empty collection get an inconsistent shape.

Also applies to: 239-268


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 16a54b94-2742-4b0e-af00-95693261ec49

📥 Commits

Reviewing files that changed from the base of the PR and between b4e14e9 and 222adca.

📒 Files selected for processing (33)
  • packages/climate-ref/src/climate_ref/cli/__init__.py
  • packages/climate-ref/src/climate_ref/cli/diagnostics.py
  • packages/climate-ref/src/climate_ref/cli/executions.py
  • packages/climate-ref/src/climate_ref/datasets/base.py
  • packages/climate-ref/src/climate_ref/datasets/utils.py
  • packages/climate-ref/src/climate_ref/migrations/versions/2026-07-02T0338_f6a7b8c9d0e1_add_version_key.py
  • packages/climate-ref/src/climate_ref/models/dataset.py
  • packages/climate-ref/src/climate_ref/models/dataset_query.py
  • packages/climate-ref/src/climate_ref/results/__init__.py
  • packages/climate-ref/src/climate_ref/results/_converters.py
  • packages/climate-ref/src/climate_ref/results/_query.py
  • packages/climate-ref/src/climate_ref/results/artifacts.py
  • packages/climate-ref/src/climate_ref/results/datasets.py
  • packages/climate-ref/src/climate_ref/results/diagnostics.py
  • packages/climate-ref/src/climate_ref/results/executions.py
  • packages/climate-ref/src/climate_ref/results/frames.py
  • packages/climate-ref/src/climate_ref/results/outliers.py
  • packages/climate-ref/src/climate_ref/results/values.py
  • packages/climate-ref/src/climate_ref/solve_helpers.py
  • packages/climate-ref/tests/unit/cli/test_datasets.py
  • packages/climate-ref/tests/unit/cli/test_diagnostics.py
  • packages/climate-ref/tests/unit/cli/test_executions.py
  • packages/climate-ref/tests/unit/cli/test_executions/test_inspect.txt
  • packages/climate-ref/tests/unit/cli/test_root.py
  • packages/climate-ref/tests/unit/datasets/test_cmip6.py
  • packages/climate-ref/tests/unit/datasets/test_dataset_query.py
  • packages/climate-ref/tests/unit/datasets/test_datasets.py
  • packages/climate-ref/tests/unit/datasets/test_migrations_version_key.py
  • packages/climate-ref/tests/unit/results/test_artifacts.py
  • packages/climate-ref/tests/unit/results/test_datasets.py
  • packages/climate-ref/tests/unit/results/test_diagnostics.py
  • packages/climate-ref/tests/unit/results/test_executions.py
  • packages/climate-ref/tests/unit/results/test_results.py

Comment thread packages/climate-ref/src/climate_ref/cli/diagnostics.py Outdated
Comment thread packages/climate-ref/src/climate_ref/cli/executions.py
Comment thread packages/climate-ref/src/climate_ref/cli/executions.py
Comment thread packages/climate-ref/src/climate_ref/datasets/utils.py Outdated
Comment thread packages/climate-ref/src/climate_ref/models/dataset_query.py Outdated
Comment thread packages/climate-ref/src/climate_ref/results/datasets.py
Comment thread packages/climate-ref/src/climate_ref/results/executions.py
Comment thread packages/climate-ref/src/climate_ref/results/executions.py
Comment thread packages/climate-ref/src/climate_ref/results/outliers.py Outdated
Comment thread packages/climate-ref/tests/unit/cli/test_executions.py

@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

🧹 Nitpick comments (1)
packages/climate-ref/src/climate_ref/results/executions.py (1)

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reaching into a private symbol across modules.

_as_facets is imported from climate_ref.models.dataset_query by its underscore-prefixed (private) name. Since it's now shared beyond its defining module, consider promoting it to a public name so the cross-module contract is explicit and stable.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dbf766f8-7305-4f0c-b996-d6fcd59fb20b

📥 Commits

Reviewing files that changed from the base of the PR and between 90f19de and 06ba64d.

📒 Files selected for processing (5)
  • packages/climate-ref/src/climate_ref/executor/result_handling.py
  • packages/climate-ref/src/climate_ref/models/dataset_query.py
  • packages/climate-ref/src/climate_ref/models/metric_value.py
  • packages/climate-ref/src/climate_ref/results/executions.py
  • packages/climate-ref/tests/unit/executor/test_stress_results_layer.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/climate-ref/src/climate_ref/models/dataset_query.py

Comment thread packages/climate-ref/src/climate_ref/models/metric_value.py
Comment thread packages/climate-ref/tests/unit/executor/test_stress_results_layer.py Outdated
lewisjared added 15 commits July 4, 2026 22:42
Introduce climate_ref.results as the single source of truth for filtering
and querying metric-value results, so notebooks, the CLI and (later) the
ref-app API share one query layer instead of re-implementing it.

- _query.py: MetricValueFilter plus select_scalar_values/select_series_values
  Select builders, count_values and latest_execution_for_group. Exact vs
  substring diagnostic/provider matching and a promoted-only default close
  the slug/name and promotion drift between the CLI and the API.
- outliers.py: source-id-aware IQR detection as read-model logic.
- frames.py: DataFrame and facet-collection helpers.
- values.py: typed ScalarValue/SeriesValue DTOs and collections with
  to_pandas(), plus the Reader facade. Scalar reads SQL-paginate when
  outlier detection is off and materialise the full set only when it is on.
- cli/executions.py: new "ref executions values <group-id>" command that
  reads through Reader, with scalar/series output, dimension filters,
  optional outlier detection, pagination and table/json formats.
Move the scalar/series read methods onto a ValuesReader sub-reader exposed
as Reader.values, making Reader a thin entry point that will hold further
per-domain sub-readers as the layer grows.

Curate the top-level package surface to export only what a caller names to
make a call: the Reader entry point, the filters you pass in, and the value
objects you pass in. DTOs, collections, and sub-reader classes now live in
their domain submodule (imported by path when a name is needed), and the
Select builders stay private in _query. This keeps the namespace small as
new domains are added.
Add the executions domain to the results read layer: an ExecutionsReader
sub-reader (reached via reader.executions) exposing groups(), statistics()
and latest_execution(), with an ExecutionGroupFilter, detached
ExecutionGroupView/ExecutionView/ExecutionStats DTOs, an
ExecutionGroupCollection, and a select_execution_statistics() builder.

groups() wraps the existing get_execution_group_and_latest_filtered helper
rather than reimplementing it, since reingest and the versioning tests also
depend on that helper; reimplementing would reintroduce the query drift this
layer exists to remove. The stats aggregate, previously inlined only in the
CLI, moves into the read layer as a reusable Select builder.

Migrate the read-only CLI commands list-groups, stats and values onto
reader.executions. delete-groups, reingest and fail-running stay on the ORM
helper by design: they mutate rows or need the original (oldest) execution,
which the detached read layer does not serve.

Export only ExecutionGroupFilter at the package top level, keeping the
public surface to the entry point plus constructible filters; views,
collection and sub-reader are imported from the domain submodule.
Adds the files/outputs domain to the climate_ref.results read layer
(RFC domain D). ArtifactsReader resolves execution output fragments
(output_fragment, Execution.path, ExecutionOutput.filename) into
filesystem paths, guarded against path escape via os.path.commonpath
containment checks. ExecutionsReader gains outputs(execution_id) to
read registered ExecutionOutput rows as detached OutputView DTOs.

Reader takes an optional results root directly (Reader(database,
results=...)) rather than through a wrapper value object -- a single
Path field doesn't need one, and the decoupling from Config comes
from not taking Config, not from wrapping the path.

ref executions inspect now shows registered outputs and resolves its
output directory through reader.artifacts instead of joining
config.paths.results by hand.
Add a first-class ORM query path for datasets: DatasetFilter and select_datasets
in models/dataset_query.py, covering facet, finalised, and execution/diagnostic
relationship filters, plus DatasetsReader exposing detached DatasetView and
DatasetFileView DTOs via reader.datasets. Latest-version selection reuses the
adapter's filter_latest_versions so there is a single definition of that policy.
DatasetAdapter.load_catalog and solve_helpers.load_local_catalogs coerced a
catalog's start_time/end_time strings to cftime with identical inline code.
Move it to a shared datasets.utils.coerce_catalog_times helper.
Route load_catalog's dataset and file queries through select_datasets so
load_catalog and reader.datasets share one definition of the datasets query.

Apply the row limit after deduplicating to the latest version. Previously the
limit was pushed into the query before deduplication, so it could be spent on
superseded versions that were then dropped, returning fewer datasets than
requested and occasionally omitting datasets entirely.
Persist a numeric version_key on the base dataset table, computed by
version_sort_key, so the latest version of a dataset can be selected in SQL
(a following change adds the window-function dedup that orders by it).

A before_insert/before_update mapper event on Dataset keeps version_key in
sync for every write path — including direct-ORM inserts — rather than relying
on register_dataset, and reads the final attribute value so a stripped-metadata
upsert cannot leave a stale key. version_key is BigInteger to hold any
version string's digits without overflow on PostgreSQL.

The migration backfills existing rows in Python via version_sort_key over the
four subclass tables; version strings are not parsed in SQL because that
mapping has no portable expression across SQLite and PostgreSQL.
Add an optional latest-version dedup to select_datasets: when latest_only is
set and the caller supplies the partition columns (the adapter's
dataset_id_metadata), keep only the rows at RANK 1 of a
RANK() OVER (PARTITION BY <group> ORDER BY version_key DESC) window. RANK
(not ROW_NUMBER) preserves ties, matching the pandas select_latest_version it
mirrors. The dedup uses an id-membership subquery rather than an aliased
entity, so callers' class-bound loader options (selectinload) keep working.

reader.datasets and load_catalog now dedup in SQL and, for the dataset-level
result, push the limit into the same statement instead of fetching the whole
table and trimming in pandas. include_files still bounds files, so its limit is
applied after exploding rows. The pandas filter_latest_versions path stays for
parquet catalogs, which have no database rows to window over.
Add a diagnostics read domain reachable via reader.diagnostics, with a
list() returning provider/slug/name/promoted_version, an all-versions
execution-group count, and promoted-version successful/inflight/total
counts merged in from the existing execution-statistics builder so the
status-count logic stays defined once. stats() delegates to that same
builder rather than duplicating the aggregate SQL.

Expose the domain through a new ref diagnostics list command that
renders the collection and documents every column in its help text.

Extract the shared bare-str-rejecting _as_str_tuple converter into
results/_converters so the filter DTOs no longer copy it per module.
Document each field on the execution, dataset, diagnostic, outlier, and
metric-value filter DTOs as griffe/mkdocstrings attribute docstrings so
they render per-attribute, replacing the stray inline comments that the
docs tooling would not pick up, and unify wording across parallel fields.
kind is a registered CV dimension, so it has its own column and was
surfacing both as the dedicated ScalarValue/SeriesValue.kind field and
inside the dimensions mapping. Strip it from dimensions on read so it is
exposed once via the typed field; the to_pandas kind column and the kind
facet are unaffected.
Fixes across the CLI, dataset query builder, and results read layer
raised in review of the results read layer PR:

- diagnostics list: always validate/select --column, even when the
  filtered result set is empty (to_pandas already emits explicit
  columns for an empty collection).
- executions inspect: fall back to the plain filename when an output
  path would escape the results root instead of propagating the
  ValueError.
- executions values: report the outlier count and --include-unverified
  hint when every value in a page was flagged as an outlier, instead
  of a bare "No scalar values found.".
- coerce_catalog_times: coerce start_time/end_time independently so a
  catalog with only one of the two columns doesn't KeyError.
- DatasetFilter facets and MetricValueFilter diagnostic/provider
  filters: guard against a bare str value being iterated
  character-by-character.
- DatasetCollection.to_pandas: emit explicit base columns for an empty
  collection, matching the other collection types.
- reader.datasets: eager-load polymorphic dataset subtypes for mixed
  (source_type=None) listings to avoid an N+1 lazy load per row.
- select_execution_statistics: resolve to a single tie-broken latest
  execution id per group before joining, so two executions tied on
  created_at no longer double-count that group's status.
- get_execution_group_and_latest: eager-load diagnostic/provider,
  used by every caller of this query.
- OutlierPolicy.method: restrict to the two implemented values.

Adds regression tests for the empty-column, tie-count, missing-column
guard, and all-outliers-hidden behaviour, plus a --kind series --format
json coverage gap for the values command.
…k_get_or_create

Dedupe the _as_facets converter: keep the canonical bare-str-guarded version in
dataset_query and import it into results.executions, dropping the read layer's
copy that lacked the guard.

Move the batch axis-resolution logic out of ingest_series_values into a
SeriesIndex.bulk_get_or_create classmethod alongside get_or_create, and add a
stress test that pins the axis path to a constant statement count (no N+1).
@lewisjared lewisjared force-pushed the feat/results-read-layer branch from 06ba64d to a4eb16a Compare July 4, 2026 12:44
Select and validate --column against DatasetCollection.to_pandas()
instead of the facet-only frame, so base fields (slug, id, finalised,
created_at, updated_at) are selectable and empty results no longer
error on a columnless frame. The --include-files branch keeps
validating against its own exploded frame, which owns the file
columns.
The --remove-outputs guard on delete-groups relied on Path.is_relative_to,
which is a purely lexical check and does not collapse '..' or resolve
symlinks. A stored output_fragment with a '..' component could pass the
guard and let shutil.rmtree delete a directory outside the results root.

Reuse the shared safe_path helper from climate_ref_core.paths, which
resolves symlinks and '..' before confirming containment under the
results root.
The list-groups and delete-groups commands wrapped their filter query
calls in a blanket except Exception that relabelled any failure as
'Error applying filters' and exited 1. This swallowed tracebacks and
misattributed DB/programming errors to the user's filter input. Bad
facet values already warn-empty rather than raising here, and
malformed --filter syntax is caught separately by parse_facet_filters,
so the removed wrappers only ever caught unexpected errors that should
surface with a full traceback.
@lewisjared lewisjared merged commit dd40d15 into main Jul 6, 2026
28 checks passed
@lewisjared lewisjared deleted the feat/results-read-layer branch July 6, 2026 03:01
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