---
title: Require non-empty columns in the generated data-view resolver
type: task
status: completed
description: >-
  The data-view generation resolver has no missing_columns check, unlike its plays sibling, so a
  columnless emission completes. The client then drops the state but still applies the run's name
  and description, renaming a view over the previous run's columns with no error anywhere.
base_uri: user:task/league/data-views/require-columns-in-the-generated-data-view-resolver.md
created_at: '2026-09-10T20:41:01.463Z'
entity_id: 7d5415c5-063c-4635-9f9c-21120ea26d35
finished_at: '2026-09-10T21:21:28.517Z'
owner_identity_uri: █████████████████████████
priority: High
public_read: true
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-10T21:21:28.517Z'
visibility_analyzed_at: '2026-11-05T23:48:59.664Z'
---

## Problem

`libs-server/data-views/generation/resolve-generated-table-state.mjs` never requires `columns`. `RESOLVER_ERROR_CODES` at lines 76-84 carries no `missing_columns` code, and the columns loop at lines 586-602 opens with `if (entries === undefined) continue`.

Its plays sibling does require it — `libs-server/plays-view/generation/resolve-generated-plays-table-state.mjs:108` rejects a state whose `columns` is absent or empty, with the reasoning that an empty table reads as "no rows matched" rather than as a malformed answer. The data-view side never got the same check.

Production proof: generation job `9c663f0b-7d13-4552-a7b5-9cc972e8cd33` (user 1, completed 2026-09-04T19:18:38.950Z, `view_kind = data_view`, `generation_branch = registry`) completed with `expressible: true` and `table_state` of exactly `{ "row_grain": ["player"] }`. Zero columns, zero prefix columns, and the gate passed it.

## Why it is worse than an empty table

Downstream the failure is silent and wrong rather than empty.

`app/core/data-view-generation/sagas.js:91-128` guards only on `table_state` being present, so a columnless state proceeds to `data_view_changed`. The reducer's `DATA_VIEW_CHANGED` branch at `app/core/data-views/reducer.js:160-163` destructures `table_state` out, finds `is_valid_table_state` false (`libs-shared/data-view-storage/validate.mjs:5-10` requires `columns` and `prefix_columns` to be arrays), and merges only `rest` — which still carries `view_name`, `view_description` and `query_id` from `sagas.js:116-125`.

Net effect: the view is renamed and re-described to match the new run, while still displaying the PREVIOUS run's columns. Nothing surfaces an error. The user cannot distinguish that from the generation having worked.

## Proposed shape

Add a `missing_columns` error code to the data-view resolver mirroring the plays one, so the emission is refused at the gate and fed back to the model for a repair round rather than reaching the client.

Consider separately whether `sagas.js:91` should refuse a `table_state` that `is_valid_table_state` rejects, instead of applying the metadata half of an unusable envelope. That is defence in depth and is arguably the more important half, since it also covers a state the resolver was never run against.

## Plan

- [ ] Add `missing_columns` to `RESOLVER_ERROR_CODES` and the check to `resolve_generated_table_state`, mirroring the plays wording
- [ ] Decide with the operator whether `handle_generation_update` should also refuse a state failing `is_valid_table_state`
- [ ] Confirm no live production behaviour depends on a columnless emission completing

## Verification

Red-before-green: feed `resolve_generated_table_state` the exact stored state `{ row_grain: ['player'] }` from job `9c663f0b-7d13-4552-a7b5-9cc972e8cd33` and assert `ok === false`. It returns `ok: true` on today's tree.
