Be exact about what the 99 fallback does

It puts adjustments after every *numbered* segment, not last outright: on the
existing version, whose segments are still named AOP and YTD Sales with no
prefix, "99 - Adjustments" sorts first, because digits precede letters. That
is the scheme working as designed rather than an edge case, so the note says
so.

Also corrects the regeneration claim: editing a label is a PATCH and needs
nothing regenerated. Generate SQL is a one-time thing per source, so its
stored load templates write label and bucket at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-17 22:45:56 -04:00
parent 03278eb091
commit 3299bfe10b

View File

@ -140,11 +140,15 @@ single definition, shared with the `/data` cursor in `routes/operations.js` —
`/agg` is generated, `/data` is not, and the two have to agree.
The one hardcoded ordinal is `ADJUSTMENT_SEGMENT` = `'99 - Adjustments'`, which
keeps unlabelled adjustments last. Labelling an adjustment's own log row
overrides it, which is how one kind of adjustment is split out from the rest.
Unlabelled loads read plain `Unlabeled` and need no ordinal, since letters follow
digits in ASCII — unlike the old `'(adjustment)'`, where `(` is `0x28` against
digits from `0x30` and so sorted *first*.
keeps unlabelled adjustments after every *numbered* segment. That proviso is the
whole scheme, not a caveat on it: ordering is string ordering, so `99` only lands
last once the loads carry `01``0n`, and an unnumbered segment sorts after it
(digits precede letters — `9` is `0x39`, `A` is `0x41`). The old `'(adjustment)'`
sorted *first* for the same reason read the other way, `(` being `0x28`.
Unlabelled loads read plain `Unlabeled` and so land at the very end, which is
where a segment nobody has named belongs. Labelling an adjustment's own log row
overrides the fallback, which is how one kind of adjustment is split out from the
rest.
**What this replaced.** The prefix used to be computed client-side, as
Perspective expression columns (`pf_bucket_ord`, `pf_segment_ord`) built from
@ -158,10 +162,11 @@ two bytes, of which `isprint(0xC2)` is false). `DEAD_ORDER_EXPRS` in
scheme. `pf.log.seq` and `pf.version.bucket_order` are no longer read; the
columns remain.
Reordering now needs a page reload to show, because the label is part of the
aggregated row rather than something the pivot can re-derive. Changing labels on
an existing source also needs **Generate SQL** re-run — the load templates that
write `label` and `bucket` onto the log row are stored in `pf.sql`.
Relabelling now needs a page reload to show, because the label is part of the
aggregated row rather than something the pivot can re-derive. Editing a label
afterwards is a `PATCH /api/log/:logid` and needs nothing regenerated, but a
source registered before this change needs **Generate SQL** run once, so its
stored load templates write `label` and `bucket` onto the log row at all.
### Forecast operations
POST to `/api/versions/:id/{scale|recode|clone}` → SQL executed with `RETURNING *` → new rows returned as JSON → `pspTable.update(rows)` — no full reload. In grain mode the operation's final CTE aggregates its own new rows to grain first; since `pf_logid` is part of `pf_gkey` those keys are always new, so `update()` **appends** and the view re-sums.