fuel-db: customer on every row, place and holder for site, licence tables
Draft migrations rewritten in place (PD-16, PD-18), with 0004 for the licence tables (PD-20) and rule tests in fuel-db/tests. Dashboards' column list regenerated; coverage_detail selects operator_uid. Checks: tests/run.sh 106 of 106; 18 views, 686 rows, 0 differences against a reference from the same corpus. Codex's fixes to its inspection are not re-inspected. Bands: persistence, Superset. Rules: 6, 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e4806f5799
commit
ab553526f6
11 files changed
+1197
-398
No files matched your search
@@ -0,0 +1,36 @@
|
||||
-- read_only.sql: what fuel_ro may and may not do. Run by run.sh, logged in as fuel_ro.
|
||||
\set ON_ERROR_STOP 1
|
||||
DO $$
|
||||
DECLARE
|
||||
v text; n integer := 0; has boolean;
|
||||
BEGIN
|
||||
FOREACH v IN ARRAY ARRAY['v_site','v_kpi_measures','v_hour_meter_evidence','v_assumption','v_baseline_version',
|
||||
'v_baseline_input','v_evidence','v_delivery','v_stock_reading','v_interval','v_runtime',
|
||||
'v_coverage','v_reconciliation','v_data_quality','v_spec_reference','v_modelled_lph',
|
||||
'v_electricity_cost','v_imported_calculation'] LOOP
|
||||
EXECUTE format('SELECT count(*) FROM public.%I', v);
|
||||
SELECT EXISTS (SELECT 1 FROM information_schema.columns c
|
||||
WHERE c.table_schema = 'public' AND c.table_name = v AND c.column_name = 'operator_uid') INTO has;
|
||||
IF NOT has THEN RAISE EXCEPTION '% has no operator_uid', v; END IF;
|
||||
n := n + 1;
|
||||
END LOOP;
|
||||
IF n <> 18 THEN RAISE EXCEPTION 'expected 18 views, checked %', n; END IF;
|
||||
IF (SELECT count(*) FROM information_schema.views WHERE table_schema = 'public' AND table_name LIKE 'v\_%') <> 18 THEN
|
||||
RAISE EXCEPTION 'public holds a view this list does not name';
|
||||
END IF;
|
||||
|
||||
FOREACH v IN ARRAY ARRAY['fm.holder','fm.operator','licence.licence','licence.plan','licence.licence_event'] LOOP
|
||||
BEGIN
|
||||
EXECUTE format('SELECT count(*) FROM %s', v);
|
||||
RAISE EXCEPTION USING ERRCODE = 'TT001', MESSAGE = v || ' is readable';
|
||||
EXCEPTION WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
BEGIN
|
||||
EXECUTE 'CREATE TABLE public.written (a integer)';
|
||||
RAISE EXCEPTION USING ERRCODE = 'TT001', MESSAGE = 'fuel_ro created a table';
|
||||
EXCEPTION WHEN insufficient_privilege OR read_only_sql_transaction THEN NULL;
|
||||
END;
|
||||
RAISE NOTICE 'read-only role: 18 views readable, each with operator_uid; fm and licence refused; writes refused';
|
||||
END $$;
|
||||
@@ -0,0 +1,361 @@
|
||||
-- rules.sql: every rule of the fuel-db migrations, each with an accepted and a refused case.
|
||||
-- Synthetic data only. Run by run.sh on a throw-away database, as a loader acting as fuel_owner.
|
||||
-- A refused case names the words its error must hold, so it fails if a row is refused for another
|
||||
-- reason than the rule under test.
|
||||
\set ON_ERROR_STOP 1
|
||||
\o /dev/null
|
||||
SET client_min_messages = warning;
|
||||
SET search_path = fm, pg_catalog, public;
|
||||
|
||||
CREATE SCHEMA rules_test;
|
||||
CREATE TABLE rules_test.result (n bigint GENERATED ALWAYS AS IDENTITY, label text NOT NULL, passed boolean NOT NULL, detail text);
|
||||
|
||||
CREATE FUNCTION rules_test.ok(label text, stmt text) RETURNS void LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
EXECUTE stmt;
|
||||
INSERT INTO rules_test.result (label, passed) VALUES (label, true);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
INSERT INTO rules_test.result (label, passed, detail) VALUES (label, false, 'refused: ' || SQLERRM);
|
||||
END $$;
|
||||
|
||||
-- A refused case always rolls back, so a wrongly accepted row cannot disturb the later cases.
|
||||
CREATE FUNCTION rules_test.no(label text, want text, stmt text) RETURNS void LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
EXECUTE stmt;
|
||||
RAISE EXCEPTION USING ERRCODE = 'TT001';
|
||||
EXCEPTION
|
||||
WHEN SQLSTATE 'TT001' THEN
|
||||
INSERT INTO rules_test.result (label, passed, detail) VALUES (label, false, 'accepted');
|
||||
WHEN OTHERS THEN
|
||||
INSERT INTO rules_test.result (label, passed, detail)
|
||||
VALUES (label, position(want IN SQLERRM) > 0,
|
||||
CASE WHEN position(want IN SQLERRM) = 0 THEN 'refused for another reason: ' || SQLERRM END);
|
||||
END;
|
||||
END $$;
|
||||
|
||||
CREATE FUNCTION rules_test.eq(label text, query text, want text) RETURNS void LANGUAGE plpgsql AS $$
|
||||
DECLARE got text;
|
||||
BEGIN
|
||||
EXECUTE query INTO got;
|
||||
INSERT INTO rules_test.result (label, passed, detail)
|
||||
VALUES (label, got IS NOT DISTINCT FROM want, format('got %s, want %s', got, want));
|
||||
END $$;
|
||||
|
||||
CREATE FUNCTION rules_test.rec(op text) RETURNS bigint LANGUAGE sql STABLE AS
|
||||
$$ SELECT min(import_record_id) FROM fm.import_record WHERE operator_uid = op $$;
|
||||
CREATE FUNCTION rules_test.batch(op text) RETURNS bigint LANGUAGE sql STABLE AS
|
||||
$$ SELECT min(import_batch_id) FROM fm.import_batch WHERE operator_uid = op $$;
|
||||
CREATE FUNCTION rules_test.visit(k text) RETURNS bigint LANGUAGE sql STABLE AS
|
||||
$$ SELECT visit_id FROM fm.visit WHERE visit_key = k $$;
|
||||
CREATE FUNCTION rules_test.sha(c text) RETURNS text LANGUAGE sql IMMUTABLE AS
|
||||
$$ SELECT repeat(c, 64) $$;
|
||||
|
||||
-- Two customers ------------------------------------------------------------------------------
|
||||
INSERT INTO fm.operator (operator_uid, name, label_issuer, programme, timezone) VALUES
|
||||
('OPR-0001', 'Customer A', 'Issuer A', 'Programme A', 'Asia/Kabul'),
|
||||
('OPR-0002', 'Customer B', 'Issuer B', 'Programme B', 'Europe/Berlin');
|
||||
INSERT INTO fm.source_document (sha256, operator_uid, source_family, kind, issuer, title, corpus_path, contains_personal_data) VALUES
|
||||
(rules_test.sha('a'), 'OPR-0001', 'family-a', 'operator_workbook', 'Issuer A', 'workbook A', 'a.xlsx', false),
|
||||
(rules_test.sha('b'), 'OPR-0002', 'family-b', 'operator_workbook', 'Issuer B', 'workbook B', 'b.xlsx', false);
|
||||
INSERT INTO fm.import_batch (operator_uid, source_kind, source_path, source_sha256, importer_version) VALUES
|
||||
('OPR-0001', 'operator_workbook', 'a.xlsx', rules_test.sha('a'), 'test'),
|
||||
('OPR-0002', 'operator_workbook', 'b.xlsx', rules_test.sha('b'), 'test');
|
||||
INSERT INTO fm.import_record (operator_uid, logical_key, source_locator, payload, payload_sha256, first_batch_id, last_seen_batch_id)
|
||||
SELECT o, 'record-' || o, 'test', '{}', rules_test.sha('c'), rules_test.batch(o), rules_test.batch(o)
|
||||
FROM unnest(ARRAY['OPR-0001','OPR-0002']) o;
|
||||
INSERT INTO fm.place (place_uid, operator_uid, role, zone, province, timezone, import_record_id) VALUES
|
||||
('PLC-0001', 'OPR-0001', 'tower_site', 'z', 'p', NULL, rules_test.rec('OPR-0001')),
|
||||
('PLC-0002', 'OPR-0002', 'depot', 'z', 'p', 'Asia/Tokyo', rules_test.rec('OPR-0002'));
|
||||
INSERT INTO fm.holder (holder_uid, operator_uid, kind, name, import_record_id) VALUES
|
||||
('SITE-0001', 'OPR-0001', 'fixed_installation', 'Holder A', rules_test.rec('OPR-0001')),
|
||||
('SITE-0002', 'OPR-0002', 'fixed_installation', 'Holder B', rules_test.rec('OPR-0002'));
|
||||
INSERT INTO fm.holder_place (holder_uid, operator_uid, place_uid, null_reasons) VALUES
|
||||
('SITE-0001', 'OPR-0001', 'PLC-0001', '{"valid_from": {}}'),
|
||||
('SITE-0002', 'OPR-0002', 'PLC-0002', '{"valid_from": {}}');
|
||||
INSERT INTO fm.holder_alias (holder_uid, operator_uid, label, issuer, is_primary, null_reasons) VALUES
|
||||
('SITE-0001', 'OPR-0001', 'A1', 'Issuer A', true, '{"valid_from": {}}'),
|
||||
('SITE-0002', 'OPR-0002', 'B1', 'Issuer B', true, '{"valid_from": {}}');
|
||||
INSERT INTO fm.participation (holder_uid, operator_uid, programme, status, valid_from) VALUES
|
||||
('SITE-0001', 'OPR-0001', 'Programme A', 'active', '2026-01-01'),
|
||||
('SITE-0002', 'OPR-0002', 'Programme B', 'active', '2026-01-01');
|
||||
INSERT INTO fm.asset (asset_uid, operator_uid, kind, import_record_id) VALUES
|
||||
('AST-A-GEN', 'OPR-0001', 'generator', rules_test.rec('OPR-0001')),
|
||||
('AST-A-GEN2', 'OPR-0001', 'generator', rules_test.rec('OPR-0001')),
|
||||
('AST-A-ENG', 'OPR-0001', 'engine', rules_test.rec('OPR-0001')),
|
||||
('AST-A-T1', 'OPR-0001', 'tank', rules_test.rec('OPR-0001')),
|
||||
('AST-A-T2', 'OPR-0001', 'tank', rules_test.rec('OPR-0001')),
|
||||
('AST-B-GEN', 'OPR-0002', 'generator', rules_test.rec('OPR-0002'));
|
||||
INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, slot_label, occupied_from, installed_on) VALUES
|
||||
('AST-A-GEN', 'OPR-0001', 'SITE-0001', 'GEN-01', '2025-01-01', '2025-01-01'),
|
||||
('AST-A-GEN2', 'OPR-0001', 'SITE-0001', 'GEN-02', '2025-01-01', '2025-01-01'),
|
||||
('AST-A-ENG', 'OPR-0001', 'SITE-0001', 'ENG-01', '2025-01-01', '2025-01-01'),
|
||||
('AST-A-T1', 'OPR-0001', 'SITE-0001', 'T1', '2025-01-01', '2025-01-01'),
|
||||
('AST-B-GEN', 'OPR-0002', 'SITE-0002', 'GEN-01', '2025-01-01', '2025-01-01');
|
||||
INSERT INTO fm.visit (visit_key, operator_uid, holder_uid, visit_on, kind) VALUES
|
||||
('SITE-0001:2026-01-01:1', 'OPR-0001', 'SITE-0001', '2026-01-01', 'stock_check'),
|
||||
('SITE-0001:2026-01-11:1', 'OPR-0001', 'SITE-0001', '2026-01-11', 'stock_check'),
|
||||
('SITE-0002:2026-01-01:1', 'OPR-0002', 'SITE-0002', '2026-01-01', 'stock_check');
|
||||
INSERT INTO fm.stock_observation (source_key, assertion_sha256, operator_uid, visit_id, holder_uid, scope, phase, litres, litres_basis, source_sha256, record_ref, import_record_id)
|
||||
SELECT 'stock:' || k, rules_test.sha('d'), 'OPR-0001', rules_test.visit(k), 'SITE-0001', 'site_total', 'standalone', l, 'stated_without_level',
|
||||
rules_test.sha('a'), 'test', rules_test.rec('OPR-0001')
|
||||
FROM (VALUES ('SITE-0001:2026-01-01:1', 100.0), ('SITE-0001:2026-01-11:1', 50.0)) x(k, l);
|
||||
|
||||
SELECT rules_test.ok('fixture holds two customers', 'SELECT 1');
|
||||
|
||||
-- Rule 1: baseline versions --------------------------------------------------------------------
|
||||
\set bv 'INSERT INTO fm.baseline_version (code, operator_uid, holder_uid, metric, population, population_key, window_start, window_end, policy_version, meets_policy, computed_at, computed_by_role'
|
||||
SELECT rules_test.no('rule 1: a version does not start approved', 'starts as draft or proposed',
|
||||
:'bv' || ', status, approved_on, decision_ref) VALUES (''BL-X'', ''OPR-0001'', ''SITE-0001'', ''fuel_l_per_calendar_day'', ''x'', ''stock:SITE-0001'', ''2026-01-01'', ''2026-01-11'', ''BP-1'', true, now(), ''r'', ''approved'', ''2026-01-12'', ''D-1'')');
|
||||
SELECT rules_test.ok('rule 1: a version starts proposed',
|
||||
:'bv' || ', status) VALUES (''BL-1'', ''OPR-0001'', ''SITE-0001'', ''fuel_l_per_calendar_day'', ''x'', ''stock:SITE-0001'', ''2026-01-01'', ''2026-01-11'', ''BP-1'', true, now(), ''r'', ''proposed'')');
|
||||
SELECT rules_test.ok('rule 1: a version below policy starts proposed',
|
||||
:'bv' || ', status) VALUES (''BL-2'', ''OPR-0001'', ''SITE-0001'', ''fuel_l_per_calendar_day'', ''x'', ''stock:SITE-0001'', ''2026-01-01'', ''2026-01-11'', ''BP-1'', false, now(), ''r'', ''proposed'')');
|
||||
SELECT rules_test.no('rule 1: below policy cannot be approved', 'does not meet',
|
||||
'UPDATE fm.baseline_version SET status = ''approved'', approved_on = ''2026-01-12'', decision_ref = ''D-1'' WHERE code = ''BL-2''');
|
||||
SELECT rules_test.no('rule 1: no inputs cannot be approved', 'its inputs changed',
|
||||
'UPDATE fm.baseline_version SET status = ''approved'', approved_on = ''2026-01-12'', decision_ref = ''D-1'' WHERE code = ''BL-1''');
|
||||
|
||||
-- Rule 6: inputs (needed before an approval) ------------------------------------------------------
|
||||
\set bi 'INSERT INTO fm.baseline_input (baseline_id, operator_uid, holder_uid, open_visit_id, close_visit_id, start_on, end_on, included, days, population_key, eligibility_at_computation, dependencies, stock_dep_sha256) SELECT b.baseline_id, ''OPR-0001'', ''SITE-0001'', rules_test.visit(''SITE-0001:2026-01-01:1''), rules_test.visit(''SITE-0001:2026-01-11:1''), '
|
||||
SELECT rules_test.no('rule 6: dates must equal the endpoint visits', 'differ from the endpoint visits',
|
||||
:'bi' || '''2026-01-02'', ''2026-01-11'', true, 10, ''stock:SITE-0001'', ''eligible'', ''{}'', d.stock_dep_sha256 FROM fm.baseline_version b, fm.v_interval_dependency d WHERE b.code = ''BL-1'' AND d.holder_uid = ''SITE-0001''');
|
||||
SELECT rules_test.no('rule 6: an included input carries the population', 'differs from version',
|
||||
:'bi' || '''2026-01-01'', ''2026-01-11'', true, 10, ''stock:OTHER'', ''eligible'', ''{}'', d.stock_dep_sha256 FROM fm.baseline_version b, fm.v_interval_dependency d WHERE b.code = ''BL-1'' AND d.holder_uid = ''SITE-0001''');
|
||||
SELECT rules_test.ok('rule 6: a correct input is accepted',
|
||||
:'bi' || '''2026-01-01'', ''2026-01-11'', true, 10, ''stock:SITE-0001'', ''eligible'', ''{}'', d.stock_dep_sha256 FROM fm.baseline_version b, fm.v_interval_dependency d WHERE b.code = ''BL-1'' AND d.holder_uid = ''SITE-0001''');
|
||||
SELECT rules_test.ok('rule 1: unchanged inputs and policy met can be approved',
|
||||
'UPDATE fm.baseline_version SET status = ''approved'', approved_on = ''2026-01-12'', approved_by_role = ''r'', decision_ref = ''D-1'' WHERE code = ''BL-1''');
|
||||
SELECT rules_test.no('rule 1: an approved version is immutable', 'approved rows are immutable',
|
||||
'UPDATE fm.baseline_version SET value = 1 WHERE code = ''BL-1''');
|
||||
SELECT rules_test.no('rule 1: an approved version is never deleted', 'never deleted',
|
||||
'DELETE FROM fm.baseline_version WHERE code = ''BL-1''');
|
||||
SELECT rules_test.no('rule 1: an approved version''s inputs are immutable', 'are immutable',
|
||||
'UPDATE fm.baseline_input SET days = 9 WHERE included');
|
||||
|
||||
-- Rules 2, 3, 10: claims -----------------------------------------------------------------------
|
||||
\set mv 'INSERT INTO fm.fuel_movement (source_key, assertion_sha256, operator_uid, event_key, visit_id, holder_uid, kind, from_tank_uid, to_tank_uid, litres, litres_state, source_sha256, record_ref, import_record_id) VALUES '
|
||||
SELECT rules_test.no('rule 3: a movement''s holder is its visit''s holder', 'differs from its visit',
|
||||
:'mv' || '(''m0'', rules_test.sha(''e''), ''OPR-0001'', ''e0'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0003'', ''delivery'', NULL, NULL, 10, ''stated'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.no('rule 2: an internal transfer''s tanks are placed', 'must be placed',
|
||||
:'mv' || '(''m1'', rules_test.sha(''e''), ''OPR-0001'', ''e1'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0001'', ''internal_transfer'', ''AST-A-T1'', ''AST-A-T2'', 10, ''stated'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, slot_label, occupied_from, installed_on)
|
||||
VALUES ('AST-A-T2', 'OPR-0001', 'SITE-0001', 'T2', '2025-01-01', '2025-01-01');
|
||||
SELECT rules_test.ok('rule 2: placed tanks are accepted',
|
||||
:'mv' || '(''m1'', rules_test.sha(''e''), ''OPR-0001'', ''e1'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0001'', ''internal_transfer'', ''AST-A-T1'', ''AST-A-T2'', 10, ''stated'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.no('rule 10: claims of one event share kind and tanks', 'must share visit',
|
||||
:'mv' || '(''m2'', rules_test.sha(''e''), ''OPR-0001'', ''e1'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0001'', ''delivery'', NULL, NULL, 10, ''stated'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.ok('rule 10: a second claim of one event is accepted',
|
||||
:'mv' || '(''m2'', rules_test.sha(''e''), ''OPR-0001'', ''e1'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0001'', ''internal_transfer'', ''AST-A-T1'', ''AST-A-T2'', 11, ''stated'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.no('rule 3: a stock claim''s holder is its visit''s holder', 'differs from its visit',
|
||||
'INSERT INTO fm.stock_observation (source_key, assertion_sha256, operator_uid, visit_id, holder_uid, scope, phase, litres, litres_basis, source_sha256, record_ref, import_record_id) VALUES (''s0'', rules_test.sha(''d''), ''OPR-0001'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0003'', ''site_total'', ''standalone'', 1, ''stated_without_level'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
|
||||
-- Rules 8, 12, 9: meters and assumptions, generator-only (decision 6) ---------------------------
|
||||
\set mt 'INSERT INTO fm.runtime_meter (meter_id, operator_uid, holder_uid, source_label, asset_uid, mapping_status) VALUES '
|
||||
SELECT rules_test.ok('rule 8: a confirmed meter on a placed generator',
|
||||
:'mt' || '(''MTR-A-1'', ''OPR-0001'', ''SITE-0001'', ''Genset 1'', ''AST-A-GEN'', ''confirmed'')');
|
||||
SELECT rules_test.no('rule 8: a meter on an engine is refused', 'is not a generator placed',
|
||||
:'mt' || '(''MTR-A-E'', ''OPR-0001'', ''SITE-0001'', ''Engine'', ''AST-A-ENG'', ''confirmed'')');
|
||||
SELECT rules_test.no('rule 8: a meter on a tank is refused', 'is not a generator placed',
|
||||
:'mt' || '(''MTR-A-T'', ''OPR-0001'', ''SITE-0001'', ''Tank'', ''AST-A-T1'', ''proposed'')');
|
||||
SELECT rules_test.ok('rule 8: an unmapped counter names no asset',
|
||||
:'mt' || '(''MTR-A-2'', ''OPR-0001'', ''SITE-0001'', ''R.H'', NULL, ''unmapped'')');
|
||||
|
||||
INSERT INTO fm.meter_epoch (meter_id, operator_uid, epoch_no, start_reason, continuity) VALUES
|
||||
('MTR-A-1', 'OPR-0001', 1, 'first_seen', 'assumed'),
|
||||
('MTR-A-2', 'OPR-0001', 1, 'first_seen', 'assumed');
|
||||
\set rd 'INSERT INTO fm.hour_meter_reading (source_key, assertion_sha256, operator_uid, visit_id, meter_id, epoch_id, source_field, date_basis, hours, reading_state, transcribed_by, source_sha256, record_ref, import_record_id) VALUES '
|
||||
SELECT rules_test.ok('rules 3, 4: a reading of the visit''s own meter and epoch',
|
||||
:'rd' || '(''h1'', rules_test.sha(''f''), ''OPR-0001'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''MTR-A-1'', (SELECT epoch_id FROM fm.meter_epoch WHERE meter_id = ''MTR-A-1''), ''current'', ''stated'', 10, ''clear'', ''source_operator'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.no('rule 4: a reading''s epoch belongs to its meter', 'does not belong to meter',
|
||||
:'rd' || '(''h2'', rules_test.sha(''f''), ''OPR-0001'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''MTR-A-1'', (SELECT epoch_id FROM fm.meter_epoch WHERE meter_id = ''MTR-A-2''), ''current'', ''stated'', 10, ''clear'', ''source_operator'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0001''))');
|
||||
|
||||
\set ax 'INSERT INTO fm.assumption (code, fingerprint, operator_uid, holder_uid, asset_uid, topic, basis, derived_from_fuel, status, rationale'
|
||||
SELECT rules_test.ok('rule 12: an idle assumption on a placed generator',
|
||||
:'ax' || ') VALUES (''A-IDLE'', rules_test.sha(''0''), ''OPR-0001'', ''SITE-0001'', ''AST-A-GEN2'', ''generator_idle'', ''our_assumption'', false, ''proposed'', ''r'')');
|
||||
SELECT rules_test.no('rule 12: an idle assumption on an engine is refused', 'is not a generator placed',
|
||||
:'ax' || ') VALUES (''A-IDLE-E'', rules_test.sha(''0''), ''OPR-0001'', ''SITE-0001'', ''AST-A-ENG'', ''generator_idle'', ''our_assumption'', false, ''proposed'', ''r'')');
|
||||
|
||||
-- Rule 5: needs a matching review first, or rule 11 refuses before the CHECK is reached
|
||||
INSERT INTO fm.review (entry_key, operator_uid, subject_kind, subject_key, assertion_sha256, verdict, reviewed_on, reviewer_role, import_batch_id)
|
||||
VALUES ('R-5', 'OPR-0001', 'assumption', 'A-R5', rules_test.sha('5'), 'accepted', '2026-01-02', 'r', rules_test.batch('OPR-0001'));
|
||||
SELECT rules_test.no('rule 5: a runtime derived from fuel cannot be accepted', 'assumption_no_runtime_from_fuel',
|
||||
:'ax' || ', accepted_on, accepted_by_role) VALUES (''A-R5'', rules_test.sha(''5''), ''OPR-0001'', ''SITE-0001'', NULL, ''runtime_hours_per_day'', ''our_assumption'', true, ''accepted'', ''r'', ''2026-01-02'', ''r'')');
|
||||
SELECT rules_test.ok('rule 5: one not derived from fuel can be accepted',
|
||||
:'ax' || ', accepted_on, accepted_by_role) VALUES (''A-R5'', rules_test.sha(''5''), ''OPR-0001'', ''SITE-0001'', NULL, ''runtime_hours_per_day'', ''our_assumption'', false, ''accepted'', ''r'', ''2026-01-02'', ''r'')');
|
||||
|
||||
SELECT rules_test.no('rule 9: members complete at commit', 'as many counters as covered',
|
||||
:'ax' || ') VALUES (''A-COV0'', rules_test.sha(''6''), ''OPR-0001'', ''SITE-0001'', NULL, ''meter_coverage'', ''our_assumption'', false, ''proposed'', ''r''); '
|
||||
'INSERT INTO fm.assumption_member (assumption_id, operator_uid, member_role, meter_id) SELECT assumption_id, ''OPR-0001'', ''counter'', ''MTR-A-2'' FROM fm.assumption WHERE code = ''A-COV0''; '
|
||||
'SET CONSTRAINTS ALL IMMEDIATE');
|
||||
SELECT rules_test.ok('rule 9: valid and complete members in the assumption''s own transaction',
|
||||
:'ax' || ') VALUES (''A-COV'', rules_test.sha(''6''), ''OPR-0001'', ''SITE-0001'', NULL, ''meter_coverage'', ''our_assumption'', false, ''proposed'', ''r''); '
|
||||
'INSERT INTO fm.assumption_member (assumption_id, operator_uid, member_role, meter_id) SELECT assumption_id, ''OPR-0001'', ''counter'', ''MTR-A-2'' FROM fm.assumption WHERE code = ''A-COV''; '
|
||||
'INSERT INTO fm.assumption_member (assumption_id, operator_uid, member_role, asset_uid) SELECT assumption_id, ''OPR-0001'', ''covered_generator'', ''AST-A-GEN'' FROM fm.assumption WHERE code = ''A-COV''; '
|
||||
'SET CONSTRAINTS ALL IMMEDIATE');
|
||||
SELECT rules_test.no('rule 9: members are written in the same transaction', 'written in the transaction',
|
||||
'INSERT INTO fm.assumption_member (assumption_id, operator_uid, member_role, asset_uid) SELECT assumption_id, ''OPR-0001'', ''idle_generator'', ''AST-A-GEN2'' FROM fm.assumption WHERE code = ''A-COV''');
|
||||
SELECT rules_test.no('rule 9: members are immutable', 'members are immutable',
|
||||
'DELETE FROM fm.assumption_member');
|
||||
SELECT rules_test.no('rule 9: an engine cannot be a covered member', 'is not a generator placed',
|
||||
'INSERT INTO fm.assumption (code, fingerprint, operator_uid, holder_uid, topic, basis, derived_from_fuel, status, rationale) '
|
||||
'SELECT ''A-COV2'', rules_test.sha(''7''), ''OPR-0002'', ''SITE-0002'', ''meter_coverage'', ''our_assumption'', false, ''proposed'', ''r''; '
|
||||
'INSERT INTO fm.asset (asset_uid, operator_uid, kind, import_record_id) VALUES (''AST-B-ENG'', ''OPR-0002'', ''engine'', rules_test.rec(''OPR-0002'')); '
|
||||
'INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, slot_label, occupied_from, installed_on) VALUES (''AST-B-ENG'', ''OPR-0002'', ''SITE-0002'', ''E1'', ''2025-01-01'', ''2025-01-01''); '
|
||||
'INSERT INTO fm.assumption_member (assumption_id, operator_uid, member_role, asset_uid) SELECT assumption_id, ''OPR-0002'', ''covered_generator'', ''AST-B-ENG'' FROM fm.assumption WHERE code = ''A-COV2''');
|
||||
|
||||
-- Rule 7: source documents ---------------------------------------------------------------------
|
||||
\set sd 'INSERT INTO fm.source_document (sha256, operator_uid, source_family, kind, issuer, title, corpus_path, contains_personal_data, supersedes_sha256) VALUES '
|
||||
SELECT rules_test.ok('rule 7: a later version in its own family',
|
||||
:'sd' || '(rules_test.sha(''1''), ''OPR-0001'', ''family-a'', ''operator_workbook'', ''Issuer A'', ''later'', ''a2.xlsx'', false, rules_test.sha(''a''))');
|
||||
SELECT rules_test.no('rule 7: no supersession across families', 'may supersede only',
|
||||
:'sd' || '(rules_test.sha(''2''), ''OPR-0001'', ''family-other'', ''operator_workbook'', ''Issuer A'', ''x'', ''x.xlsx'', false, rules_test.sha(''1''))');
|
||||
SELECT rules_test.no('rule 7: a document cannot be shared with a second customer', 'another customer',
|
||||
:'sd' || '(rules_test.sha(''a''), ''OPR-0002'', ''family-a'', ''operator_workbook'', ''Issuer A'', ''x'', ''x.xlsx'', false, NULL)');
|
||||
|
||||
-- Rule 11: status from a review ------------------------------------------------------------------
|
||||
SELECT rules_test.no('rule 11: no status without a review', 'needs a review',
|
||||
'UPDATE fm.stock_observation SET claim_status = ''reviewed'' WHERE source_key = ''stock:SITE-0001:2026-01-01:1''');
|
||||
SELECT rules_test.no('rule 11: a value is never corrected in place', 'only the status columns',
|
||||
'UPDATE fm.stock_observation SET litres = 101 WHERE source_key = ''stock:SITE-0001:2026-01-01:1''');
|
||||
INSERT INTO fm.review (entry_key, operator_uid, subject_kind, subject_key, assertion_sha256, verdict, reviewed_on, reviewer_role, import_batch_id) VALUES
|
||||
('R-OTHER', 'OPR-0002', 'claim', 'stock:SITE-0001:2026-01-01:1', rules_test.sha('d'), 'reviewed', '2026-01-02', 'r', rules_test.batch('OPR-0002'));
|
||||
SELECT rules_test.no('rule 11: another customer''s review does not count', 'needs a review',
|
||||
'UPDATE fm.stock_observation SET claim_status = ''reviewed'' WHERE source_key = ''stock:SITE-0001:2026-01-01:1''');
|
||||
INSERT INTO fm.review (entry_key, operator_uid, subject_kind, subject_key, assertion_sha256, verdict, reviewed_on, reviewer_role, import_batch_id) VALUES
|
||||
('R-OTHER', 'OPR-0001', 'claim', 'stock:SITE-0001:2026-01-01:1', rules_test.sha('d'), 'reviewed', '2026-01-02', 'r', rules_test.batch('OPR-0001'));
|
||||
SELECT rules_test.ok('rule 11: a matching review gives the status; entry ids are per customer',
|
||||
'UPDATE fm.stock_observation SET claim_status = ''reviewed'' WHERE source_key = ''stock:SITE-0001:2026-01-01:1''');
|
||||
SELECT rules_test.no('review: an entry id is unique within one customer', 'duplicate key',
|
||||
'INSERT INTO fm.review (entry_key, operator_uid, subject_kind, subject_key, assertion_sha256, verdict, reviewed_on, reviewer_role, import_batch_id) VALUES (''R-OTHER'', ''OPR-0001'', ''claim'', ''elsewhere'', rules_test.sha(''d''), ''disputed'', ''2026-01-03'', ''r'', rules_test.batch(''OPR-0001''))');
|
||||
|
||||
-- Rule 13: identity never changes in place -------------------------------------------------------
|
||||
SELECT rules_test.no('rule 13: a visit''s date', 'never changes in place', 'UPDATE fm.visit SET visit_on = ''2026-01-02'' WHERE visit_key = ''SITE-0001:2026-01-01:1''');
|
||||
SELECT rules_test.no('rule 13: a document''s family', 'never changes in place', 'UPDATE fm.source_document SET source_family = ''x'' WHERE sha256 = rules_test.sha(''a'')');
|
||||
SELECT rules_test.no('rule 13: an asset''s kind', 'never changes in place', 'UPDATE fm.asset SET kind = ''engine'' WHERE asset_uid = ''AST-A-GEN''');
|
||||
SELECT rules_test.no('rule 13: a meter''s holder', 'never changes in place', 'UPDATE fm.runtime_meter SET holder_uid = ''SITE-0002'' WHERE meter_id = ''MTR-A-1''');
|
||||
SELECT rules_test.no('rule 13: an epoch''s meter', 'never changes in place', 'UPDATE fm.meter_epoch SET meter_id = ''MTR-A-2'' WHERE meter_id = ''MTR-A-1''');
|
||||
SELECT rules_test.no('rule 13: a holder''s kind', 'never changes in place', 'UPDATE fm.holder SET kind = ''trailer'' WHERE holder_uid = ''SITE-0001''');
|
||||
SELECT rules_test.ok('rule 13: a descriptive field may change', 'UPDATE fm.holder SET name = ''Renamed'' WHERE holder_uid = ''SITE-0001''');
|
||||
CREATE TABLE rules_test.wrong_name (a integer);
|
||||
CREATE TRIGGER wrong_name BEFORE UPDATE ON rules_test.wrong_name FOR EACH ROW EXECUTE FUNCTION fm.tg_fixed_columns('nope');
|
||||
INSERT INTO rules_test.wrong_name VALUES (1);
|
||||
SELECT rules_test.no('rule 13: a wrong column name raises, not a silent pass', 'does not have', 'UPDATE rules_test.wrong_name SET a = 2');
|
||||
|
||||
-- Rules 14, 15 --------------------------------------------------------------------------------
|
||||
SELECT rules_test.no('rule 14: an unknown time zone', 'unknown time zone', 'UPDATE fm.operator SET timezone = ''Asia/Nowhere'' WHERE operator_uid = ''OPR-0001''');
|
||||
SELECT rules_test.no('rule 14: an unknown time zone on a place', 'unknown time zone', 'UPDATE fm.place SET timezone = ''Asia/Nowhere'' WHERE place_uid = ''PLC-0001''');
|
||||
SELECT rules_test.ok('rule 14: a known time zone', 'UPDATE fm.place SET timezone = ''Asia/Kabul'' WHERE place_uid = ''PLC-0001''');
|
||||
|
||||
-- Two customers: a child cannot name another customer's parent -----------------------------------
|
||||
SELECT rules_test.no('customers: a visit on another''s holder', 'foreign key',
|
||||
'INSERT INTO fm.visit (visit_key, operator_uid, holder_uid, visit_on, kind) VALUES (''x'', ''OPR-0002'', ''SITE-0001'', ''2026-02-01'', ''survey'')');
|
||||
SELECT rules_test.no('customers: a claim on another''s visit', 'foreign key',
|
||||
:'mv' || '(''mx'', rules_test.sha(''e''), ''OPR-0002'', ''ex'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''SITE-0001'', ''delivery'', NULL, NULL, 10, ''stated'', rules_test.sha(''b''), ''r'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.no('customers: a placement of another''s asset', 'foreign key',
|
||||
'INSERT INTO fm.asset (asset_uid, operator_uid, kind, import_record_id) VALUES (''AST-A-SPARE'', ''OPR-0001'', ''generator'', rules_test.rec(''OPR-0001'')); '
|
||||
'INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, slot_label, null_reasons) VALUES (''AST-A-SPARE'', ''OPR-0002'', ''SITE-0002'', ''X'', ''{"installed_on": {}}'')');
|
||||
SELECT rules_test.no('customers: a meter on another''s holder', 'foreign key',
|
||||
:'mt' || '(''MTR-X'', ''OPR-0002'', ''SITE-0001'', ''X'', NULL, ''unmapped'')');
|
||||
SELECT rules_test.no('customers: a reading of another''s meter', 'foreign key',
|
||||
:'rd' || '(''hx'', rules_test.sha(''f''), ''OPR-0002'', rules_test.visit(''SITE-0001:2026-01-01:1''), ''MTR-A-1'', NULL, ''current'', ''stated'', 10, ''clear'', ''source_operator'', rules_test.sha(''b''), ''r'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.no('customers: a baseline on another''s holder', 'foreign key',
|
||||
:'bv' || ', status) VALUES (''BL-X2'', ''OPR-0002'', ''SITE-0001'', ''fuel_l_per_calendar_day'', ''x'', ''k'', ''2026-01-01'', ''2026-01-11'', ''BP-1'', true, now(), ''r'', ''proposed'')');
|
||||
SELECT rules_test.no('customers: an input of another''s baseline', 'foreign key',
|
||||
'INSERT INTO fm.baseline_input (baseline_id, operator_uid, holder_uid, open_visit_id, close_visit_id, start_on, end_on, included, exclusion_reason, eligibility_at_computation, dependencies, stock_dep_sha256) SELECT b.baseline_id, ''OPR-0002'', ''SITE-0002'', rules_test.visit(''SITE-0002:2026-01-01:1''), rules_test.visit(''SITE-0002:2026-01-01:1''), ''2026-01-01'', ''2026-01-01'', false, ''x'', ''excluded'', ''{}'', ''x'' FROM fm.baseline_version b WHERE b.code = ''BL-2''');
|
||||
SELECT rules_test.no('customers: a claim citing another''s document', 'foreign key',
|
||||
:'mv' || '(''my'', rules_test.sha(''e''), ''OPR-0002'', ''ey'', rules_test.visit(''SITE-0002:2026-01-01:1''), ''SITE-0002'', ''delivery'', NULL, NULL, 10, ''stated'', rules_test.sha(''a''), ''r'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.no('customers: a holder standing at another''s place', 'foreign key',
|
||||
'INSERT INTO fm.holder (holder_uid, operator_uid, kind, import_record_id) VALUES (''SITE-0004'', ''OPR-0002'', ''fixed_installation'', rules_test.rec(''OPR-0002'')); '
|
||||
'INSERT INTO fm.holder_place (holder_uid, operator_uid, place_uid, valid_from) VALUES (''SITE-0004'', ''OPR-0002'', ''PLC-0001'', ''2030-01-01'')');
|
||||
\set me 'INSERT INTO fm.fuel_movement (source_key, assertion_sha256, operator_uid, event_key, visit_id, holder_uid, kind, litres, litres_state, unit_price, price_currency, source_sha256, record_ref, import_record_id) VALUES '
|
||||
SELECT rules_test.ok('customers: one event key under the first customer',
|
||||
:'me' || '(''shared-a'', rules_test.sha(''e''), ''OPR-0001'', ''shared-event'', rules_test.visit(''SITE-0001:2026-01-11:1''), ''SITE-0001'', ''delivery'', 10, ''stated'', 70, ''AAA'', rules_test.sha(''a''), ''ref-a'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.ok('customers: the same event key under the second, with another kind (rule 10 is per customer)',
|
||||
:'me' || '(''shared-b'', rules_test.sha(''e''), ''OPR-0002'', ''shared-event'', rules_test.visit(''SITE-0002:2026-01-01:1''), ''SITE-0002'', ''withdrawal'', 20, ''stated'', 90, ''BBB'', rules_test.sha(''b''), ''ref-b'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.ok('customers: and as a delivery',
|
||||
:'me' || '(''shared-c'', rules_test.sha(''e''), ''OPR-0002'', ''shared-event-2'', rules_test.visit(''SITE-0002:2026-01-01:1''), ''SITE-0002'', ''delivery'', 20, ''stated'', 90, ''BBB'', rules_test.sha(''b''), ''ref-b'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.ok('customers: the first again, under the second''s delivery key',
|
||||
:'me' || '(''shared-d'', rules_test.sha(''e''), ''OPR-0001'', ''shared-event-2'', rules_test.visit(''SITE-0001:2026-01-11:1''), ''SITE-0001'', ''delivery'', 10, ''stated'', 70, ''AAA'', rules_test.sha(''a''), ''ref-a'', rules_test.rec(''OPR-0001''))');
|
||||
SELECT rules_test.eq('customers: v_delivery gives each its own price and reference under one event key',
|
||||
'SELECT string_agg(operator_uid || '':'' || unit_price || '':'' || currency || '':'' || source_ref, '','' ORDER BY operator_uid) FROM public.v_delivery WHERE event_key = ''shared-event-2''',
|
||||
'OPR-0001:70.00:AAA:ref-a,OPR-0002:90.00:BBB:ref-b');
|
||||
SELECT rules_test.no('customers: a row''s customer cannot change: holder', 'never changes in place', 'UPDATE fm.holder SET operator_uid = ''OPR-0002'' WHERE holder_uid = ''SITE-0001''');
|
||||
SELECT rules_test.no('customers: a row''s customer cannot change: review', 'never changes in place', 'UPDATE fm.review SET operator_uid = ''OPR-0002'' WHERE entry_key = ''R-5''');
|
||||
SELECT rules_test.no('customers: a row''s customer cannot change: batch', 'never changes in place', 'UPDATE fm.import_batch SET operator_uid = ''OPR-0002'' WHERE import_batch_id = rules_test.batch(''OPR-0001'')');
|
||||
SELECT rules_test.eq('customers: every table but asset_kind has the fixed-customer trigger',
|
||||
'SELECT string_agg(c.relname, '','' ORDER BY c.relname) FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ''fm'' AND c.relkind = ''r'' AND NOT EXISTS (SELECT 1 FROM pg_trigger t WHERE t.tgrelid = c.oid AND t.tgname = ''operator_fixed'')',
|
||||
'asset_kind');
|
||||
SELECT rules_test.eq('customers: v_site gives each holder its customer',
|
||||
'SELECT string_agg(site_uid || ''='' || operator_uid, '','' ORDER BY site_uid) FROM public.v_site', 'SITE-0001=OPR-0001,SITE-0002=OPR-0002');
|
||||
SELECT rules_test.eq('customers: labels and programme come from each customer''s record',
|
||||
'SELECT string_agg(site_code || '':'' || pilot_status, '','' ORDER BY site_uid) FROM public.v_site', 'A1:active,B1:active');
|
||||
SELECT rules_test.eq('customers: local day uses the place''s zone, else the customer''s',
|
||||
'SELECT string_agg(holder_uid || '':'' || timezone, '','' ORDER BY holder_uid) FROM fm.v_holder_current', 'SITE-0001:Asia/Kabul,SITE-0002:Asia/Tokyo');
|
||||
|
||||
-- A vehicle is both an asset and a holder (PD-18) -------------------------------------------------
|
||||
INSERT INTO fm.asset (asset_uid, operator_uid, kind, import_record_id) VALUES
|
||||
('AST-VEH-1', 'OPR-0002', 'vehicle', rules_test.rec('OPR-0002')),
|
||||
('AST-VEH-T', 'OPR-0002', 'vehicle_tank', rules_test.rec('OPR-0002')),
|
||||
('AST-VEH-E', 'OPR-0002', 'engine', rules_test.rec('OPR-0002'));
|
||||
\set hd 'INSERT INTO fm.holder (holder_uid, operator_uid, kind, asset_uid, import_record_id) VALUES '
|
||||
SELECT rules_test.no('vehicle: a vehicle holder needs its asset', 'holder_check', :'hd' || '(''VEH-0001'', ''OPR-0002'', ''vehicle'', NULL, rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.no('vehicle: a fixed holder has no asset', 'holder_check', :'hd' || '(''SITE-0009'', ''OPR-0002'', ''fixed_installation'', ''AST-VEH-1'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.no('rule 15: a vehicle holder''s asset is a vehicle', 'is not a vehicle', :'hd' || '(''VEH-0001'', ''OPR-0002'', ''vehicle'', ''AST-VEH-T'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.ok('vehicle: asset and holder', :'hd' || '(''VEH-0001'', ''OPR-0002'', ''vehicle'', ''AST-VEH-1'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.no('rule 15: a vehicle stands at no place', 'not a fixed installation',
|
||||
'INSERT INTO fm.holder_place (holder_uid, operator_uid, place_uid, valid_from) VALUES (''VEH-0001'', ''OPR-0002'', ''PLC-0002'', ''2026-01-01'')');
|
||||
SELECT rules_test.ok('vehicle: a tank placed on it',
|
||||
'INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, slot_label, occupied_from, installed_on) VALUES (''AST-VEH-T'', ''OPR-0002'', ''VEH-0001'', ''C1'', ''2026-01-01'', ''2026-01-01'')');
|
||||
SELECT rules_test.ok('vehicle: an engine placed on it',
|
||||
'INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, slot_label, occupied_from, installed_on) VALUES (''AST-VEH-E'', ''OPR-0002'', ''VEH-0001'', ''E1'', ''2026-01-01'', ''2026-01-01'')');
|
||||
SELECT rules_test.no('rule 8: a meter on the vehicle''s engine is refused', 'is not a generator placed',
|
||||
:'mt' || '(''MTR-V-E'', ''OPR-0002'', ''VEH-0001'', ''Engine'', ''AST-VEH-E'', ''confirmed'')');
|
||||
SELECT rules_test.ok('vehicle: a visit', 'INSERT INTO fm.visit (visit_key, operator_uid, holder_uid, visit_on, kind) VALUES (''VEH-0001:2026-02-01:1'', ''OPR-0002'', ''VEH-0001'', ''2026-02-01'', ''delivery'')');
|
||||
SELECT rules_test.ok('vehicle: a delivery on its visit',
|
||||
:'mv' || '(''m-veh'', rules_test.sha(''e''), ''OPR-0002'', ''e-veh'', rules_test.visit(''VEH-0001:2026-02-01:1''), ''VEH-0001'', ''delivery'', NULL, NULL, 200, ''stated'', rules_test.sha(''b''), ''r'', rules_test.rec(''OPR-0002''))');
|
||||
SELECT rules_test.eq('vehicle: the delivery reaches v_delivery with its customer',
|
||||
'SELECT operator_uid || '':'' || delivered_l FROM public.v_delivery WHERE site_uid = ''VEH-0001''', 'OPR-0002:200.0');
|
||||
SELECT rules_test.eq('vehicle: v_site lists it, with no place', 'SELECT coalesce(province, ''no place'') FROM public.v_site WHERE site_uid = ''VEH-0001''', 'no place');
|
||||
|
||||
-- Placement: in or on a holder, or at a place, exactly one -----------------------------------------
|
||||
\set pl 'INSERT INTO fm.asset_placement (asset_uid, operator_uid, holder_uid, place_uid, slot_label, occupied_from, installed_on) VALUES '
|
||||
INSERT INTO fm.asset (asset_uid, operator_uid, kind, import_record_id) VALUES
|
||||
('AST-B-T1', 'OPR-0002', 'tank', rules_test.rec('OPR-0002')), ('AST-B-T2', 'OPR-0002', 'tank', rules_test.rec('OPR-0002'));
|
||||
SELECT rules_test.no('placement: not both', 'asset_placement_check', :'pl' || '(''AST-B-T1'', ''OPR-0002'', ''SITE-0002'', ''PLC-0002'', ''P1'', ''2026-01-01'', ''2026-01-01'')');
|
||||
SELECT rules_test.no('placement: not neither', 'asset_placement_check', :'pl' || '(''AST-B-T1'', ''OPR-0002'', NULL, NULL, ''P1'', ''2026-01-01'', ''2026-01-01'')');
|
||||
SELECT rules_test.ok('placement: at a place', :'pl' || '(''AST-B-T1'', ''OPR-0002'', NULL, ''PLC-0002'', ''P1'', ''2026-01-01'', ''2026-01-01'')');
|
||||
SELECT rules_test.no('placement: one slot at a place at a time', 'conflicting key', :'pl' || '(''AST-B-T2'', ''OPR-0002'', NULL, ''PLC-0002'', ''P1'', ''2026-06-01'', ''2026-06-01'')');
|
||||
SELECT rules_test.no('placement: one slot on a holder at a time', 'conflicting key', :'pl' || '(''AST-B-T2'', ''OPR-0002'', ''SITE-0002'', NULL, ''GEN-01'', ''2026-06-01'', ''2026-06-01'')');
|
||||
SELECT rules_test.no('placement: an asset is in one place at a time', 'conflicting key', :'pl' || '(''AST-B-T1'', ''OPR-0002'', ''SITE-0002'', NULL, ''P2'', ''2026-06-01'', ''2026-06-01'')');
|
||||
SELECT rules_test.eq('placement: a place-held tank does not break the views',
|
||||
'SELECT count(*)::text FROM public.v_spec_reference WHERE asset_uid = ''AST-B-T1''', '0');
|
||||
|
||||
-- Licences -------------------------------------------------------------------------------------
|
||||
\set li 'INSERT INTO licence.licence (operator_uid, plan_code, payment_term, valid_from, valid_until, grace_until, status, contract_ref, created_by) VALUES '
|
||||
SELECT rules_test.eq('licence: PD-19''s prices are seeded',
|
||||
'SELECT string_agg(payment_term || ''='' || price, '','' ORDER BY payment_term) FROM licence.plan_price WHERE plan_code = ''standard''', 'annual=15.00,monthly=17.00');
|
||||
SELECT rules_test.eq('licence: no customer''s licence is seeded', 'SELECT count(*)::text FROM licence.licence', '0');
|
||||
SELECT rules_test.eq('licence: seeds are not in the log', 'SELECT count(*)::text FROM licence.licence_event', '0');
|
||||
SELECT rules_test.ok('licence: a first licence', :'li' || '(''OPR-0001'', ''pilot'', ''annual'', ''2026-01-01'', ''2026-12-31'', ''2027-01-30'', ''active'', ''agreement'', ''our admin'')');
|
||||
SELECT rules_test.no('licence: overlapping dates refused', 'conflicting key', :'li' || '(''OPR-0001'', ''standard'', ''annual'', ''2026-12-31'', ''2027-12-31'', ''2028-01-30'', ''active'', ''agreement'', ''our admin'')');
|
||||
SELECT rules_test.ok('licence: a renewal may start during the old grace', :'li' || '(''OPR-0001'', ''standard'', ''annual'', ''2027-01-01'', ''2027-12-31'', ''2028-01-30'', ''active'', ''agreement'', ''our admin'')');
|
||||
SELECT rules_test.ok('licence: another customer may share the dates', :'li' || '(''OPR-0002'', ''standard'', ''monthly'', ''2026-01-01'', ''2026-12-31'', ''2027-01-30'', ''active'', ''agreement'', ''our admin'')');
|
||||
SELECT rules_test.no('licence: grace at least until expiry', 'licence_check', :'li' || '(''OPR-0002'', ''standard'', ''annual'', ''2028-01-01'', ''2028-12-31'', ''2028-12-30'', ''active'', ''agreement'', ''our admin'')');
|
||||
SELECT rules_test.no('licence: an unknown customer', 'foreign key', :'li' || '(''OPR-0009'', ''standard'', ''annual'', ''2028-01-01'', ''2028-12-31'', ''2029-01-30'', ''active'', ''agreement'', ''our admin'')');
|
||||
SELECT rules_test.eq('licence: every change writes an event', 'SELECT count(*)::text FROM licence.licence_event WHERE table_name = ''licence'' AND action = ''INSERT''', '3');
|
||||
SELECT rules_test.ok('licence: a suspension', 'UPDATE licence.licence SET status = ''suspended'' WHERE operator_uid = ''OPR-0002''');
|
||||
SELECT rules_test.eq('licence: the event holds the old and new row',
|
||||
'SELECT old_row->>''status'' || ''>'' || (new_row->>''status'') FROM licence.licence_event WHERE action = ''UPDATE''', 'active>suspended');
|
||||
SELECT rules_test.no('licence: the log is never changed', 'never changed', 'DELETE FROM licence.licence_event');
|
||||
|
||||
-- Result ------------------------------------------------------------------------------------------
|
||||
\o
|
||||
\pset format unaligned
|
||||
\pset tuples_only on
|
||||
SELECT CASE WHEN passed THEN 'ok ' ELSE 'FAIL ' END || label || CASE WHEN NOT passed THEN ' - ' || coalesce(detail, '') ELSE '' END
|
||||
FROM rules_test.result ORDER BY n;
|
||||
SELECT 'rules: ' || count(*) || ' checks, ' || count(*) FILTER (WHERE NOT passed) || ' failed' FROM rules_test.result;
|
||||
DO $$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM rules_test.result WHERE NOT passed) THEN RAISE EXCEPTION 'rule tests failed'; END IF;
|
||||
END $$;
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Applies the migrations to a throw-away database and runs the rule tests. Synthetic data only.
|
||||
# Needs a running fuel-db container with this repository's roles (fuel_owner, fuel_ro), e.g. the
|
||||
# local stack: FUEL_DB_CONTAINER=fmdb-scratch-db stacks/fuel-monitoring/fuel-db/tests/run.sh
|
||||
# Never point it at the server: it creates a role and a database.
|
||||
set -euo pipefail
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
container=${FUEL_DB_CONTAINER:?set FUEL_DB_CONTAINER to a local fuel-db container}
|
||||
db=fuel_rules_$$
|
||||
loader=fuel_rules_loader
|
||||
|
||||
psql_as() {
|
||||
local user=$1
|
||||
shift
|
||||
docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U "$user" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
psql_as postgres -d postgres -c "DROP DATABASE IF EXISTS $db;" >/dev/null
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
psql_as postgres -d postgres -c "CREATE DATABASE $db;" >/dev/null
|
||||
psql_as postgres -d postgres >/dev/null <<SQL
|
||||
DO \$\$ BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$loader') THEN
|
||||
CREATE ROLE $loader LOGIN;
|
||||
GRANT fuel_owner TO $loader WITH INHERIT FALSE, SET TRUE;
|
||||
ALTER ROLE $loader SET role TO 'fuel_owner';
|
||||
END IF;
|
||||
END \$\$;
|
||||
ALTER DATABASE $db OWNER TO fuel_owner;
|
||||
GRANT CONNECT ON DATABASE $db TO fuel_ro;
|
||||
SQL
|
||||
psql_as postgres -d "$db" >/dev/null <<SQL
|
||||
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
||||
GRANT USAGE ON SCHEMA public TO fuel_ro;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE fuel_owner IN SCHEMA public GRANT SELECT ON TABLES TO fuel_ro;
|
||||
SQL
|
||||
|
||||
for file in "$here"/../migrations/*.sql; do
|
||||
psql_as "$loader" -d "$db" <"$file" >/dev/null
|
||||
done
|
||||
echo "migrations applied: $(cd "$here/../migrations" && echo *.sql)"
|
||||
|
||||
psql_as "$loader" -d "$db" <"$here/rules.sql"
|
||||
psql_as fuel_ro -d "$db" <"$here/read_only.sql" 2>&1 | sed 's/^psql:[^ ]* NOTICE: //'
|
||||
Reference in new issue
Block a user