There was no way to express "these segments together are the forecast". pf_iter cannot say it: it answers whether operations may write to a row, and Open Orders is loaded as reference precisely so nothing adjusts it while still being part of the forecast number. The two questions are independent, so one cannot be derived from the other. pf.log.bucket is the second axis. Free text with suggestions -- Forecast, Prior Year, Prior Prior Year, Plan -- rather than an enum, so another banner needs no migration. Blank by default, falling back in the pivot to the segment's own name, so nothing changes until something is labelled. /data and /agg emit it as pf_bucket beside pf_segment, through the pf.log join that is already there. Set it per segment in the Baseline list, which is where loads live now that the change log only shows adjustments. Needs 01_schema.sql for the column and Generate SQL for /agg to select it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
5.8 KiB
SQL
130 lines
5.8 KiB
SQL
-- Pivot Forecast schema install
|
|
-- Run once against target database: psql -d <db> -f setup_sql/01_schema.sql
|
|
|
|
CREATE SCHEMA IF NOT EXISTS pf;
|
|
|
|
CREATE TABLE IF NOT EXISTS pf.source (
|
|
id serial PRIMARY KEY,
|
|
schema text NOT NULL,
|
|
tname text NOT NULL,
|
|
label text,
|
|
status text NOT NULL DEFAULT 'active', -- active | archived
|
|
default_layout jsonb, -- Perspective view config used as the per-source default
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
created_by text,
|
|
UNIQUE (schema, tname)
|
|
);
|
|
|
|
-- backfill columns for existing installs
|
|
ALTER TABLE pf.source ADD COLUMN IF NOT EXISTS default_layout jsonb;
|
|
|
|
-- 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,
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE CASCADE,
|
|
cname text NOT NULL,
|
|
label text,
|
|
role text NOT NULL DEFAULT 'ignore', -- dimension | value | units | date | ignore
|
|
is_key boolean NOT NULL DEFAULT false, -- true = usable in WHERE slice
|
|
dim_group text, -- groups functionally dependent columns
|
|
dim_period_col text, -- pf.dim_period column this dimension derives from
|
|
in_grain boolean NOT NULL DEFAULT false, -- true = column defines the display grain
|
|
opos integer,
|
|
UNIQUE (source_id, cname)
|
|
);
|
|
|
|
-- backfill columns for existing installs (must follow the CREATE above)
|
|
ALTER TABLE pf.col_meta ADD COLUMN IF NOT EXISTS dim_group text;
|
|
ALTER TABLE pf.col_meta ADD COLUMN IF NOT EXISTS dim_period_col text;
|
|
ALTER TABLE pf.col_meta ADD COLUMN IF NOT EXISTS in_grain boolean NOT NULL DEFAULT false;
|
|
|
|
CREATE TABLE IF NOT EXISTS pf.version (
|
|
id serial PRIMARY KEY,
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE RESTRICT,
|
|
name text NOT NULL,
|
|
description text,
|
|
status text NOT NULL DEFAULT 'open', -- open | closed
|
|
exclude_iters jsonb NOT NULL DEFAULT '["reference"]'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
created_by text,
|
|
closed_at timestamptz,
|
|
closed_by text,
|
|
UNIQUE (source_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pf.log (
|
|
id bigserial PRIMARY KEY,
|
|
version_id integer NOT NULL REFERENCES pf.version(id) ON DELETE CASCADE,
|
|
pf_user text NOT NULL,
|
|
stamp timestamptz NOT NULL DEFAULT now(),
|
|
operation text NOT NULL, -- baseline | reference | scale | recode | clone
|
|
slice jsonb,
|
|
params jsonb,
|
|
note text,
|
|
tag text -- initiative label, e.g. 'reduce_spend'; groups
|
|
-- adjustments into a bridge from baseline to current
|
|
);
|
|
|
|
-- adding tags to an install that predates them
|
|
ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS tag text;
|
|
CREATE INDEX IF NOT EXISTS log_tag_idx ON pf.log (tag) WHERE tag IS NOT NULL;
|
|
|
|
-- seed tags for loads that predate the column: a baseline/reference note is the
|
|
-- segment's name ('Open Orders', 'Prior Year'), which is exactly what tag holds.
|
|
-- Adjustment notes are free text, not labels, so they are left alone.
|
|
UPDATE pf.log
|
|
SET tag = note
|
|
WHERE TRUE
|
|
AND tag IS NULL
|
|
AND note IS NOT NULL
|
|
AND note <> ''
|
|
AND operation IN ('baseline', 'reference');
|
|
|
|
-- What a segment contributes to, independent of pf_iter.
|
|
--
|
|
-- pf_iter answers "can operations write to these rows"; bucket answers "does this
|
|
-- belong in the forecast number". Those are not the same question -- Open Orders is
|
|
-- loaded as reference so nothing adjusts it, yet it is part of the forecast -- so
|
|
-- neither can be derived from the other.
|
|
--
|
|
-- Free text with suggested values (Forecast / Prior Year / Prior Prior Year / Plan)
|
|
-- rather than an enum, so a new banner does not need a migration. Blank by default:
|
|
-- until a segment is labelled, the pivot falls back to showing its own name.
|
|
ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS bucket text;
|
|
CREATE INDEX IF NOT EXISTS log_bucket_idx ON pf.log (bucket) WHERE bucket IS NOT NULL;
|
|
|
|
-- Master data for a dim_group: one row per key value, with its sibling columns.
|
|
--
|
|
-- The source is transactional and often a view over all history, so deriving a
|
|
-- member list from it is both slow and wrong -- slow because it means scanning
|
|
-- millions of rows, wrong because it can only describe what was transacted and
|
|
-- has no way to say a part is discontinued or that a new one exists before it
|
|
-- has sold. This table is the app's own list, refreshed from the source but
|
|
-- curatable independently of it.
|
|
CREATE TABLE IF NOT EXISTS pf.dim_member (
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE CASCADE,
|
|
dim_group text NOT NULL, -- matches pf.col_meta.dim_group
|
|
key_value text NOT NULL, -- the is_key column's value
|
|
attrs jsonb NOT NULL DEFAULT '{}'::jsonb, -- the sibling columns
|
|
is_active boolean NOT NULL DEFAULT true,
|
|
source_seen boolean NOT NULL DEFAULT true, -- present in the source at last refresh
|
|
added_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
refreshed_at timestamptz,
|
|
PRIMARY KEY (source_id, dim_group, key_value)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS dim_member_active_idx
|
|
ON pf.dim_member (source_id, dim_group) WHERE is_active;
|
|
|
|
-- generated operation SQL per source, stored after col_meta is configured
|
|
CREATE TABLE IF NOT EXISTS pf.sql (
|
|
id serial PRIMARY KEY,
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE CASCADE,
|
|
operation text NOT NULL, -- get_data | baseline | reference | scale | recode | clone | undo
|
|
sql text NOT NULL,
|
|
generated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (source_id, operation)
|
|
);
|