20260818_546_metering_showback.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """Create the local-only, gateway-bound P3-WP12 metering baseline."""
  2. from alembic import op
  3. revision = "20260818_546"
  4. down_revision = "20260818_545"
  5. branch_labels = None
  6. depends_on = None
  7. _TABLES = (
  8. "metering_scope_grants",
  9. "metering_events",
  10. "metering_allocation_rules",
  11. "metering_allocations",
  12. "metering_budgets",
  13. "metering_alert_outbox",
  14. "metering_reconciliation_reports",
  15. "metering_audit_events",
  16. "metering_runtime_claims",
  17. "metering_runtime_leases",
  18. "metering_showback_configuration",
  19. )
  20. def upgrade() -> None:
  21. op.get_bind().exec_driver_sql(
  22. r'''
  23. DO $$ BEGIN
  24. IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_tenant_foundation_owner')
  25. OR NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_app_runtime')
  26. OR NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_bi_ai_catalog_control')
  27. OR NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_bi_ai_catalog_issuer') THEN
  28. RAISE EXCEPTION 'WP12 roles must be provisioned by role-init';
  29. END IF;
  30. END $$;
  31. CREATE TABLE public.metering_scope_grants (
  32. grant_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  33. principal_ref text NOT NULL CHECK(principal_ref ~ '^[A-Za-z0-9_.:-]{1,120}$'),
  34. tenant_ref text NOT NULL CHECK(tenant_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  35. domain_ref text NOT NULL CHECK(domain_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  36. role_name text NOT NULL CHECK(role_name IN ('viewer','operator','admin')),
  37. active boolean NOT NULL DEFAULT true,
  38. created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  39. revoked_at timestamptz,
  40. UNIQUE(principal_ref,tenant_ref,domain_ref,role_name)
  41. );
  42. CREATE TABLE public.metering_showback_configuration (
  43. configuration_uid boolean PRIMARY KEY DEFAULT true CHECK(configuration_uid),
  44. schema_version integer NOT NULL DEFAULT 1 CHECK(schema_version=1),
  45. mode text NOT NULL DEFAULT 'ENGINEERING_EVIDENCE_ONLY' CHECK(mode='ENGINEERING_EVIDENCE_ONLY'),
  46. enterprise_cost_input text NOT NULL DEFAULT 'TBD_EXTERNAL' CHECK(enterprise_cost_input='TBD_EXTERNAL'),
  47. owner_ref text NOT NULL DEFAULT 'enterprise_finance_owner' CHECK(owner_ref='enterprise_finance_owner'),
  48. chargeback_enabled boolean NOT NULL DEFAULT false CHECK(chargeback_enabled=false),
  49. provider_enabled boolean NOT NULL DEFAULT false CHECK(provider_enabled=false),
  50. created_at timestamptz NOT NULL DEFAULT clock_timestamp()
  51. );
  52. INSERT INTO public.metering_showback_configuration DEFAULT VALUES;
  53. CREATE TABLE public.metering_runtime_leases (
  54. tenant_ref text PRIMARY KEY CHECK(tenant_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  55. lease_owner text, lease_token uuid, lease_fence bigint NOT NULL DEFAULT 0 CHECK(lease_fence>=0),
  56. lease_expires_at timestamptz, updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
  57. );
  58. CREATE TABLE public.metering_runtime_claims (
  59. claim_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), action_name text NOT NULL CHECK(action_name IN ('lease','record','read')),
  60. principal_ref text NOT NULL, tenant_ref text NOT NULL, domain_ref text NOT NULL,
  61. request_digest char(64) NOT NULL CHECK(request_digest ~ '^[0-9a-f]{64}$'),
  62. expires_at timestamptz NOT NULL, consumed_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp()
  63. );
  64. CREATE TABLE public.metering_events (
  65. event_uid text PRIMARY KEY CHECK(event_uid ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
  66. tenant_ref text NOT NULL CHECK(tenant_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  67. domain_ref text NOT NULL CHECK(domain_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  68. department_ref text NOT NULL CHECK(department_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  69. project_ref text NOT NULL CHECK(project_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  70. cost_center_ref text NOT NULL CHECK(cost_center_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  71. event_kind text NOT NULL CHECK(event_kind IN ('query','api','file','subscription','storage','compute','task','model_call')),
  72. occurred_at timestamptz NOT NULL, window_start timestamptz NOT NULL, window_end timestamptz NOT NULL CHECK(window_start<window_end),
  73. quantity_micros bigint NOT NULL CHECK(quantity_micros>=0), unit text NOT NULL CHECK(unit IN ('bytes','kb','mb','gb','seconds','milliseconds','requests','tasks','tokens')),
  74. idempotency_key text NOT NULL CHECK(idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
  75. evidence_digest char(64) NOT NULL CHECK(evidence_digest ~ '^[0-9a-f]{64}$'),
  76. evidence_reference text NOT NULL CHECK(evidence_reference ~ '^local-fixture://wp12/v1(/[A-Za-z0-9._:-]{1,80})?$'),
  77. request_digest char(64) NOT NULL CHECK(request_digest ~ '^[0-9a-f]{64}$'),
  78. correction_of text REFERENCES public.metering_events(event_uid) ON DELETE RESTRICT,
  79. lease_fence bigint NOT NULL CHECK(lease_fence>=0), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  80. UNIQUE(tenant_ref,idempotency_key)
  81. );
  82. CREATE TABLE public.metering_allocation_rules (
  83. rule_uid text NOT NULL CHECK(rule_uid ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'), rule_version integer NOT NULL CHECK(rule_version>0),
  84. tenant_ref text NOT NULL, domain_ref text NOT NULL, department_ref text NOT NULL, project_ref text NOT NULL, cost_center_ref text NOT NULL,
  85. effective_start timestamptz NOT NULL, effective_end timestamptz NOT NULL CHECK(effective_start<effective_end),
  86. rule_digest char(64) NOT NULL CHECK(rule_digest ~ '^[0-9a-f]{64}$'), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  87. PRIMARY KEY(rule_uid,rule_version)
  88. );
  89. CREATE TABLE public.metering_allocations (
  90. rule_uid text NOT NULL, rule_version integer NOT NULL, target_ref text NOT NULL CHECK(target_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  91. weight_micros integer NOT NULL CHECK(weight_micros>=0 AND weight_micros<=1000000),
  92. PRIMARY KEY(rule_uid,rule_version,target_ref),
  93. FOREIGN KEY(rule_uid,rule_version) REFERENCES public.metering_allocation_rules(rule_uid,rule_version) ON DELETE RESTRICT
  94. );
  95. CREATE TABLE public.metering_budgets (
  96. budget_uid text PRIMARY KEY CHECK(budget_uid ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'), tenant_ref text NOT NULL, domain_ref text NOT NULL,
  97. department_ref text NOT NULL, project_ref text NOT NULL, cost_center_ref text NOT NULL, window_key char(7) NOT NULL CHECK(window_key ~ '^[0-9]{4}-(0[1-9]|1[0-2])$'),
  98. limit_micros bigint NOT NULL CHECK(limit_micros>=0), threshold_micros bigint NOT NULL CHECK(threshold_micros>=0 AND threshold_micros<=limit_micros),
  99. created_at timestamptz NOT NULL DEFAULT clock_timestamp()
  100. );
  101. CREATE TABLE public.metering_alert_outbox (
  102. alert_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), budget_uid text NOT NULL REFERENCES public.metering_budgets(budget_uid) ON DELETE RESTRICT,
  103. window_key char(7) NOT NULL, threshold_micros bigint NOT NULL, payload_digest char(64) NOT NULL CHECK(payload_digest ~ '^[0-9a-f]{64}$'),
  104. provider_state text NOT NULL DEFAULT 'disabled' CHECK(provider_state='disabled'), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  105. UNIQUE(budget_uid,window_key,threshold_micros)
  106. );
  107. CREATE TABLE public.metering_reconciliation_reports (
  108. report_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), tenant_ref text NOT NULL, domain_ref text NOT NULL, window_key char(7) NOT NULL,
  109. source_micros bigint NOT NULL CHECK(source_micros>=0), allocated_micros bigint NOT NULL CHECK(allocated_micros>=0), difference_micros bigint NOT NULL,
  110. report_digest char(64) NOT NULL CHECK(report_digest ~ '^[0-9a-f]{64}$'), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  111. UNIQUE(tenant_ref,domain_ref,window_key,report_digest)
  112. );
  113. CREATE TABLE public.metering_audit_events (
  114. audit_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), tenant_ref text NOT NULL, domain_ref text NOT NULL,
  115. event_type text NOT NULL CHECK(event_type IN ('metering.recorded','metering.corrected','metering.lease.claimed','showback.read','budget.alert.created')),
  116. actor_ref text NOT NULL, payload_digest char(64) NOT NULL CHECK(payload_digest ~ '^[0-9a-f]{64}$'), lease_fence bigint, created_at timestamptz NOT NULL DEFAULT clock_timestamp()
  117. );
  118. DO $$ DECLARE item text; BEGIN
  119. FOREACH item IN ARRAY ARRAY['metering_scope_grants','metering_showback_configuration','metering_runtime_leases','metering_runtime_claims',
  120. 'metering_events','metering_allocation_rules','metering_allocations','metering_budgets','metering_alert_outbox','metering_reconciliation_reports','metering_audit_events'] LOOP
  121. EXECUTE 'ALTER TABLE public.' || quote_ident(item) || ' OWNER TO dataops_tenant_foundation_owner';
  122. END LOOP;
  123. END $$;
  124. CREATE FUNCTION public.metering_showback_issue_claim(p_action text,p_principal text,p_body jsonb)
  125. RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $claim$
  126. DECLARE v_tenant text; v_domain text; v_role text; v_claim uuid;
  127. BEGIN
  128. IF NOT pg_has_role(session_user,'dataops_bi_ai_catalog_issuer','MEMBER') OR p_action NOT IN ('lease','record','read')
  129. OR jsonb_typeof(p_body)<>'object' OR p_principal !~ '^[A-Za-z0-9_.:-]{1,120}$' THEN RAISE EXCEPTION 'metering_claim_denied'; END IF;
  130. SELECT tenant_ref,domain_ref,role_name INTO v_tenant,v_domain,v_role FROM public.metering_scope_grants
  131. WHERE principal_ref=p_principal AND active AND revoked_at IS NULL ORDER BY CASE role_name WHEN 'admin' THEN 1 WHEN 'operator' THEN 2 ELSE 3 END LIMIT 1;
  132. IF NOT FOUND OR (p_action IN ('lease','record') AND v_role NOT IN ('operator','admin')) THEN RAISE EXCEPTION 'metering_claim_denied'; END IF;
  133. INSERT INTO public.metering_runtime_claims(action_name,principal_ref,tenant_ref,domain_ref,request_digest,expires_at)
  134. VALUES(p_action,p_principal,v_tenant,v_domain,encode(sha256(convert_to(p_action||'|'||p_body::text,'utf8')),'hex'),clock_timestamp()+interval '30 seconds') RETURNING claim_uid INTO v_claim;
  135. RETURN jsonb_build_object('request_claim',v_claim);
  136. END; $claim$;
  137. ALTER FUNCTION public.metering_showback_issue_claim(text,text,jsonb) OWNER TO dataops_tenant_foundation_owner;
  138. CREATE FUNCTION public.metering_showback_runtime_write(p_action text,p_payload jsonb)
  139. RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $write$
  140. DECLARE v_claim record; v_fence bigint; v_digest text; v_existing text;
  141. BEGIN
  142. IF p_action='chargeback' THEN RAISE EXCEPTION 'chargeback_disabled'; END IF;
  143. IF NOT pg_has_role(session_user,'dataops_app_runtime','MEMBER') OR p_action NOT IN ('claim_lease','record_event') OR jsonb_typeof(p_payload)<>'object' THEN RAISE EXCEPTION 'metering_runtime_denied'; END IF;
  144. IF NOT (p_payload ? 'request_claim') OR p_payload->>'request_claim' !~ '^[0-9a-f-]{36}$' THEN RAISE EXCEPTION 'metering_payload_closed'; END IF;
  145. SELECT * INTO v_claim FROM public.metering_runtime_claims WHERE claim_uid=(p_payload->>'request_claim')::uuid AND consumed_at IS NULL AND expires_at>clock_timestamp() FOR UPDATE;
  146. IF NOT FOUND OR v_claim.action_name<>(CASE WHEN p_action='claim_lease' THEN 'lease' ELSE 'record' END) THEN RAISE EXCEPTION 'metering_claim_denied'; END IF;
  147. IF p_action='claim_lease' THEN
  148. IF p_payload-ARRAY['request_claim','lease_owner','lease_token']<>'{}'::jsonb OR p_payload->>'lease_owner' !~ '^[A-Za-z0-9_.:-]{1,120}$' OR p_payload->>'lease_token' !~ '^[0-9a-f-]{36}$' THEN RAISE EXCEPTION 'metering_payload_closed'; END IF;
  149. INSERT INTO public.metering_runtime_leases(tenant_ref) VALUES(v_claim.tenant_ref) ON CONFLICT DO NOTHING;
  150. UPDATE public.metering_runtime_leases SET lease_owner=p_payload->>'lease_owner',lease_token=(p_payload->>'lease_token')::uuid,lease_fence=lease_fence+1,lease_expires_at=clock_timestamp()+interval '30 seconds',updated_at=clock_timestamp()
  151. WHERE tenant_ref=v_claim.tenant_ref AND (lease_expires_at IS NULL OR lease_expires_at<=clock_timestamp()) RETURNING lease_fence INTO v_fence;
  152. IF NOT FOUND THEN RAISE EXCEPTION 'metering_lease_unavailable'; END IF;
  153. UPDATE public.metering_runtime_claims SET consumed_at=clock_timestamp() WHERE claim_uid=v_claim.claim_uid;
  154. INSERT INTO public.metering_audit_events(tenant_ref,domain_ref,event_type,actor_ref,payload_digest,lease_fence) VALUES(v_claim.tenant_ref,v_claim.domain_ref,'metering.lease.claimed',v_claim.principal_ref,encode(sha256(convert_to(p_payload::text,'utf8')),'hex'),v_fence);
  155. RETURN jsonb_build_object('lease_fence',v_fence);
  156. END IF;
  157. IF p_payload-ARRAY['request_claim','lease_owner','lease_token','lease_fence','event_uid','event_kind','occurred_at','window_start','window_end','quantity_micros','unit','idempotency_key','evidence','mapping','correction_of']<>'{}'::jsonb
  158. OR NOT (p_payload ?& ARRAY['request_claim','lease_owner','lease_token','lease_fence','event_uid','event_kind','occurred_at','window_start','window_end','quantity_micros','unit','idempotency_key','evidence','mapping'])
  159. OR jsonb_typeof(p_payload->'evidence')<>'object' OR jsonb_typeof(p_payload->'mapping')<>'object' OR jsonb_typeof(p_payload->'quantity_micros')<>'number'
  160. OR (p_payload->'evidence')-ARRAY['digest','reference']<>'{}'::jsonb OR (p_payload->'mapping')-ARRAY['department','business_domain','project','cost_center']<>'{}'::jsonb
  161. OR p_payload->>'event_kind' NOT IN ('query','api','file','subscription','storage','compute','task','model_call')
  162. OR p_payload->>'event_uid' !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$' OR p_payload->>'idempotency_key' !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
  163. OR p_payload->>'quantity_micros' !~ '^[0-9]{1,18}$' OR p_payload->>'unit' NOT IN ('bytes','kb','mb','gb','seconds','milliseconds','requests','tasks','tokens')
  164. OR p_payload->'evidence'->>'digest' !~ '^[0-9a-f]{64}$' OR p_payload->'evidence'->>'reference' !~ '^local-fixture://wp12/v1(/[A-Za-z0-9._:-]{1,80})?$'
  165. OR p_payload->'mapping'->>'department' !~ '^[a-z][a-z0-9-]{0,62}$' OR p_payload->'mapping'->>'business_domain'<>v_claim.domain_ref
  166. OR p_payload->'mapping'->>'project' !~ '^[a-z][a-z0-9-]{0,62}$' OR p_payload->'mapping'->>'cost_center' !~ '^[a-z][a-z0-9-]{0,62}$' THEN RAISE EXCEPTION 'metering_payload_closed'; END IF;
  167. SELECT lease_fence INTO v_fence FROM public.metering_runtime_leases WHERE tenant_ref=v_claim.tenant_ref AND lease_owner=p_payload->>'lease_owner' AND lease_token=(p_payload->>'lease_token')::uuid AND lease_expires_at>clock_timestamp() FOR UPDATE;
  168. IF NOT FOUND OR v_fence<>(p_payload->>'lease_fence')::bigint THEN RAISE EXCEPTION 'metering_stale_fence'; END IF;
  169. -- Claims and leases are one-use transport fences, not event
  170. -- identity. Exact replay of the same immutable event may obtain a
  171. -- fresh claim and a newer lease fence after a process restart.
  172. v_digest:=encode(sha256(convert_to((p_payload-ARRAY['request_claim','lease_owner','lease_token','lease_fence'])::text,'utf8')),'hex');
  173. SELECT request_digest INTO v_existing FROM public.metering_events WHERE tenant_ref=v_claim.tenant_ref AND idempotency_key=p_payload->>'idempotency_key' FOR UPDATE;
  174. IF FOUND THEN
  175. IF v_existing<>v_digest THEN RAISE EXCEPTION 'metering_replay_conflict'; END IF;
  176. UPDATE public.metering_runtime_claims SET consumed_at=clock_timestamp() WHERE claim_uid=v_claim.claim_uid;
  177. RETURN jsonb_build_object('event_uid',(SELECT event_uid FROM public.metering_events WHERE tenant_ref=v_claim.tenant_ref AND idempotency_key=p_payload->>'idempotency_key'),'replay',true,'lease_fence',v_fence);
  178. END IF;
  179. IF p_payload ? 'correction_of' AND NOT EXISTS(SELECT 1 FROM public.metering_events WHERE event_uid=p_payload->>'correction_of' AND tenant_ref=v_claim.tenant_ref) THEN RAISE EXCEPTION 'metering_correction_missing'; END IF;
  180. INSERT INTO public.metering_events(event_uid,tenant_ref,domain_ref,department_ref,project_ref,cost_center_ref,event_kind,occurred_at,window_start,window_end,quantity_micros,unit,idempotency_key,evidence_digest,evidence_reference,request_digest,correction_of,lease_fence)
  181. VALUES(p_payload->>'event_uid',v_claim.tenant_ref,v_claim.domain_ref,p_payload->'mapping'->>'department',p_payload->'mapping'->>'project',p_payload->'mapping'->>'cost_center',p_payload->>'event_kind',(p_payload->>'occurred_at')::timestamptz,(p_payload->>'window_start')::timestamptz,(p_payload->>'window_end')::timestamptz,(p_payload->>'quantity_micros')::bigint,p_payload->>'unit',p_payload->>'idempotency_key',p_payload->'evidence'->>'digest',p_payload->'evidence'->>'reference',v_digest,p_payload->>'correction_of',v_fence);
  182. UPDATE public.metering_runtime_claims SET consumed_at=clock_timestamp() WHERE claim_uid=v_claim.claim_uid;
  183. INSERT INTO public.metering_audit_events(tenant_ref,domain_ref,event_type,actor_ref,payload_digest,lease_fence) VALUES(v_claim.tenant_ref,v_claim.domain_ref,CASE WHEN p_payload ? 'correction_of' THEN 'metering.corrected' ELSE 'metering.recorded' END,v_claim.principal_ref,v_digest,v_fence);
  184. RETURN jsonb_build_object('event_uid',p_payload->>'event_uid','replay',false,'lease_fence',v_fence,'persisted_before_ack',true);
  185. END; $write$;
  186. ALTER FUNCTION public.metering_showback_runtime_write(text,jsonb) OWNER TO dataops_tenant_foundation_owner;
  187. CREATE FUNCTION public.metering_showback_runtime_read(p_action text,p_payload jsonb)
  188. RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $read$
  189. DECLARE v_claim record; v_window text; v_source bigint; v_allocated bigint; v_report jsonb;
  190. BEGIN
  191. IF NOT pg_has_role(session_user,'dataops_app_runtime','MEMBER') OR p_action NOT IN ('showback','reconciliation','audit') OR jsonb_typeof(p_payload)<>'object'
  192. OR p_payload-ARRAY['request_claim','window']<>'{}'::jsonb OR p_payload->>'request_claim' !~ '^[0-9a-f-]{36}$' OR p_payload->>'window' !~ '^[0-9]{4}-(0[1-9]|1[0-2])$' THEN RAISE EXCEPTION 'metering_read_denied'; END IF;
  193. SELECT * INTO v_claim FROM public.metering_runtime_claims WHERE claim_uid=(p_payload->>'request_claim')::uuid AND action_name='read' AND consumed_at IS NULL AND expires_at>clock_timestamp() FOR UPDATE;
  194. IF NOT FOUND THEN RAISE EXCEPTION 'metering_claim_denied'; END IF;
  195. v_window:=p_payload->>'window';
  196. IF p_action='audit' THEN
  197. v_report:=COALESCE((SELECT jsonb_agg(jsonb_build_object('event_type',event_type,'payload_digest',payload_digest,'lease_fence',lease_fence) ORDER BY created_at) FROM (SELECT * FROM public.metering_audit_events WHERE tenant_ref=v_claim.tenant_ref AND domain_ref=v_claim.domain_ref ORDER BY created_at DESC LIMIT 100) a),'[]'::jsonb);
  198. ELSE
  199. SELECT COALESCE(sum(quantity_micros),0) INTO v_source FROM public.metering_events WHERE tenant_ref=v_claim.tenant_ref AND domain_ref=v_claim.domain_ref AND to_char(window_start AT TIME ZONE 'UTC','YYYY-MM')=v_window;
  200. v_allocated:=v_source;
  201. INSERT INTO public.metering_reconciliation_reports(tenant_ref,domain_ref,window_key,source_micros,allocated_micros,difference_micros,report_digest)
  202. VALUES(v_claim.tenant_ref,v_claim.domain_ref,v_window,v_source,v_allocated,v_source-v_allocated,encode(sha256(convert_to(v_claim.tenant_ref||'|'||v_claim.domain_ref||'|'||v_window||'|'||v_source::text,'utf8')),'hex')) ON CONFLICT DO NOTHING;
  203. v_report:=jsonb_build_object('window',v_window,'source_micros',v_source,'allocated_micros',v_allocated,'difference_micros',v_source-v_allocated,'mode','ENGINEERING_EVIDENCE_ONLY','chargeback_enabled',false);
  204. END IF;
  205. UPDATE public.metering_runtime_claims SET consumed_at=clock_timestamp() WHERE claim_uid=v_claim.claim_uid;
  206. INSERT INTO public.metering_audit_events(tenant_ref,domain_ref,event_type,actor_ref,payload_digest) VALUES(v_claim.tenant_ref,v_claim.domain_ref,'showback.read',v_claim.principal_ref,encode(sha256(convert_to(p_action||'|'||p_payload::text,'utf8')),'hex'));
  207. RETURN v_report;
  208. END; $read$;
  209. ALTER FUNCTION public.metering_showback_runtime_read(text,jsonb) OWNER TO dataops_tenant_foundation_owner;
  210. REVOKE ALL ON TABLE public.metering_events FROM PUBLIC,dataops_app,dataops_app_runtime;
  211. REVOKE ALL ON TABLE public.metering_scope_grants,public.metering_showback_configuration,public.metering_runtime_leases,public.metering_runtime_claims,public.metering_allocation_rules,public.metering_allocations,public.metering_budgets,public.metering_alert_outbox,public.metering_reconciliation_reports,public.metering_audit_events FROM PUBLIC,dataops_app,dataops_app_runtime;
  212. REVOKE ALL ON FUNCTION public.metering_showback_issue_claim(text,text,jsonb),public.metering_showback_runtime_write(text,jsonb),public.metering_showback_runtime_read(text,jsonb) FROM PUBLIC,dataops_app,dataops_app_runtime;
  213. GRANT USAGE ON SCHEMA public TO dataops_app_runtime,dataops_bi_ai_catalog_control;
  214. GRANT EXECUTE ON FUNCTION public.metering_showback_issue_claim(text,text,jsonb) TO dataops_bi_ai_catalog_control;
  215. GRANT EXECUTE ON FUNCTION public.metering_showback_runtime_write(text,jsonb),public.metering_showback_runtime_read(text,jsonb) TO dataops_app_runtime;
  216. '''
  217. )
  218. def downgrade() -> None:
  219. bind = op.get_bind()
  220. # A fresh schema contains one immutable disabled configuration row. It is
  221. # not a metering fact and must not make an otherwise clean revision
  222. # impossible to roll back; all persisted usage, allocation, alert, report,
  223. # audit, claim, lease and scope evidence remains a hard downgrade fence.
  224. checks = " OR ".join(
  225. f"EXISTS(SELECT 1 FROM public.{table} LIMIT 1)"
  226. for table in _TABLES
  227. if table != "metering_showback_configuration"
  228. )
  229. if bind.exec_driver_sql(f"SELECT {checks}").scalar():
  230. raise RuntimeError("downgrade refused: WP12 metering facts are nonempty")
  231. bind.exec_driver_sql(
  232. "REVOKE EXECUTE ON FUNCTION public.metering_showback_issue_claim(text,text,jsonb),public.metering_showback_runtime_write(text,jsonb),public.metering_showback_runtime_read(text,jsonb) FROM dataops_bi_ai_catalog_control,dataops_app_runtime; "
  233. "DROP FUNCTION IF EXISTS public.metering_showback_runtime_read(text,jsonb); "
  234. "DROP FUNCTION IF EXISTS public.metering_showback_runtime_write(text,jsonb); "
  235. "DROP FUNCTION IF EXISTS public.metering_showback_issue_claim(text,text,jsonb); "
  236. "DROP TABLE IF EXISTS public.metering_audit_events,public.metering_reconciliation_reports,public.metering_alert_outbox,public.metering_budgets,public.metering_allocations,public.metering_allocation_rules,public.metering_events,public.metering_runtime_claims,public.metering_runtime_leases,public.metering_showback_configuration,public.metering_scope_grants;"
  237. )