---
title: Where clause on a dynamic-column data-view definition always 500s
type: task
status: planned
description: >-
  A where clause on team_unit_dvoa emits the static placeholder column_name instead of the
  dvoa_type-resolved physical column, so it 500s against production every time; latent only because
  no saved view currently filters on it.
base_uri: user:task/league/data-views/fix-where-clause-dynamic-column-resolution.md
created_at: '2026-08-04T17:54:09.989Z'
entity_id: ac3e3e3e-ea48-417d-a71d-d4e130ae4ce8
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[█████████████████████-██████████████-████-████-█████-████████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-08-04T17:54:47.032Z'
visibility_analyzed_at: '2026-11-05T23:48:52.770Z'
---

# Fix where-clause column resolution for dynamic-column data-view definitions

## Problem

A `where` clause on `team_unit_dvoa` returns **500 against production**, every time. Confirmed
2026-08-04 on `6730dc39b`:

    column t9f91a8a0ecf83024658b66360ed59a01.team_unit_dvoa does not exist

`team_unit_dvoa` is not a physical column. The physical column is chosen at runtime from the
`dvoa_type` param — `total_dvoa`, `total_dvoa_rank`, and so on — by `get_dvoa_column_name`
(`libs-server/data-views-column-definitions/team-dvoa-column-definitions.mjs:96-101`). The column
definition carries the static placeholder `column_name: 'team_unit_dvoa'` (`:111`) and overrides
`main_select` / `main_group_by` to emit the dynamic name.

## Root cause

`libs-server/data-views/where-string.mjs:13-17`:

    const use_select_as =
      column_definition.select_as && is_main_select && column_definition.with
    const column_name = use_select_as
      ? column_definition.select_as({ params: where_clause.params })
      : column_definition.column_name

Neither branch can produce the dynamic physical name.

- `use_select_as` requires `column_definition.with`. The dvoa column is join-based and has no `with`,
  so the gate is false and it falls through.
- The fallback is the static `column_name`, which is a placeholder, not a real column.

Line 50 then emits `${table_name}.${column_name}`, producing the non-existent reference above.

Note `select_as` would be wrong here anyway — it returns the OUTPUT alias (`team_unit_dvoa`), which is
also not the physical column.

## Impact

Latent but real. 7 production saved views carry the column; **none currently filter on it**, which is
the only reason this is not a live incident. Any user adding a filter on a DVOA column gets a 500.

The select path is unaffected and works correctly.

## Notes

- Found during the 2026-08-04 post-deploy verification sweep, while probing an unrelated
  SQL-injection fix. It is **not** a regression from `7dea80f91` — that commit only changed how
  `dvoa_type` is derived, and the static `column_name` fallback predates it.
- Check for the same shape in other definitions that override `main_select` with a dynamic column
  while carrying a static `column_name`. The betting-market columns build their name from
  `hit_type` / `historical_range` the same way
  (`player-betting-market-column-definitions.mjs:589-611`) and are the first place to look.

## Plan

- [ ] **Reproduce as a failing spec**
  - Assert a `where` clause on `team_unit_dvoa` generates SQL naming the resolved physical column.
  - Must fail at HEAD in a clean worktree before the fix.

- [ ] **Resolve the physical column name in the where path**
  - File: `libs-server/data-views/where-string.mjs`
  - Give a column definition a way to declare its physical column as a function of params, and have
    the where path use it. Prefer one resolution helper the select, group-by and where paths all
    share over a third spelling of the same lookup.

- [ ] **Sweep sibling definitions**
  - Any definition with a dynamic `main_select` and a static `column_name`. Betting-market columns
    first.

- [ ] **Gates**
  - Goldens proven green at HEAD first, then regenerate only named fixtures.
  - `check-data-view-sql-validity.mjs` clean from a worktree at HEAD.
  - Confirm the fix against production with a novel payload — a warm redis key can serve a pre-fix
    entry, and from-plays responses can hold a `one_week` TTL.
