Skip to content

1199: Error when as_forecast_<type>() renaming would create duplicate columns#1212

Open
nikosbosse wants to merge 1 commit into
mainfrom
issue-1199-rename-duplicate-columns
Open

1199: Error when as_forecast_<type>() renaming would create duplicate columns#1212
nikosbosse wants to merge 1 commit into
mainfrom
issue-1199-rename-duplicate-columns

Conversation

@nikosbosse

Copy link
Copy Markdown
Collaborator

Description

This PR closes #1199.

as_forecast_generic() called data.table::setnames() without checking whether a rename target already existed as a column in the data. Calling e.g. as_forecast_binary(dt, predicted = "prob") while a stale predicted column was present silently created a forecast object with two predicted columns. That object passed assert_forecast() and was then scored on the wrong (stale) column, producing silently wrong scores. All eight as_forecast_<type>() constructors route through this function.

The fix (per the settled maintainer decision — error, no silent overwrite, no deprecation period):

  • as_forecast_generic() now errors with a clear cli_abort() message when a rename would collide with an existing column that is not itself being renamed away. Identity renames (observed = "observed") and simultaneous swaps of renameable columns still work.
  • Defense in depth: assert_forecast_generic() now rejects data with duplicate column names, so corrupt objects reached by any other construction route fail validation instead of passing silently.

Tests were written first and failed against the unfixed code:

── 1. Failure ('test-class-forecast.R:16:3'): as_forecast_generic() errors when
Expected `as_forecast_binary(dt, predicted = "prob")` to throw a error.

── 2. Failure ('test-class-forecast.R:30:3'): as_forecast_generic() errors when
Expected `as_forecast_quantile(quantile_dt, quantile_level = "q")` to throw a error.

── 3. Failure ('test-class-forecast.R:93:3'): assert_forecast_generic() errors o
Expected `assert_forecast_generic(dt)` to throw a error.

With the fix, the affected test files and the full test suite pass (0 failed, 910 passed, 0 skipped).

Dev note: bug identified by an LLM audit (#1189); fix and tests implemented with LLM support, directed by @nikosbosse.

Checklist

  • My PR is based on a package issue and I have explicitly linked it.
  • I have included the target issue or issues in the PR title as follows: issue-number: PR title
  • I have tested my changes locally.
  • I have added or updated unit tests where necessary.
  • I have updated the documentation if required (N/A — only an internal @importFrom roxygen tag was added; devtools::document() produced no changes to man/ or NAMESPACE).
  • I have built the package locally and run rebuilt docs using roxygen2 (no doc changes resulted).
  • My code follows the established coding standards and I have run lintr on the changed files; no new lints were introduced.
  • I have added a news item linked to this PR.
  • I have reviewed CI checks for this PR and addressed them as far as I am able (CI pending at time of opening; will be monitored).

…1199)

as_forecast_generic() called setnames() without checking whether a
rename target already existed as a column in the data. Renaming e.g.
predicted = "prob" while a stale `predicted` column was present created
a forecast object with two `predicted` columns that passed validation
and was scored on the wrong column.

- as_forecast_generic() now errors with a clear message when a rename
  would collide with an existing column that is not itself being
  renamed away. Identity renames and simultaneous swaps still work.
- assert_forecast_generic() now rejects data with duplicate column
  names, so corrupt objects from any other construction route fail
  validation instead of passing silently.

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

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.17%. Comparing base (06ed56d) to head (7a93d80).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1212      +/-   ##
==========================================
+ Coverage   98.15%   98.17%   +0.01%     
==========================================
  Files          41       41              
  Lines        2225     2241      +16     
==========================================
+ Hits         2184     2200      +16     
  Misses         41       41              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@nikosbosse

Copy link
Copy Markdown
Collaborator Author

Automated review (Claude Fable, directed by @nikosbosse):

Verdict: approve.

Verified independently in an isolated checkout of the branch (7a93d80):

  • Fail-before/pass-after confirmed. With the branch's tests but main's R/, exactly the 3 new expectations fail (no error raised on rename collision; assert_forecast_generic() silently accepts duplicate columns). With the fix restored, the changed file and the full suite pass: 0 failed, 910 passed, 0 errors, 0 skipped.
  • Edge cases probed beyond the PR's tests, all correct: original repro (stale predicted + prob) errors clearly and leaves the input data.table unmutated; two simultaneous collisions render a correctly pluralised cli message; swapping observed/predicted still works; stale observed collision caught; NULL rename args mixed with a collision handled; sample_id collision caught; minimal 1-row input unaffected; corrupt duplicate-column objects from other routes now rejected by assert_forecast_generic().
  • Brief compliance: matches the settled maintainer decision (error via cli_abort, no silent overwrite, no deprecation; defense-in-depth check in assert_forecast_generic()). Identity renames preserved as required.
  • Scope, NEWS, title: diff touches only R/class-forecast.R, the test file, and NEWS. NEWS entry accurate and references as_forecast_*() renaming onto an existing column creates duplicate columns and silently wrong scores #1199. Title follows the NNNN: title convention. importFrom(cli, cli_abort) already present in NAMESPACE, so no doc changes needed — checklist claims check out.

Minor (non-blocking): the collision error names only the target column (e.g. predicted), not the source column being renamed; including both could speed up debugging, but the message is clear as-is.

No must-fix findings.

@nikosbosse
nikosbosse marked this pull request as draft July 18, 2026 13:53
@nikosbosse
nikosbosse marked this pull request as ready for review July 18, 2026 20:41
@nikosbosse
nikosbosse requested a review from sbfnk July 18, 2026 20:41
@nikosbosse

Copy link
Copy Markdown
Collaborator Author

We're now enforcing unique column names, which is a slight behaviour change, but I think it's a good one

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.

as_forecast_*() renaming onto an existing column creates duplicate columns and silently wrong scores

1 participant