-- 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; -- Each holder's customer, current place and local day. The current place is found by the customer's -- local day; a holder without a place, or a place without its own zone, uses the customer's zone. CREATE VIEW fm.v_holder_current AS SELECT h.holder_uid, h.operator_uid, hp.place_uid, coalesce(pl.timezone, o.timezone) AS timezone, (now() AT TIME ZONE coalesce(pl.timezone, o.timezone))::date AS today FROM fm.holder h JOIN fm.operator o ON o.operator_uid = h.operator_uid LEFT JOIN fm.holder_place hp ON hp.holder_uid = h.holder_uid AND daterange(coalesce(hp.valid_from, '-infinity'::date), hp.valid_to, '[)') @> (now() AT TIME ZONE o.timezone)::date LEFT JOIN fm.place pl ON pl.place_uid = hp.place_uid; 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.holder_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.holder_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.holder_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.holder_uid = o.holder_uid AND c.n = o.n + 1) SELECT p.holder_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.holder_uid = p.holder_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.holder_uid = i.holder_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.holder_uid = i.holder_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.holder_uid = i.holder_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.holder_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.holder_uid = i.holder_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.holder_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.holder_uid = i.holder_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.holder_uid = i.holder_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.holder_uid = i.holder_uid UNION ALL -- placements at this site, and anywhere of the assets its meters name SELECT 'placement:' || p.asset_uid || ':' || coalesce(p.holder_uid, p.place_uid) || ':' || coalesce(p.occupied_from::text, '-') || ':' || coalesce(p.occupied_to::text, '-') FROM fm.asset_placement p WHERE (p.holder_uid = i.holder_uid OR p.asset_uid IN (SELECT m.asset_uid FROM fm.runtime_meter m WHERE m.holder_uid = i.holder_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.holder_uid = i.holder_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.holder_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.holder_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.holder_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.holder_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.holder_uid = x.holder_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 holder comes from every row the subject key has -- had, in any status; for an asset's specification or engine total, from every holder the asset was -- placed on; for a location, from every holder that has stood at the place. So one entry can give -- one row per holder, and none is lost when its subject leaves. CREATE VIEW fm.v_review_stale AS WITH subj AS ( SELECT operator_uid, 'claim' AS kind, source_key AS subject_key, assertion_sha256, holder_uid, claim_status IN ('asserted','reviewed','disputed') AS live FROM fm.fuel_movement UNION ALL SELECT operator_uid, 'claim', source_key, assertion_sha256, holder_uid, claim_status IN ('asserted','reviewed','disputed') FROM fm.stock_observation UNION ALL SELECT h.operator_uid, 'claim', h.source_key, h.assertion_sha256, m.holder_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 operator_uid, 'claim', source_key, assertion_sha256, holder_uid, claim_status IN ('asserted','reviewed','disputed') FROM fm.electricity_bill UNION ALL SELECT operator_uid, 'claim', source_key, assertion_sha256, holder_uid, claim_status IN ('asserted','reviewed','disputed') FROM fm.imported_calculation UNION ALL SELECT e.operator_uid, 'claim', e.source_key, e.assertion_sha256, p.holder_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 operator_uid, 'claim', source_key, assertion_sha256, holder_uid, claim_status IN ('asserted','reviewed','disputed') FROM fm.runtime_interval UNION ALL SELECT t.operator_uid, 'claim', t.source_key, t.assertion_sha256, p.holder_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 l.operator_uid, 'location', l.source_key, l.assertion_sha256, hp.holder_uid, l.acceptance IN ('pending','accepted','rejected') FROM fm.place_location l LEFT JOIN fm.holder_place hp ON hp.place_uid = l.place_uid UNION ALL SELECT operator_uid, 'assumption', code, fingerprint, holder_uid, status <> 'superseded' FROM fm.assumption) SELECT r.operator_uid, r.review_id, r.entry_key, r.subject_kind, r.subject_key, r.assertion_sha256, r.verdict, r.reviewed_on, r.reviewer_role, st.holder_uid FROM fm.review r LEFT JOIN LATERAL (SELECT DISTINCT s.holder_uid FROM subj s WHERE s.operator_uid = r.operator_uid AND s.kind = r.subject_kind AND s.subject_key = r.subject_key AND s.holder_uid IS NOT NULL) st ON true WHERE NOT EXISTS (SELECT 1 FROM subj l WHERE l.live AND l.operator_uid = r.operator_uid 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;