trino/ deleted; the role trino_ro becomes fuel_ro, its password read from a secret file at connect time; dashboards' SQL and column list for PostgreSQL; the Compose project is renamed petrolyst, with volumes pinned to their old names. AGENTS.md points to the architecture context. Not deployed: the next deploy needs README's one-time steps first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
504 lines
33 KiB
PL/PgSQL
504 lines
33 KiB
PL/PgSQL
-- 0002_fm_derivations.sql: internal derivations in schema `fm` (not readable by fuel_ro).
|
|
-- DRAFT, not applied. Applied after 0001 by a loader acting as fuel_owner (README in this folder).
|
|
-- Design: fuel-monitoring .context/plans/2026-10-24-fuel-db-baseline-dashboard.md, §4 and §5.1.
|
|
BEGIN;
|
|
SET LOCAL search_path = fm, pg_catalog, public;
|
|
|
|
CREATE VIEW fm.v_source_rank AS
|
|
SELECT sha256, kind, CASE kind WHEN 'delivery_form' THEN 1 WHEN 'site_logbook' THEN 2
|
|
WHEN 'operator_record' THEN 3 WHEN 'operator_workbook' THEN 3 ELSE 9 END AS rank
|
|
FROM fm.source_document;
|
|
|
|
-- The analytical visit set: visits that still carry a live claim. A visit whose claims were all
|
|
-- superseded or retracted stays in fm.visit for provenance only and is invisible from here on.
|
|
CREATE VIEW fm.v_visit_live AS
|
|
SELECT v.* FROM fm.visit v
|
|
WHERE EXISTS (SELECT 1 FROM fm.fuel_movement m WHERE m.visit_id = v.visit_id
|
|
AND m.claim_status IN ('asserted','reviewed','disputed'))
|
|
OR EXISTS (SELECT 1 FROM fm.stock_observation s WHERE s.visit_id = v.visit_id
|
|
AND s.claim_status IN ('asserted','reviewed','disputed'))
|
|
OR EXISTS (SELECT 1 FROM fm.hour_meter_reading h WHERE h.visit_id = v.visit_id
|
|
AND h.claim_status IN ('asserted','reviewed','disputed'));
|
|
|
|
-- One row per physical movement (event_key; trigger rule 10 keeps kind and tanks equal within it).
|
|
-- Value = the best-ranked asserted or reviewed claim. The spread covers every live claim, and a
|
|
-- disputed claim makes the event unreviewed. No value = an unquantified movement.
|
|
CREATE VIEW fm.v_movement_event AS
|
|
SELECT m.visit_id, m.event_key, m.kind, m.from_tank_uid, m.to_tank_uid,
|
|
(array_agg(m.litres ORDER BY r.rank, m.movement_id)
|
|
FILTER (WHERE m.claim_status IN ('asserted','reviewed') AND m.litres IS NOT NULL))[1] AS litres,
|
|
coalesce(max(m.litres) - min(m.litres), 0) AS spread_l,
|
|
bool_and(m.claim_status = 'reviewed') AS all_reviewed,
|
|
count(*) AS n_claims
|
|
FROM fm.fuel_movement m JOIN fm.v_source_rank r ON r.sha256 = m.source_sha256
|
|
WHERE m.claim_status IN ('asserted','reviewed','disputed')
|
|
GROUP BY m.visit_id, m.event_key, m.kind, m.from_tank_uid, m.to_tank_uid;
|
|
|
|
-- Quantified site-level deliveries per visit, summed over events (never over claims).
|
|
CREATE VIEW fm.v_visit_delivery AS
|
|
SELECT visit_id, sum(litres) AS delivered_l, sum(spread_l) AS delivered_spread_l,
|
|
bool_and(all_reviewed) AS all_reviewed, count(*) AS n_events
|
|
FROM fm.v_movement_event
|
|
WHERE kind = 'delivery' AND to_tank_uid IS NULL AND litres IS NOT NULL
|
|
GROUP BY visit_id;
|
|
|
|
-- Every other external movement event (internal transfers cancel out at site level). Between two
|
|
-- endpoints they are booked; AT an endpoint visit their order relative to the stock readings is
|
|
-- unknown, so the interval is excluded instead of guessed (see v_stock_interval).
|
|
CREATE VIEW fm.v_visit_other_movement AS
|
|
SELECT visit_id,
|
|
sum(litres) FILTER (WHERE kind IN ('withdrawal','drain','spill','loss_reported')) AS out_l,
|
|
coalesce(sum(spread_l) FILTER (WHERE kind IN ('withdrawal','drain','spill','loss_reported')), 0) AS out_spread_l,
|
|
count(*) AS n_external,
|
|
count(*) FILTER (WHERE litres IS NULL OR kind = 'external_in'
|
|
OR (kind = 'delivery' AND to_tank_uid IS NOT NULL)) AS n_unquantified,
|
|
bool_and(all_reviewed) AS all_reviewed
|
|
FROM fm.v_movement_event
|
|
WHERE kind <> 'internal_transfer'
|
|
AND NOT (kind = 'delivery' AND to_tank_uid IS NULL AND litres IS NOT NULL)
|
|
GROUP BY visit_id;
|
|
|
|
CREATE VIEW fm.v_visit_stock AS -- site-level stock per visit and phase; same claim rules
|
|
SELECT s.visit_id, s.phase,
|
|
(array_agg(s.litres ORDER BY r.rank, s.stock_obs_id)
|
|
FILTER (WHERE s.claim_status IN ('asserted','reviewed') AND s.litres IS NOT NULL))[1] AS litres,
|
|
max(s.litres) - min(s.litres) AS spread_l,
|
|
max(s.resolution_l) FILTER (WHERE s.litres IS NOT NULL) AS resolution_l,
|
|
bool_and(s.claim_status = 'reviewed') AS all_reviewed, -- every live claim, with a number or not
|
|
count(*) AS n_claims
|
|
FROM fm.stock_observation s JOIN fm.v_source_rank r ON r.sha256 = s.source_sha256
|
|
WHERE s.claim_status IN ('asserted','reviewed','disputed') AND s.scope = 'site_total'
|
|
GROUP BY s.visit_id, s.phase
|
|
HAVING count(*) FILTER (WHERE s.claim_status IN ('asserted','reviewed') AND s.litres IS NOT NULL) > 0;
|
|
-- no asserted or reviewed number (only disputed, illegible or placeholder claims): no reading
|
|
-- v1: tank-level rows are summed only in a later version, and only when every placed tank has one.
|
|
|
|
CREATE VIEW fm.v_stock_point AS -- stock on arrival and on departure of each live visit
|
|
SELECT v.visit_id, v.visit_key, v.site_uid, v.visit_on, v.seq_in_day, v.kind,
|
|
d.delivered_l, coalesce(o.n_external, 0) AS n_other_movements,
|
|
coalesce(b.litres, st.litres, a.litres - d.delivered_l) AS arrive_l,
|
|
CASE WHEN coalesce(b.litres, st.litres) IS NOT NULL THEN 'stated'
|
|
WHEN a.litres - d.delivered_l IS NOT NULL THEN 'derived' END AS arrive_basis,
|
|
CASE WHEN b.litres IS NOT NULL THEN b.resolution_l WHEN st.litres IS NOT NULL THEN st.resolution_l
|
|
ELSE a.resolution_l END AS arrive_res_l,
|
|
CASE WHEN b.litres IS NOT NULL THEN b.spread_l WHEN st.litres IS NOT NULL THEN st.spread_l
|
|
ELSE a.spread_l + d.delivered_spread_l END AS arrive_spread_l,
|
|
CASE WHEN b.litres IS NOT NULL THEN b.all_reviewed WHEN st.litres IS NOT NULL THEN st.all_reviewed
|
|
ELSE a.all_reviewed AND d.all_reviewed END AS arrive_reviewed,
|
|
coalesce(a.litres, st.litres, b.litres + d.delivered_l) AS depart_l,
|
|
CASE WHEN coalesce(a.litres, st.litres) IS NOT NULL THEN 'stated'
|
|
WHEN b.litres + d.delivered_l IS NOT NULL THEN 'derived' END AS depart_basis,
|
|
CASE WHEN a.litres IS NOT NULL THEN a.resolution_l WHEN st.litres IS NOT NULL THEN st.resolution_l
|
|
ELSE b.resolution_l END AS depart_res_l,
|
|
CASE WHEN a.litres IS NOT NULL THEN a.spread_l WHEN st.litres IS NOT NULL THEN st.spread_l
|
|
ELSE b.spread_l + d.delivered_spread_l END AS depart_spread_l,
|
|
CASE WHEN a.litres IS NOT NULL THEN a.all_reviewed WHEN st.litres IS NOT NULL THEN st.all_reviewed
|
|
ELSE b.all_reviewed AND d.all_reviewed END AS depart_reviewed,
|
|
a.litres - (b.litres + d.delivered_l) AS after_vs_before_plus_delivered_l -- QA
|
|
FROM fm.v_visit_live v
|
|
LEFT JOIN fm.v_visit_delivery d ON d.visit_id = v.visit_id
|
|
LEFT JOIN fm.v_visit_other_movement o ON o.visit_id = v.visit_id
|
|
LEFT JOIN fm.v_visit_stock b ON b.visit_id = v.visit_id AND b.phase = 'before_delivery'
|
|
LEFT JOIN fm.v_visit_stock a ON a.visit_id = v.visit_id AND a.phase = 'after_delivery'
|
|
LEFT JOIN fm.v_visit_stock st ON st.visit_id = v.visit_id AND st.phase = 'standalone';
|
|
|
|
CREATE VIEW fm.v_stock_interval AS -- consecutive stock points per site; no overlap by construction
|
|
WITH pts AS (
|
|
SELECT p.*, row_number() OVER (PARTITION BY p.site_uid ORDER BY p.visit_on, p.seq_in_day) AS n
|
|
FROM fm.v_stock_point p WHERE p.arrive_l IS NOT NULL OR p.depart_l IS NOT NULL),
|
|
pr AS (
|
|
SELECT o.site_uid, o.visit_key || '>' || c.visit_key AS interval_key,
|
|
o.visit_id AS open_visit_id, c.visit_id AS close_visit_id,
|
|
o.visit_on AS start_on, c.visit_on AS end_on, c.visit_on - o.visit_on AS days,
|
|
o.depart_l AS opening_l, o.depart_basis AS opening_basis,
|
|
c.arrive_l AS closing_l, c.arrive_basis AS closing_basis,
|
|
coalesce(o.depart_res_l,0) + coalesce(c.arrive_res_l,0) AS endpoint_res_l,
|
|
coalesce(o.depart_spread_l,0) + coalesce(c.arrive_spread_l,0) AS endpoint_spread_l,
|
|
coalesce(o.depart_reviewed AND c.arrive_reviewed, false) AS endpoints_reviewed,
|
|
o.n_other_movements + c.n_other_movements AS n_endpoint_movements,
|
|
o.visit_on AS o_on, o.seq_in_day AS o_seq, c.visit_on AS c_on, c.seq_in_day AS c_seq
|
|
FROM pts o JOIN pts c ON c.site_uid = o.site_uid AND c.n = o.n + 1)
|
|
SELECT p.site_uid, p.interval_key, p.open_visit_id, p.close_visit_id, p.start_on, p.end_on, p.days,
|
|
p.opening_l, p.opening_basis, p.closing_l, p.closing_basis,
|
|
coalesce(mid.delivered_l, 0) AS deliveries_between_l, coalesce(mid.out_l, 0) AS out_l,
|
|
p.opening_l + coalesce(mid.delivered_l,0) - coalesce(mid.out_l,0) - p.closing_l AS consumed_l,
|
|
p.endpoint_res_l + p.endpoint_spread_l + coalesce(mid.spread_l,0) AS consumed_unc_l,
|
|
(p.endpoint_spread_l + coalesce(mid.spread_l,0)) > 0 AS inputs_conflict, -- any disagreement, as v_data_quality
|
|
p.endpoints_reviewed AND coalesce(mid.all_reviewed, true) AS inputs_reviewed,
|
|
CASE WHEN p.opening_l IS NULL OR p.closing_l IS NULL THEN 'no_opening_or_closing'
|
|
WHEN p.n_endpoint_movements > 0 THEN 'endpoint_movement_order_unknown'
|
|
WHEN mid.n_unknown > 0 THEN 'unquantified_movement'
|
|
WHEN p.days = 0 THEN 'same_day'
|
|
WHEN p.opening_l + coalesce(mid.delivered_l,0) - coalesce(mid.out_l,0) - p.closing_l
|
|
< -(p.endpoint_res_l + p.endpoint_spread_l + coalesce(mid.spread_l,0)) THEN 'negative'
|
|
ELSE 'ok' END AS stock_status
|
|
-- mid: live visits strictly between the endpoints. A visit counts only through its live movement
|
|
-- events, never through its kind.
|
|
FROM pr p
|
|
CROSS JOIN LATERAL (
|
|
SELECT sum(d.delivered_l) AS delivered_l,
|
|
sum(coalesce(d.delivered_spread_l, 0) + coalesce(x.out_spread_l, 0)) AS spread_l,
|
|
sum(x.out_l) AS out_l,
|
|
bool_and(coalesce(d.all_reviewed, true) AND coalesce(x.all_reviewed, true)) AS all_reviewed,
|
|
coalesce(sum(x.n_unquantified), 0) AS n_unknown
|
|
FROM fm.v_visit_live v
|
|
LEFT JOIN fm.v_visit_delivery d ON d.visit_id = v.visit_id
|
|
LEFT JOIN fm.v_visit_other_movement x ON x.visit_id = v.visit_id
|
|
WHERE v.site_uid = p.site_uid
|
|
AND (v.visit_on, v.seq_in_day) > (p.o_on, p.o_seq)
|
|
AND (v.visit_on, v.seq_in_day) < (p.c_on, p.c_seq)) mid;
|
|
|
|
CREATE VIEW fm.v_visit_reading AS -- one reading per visit and meter, with the claims' spread
|
|
SELECT h.visit_id, h.meter_id,
|
|
min(h.epoch_id) FILTER (WHERE h.hours IS NOT NULL) AS epoch_id,
|
|
count(DISTINCT h.epoch_id) FILTER (WHERE h.hours IS NOT NULL) AS n_epochs,
|
|
count(*) FILTER (WHERE h.hours IS NOT NULL AND h.epoch_id IS NULL) AS n_unassigned_epoch,
|
|
(array_agg(h.hours ORDER BY r.rank, h.reading_id)
|
|
FILTER (WHERE h.claim_status IN ('asserted','reviewed') AND h.hours IS NOT NULL))[1] AS hours,
|
|
min(h.hours) AS hours_min, max(h.hours) AS hours_max,
|
|
max(h.resolution_h) FILTER (WHERE h.hours IS NOT NULL) AS resolution_h,
|
|
bool_or(h.date_basis <> 'stated') FILTER (WHERE h.hours IS NOT NULL) AS date_inferred,
|
|
bool_and(h.claim_status = 'reviewed') AS all_reviewed -- every live claim, with a number or not
|
|
FROM fm.hour_meter_reading h JOIN fm.v_source_rank r ON r.sha256 = h.source_sha256
|
|
WHERE h.claim_status IN ('asserted','reviewed','disputed')
|
|
GROUP BY h.visit_id, h.meter_id
|
|
HAVING count(*) FILTER (WHERE h.claim_status IN ('asserted','reviewed') AND h.hours IS NOT NULL) > 0;
|
|
-- no asserted or reviewed number (only disputed or illegible claims): no reading
|
|
|
|
-- M2 per stock interval. Only a validated population is summed (CTE sel):
|
|
-- * a counter CONFIRMED for a generator placed at the site in the interval, when that generator
|
|
-- has exactly one such counter and no idle assumption;
|
|
-- * counters not tied to a set (unmapped, or mapping only proposed), only when the one live
|
|
-- meter_coverage assumption for the dates names exactly these counters, exactly the generators
|
|
-- left uncovered and exactly the idle sets. Such intervals give only the summed-set-hour metric.
|
|
-- A counter confirmed for a generator NOT placed at the site in the interval is never summed and
|
|
-- excludes the interval. Runtime and L/h are reported only for a valid population.
|
|
CREATE VIEW fm.v_lph_interval AS
|
|
WITH gen AS ( -- generators placed at the site at any time in the interval; full_cover =
|
|
-- one placement spans the whole interval, both endpoint dates included
|
|
SELECT i.interval_key, p.asset_uid,
|
|
bool_or(daterange(coalesce(p.occupied_from,'-infinity'::date), p.occupied_to, '[)')
|
|
@> daterange(i.start_on, i.end_on, '[]')) AS full_cover
|
|
FROM fm.v_stock_interval i
|
|
JOIN fm.asset_placement p ON p.site_uid = i.site_uid
|
|
JOIN fm.asset a ON a.asset_uid = p.asset_uid AND a.kind = 'generator'
|
|
WHERE daterange(coalesce(p.occupied_from,'-infinity'::date), p.occupied_to, '[)')
|
|
&& daterange(i.start_on, i.end_on, '[]')
|
|
GROUP BY i.interval_key, p.asset_uid),
|
|
pair AS ( -- every counter read at both endpoints, classified
|
|
SELECT i.interval_key, m.meter_id,
|
|
CASE WHEN m.mapping_status = 'confirmed' THEN m.asset_uid END AS asset_uid,
|
|
CASE WHEN m.mapping_status <> 'confirmed' THEN 'not_tied'
|
|
WHEN EXISTS (SELECT 1 FROM gen g
|
|
WHERE g.interval_key = i.interval_key AND g.asset_uid = m.asset_uid) THEN 'tied'
|
|
ELSE 'tied_elsewhere' END AS tie,
|
|
(o.n_epochs = 1 AND c.n_epochs = 1 AND o.n_unassigned_epoch = 0 AND c.n_unassigned_epoch = 0
|
|
AND o.epoch_id = c.epoch_id) AS same_epoch,
|
|
c.hours - o.hours AS runtime_h,
|
|
c.hours_min - o.hours_max - o.resolution_h - c.resolution_h AS runtime_low_h,
|
|
c.hours_max - o.hours_min + o.resolution_h + c.resolution_h AS runtime_high_h,
|
|
o.date_inferred OR c.date_inferred AS date_inferred,
|
|
o.all_reviewed AND c.all_reviewed AS reviewed,
|
|
(o.hours_max > o.hours_min OR c.hours_max > c.hours_min) AS conflict
|
|
FROM fm.v_stock_interval i
|
|
JOIN fm.runtime_meter m ON m.site_uid = i.site_uid
|
|
JOIN fm.v_visit_reading o ON o.visit_id = i.open_visit_id AND o.meter_id = m.meter_id
|
|
JOIN fm.v_visit_reading c ON c.visit_id = i.close_visit_id AND c.meter_id = m.meter_id),
|
|
idle AS ( -- at most one live generator_idle per asset and date (EXCLUDE)
|
|
SELECT g.interval_key, g.asset_uid, x.code, x.status
|
|
FROM gen g JOIN fm.v_stock_interval i USING (interval_key)
|
|
JOIN fm.assumption x ON x.asset_uid = g.asset_uid AND x.site_uid = i.site_uid -- this site's only
|
|
AND x.topic = 'generator_idle' AND x.status IN ('proposed','accepted')
|
|
AND daterange(x.applies_from, x.applies_to, '[]') @> daterange(i.start_on, i.end_on, '[]')),
|
|
per_gen AS (
|
|
SELECT g.interval_key, g.asset_uid, g.full_cover,
|
|
count(DISTINCT p.meter_id) AS n_counters,
|
|
count(DISTINCT d.code) > 0 AS is_idle,
|
|
coalesce(bool_or(d.status = 'proposed'), false) AS idle_proposed
|
|
FROM gen g
|
|
LEFT JOIN pair p ON p.interval_key = g.interval_key AND p.asset_uid = g.asset_uid AND p.tie = 'tied'
|
|
LEFT JOIN idle d ON d.interval_key = g.interval_key AND d.asset_uid = g.asset_uid
|
|
GROUP BY g.interval_key, g.asset_uid, g.full_cover),
|
|
cov AS (
|
|
SELECT interval_key, count(*) AS n_generators,
|
|
count(*) FILTER (WHERE NOT full_cover) AS n_partial,
|
|
count(*) FILTER (WHERE n_counters > 1) AS n_double_counted,
|
|
count(*) FILTER (WHERE n_counters >= 1 AND is_idle) AS n_metered_and_idle,
|
|
count(*) FILTER (WHERE n_counters = 0 AND NOT is_idle) AS n_uncovered,
|
|
coalesce(array_agg(asset_uid ORDER BY asset_uid)
|
|
FILTER (WHERE n_counters = 0 AND NOT is_idle), '{}') AS uncovered_set,
|
|
coalesce(array_agg(asset_uid ORDER BY asset_uid) FILTER (WHERE is_idle), '{}') AS idle_set,
|
|
bool_or(idle_proposed) AS idle_proposed
|
|
FROM per_gen GROUP BY interval_key),
|
|
loose AS ( -- counters read at both ends but not tied to a set
|
|
SELECT interval_key, array_agg(meter_id ORDER BY meter_id) AS meters
|
|
FROM pair WHERE tie = 'not_tied' GROUP BY interval_key),
|
|
cova AS ( -- live meter_coverage assumptions with their exact members
|
|
SELECT x.assumption_id, x.code, x.site_uid, x.status, x.applies_from, x.applies_to,
|
|
coalesce(array_agg(m.meter_id ORDER BY m.meter_id)
|
|
FILTER (WHERE m.member_role = 'counter'), '{}') AS meters,
|
|
coalesce(array_agg(m.asset_uid ORDER BY m.asset_uid)
|
|
FILTER (WHERE m.member_role = 'covered_generator'), '{}') AS covered,
|
|
coalesce(array_agg(m.asset_uid ORDER BY m.asset_uid)
|
|
FILTER (WHERE m.member_role = 'idle_generator'), '{}') AS idle_set
|
|
FROM fm.assumption x JOIN fm.assumption_member m ON m.assumption_id = x.assumption_id
|
|
WHERE x.topic = 'meter_coverage' AND x.status IN ('proposed','accepted')
|
|
GROUP BY x.assumption_id),
|
|
unm AS ( -- at most one live coverage assumption per site and date (EXCLUDE)
|
|
SELECT l.interval_key, cardinality(l.meters) AS n_unmapped, c.code AS cov_code, c.status AS cov_status,
|
|
c.covered AS cov_covered,
|
|
coalesce(c.meters = l.meters
|
|
AND c.covered = coalesce(cv.uncovered_set, '{}')
|
|
AND c.idle_set = coalesce(cv.idle_set, '{}')
|
|
AND cardinality(c.meters) = cardinality(c.covered), false) AS cov_matches
|
|
FROM loose l
|
|
JOIN fm.v_stock_interval i USING (interval_key)
|
|
LEFT JOIN cov cv USING (interval_key)
|
|
LEFT JOIN cova c ON c.site_uid = i.site_uid
|
|
AND daterange(c.applies_from, c.applies_to, '[]') @> daterange(i.start_on, i.end_on, '[]')),
|
|
sel AS ( -- the validated population; nothing else is summed
|
|
SELECT p.* FROM pair p
|
|
JOIN per_gen g ON g.interval_key = p.interval_key AND g.asset_uid = p.asset_uid
|
|
WHERE p.tie = 'tied' AND g.n_counters = 1 AND NOT g.is_idle
|
|
UNION ALL
|
|
SELECT p.* FROM pair p JOIN unm u ON u.interval_key = p.interval_key
|
|
WHERE p.tie = 'not_tied' AND u.cov_matches),
|
|
agg AS (
|
|
SELECT interval_key, count(*) AS n_meters, sum(runtime_h) AS runtime_h,
|
|
sum(runtime_low_h) AS runtime_low_h, sum(runtime_high_h) AS runtime_high_h,
|
|
bool_and(same_epoch) AS same_epoch, bool_and(runtime_h >= 0) AS monotonic,
|
|
max(runtime_h) AS max_meter_h, bool_or(date_inferred) AS date_inferred,
|
|
bool_and(reviewed) AS runtime_reviewed, bool_or(conflict) AS runtime_conflict,
|
|
string_agg(coalesce(asset_uid, 'meter:' || meter_id), ','
|
|
ORDER BY coalesce(asset_uid, 'meter:' || meter_id)) AS population_key
|
|
FROM sel GROUP BY interval_key),
|
|
rd AS ( -- what was read at both ends, before validation
|
|
SELECT interval_key, count(*) FILTER (WHERE tie = 'tied_elsewhere') AS n_tied_elsewhere
|
|
FROM pair GROUP BY interval_key),
|
|
pv AS ( -- valid = the summed population covers every placed generator exactly once
|
|
SELECT rd.interval_key,
|
|
coalesce(cv.n_generators > 0 AND rd.n_tied_elsewhere = 0 AND cv.n_partial = 0
|
|
AND cv.n_double_counted = 0 AND cv.n_metered_and_idle = 0
|
|
AND CASE WHEN u.interval_key IS NULL THEN cv.n_uncovered = 0 ELSE u.cov_matches END
|
|
AND a.n_meters >= 1, false) AS population_valid
|
|
FROM rd LEFT JOIN cov cv USING (interval_key) LEFT JOIN unm u USING (interval_key)
|
|
LEFT JOIN agg a USING (interval_key)),
|
|
x AS (
|
|
SELECT i.*,
|
|
coalesce(pv.population_valid, false) AS population_valid,
|
|
a.n_meters, u.cov_code,
|
|
CASE WHEN pv.population_valid THEN a.runtime_h END AS runtime_h,
|
|
CASE WHEN pv.population_valid THEN a.runtime_low_h END AS runtime_low_h,
|
|
CASE WHEN pv.population_valid THEN a.runtime_high_h END AS runtime_high_h,
|
|
CASE WHEN pv.population_valid THEN -- counters not tied to a set also name the sets they cover
|
|
(SELECT string_agg(k, ',' ORDER BY k)
|
|
FROM unnest(string_to_array(a.population_key, ',')
|
|
|| CASE WHEN u.cov_matches THEN array(SELECT 'covers:' || g FROM unnest(u.cov_covered) g)
|
|
ELSE '{}'::text[] END) k) END AS population_key,
|
|
a.date_inferred, a.runtime_reviewed, a.runtime_conflict,
|
|
coalesce(cv.n_generators, 0) AS n_generators, coalesce(u.n_unmapped, 0) AS n_unmapped,
|
|
coalesce(cv.idle_proposed, false)
|
|
OR (coalesce(u.cov_matches, false) AND u.cov_status = 'proposed') AS depends_on_proposed,
|
|
concat_ws('; ',
|
|
CASE WHEN i.stock_status <> 'ok' THEN 'stock interval: ' || i.stock_status END,
|
|
CASE WHEN rd.interval_key IS NULL THEN 'no timer reading at both ends' END,
|
|
CASE WHEN coalesce(cv.n_generators, 0) = 0 THEN 'no generator placed at the site' END,
|
|
CASE WHEN cv.n_partial > 0
|
|
THEN 'a generator was placed at or removed from the site within the interval' END,
|
|
CASE WHEN rd.n_tied_elsewhere > 0
|
|
THEN 'a counter is confirmed for a generator not placed here in the interval' END,
|
|
CASE WHEN cv.n_double_counted > 0 THEN 'two counters for one generator' END,
|
|
CASE WHEN cv.n_metered_and_idle > 0 THEN 'a generator is both metered and assumed idle' END,
|
|
CASE WHEN u.interval_key IS NULL AND cv.n_uncovered > 0
|
|
THEN 'a generator has neither a counter nor an idle assumption' END,
|
|
CASE WHEN u.interval_key IS NOT NULL AND u.cov_code IS NULL
|
|
THEN 'counters not tied to sets, and no meter-coverage assumption for the whole interval' END,
|
|
CASE WHEN u.cov_code IS NOT NULL AND NOT u.cov_matches
|
|
THEN 'meter-coverage assumption ' || u.cov_code
|
|
|| ' does not name exactly these counters, uncovered sets and idle sets' END,
|
|
CASE WHEN NOT a.same_epoch THEN 'readings not in one meter epoch' END,
|
|
CASE WHEN NOT a.monotonic THEN 'timer went backwards' END,
|
|
CASE WHEN a.max_meter_h > 24 * i.days THEN 'over 24 h/day on one meter' END,
|
|
CASE WHEN a.runtime_h = 0 AND i.consumed_l > 0 THEN 'fuel used with zero runtime' END,
|
|
CASE WHEN a.runtime_h = 0 AND i.consumed_l <= 0 THEN 'no operation in the interval' END,
|
|
CASE WHEN i.days < 7 THEN 'shorter than 7 days' END) AS exclusion_reasons
|
|
FROM fm.v_stock_interval i
|
|
LEFT JOIN rd USING (interval_key) LEFT JOIN agg a USING (interval_key)
|
|
LEFT JOIN cov cv USING (interval_key) LEFT JOIN unm u USING (interval_key)
|
|
LEFT JOIN pv USING (interval_key))
|
|
SELECT x.*,
|
|
CASE WHEN NOT population_valid THEN NULL
|
|
WHEN n_unmapped = 0 AND n_meters = 1 THEN 'apparent_l_per_generator_h'
|
|
ELSE 'apparent_l_per_set_h' END AS metric,
|
|
CASE WHEN runtime_h > 0 THEN consumed_l / runtime_h END AS l_per_h,
|
|
CASE WHEN runtime_high_h > 0 THEN (consumed_l - consumed_unc_l) / runtime_high_h END AS l_per_h_low,
|
|
CASE WHEN runtime_low_h > 0 THEN (consumed_l + consumed_unc_l) / runtime_low_h END AS l_per_h_high,
|
|
-- NULL beside a non-NULL l_per_h = unbounded
|
|
CASE WHEN exclusion_reasons <> '' OR NOT population_valid THEN 'excluded'
|
|
WHEN depends_on_proposed THEN 'conditional' ELSE 'eligible' END AS eligibility,
|
|
CASE WHEN opening_basis = 'stated' AND closing_basis = 'stated' AND inputs_reviewed AND NOT inputs_conflict
|
|
AND NOT date_inferred AND runtime_reviewed AND NOT runtime_conflict THEN 'G1' ELSE 'G2' END AS evidence_grade
|
|
FROM x;
|
|
|
|
-- M4 (L per calendar day) per stock interval: no runtime, so no generator population. The
|
|
-- population is the site's whole stock. Input to the L/day proposal path only; never a KPI.
|
|
CREATE VIEW fm.v_m4_interval AS
|
|
SELECT i.*, 'stock:' || i.site_uid AS population_key,
|
|
CASE WHEN i.days > 0 THEN i.consumed_l / i.days END AS l_per_day,
|
|
CASE WHEN i.stock_status = 'ok' THEN 'eligible' ELSE 'excluded' END AS eligibility,
|
|
CASE WHEN i.stock_status <> 'ok' THEN 'stock interval: ' || i.stock_status ELSE '' END AS exclusion_reasons,
|
|
CASE WHEN i.inputs_reviewed AND NOT i.inputs_conflict THEN 'G1' ELSE 'G2' END AS evidence_grade
|
|
FROM fm.v_stock_interval i;
|
|
|
|
-- Everything an interval's value and eligibility rest on, as sorted lists, with their hashes. Claims
|
|
-- are listed in every status, so a review, supersession or retraction changes the hash. `baseline
|
|
-- propose` snapshots both lists; fm.v_baseline_validity compares them with today's state.
|
|
CREATE VIEW fm.v_interval_dependency AS
|
|
WITH span AS ( -- the endpoint visits and every visit between them, live or not
|
|
SELECT i.interval_key, v.visit_id
|
|
FROM fm.v_stock_interval i
|
|
JOIN fm.visit o ON o.visit_id = i.open_visit_id
|
|
JOIN fm.visit c ON c.visit_id = i.close_visit_id
|
|
JOIN fm.visit v ON v.site_uid = i.site_uid
|
|
AND (v.visit_on, v.seq_in_day) >= (o.visit_on, o.seq_in_day)
|
|
AND (v.visit_on, v.seq_in_day) <= (c.visit_on, c.seq_in_day)),
|
|
stock_deps AS (
|
|
SELECT s.interval_key, jsonb_agg(d.item ORDER BY d.item) AS deps
|
|
FROM span s CROSS JOIN LATERAL (
|
|
SELECT 'stock:' || x.source_key || ':' || x.assertion_sha256 || ':' || x.claim_status AS item
|
|
FROM fm.stock_observation x WHERE x.visit_id = s.visit_id
|
|
UNION ALL
|
|
SELECT 'movement:' || x.event_key || ':' || x.assertion_sha256 || ':' || x.claim_status
|
|
FROM fm.fuel_movement x WHERE x.visit_id = s.visit_id) d
|
|
GROUP BY s.interval_key),
|
|
runtime_deps AS (
|
|
SELECT i.interval_key, jsonb_agg(d.item ORDER BY d.item) AS deps
|
|
FROM fm.v_stock_interval i CROSS JOIN LATERAL (
|
|
SELECT 'reading:' || x.meter_id || ':' || x.assertion_sha256 || ':' || x.claim_status
|
|
|| ':' || coalesce(x.epoch_id::text, '-') AS item
|
|
FROM fm.hour_meter_reading x WHERE x.visit_id IN (i.open_visit_id, i.close_visit_id)
|
|
UNION ALL
|
|
SELECT 'meter:' || m.meter_id || ':' || m.mapping_status || ':' || coalesce(m.asset_uid, '-')
|
|
FROM fm.runtime_meter m WHERE m.site_uid = i.site_uid
|
|
UNION ALL
|
|
SELECT 'epoch:' || e.meter_id || ':' || e.epoch_id || ':' || coalesce(e.starts_on::text, '-')
|
|
|| ':' || e.start_reason || ':' || e.continuity || ':' || coalesce(e.rollover_at::text, '-')
|
|
FROM fm.meter_epoch e JOIN fm.runtime_meter m ON m.meter_id = e.meter_id
|
|
WHERE m.site_uid = i.site_uid
|
|
UNION ALL -- placements at this site, and anywhere of the assets its meters name
|
|
SELECT 'placement:' || p.asset_uid || ':' || p.site_uid || ':' || coalesce(p.occupied_from::text, '-')
|
|
|| ':' || coalesce(p.occupied_to::text, '-')
|
|
FROM fm.asset_placement p
|
|
WHERE (p.site_uid = i.site_uid
|
|
OR p.asset_uid IN (SELECT m.asset_uid FROM fm.runtime_meter m WHERE m.site_uid = i.site_uid))
|
|
AND daterange(coalesce(p.occupied_from, '-infinity'::date), p.occupied_to, '[)')
|
|
&& daterange(i.start_on, i.end_on, '[]')
|
|
UNION ALL
|
|
SELECT 'assumption:' || x.code || ':' || x.fingerprint || ':' || x.status
|
|
FROM fm.assumption x
|
|
WHERE x.site_uid = i.site_uid AND x.topic IN ('generator_idle','meter_coverage')
|
|
AND daterange(x.applies_from, x.applies_to, '[]') && daterange(i.start_on, i.end_on, '[]')) d
|
|
GROUP BY i.interval_key)
|
|
SELECT i.interval_key, i.site_uid, i.open_visit_id, i.close_visit_id,
|
|
coalesce(s.deps, '[]'::jsonb) AS stock_deps, coalesce(r.deps, '[]'::jsonb) AS runtime_deps,
|
|
encode(sha256(convert_to(coalesce(s.deps, '[]'::jsonb)::text, 'UTF8')), 'hex') AS stock_dep_sha256,
|
|
encode(sha256(convert_to(coalesce(r.deps, '[]'::jsonb)::text, 'UTF8')), 'hex') AS runtime_dep_sha256
|
|
FROM fm.v_stock_interval i
|
|
LEFT JOIN stock_deps s USING (interval_key) LEFT JOIN runtime_deps r USING (interval_key);
|
|
|
|
-- Does each version still rest on what it was computed from? An input whose interval no longer
|
|
-- exists, or whose dependency hash differs, makes the whole version "inputs changed".
|
|
CREATE VIEW fm.v_baseline_validity AS
|
|
SELECT b.baseline_id,
|
|
count(bi.baseline_id) > 0
|
|
AND coalesce(bool_and(d.interval_key IS NOT NULL
|
|
AND d.stock_dep_sha256 = bi.stock_dep_sha256
|
|
AND (b.metric = 'fuel_l_per_calendar_day'
|
|
OR d.runtime_dep_sha256 = bi.runtime_dep_sha256)), false) AS inputs_unchanged
|
|
FROM fm.baseline_version b
|
|
LEFT JOIN fm.baseline_input bi ON bi.baseline_id = b.baseline_id
|
|
LEFT JOIN fm.v_interval_dependency d
|
|
ON d.open_visit_id = bi.open_visit_id AND d.close_visit_id = bi.close_visit_id
|
|
GROUP BY b.baseline_id, b.metric;
|
|
|
|
-- Consecutive readings per meter (live visits), giving hours per day: R1 / public.v_runtime.
|
|
CREATE VIEW fm.v_runtime_pair AS
|
|
WITH r AS (
|
|
SELECT v.site_uid, v.visit_id, v.visit_on, v.seq_in_day, x.meter_id, x.epoch_id, x.n_epochs,
|
|
x.n_unassigned_epoch, x.hours, x.all_reviewed,
|
|
row_number() OVER (PARTITION BY x.meter_id ORDER BY v.visit_on, v.seq_in_day) AS n
|
|
FROM fm.v_visit_reading x JOIN fm.v_visit_live v ON v.visit_id = x.visit_id)
|
|
SELECT o.site_uid, o.meter_id, o.visit_id AS open_visit_id, c.visit_id AS close_visit_id,
|
|
o.visit_on AS start_on, c.visit_on AS end_on, c.visit_on - o.visit_on AS days,
|
|
c.hours - o.hours AS runtime_h,
|
|
CASE WHEN c.visit_on > o.visit_on THEN (c.hours - o.hours) / (c.visit_on - o.visit_on) END AS h_per_day,
|
|
(o.n_epochs = 1 AND c.n_epochs = 1 AND o.n_unassigned_epoch = 0 AND c.n_unassigned_epoch = 0
|
|
AND o.epoch_id = c.epoch_id) AS same_epoch,
|
|
c.hours >= o.hours AS monotonic,
|
|
c.hours - o.hours > 24 * (c.visit_on - o.visit_on) AS over_24h_per_day,
|
|
o.all_reviewed AND c.all_reviewed AS reviewed
|
|
FROM r o JOIN r c ON c.meter_id = o.meter_id AND c.n = o.n + 1;
|
|
|
|
-- M3, modelled L/h: L per calendar day over the usable intervals that lie wholly inside the
|
|
-- assumption's dates, divided by that independently sourced runtime assumption (never one derived
|
|
-- from fuel). The window is the matched one. Reference tab only; never a KPI (design §4.1).
|
|
CREATE VIEW fm.v_modelled_lph AS
|
|
SELECT x.site_uid, x.code, x.status, x.basis,
|
|
x.value_num AS h_per_day, x.low_num AS h_per_day_low, x.high_num AS h_per_day_high,
|
|
m4.window_start, m4.window_end, m4.days AS matched_days, m4.fuel_l / NULLIF(m4.days, 0) AS l_per_day,
|
|
m4.fuel_l / NULLIF(m4.days, 0) / NULLIF(x.value_num, 0) AS modelled_l_per_h,
|
|
m4.fuel_l / NULLIF(m4.days, 0) / NULLIF(x.high_num, 0) AS modelled_l_per_h_low,
|
|
m4.fuel_l / NULLIF(m4.days, 0) / NULLIF(x.low_num, 0) AS modelled_l_per_h_high
|
|
FROM fm.assumption x
|
|
CROSS JOIN LATERAL (
|
|
SELECT sum(i.consumed_l) AS fuel_l, sum(i.days) AS days,
|
|
min(i.start_on) AS window_start, max(i.end_on) AS window_end
|
|
FROM fm.v_m4_interval i
|
|
WHERE i.site_uid = x.site_uid AND i.eligibility = 'eligible'
|
|
AND daterange(x.applies_from, x.applies_to, '[]') @> daterange(i.start_on, i.end_on, '[]')) m4
|
|
WHERE x.topic = 'runtime_hours_per_day' AND NOT x.derived_from_fuel AND x.status IN ('proposed','accepted')
|
|
AND m4.days > 0;
|
|
|
|
-- Review entries whose fingerprint matches no live row: the value changed after it was reviewed,
|
|
-- or the subject was superseded or retracted. The site comes from every row the subject key has
|
|
-- had, in any status; for an asset's specification or engine total, from every site the asset was
|
|
-- placed at. So one entry can give one row per site, and none is lost when its subject leaves.
|
|
CREATE VIEW fm.v_review_stale AS
|
|
WITH subj AS (
|
|
SELECT 'claim' AS kind, source_key AS subject_key, assertion_sha256, site_uid,
|
|
claim_status IN ('asserted','reviewed','disputed') AS live
|
|
FROM fm.fuel_movement
|
|
UNION ALL SELECT 'claim', source_key, assertion_sha256, site_uid, claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.stock_observation
|
|
UNION ALL SELECT 'claim', h.source_key, h.assertion_sha256, m.site_uid, h.claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.hour_meter_reading h JOIN fm.runtime_meter m ON m.meter_id = h.meter_id
|
|
UNION ALL SELECT 'claim', source_key, assertion_sha256, site_uid, claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.electricity_bill
|
|
UNION ALL SELECT 'claim', source_key, assertion_sha256, site_uid, claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.imported_calculation
|
|
UNION ALL SELECT 'claim', e.source_key, e.assertion_sha256, p.site_uid, e.claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.equipment_spec e LEFT JOIN fm.asset_placement p ON p.asset_uid = e.asset_uid
|
|
UNION ALL SELECT 'claim', source_key, assertion_sha256, site_uid, claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.runtime_interval
|
|
UNION ALL SELECT 'claim', t.source_key, t.assertion_sha256, p.site_uid, t.claim_status IN ('asserted','reviewed','disputed')
|
|
FROM fm.engine_fuel_total t LEFT JOIN fm.asset_placement p ON p.asset_uid = t.asset_uid
|
|
UNION ALL SELECT 'location', source_key, assertion_sha256, site_uid, acceptance IN ('pending','accepted','rejected')
|
|
FROM fm.site_location
|
|
UNION ALL SELECT 'assumption', code, fingerprint, site_uid, status <> 'superseded'
|
|
FROM fm.assumption)
|
|
SELECT r.review_id, r.entry_key, r.subject_kind, r.subject_key, r.assertion_sha256, r.verdict,
|
|
r.reviewed_on, r.reviewer_role, st.site_uid
|
|
FROM fm.review r
|
|
LEFT JOIN LATERAL (SELECT DISTINCT s.site_uid FROM subj s
|
|
WHERE s.kind = r.subject_kind AND s.subject_key = r.subject_key AND s.site_uid IS NOT NULL) st ON true
|
|
WHERE NOT EXISTS (SELECT 1 FROM subj l
|
|
WHERE l.live AND l.kind = r.subject_kind AND l.subject_key = r.subject_key
|
|
AND l.assertion_sha256 = r.assertion_sha256);
|
|
|
|
-- Visits kept for provenance only: every claim superseded or retracted.
|
|
CREATE VIEW fm.v_visit_provenance_only AS
|
|
SELECT v.* FROM fm.visit v
|
|
WHERE NOT EXISTS (SELECT 1 FROM fm.v_visit_live l WHERE l.visit_id = v.visit_id);
|
|
|
|
COMMIT;
|