From 0e87f22e2470bd9330647dfa99b9b034e5d943c3 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Wed, 16 Sep 2026 17:42:14 -0400 Subject: [PATCH] Carry every date and value column on baseline and reference loads The generator picked columns with find(), so a source with more than one date or value column silently loaded only the first of each. The version's DDL is built from col_meta separately, so the extra columns existed in the forecast table and stayed null for its whole life -- gs.osm_skinny lost sdate and stdcost_usd across 295k rows that way. Loads copy the source row wholesale, so they now carry all of them, with the offset shifting every date column together. The adjustment operations are genuinely single-measure -- scale distributes one {{value_incr}} -- so they keep the narrow column list until that has a defined meaning. Co-Authored-By: Claude Opus 5 (1M context) --- lib/sql_generator.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/sql_generator.js b/lib/sql_generator.js index c9942e8..97efa46 100644 --- a/lib/sql_generator.js +++ b/lib/sql_generator.js @@ -16,9 +16,21 @@ function generateSQL(source, colMeta) { .sort((a, b) => (a.opos || 0) - (b.opos || 0)) .map(c => c.cname); - const valueCol = colMeta.find(c => c.role === 'value')?.cname; - const unitsCol = colMeta.find(c => c.role === 'units')?.cname; - const dateCol = colMeta.find(c => c.role === 'date')?.cname; + // Every column of each measure/date role, in col_meta order. Loads carry all of + // them; the adjustment operations are single-measure (scale distributes one + // {{value_incr}}) and use only the first of each, below. + const byRole = role => colMeta + .filter(c => c.role === role) + .sort((a, b) => (a.opos || 0) - (b.opos || 0)) + .map(c => c.cname); + + const valueCols = byRole('value'); + const unitsCols = byRole('units'); + const dateCols = byRole('date'); + + const valueCol = valueCols[0]; + const unitsCol = unitsCols[0]; + const dateCol = dateCols[0]; if (!valueCol) throw new Error('No value column defined in col_meta'); if (!dateCol) throw new Error('No date column defined in col_meta'); @@ -32,6 +44,13 @@ function generateSQL(source, colMeta) { const selectData = dataCols.map(q).join(', '); const dimsJoined = dims.map(q).join(', '); + // Baseline and reference copy the source row wholesale, so they carry every + // measure and every date — not just the primary one the operations act on. + // Dropping the others would leave those columns null for the life of the version. + const loadCols = [...dims, ...dateCols, ...valueCols, ...unitsCols]; + const loadInsertCols = [...loadCols.map(q), 'pf_iter', 'pf_logid', 'pf_user', 'pf_created_at'].join(', '); + const dateColSet = new Set(dateCols); + // dim_period JOIN support: if the date column is the is_key of a dim_group, // dimension siblings with dim_period_col set are derived from pf.dim_period // instead of being copied raw from the source on baseline/reference load. @@ -61,8 +80,9 @@ function generateSQL(source, colMeta) { function buildLoadSelect(pfx) { // pfx: table alias prefix ('s.' when joining dim_period, '' otherwise) - return dataCols.map(c => { - if (c === dateCol) return `(${pfx}${q(c)} + '{{date_offset}}'::interval)::date`; + // The offset shifts every date column, so order date and ship date stay in step. + return loadCols.map(c => { + if (dateColSet.has(c)) return `(${pfx}${q(c)} + '{{date_offset}}'::interval)::date`; if (dimPeriodMap.has(c)) return `dp.${q(dimPeriodMap.get(c))} AS ${q(c)}`; return `${pfx}${q(c)}`; }).join(',\n '); @@ -83,7 +103,7 @@ ilog AS ( RETURNING id ) ,ins AS ( - INSERT INTO {{fc_table}} (${insertCols}) + INSERT INTO {{fc_table}} (${loadInsertCols}) SELECT ${buildLoadSelect(hasDimPeriod ? 's.' : '')}, 'baseline', (SELECT id FROM ilog), '{{pf_user}}', now() @@ -103,7 +123,7 @@ ilog AS ( RETURNING id ) ,ins AS ( - INSERT INTO {{fc_table}} (${insertCols}) + INSERT INTO {{fc_table}} (${loadInsertCols}) SELECT ${buildLoadSelect(hasDimPeriod ? 's.' : '')}, 'reference', (SELECT id FROM ilog), '{{pf_user}}', now()