---
title: Retype Cap Dollar Columns To Integer
type: task
status: planned
description: >-
  Three cap-dollar columns are numeric(6,2) holding integers, promising cents the arithmetic does
  not produce and the domain does not have. The false precision was half of why two implementations
  of the pricing rate could not be compared, since one rounded and one stored toFixed(2). Retypes
  earned_salary, best_season_earned_salary and market_salary to integer in one migration.
base_uri: user:task/league/retype-cap-dollar-columns-to-integer.md
created_at: '2026-08-03T23:06:29.717Z'
entity_id: 1b2cdb7c-aa50-405b-8dc9-f95e9ea3a2a9
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
relations:
  - relates [[████████████████████████-███████-█████-████████████]]
  - relates [[user:task/league/price-both-points-added-variants.md]]
  - relates [[███████████████████████-█████-█████████-████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-26T05:00:40.881Z'
visibility_analyzed_at: '2026-11-05T23:50:15.264Z'
---

# Retype Cap Dollar Columns To Integer

## Overview

Three cap-dollar columns are `numeric(6,2)` holding integers. The type promises cents that the arithmetic does not produce and that the domain does not have — a league salary is an auction bid, and bids are whole dollars.

Identified 2026-08-03 while collapsing the duplicate points-added-to-cap-dollars arithmetic (league `7a15d2b73`), and deliberately not folded into that change: a retype carries its own consumer sweep and its own ███████████████████ exposure, and bundling it would have put a schema migration inside a correctness fix.

## Background

| column | table |
| --- | --- |
| `earned_salary` | `league_format_player_seasonlogs` |
| `best_season_earned_salary` | `league_format_player_careerlogs` |
| `market_salary` | `league_format_player_projection_values` (and its `_history` companion) |

`market_salary` has always been integer-valued — `calculatePrices` returns `Math.round(...)`. `earned_salary` carried two decimal places only because its now-retired open-coded producer stored `.toFixed(2)`; that producer is gone and the surviving implementation rounds, so the cents are already always `.00` going forward.

**The false precision was not harmless.** It is half of why the two implementations of the pricing rate could not be compared: one rounded and one did not, so 8,032 of `genesis_10_team`'s 16,979 seasonlog rows differed on representation alone with the rate provably unchanged. A verify written as "agrees within $1" read as a tolerance for arithmetic drift when it was actually absorbing a type mismatch.

## Design

Retype all three to `integer` in one change. They are one concept — cap dollars — and splitting the retype across three migrations means three consumer sweeps for one decision.

**A retype's blast radius is wider than a rename's and cannot be contained behind a wrapper.** Callers pass the old representation and Postgres rejects it at the bind rather than coercing, so every reader is in scope regardless of how its query is written. Repo `CLAUDE.md` records this as the reason to size a mixed cluster on the retype.

Sweep for the reads that silently change shape rather than failing loudly: a JS consumer that receives a `numeric` gets a string from `pg` and an `integer` gets a number, so `===` comparisons, `.toFixed()` calls and string concatenations all change behaviour without throwing. That is the class to grep for, not just the column name.

**No intermediate nullable column, no dual-write.** `ALTER TABLE ... ALTER COLUMN ... TYPE integer USING round(...)` in one transaction.

### Sequencing

Must land AFTER the pending `earned_salary` regeneration under
[[████████████████████████-███████-█████-████████████]]'s cap-collapse work — regenerating into a column whose type is mid-flight is two migrations racing on one table.

Independent of [[user:task/league/price-both-points-added-variants.md]], but that task adds `*_cap_dollars` columns which must be `integer` from birth rather than being retyped later.

### Knowledge Base Impact

- [[███████████████████████-█████-█████████-████████]] — § Where each quantity is produced and stored names these columns
- No guideline change; repo `CLAUDE.md` already carries the retype-blast-radius rule

### Working tier

None needed.

### Orchestrator shape

Single task entity. Three columns, one migration, one sweep.

## Plan

- [ ] **Confirm every value is already integral before retyping**
  - Purpose: `USING round(...)` would silently absorb a real fractional value if one exists, converting a data question into a rounding
  - Verify: `count(*) where col <> round(col)` is zero on all three columns; if not, stop and report rather than rounding

- [ ] **`db/adhoc/YYYY-MM-DD-retype-cap-dollar-columns-to-integer.sql`**
  - All three columns plus `league_format_player_projection_values_history`, one transaction, `-- STATUS: PENDING` banner
  - Apply with `yarn db:exec`, `yarn export:schema`, commit both together — keep the apply-to-commit window short, since a retype is the DDL class that loses races to a sibling's schema export
  - Run `node db/gates/check-schema-conformance-ratchet.mjs` before pushing; it is CI-only and a red master defers every session's push to `mistakia/league`
  - Verify: the exported schema diff is exactly these columns and nothing else rode along

- [ ] **Sweep the consumers for representation-dependent reads**
  - `api/routes/players.mjs`, the data-view column definitions naming these columns, the careerlogs and seasonlogs generators, the SPA components that render a salary
  - Grep for `.toFixed(`, `Number(`, `parseFloat(` and `===` against these fields — a `numeric`-to-`integer` change turns a string into a number with no error
  - Validate each grep against a known occurrence before trusting a zero
  - Verify: `yarn test --reporter min` with CI's excludes applied; and `node db/gates/check-data-view-sql-validity.mjs` from a clean worktree at HEAD

- [ ] **Confirm on the deployed surface**
  - Verify: a salary rendered on the SPA is unchanged, read from the bundle production serves rather than from source
