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 <noreply@anthropic.com>
This commit is contained in:
parent
c4ba90ae4c
commit
56733df5d4
@ -89,20 +89,22 @@ module.exports = function(pool) {
|
|||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
for (const col of cols) {
|
for (const col of cols) {
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO pf.col_meta (source_id, cname, label, role, is_key, opos)
|
INSERT INTO pf.col_meta (source_id, cname, label, role, is_key, dim_group, opos)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
ON CONFLICT (source_id, cname) DO UPDATE SET
|
ON CONFLICT (source_id, cname) DO UPDATE SET
|
||||||
label = EXCLUDED.label,
|
label = EXCLUDED.label,
|
||||||
role = EXCLUDED.role,
|
role = EXCLUDED.role,
|
||||||
is_key = EXCLUDED.is_key,
|
is_key = EXCLUDED.is_key,
|
||||||
opos = EXCLUDED.opos
|
dim_group = EXCLUDED.dim_group,
|
||||||
|
opos = EXCLUDED.opos
|
||||||
`, [
|
`, [
|
||||||
sourceId,
|
sourceId,
|
||||||
col.cname,
|
col.cname,
|
||||||
col.label || null,
|
col.label || null,
|
||||||
col.role || 'ignore',
|
col.role || 'ignore',
|
||||||
col.is_key || false,
|
col.is_key || false,
|
||||||
col.opos || null
|
col.dim_group || null,
|
||||||
|
col.opos || null
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
|
|||||||
@ -15,8 +15,11 @@ CREATE TABLE IF NOT EXISTS pf.source (
|
|||||||
UNIQUE (schema, tname)
|
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.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 (
|
CREATE TABLE IF NOT EXISTS pf.col_meta (
|
||||||
id serial PRIMARY KEY,
|
id serial PRIMARY KEY,
|
||||||
|
|||||||
94
setup_sql/gen_dim_period.sql
Normal file
94
setup_sql/gen_dim_period.sql
Normal file
@ -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;
|
||||||
@ -281,6 +281,7 @@ export default function Setup({ refreshSources }) {
|
|||||||
<th className="px-3 py-1.5 font-medium">column</th>
|
<th className="px-3 py-1.5 font-medium">column</th>
|
||||||
<th className="px-3 py-1.5 font-medium">role</th>
|
<th className="px-3 py-1.5 font-medium">role</th>
|
||||||
<th className="px-3 py-1.5 font-medium text-center">key</th>
|
<th className="px-3 py-1.5 font-medium text-center">key</th>
|
||||||
|
<th className="px-3 py-1.5 font-medium">group</th>
|
||||||
<th className="px-3 py-1.5 font-medium">label</th>
|
<th className="px-3 py-1.5 font-medium">label</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -302,10 +303,20 @@ export default function Setup({ refreshSources }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!!col.is_key}
|
checked={!!col.is_key}
|
||||||
onChange={e => updateCol(i, 'is_key', e.target.checked)}
|
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"
|
className="cursor-pointer disabled:opacity-20"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-3 py-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={col.dim_group || ''}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td className="px-3 py-1.5">
|
<td className="px-3 py-1.5">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user