From 98322a608023c75d38bfcc7e08d2c682d9a20950 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Thu, 17 Sep 2026 22:51:34 -0400 Subject: [PATCH] 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) --- routes/log.js | 13 +++++-------- routes/versions.js | 16 +++++++--------- setup_sql/01_schema.sql | 14 +++++--------- ui/src/views/Forecast.jsx | 4 ---- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/routes/log.js b/routes/log.js index 917f4ff..3f4614c 100644 --- a/routes/log.js +++ b/routes/log.js @@ -131,11 +131,11 @@ module.exports = function(pool) { // a closed version, where relabelling history is still legitimate. router.patch('/log/:logid', async (req, res) => { const logId = parseInt(req.params.logid); - const { note, tag, bucket, seq, label } = req.body; - if (note === undefined && tag === undefined && bucket === undefined - && seq === undefined && label === undefined) { + const { note, tag, bucket, label } = req.body; + if (note === undefined && tag === undefined + && bucket === undefined && label === undefined) { 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 { @@ -146,16 +146,13 @@ module.exports = function(pool) { note = CASE WHEN $2::bool THEN $3::text ELSE note END, tag = CASE WHEN $4::bool THEN $5::text ELSE tag 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 $10::bool THEN $11::text ELSE label END + label = CASE WHEN $8::bool THEN $9::text ELSE label END WHERE id = $1 RETURNING *`, [ logId, note !== undefined, note === undefined ? null : (String(note).trim() || null), tag !== undefined, tag === undefined ? null : (String(tag).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), ] ); diff --git a/routes/versions.js b/routes/versions.js index 7087bed..d5a4a17 100644 --- a/routes/versions.js +++ b/routes/versions.js @@ -308,27 +308,25 @@ ${colDefs}, }); // 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) => { - const { name, description, exclude_iters, bucket_order } = req.body; + const { name, description, exclude_iters } = req.body; 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(` UPDATE pf.version SET name = COALESCE($2, name), description = COALESCE($3, description), - exclude_iters = COALESCE($4, exclude_iters), - bucket_order = CASE WHEN $5::bool THEN $6::jsonb ELSE bucket_order END + exclude_iters = COALESCE($4, exclude_iters) WHERE id = $1 RETURNING * `, [ req.params.id, name || null, description || null, - exclude_iters ? JSON.stringify(exclude_iters) : null, - bucket_order !== undefined, - bucket_order === undefined ? null : JSON.stringify(bucket_order || []) + exclude_iters ? JSON.stringify(exclude_iters) : null ]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Version not found' }); diff --git a/setup_sql/01_schema.sql b/setup_sql/01_schema.sql index cd66384..29f2365 100644 --- a/setup_sql/01_schema.sql +++ b/setup_sql/01_schema.sql @@ -83,15 +83,6 @@ WHERE TRUE -- 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. -- -- Separate from both because those have jobs already -- tag groups adjustments @@ -101,6 +92,11 @@ WHERE TRUE -- note would put it in every note. 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.log ADD COLUMN IF NOT EXISTS seq integer; diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 390cafa..c014557 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -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. // // Values fit on their own: draw() calls regular_table.resetAutoSize(), which