From 56733df5d488dc878a73f23633436c4cffe14bc6 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 23 May 2026 01:46:16 -0400 Subject: [PATCH] Add dim_group to col_meta and pf.dim_period calendar table - col_meta: add dim_group field to group related columns (dimension hierarchies, date-adjacent columns); is_key now enabled for date role to mark group parent - sources.js: upsert includes dim_group - Setup.jsx: group column in col_meta editor, key checkbox enabled for date role - gen_dim_period.sql: create and populate pf.dim_period with calendar and fiscal period cuts (monthly grain, 2018-2035) Co-Authored-By: Claude Sonnet 4.6 --- routes/sources.js | 22 +++++---- setup_sql/01_schema.sql | 5 +- setup_sql/gen_dim_period.sql | 94 ++++++++++++++++++++++++++++++++++++ ui/src/views/Setup.jsx | 13 ++++- 4 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 setup_sql/gen_dim_period.sql diff --git a/routes/sources.js b/routes/sources.js index cc27fa7..4347774 100644 --- a/routes/sources.js +++ b/routes/sources.js @@ -89,20 +89,22 @@ module.exports = function(pool) { await client.query('BEGIN'); for (const col of cols) { await client.query(` - INSERT INTO pf.col_meta (source_id, cname, label, role, is_key, opos) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO pf.col_meta (source_id, cname, label, role, is_key, dim_group, opos) + VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (source_id, cname) DO UPDATE SET - label = EXCLUDED.label, - role = EXCLUDED.role, - is_key = EXCLUDED.is_key, - opos = EXCLUDED.opos + label = EXCLUDED.label, + role = EXCLUDED.role, + is_key = EXCLUDED.is_key, + dim_group = EXCLUDED.dim_group, + opos = EXCLUDED.opos `, [ sourceId, col.cname, - col.label || null, - col.role || 'ignore', - col.is_key || false, - col.opos || null + col.label || null, + col.role || 'ignore', + col.is_key || false, + col.dim_group || null, + col.opos || null ]); } await client.query('COMMIT'); diff --git a/setup_sql/01_schema.sql b/setup_sql/01_schema.sql index ea9ed8c..dfd677d 100644 --- a/setup_sql/01_schema.sql +++ b/setup_sql/01_schema.sql @@ -15,8 +15,11 @@ CREATE TABLE IF NOT EXISTS pf.source ( UNIQUE (schema, tname) ); --- backfill column for existing installs +-- backfill columns for existing installs ALTER TABLE pf.source ADD COLUMN IF NOT EXISTS default_layout jsonb; +ALTER TABLE pf.col_meta ADD COLUMN IF NOT EXISTS dim_group text; + +-- pf.dim_period: run setup_sql/gen_dim_period.sql to create and populate CREATE TABLE IF NOT EXISTS pf.col_meta ( id serial PRIMARY KEY, diff --git a/setup_sql/gen_dim_period.sql b/setup_sql/gen_dim_period.sql new file mode 100644 index 0000000..fd8009f --- /dev/null +++ b/setup_sql/gen_dim_period.sql @@ -0,0 +1,94 @@ +-- pf.dim_period — create and populate +-- Adjust fiscal_start_month: 1=Jan (calendar year), 4=Apr, 6=Jun, 7=Jul, 10=Oct, etc. +-- Safe to re-run: ON CONFLICT DO NOTHING, so existing rows are never overwritten. + +CREATE TABLE IF NOT EXISTS pf.dim_period ( + sdat date NOT NULL PRIMARY KEY, + edat date NOT NULL, + drange daterange NOT NULL, + ndays integer NOT NULL, + -- calendar + cal_year integer NOT NULL, + cal_quarter integer NOT NULL, + cal_month integer NOT NULL, + cal_month_abbr text NOT NULL, -- 01 - Jan + cal_month_name text NOT NULL, -- 01 - January + cal_label text NOT NULL, -- 2025-01 Jan + -- fiscal + fisc_year integer NOT NULL, + fisc_quarter integer NOT NULL, + fisc_quarter_label text NOT NULL, -- FY2025 Q3 + fisc_month integer NOT NULL, + fisc_month_abbr text NOT NULL, -- 07 - Jan + fisc_month_name text NOT NULL, -- 07 - January + fisc_label text NOT NULL, -- FY2025 P07 + -- sort key + period_key text NOT NULL -- 2025.07 (ltree-compatible) +); + +CREATE INDEX IF NOT EXISTS dim_period_drange_idx ON pf.dim_period USING gist (drange); +CREATE INDEX IF NOT EXISTS dim_period_fisc_idx ON pf.dim_period (fisc_year, fisc_month); +CREATE INDEX IF NOT EXISTS dim_period_cal_idx ON pf.dim_period (cal_year, cal_month); + +WITH +cfg AS ( + SELECT 6 AS fiscal_start_month -- change to match your fiscal year start month +) +,periods AS ( + SELECT + gs.d::date AS sdat, + (gs.d + '1 month'::interval)::date AS edat, + extract(year FROM gs.d)::int AS cal_year, + extract(month FROM gs.d)::int AS cal_month, + extract(quarter FROM gs.d)::int AS cal_quarter, + ((extract(month FROM gs.d)::int - cfg.fiscal_start_month + 12) % 12) + 1 + AS fisc_month, + extract(year FROM gs.d)::int + + CASE + WHEN cfg.fiscal_start_month > 1 + AND extract(month FROM gs.d)::int >= cfg.fiscal_start_month + THEN 1 ELSE 0 + END AS fisc_year + FROM + generate_series('2018-01-01'::date, '2035-12-01'::date, '1 month') gs(d) + CROSS JOIN cfg +) +INSERT INTO pf.dim_period ( + sdat, edat, drange, ndays, + cal_year, cal_quarter, cal_month, + cal_month_abbr, cal_month_name, cal_label, + fisc_year, fisc_quarter, fisc_quarter_label, + fisc_month, fisc_month_abbr, fisc_month_name, + fisc_label, period_key +) +SELECT + sdat, + edat, + daterange(sdat, edat) AS drange, + edat - sdat AS ndays, + cal_year, + cal_quarter, + cal_month, + to_char(cal_month, 'FM00') || ' - ' || to_char(sdat, 'Mon') AS cal_month_abbr, + to_char(cal_month, 'FM00') || ' - ' || to_char(sdat, 'Month') AS cal_month_name, + to_char(sdat, 'YYYY-MM') || ' ' || to_char(sdat, 'Mon') AS cal_label, + fisc_year, + ceil(fisc_month / 3.0)::int AS fisc_quarter, + 'FY' || fisc_year || ' Q' || ceil(fisc_month / 3.0)::int AS fisc_quarter_label, + fisc_month, + to_char(fisc_month, 'FM00') || ' - ' || to_char(sdat, 'Mon') AS fisc_month_abbr, + to_char(fisc_month, 'FM00') || ' - ' || to_char(sdat, 'Month') AS fisc_month_name, + 'FY' || fisc_year || ' P' || to_char(fisc_month, 'FM00') AS fisc_label, + to_char(fisc_year, 'FM0000') || '.' || to_char(fisc_month, 'FM00') AS period_key +FROM periods +ON CONFLICT (sdat) DO NOTHING; + +-- preview first 24 months +SELECT + period_key, sdat, edat, ndays, + cal_year, cal_quarter, cal_month, cal_month_abbr, + fisc_year, fisc_quarter, fisc_month, fisc_month_abbr, + fisc_quarter_label, fisc_label, cal_label +FROM pf.dim_period +ORDER BY sdat +LIMIT 24; diff --git a/ui/src/views/Setup.jsx b/ui/src/views/Setup.jsx index d267417..edac978 100644 --- a/ui/src/views/Setup.jsx +++ b/ui/src/views/Setup.jsx @@ -281,6 +281,7 @@ export default function Setup({ refreshSources }) { column role key + group label @@ -302,10 +303,20 @@ export default function Setup({ refreshSources }) { type="checkbox" checked={!!col.is_key} onChange={e => updateCol(i, 'is_key', e.target.checked)} - disabled={col.role !== 'dimension'} + disabled={col.role !== 'dimension' && col.role !== 'date'} className="cursor-pointer disabled:opacity-20" /> + + updateCol(i, 'dim_group', e.target.value || null)} + placeholder="—" + disabled={col.role !== 'dimension' && col.role !== 'date'} + className="border border-transparent hover:border-gray-200 focus:border-gray-300 rounded px-1.5 py-0.5 w-full outline-none bg-transparent disabled:opacity-20 disabled:cursor-default" + /> +