From 469e4cc957dc4506150e123999c66c1f0ff2a29c Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Fri, 18 Sep 2026 13:49:44 -0400 Subject: [PATCH] Name each loaded table uniquely Refresh Data aborted with Table "fc_29" already exists. The name was fixed per version and the cleanup before it is best-effort: the viewer is still holding the previous table when the new one is built, so deleting it does not free the registry entry, and creating a second under the same name fails. Nothing reads the name -- the viewer is loaded by reference, and cleanLayout strips it out of saved configs -- so it only has to be unique. The previous name is remembered and freed on the next load, when the viewer has let go of it. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/views/Forecast.jsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index ce2416a..24847a7 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -246,6 +246,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // the datagrid plugin element, for reading cell coordinates and driving its // own selection highlight (see the selection-highlight effect below) const gridRef = useRef(null) + // the worker-registry name of the table currently loaded, so the next load can + // try to free it + const tableNameRef = useRef(null) // { x, y } of the body cell the last mousedown landed on, or null const downCellRef = useRef(null) // the region a drag is building, as a Perspective ViewWindow; committed on mouseup @@ -765,7 +768,13 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio ...meta.filter(c => ['dimension','value','units','date'].includes(c.role)).map(c => c.cname), 'pf_id', 'pf_iter', 'pf_logid', 'pf_user', 'created_at', 'pf_segment', 'pf_bucket', 'pf_note', 'pf_op', ]) - const tableName = `fc_${vid}` + // Unique per load, not per version. A fixed name collides on refresh: + // the viewer still holds the previous table, so deleting it does not + // actually free the registry entry, and creating the new one aborts with + // "Table `fc_29` already exists". Nothing reads the name -- the viewer is + // loaded by reference and cleanLayout strips the name out of saved + // configs -- so it only has to be unique. + const tableName = `fc_${vid}_${myId}` if (rowCount >= 500000) setLargeDataset(true) @@ -774,16 +783,21 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio if (!workerRef.current) workerRef.current = await perspective.worker() const worker = workerRef.current - // Clean up the previous table — by JS reference first, then by name in the - // worker registry (covers the case where the ref was lost or delete failed). + // Clean up the previous table — by JS reference first, then by the name we + // gave it (covers the case where the ref was lost or the delete failed). + // Best-effort: a table the viewer is still holding will refuse, which is + // why the new one no longer reuses its name. if (tableRef.current) { try { await tableRef.current.delete() } catch {} tableRef.current = null } - try { - const stale = await worker.open_table(tableName) - if (stale) await stale.delete() - } catch {} + if (tableNameRef.current && tableNameRef.current !== tableName) { + try { + const stale = await worker.open_table(tableNameRef.current) + if (stale) await stale.delete() + } catch {} + } + tableNameRef.current = tableName // An empty result gets no index. `[]` carries no columns, so naming one // aborts the worker outright -- "Specified index `pf_gkey` does not exist