Close the write-only surface left behind by the ordinals

bucket_order and log.seq were still settable -- PUT /versions/:id took the
first, PATCH /log/:logid the second -- with nothing left to read either. A
field that only ever gets written is worse than a missing one: the call
succeeds, so the caller has no way to find out it did nothing.

The columns themselves stay, marked vestigial where they are declared.
Dropping a column is not worth a migration to reclaim two that cost nothing.

pf.log.bucket is untouched and stays exactly as it was -- what a row counts
toward, read first by BUCKET_EXPR. It is the bucket *order* that no longer
needs storing, the text having become the order.

Also removes a comment head in Forecast.jsx that survived its function and
had glued itself onto fitColumns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-17 22:51:34 -04:00
parent 3299bfe10b
commit 98322a6080
4 changed files with 17 additions and 30 deletions

View File

@ -131,11 +131,11 @@ module.exports = function(pool) {
// a closed version, where relabelling history is still legitimate. // a closed version, where relabelling history is still legitimate.
router.patch('/log/:logid', async (req, res) => { router.patch('/log/:logid', async (req, res) => {
const logId = parseInt(req.params.logid); const logId = parseInt(req.params.logid);
const { note, tag, bucket, seq, label } = req.body; const { note, tag, bucket, label } = req.body;
if (note === undefined && tag === undefined && bucket === undefined if (note === undefined && tag === undefined
&& seq === undefined && label === undefined) { && bucket === undefined && label === undefined) {
return res.status(400).json({ return res.status(400).json({
error: 'Nothing to update — send note, tag, bucket, label and/or seq' error: 'Nothing to update — send note, tag, bucket and/or label'
}); });
} }
try { try {
@ -146,16 +146,13 @@ module.exports = function(pool) {
note = CASE WHEN $2::bool THEN $3::text ELSE note END, note = CASE WHEN $2::bool THEN $3::text ELSE note END,
tag = CASE WHEN $4::bool THEN $5::text ELSE tag END, tag = CASE WHEN $4::bool THEN $5::text ELSE tag END,
bucket = CASE WHEN $6::bool THEN $7::text ELSE bucket END, bucket = CASE WHEN $6::bool THEN $7::text ELSE bucket END,
seq = CASE WHEN $8::bool THEN $9::int ELSE seq END, label = CASE WHEN $8::bool THEN $9::text ELSE label END
label = CASE WHEN $10::bool THEN $11::text ELSE label END
WHERE id = $1 RETURNING *`, WHERE id = $1 RETURNING *`,
[ [
logId, logId,
note !== undefined, note === undefined ? null : (String(note).trim() || null), note !== undefined, note === undefined ? null : (String(note).trim() || null),
tag !== undefined, tag === undefined ? null : (String(tag).trim() || null), tag !== undefined, tag === undefined ? null : (String(tag).trim() || null),
bucket !== undefined, bucket === undefined ? null : (String(bucket).trim() || null), bucket !== undefined, bucket === undefined ? null : (String(bucket).trim() || null),
seq !== undefined, (seq === undefined || seq === null || seq === '')
? null : parseInt(seq),
label !== undefined, label === undefined ? null : (String(label).trim() || null), label !== undefined, label === undefined ? null : (String(label).trim() || null),
] ]
); );

View File

@ -308,27 +308,25 @@ ${colDefs},
}); });
// update version name, description, or exclude_iters // update version name, description, or exclude_iters
//
// bucket_order is deliberately not settable: the bucket column order is the
// text in pf.log.bucket now, so a stored order would be a second answer to
// the same question, and a silent one -- nothing reads it.
router.put('/versions/:id', async (req, res) => { router.put('/versions/:id', async (req, res) => {
const { name, description, exclude_iters, bucket_order } = req.body; const { name, description, exclude_iters } = req.body;
try { try {
// bucket_order is a flag-and-value pair rather than COALESCE: an empty
// array is a meaningful value (no ordering), and COALESCE could not tell
// it from "not mentioned".
const result = await pool.query(` const result = await pool.query(`
UPDATE pf.version SET UPDATE pf.version SET
name = COALESCE($2, name), name = COALESCE($2, name),
description = COALESCE($3, description), description = COALESCE($3, description),
exclude_iters = COALESCE($4, exclude_iters), exclude_iters = COALESCE($4, exclude_iters)
bucket_order = CASE WHEN $5::bool THEN $6::jsonb ELSE bucket_order END
WHERE id = $1 WHERE id = $1
RETURNING * RETURNING *
`, [ `, [
req.params.id, req.params.id,
name || null, name || null,
description || null, description || null,
exclude_iters ? JSON.stringify(exclude_iters) : null, exclude_iters ? JSON.stringify(exclude_iters) : null
bucket_order !== undefined,
bucket_order === undefined ? null : JSON.stringify(bucket_order || [])
]); ]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
return res.status(404).json({ error: 'Version not found' }); return res.status(404).json({ error: 'Version not found' });

View File

@ -83,15 +83,6 @@ WHERE TRUE
-- Display order for the pivot's segment and bucket columns. -- Display order for the pivot's segment and bucket columns.
-- --
-- Perspective orders column groups by the value string, and SortDir's col asc /
-- col desc only reverses that -- so no sort setting can produce
-- Prior Year -> Plan -> Actual -> Forecast, which is alphabetical in neither
-- direction. The order has to be carried in the value itself, as a "01 · " style
-- prefix applied when the rows are served.
--
-- bucket_order lives on the version rather than the source because the Baseline
-- page, where it is maintained, is version-scoped. log.seq orders the segments
-- within that.
-- The segment's display name in the pivot, falling back to tag then note. -- The segment's display name in the pivot, falling back to tag then note.
-- --
-- Separate from both because those have jobs already -- tag groups adjustments -- Separate from both because those have jobs already -- tag groups adjustments
@ -101,6 +92,11 @@ WHERE TRUE
-- note would put it in every note. -- note would put it in every note.
ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS label text; ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS label text;
-- Vestigial, both of them. They held the ordinal when the "01 - " prefix was
-- computed for the pivot rather than typed into label and bucket: bucket_order
-- sequenced the bucket columns, log.seq the segments within them. Nothing reads
-- either now, and nothing writes them -- kept only because dropping a column is
-- not worth a migration to reclaim two that cost nothing.
ALTER TABLE pf.version ADD COLUMN IF NOT EXISTS bucket_order jsonb; ALTER TABLE pf.version ADD COLUMN IF NOT EXISTS bucket_order jsonb;
ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS seq integer; ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS seq integer;

View File

@ -841,10 +841,6 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
} }
} }
// Keep the ordering expressions in step with the version's bucket_order and the
// log's seq values. Merged into the live config rather than replacing
// expressions, so anything the user defined themselves survives.
//
// Size every column to its contents. // Size every column to its contents.
// //
// Values fit on their own: draw() calls regular_table.resetAutoSize(), which // Values fit on their own: draw() calls regular_table.resetAutoSize(), which