diff --git a/README.md b/README.md index 62ea566..9d9b1af 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ DDS 변형의 능력 (멀티테넌트, OAuth2 federated identity, MAC 모드, | VPD 백오피스 화면 | `src/main/resources/templates/` | Thymeleaf 서버 렌더링 화면 | | VPD 백오피스 CSS/JS | `src/main/resources/static/` | 화면 동작과 스타일 | | VPD/ASO/FGA/ORDS/Select AI SQL | `sql/adb/` | ADB 적용·검증 SQL 스크립트 | +| 원격 원장 seed/검증 SQL | `sql/source/` | PostgreSQL·MySQL 가상 원장과 HMM 선사 실적 데이터 | | DDS 백오피스 | `dds-backoffice/` | VPD와 분리된 DDS 전용 Spring Boot 데모 | | Streamlit 외피 / PoC4 MCP AI Console | `poc4_active_source_20260714/` | ReAct-style로 MCP tool을 호출하는 PoC4 데모 소스 snapshot | | Streamlit 외피 메인 화면 | `poc4_active_source_20260714/apps/poc4/mcp_discovery_ui.py` | MCP discovery, tool routing, ReAct-style agent loop, FGA evidence 표시 | @@ -97,6 +98,7 @@ Git 기준은 다음과 같다. | 계층 | 객체 | 역할 | |---|---|---| | 원격 | `public.customers` (PG), `ecommerce_poc.customers` (MySQL) | 원본 데이터 | +| 원격 HMM 데모 | `hmm_demo.carriers`, `hmm_demo.carrier_monthly_performance` (PG) | 가상 선사 기준정보와 최근 18개월 월간 KPI | | ADB - 연결 | `RDS_POSTGRES_LINK`, `RDS_LINK` | `DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK` 로 만든 heterogeneous DB Link | | ADB - 매핑 | `app_customer`, `app_user`, `app_group`, `user_group`, `db_source`, `permission` | 누가 어느 소스의 어느 region 을 볼 수 있는지 | | ADB - 컨텍스트 | `vpd_ctx` (Secure Application Context) + `ctx_pkg` | 로그인 시 권한을 세션 컨텍스트로 로딩 | @@ -261,6 +263,8 @@ CREATE DATA GRANT admin.alice_apac_grant ├── sql/ │ ├── source/ │ │ ├── postgres_setup.sql # 원격 PG: customers + 12 rows +│ │ ├── postgres_hmm_carrier_performance.sql # 원격 PG: HMM 가상 선사 8개 + 월간 KPI 144 rows +│ │ ├── postgres_hmm_carrier_performance_verify.sql # HMM 선사 실적 건수·패턴·제약·TLS·권한 검증 │ │ └── mysql_setup.sql # 원격 MySQL: customers + 12 rows │ └── adb/ │ ├── 00_cleanup.sql # 멱등 teardown diff --git a/docs/design/729-rds-carrier-performance-demo/README.md b/docs/design/729-rds-carrier-performance-demo/README.md new file mode 100644 index 0000000..897c6a2 --- /dev/null +++ b/docs/design/729-rds-carrier-performance-demo/README.md @@ -0,0 +1,139 @@ +# HMM RDS PostgreSQL 가상 선사 실적 데이터 + +- Redmine: #729 +- 적용 브랜치: `hmm-backoffice` +- 원격 원장: AWS RDS for PostgreSQL +- 대상 스키마: `hmm_demo` +- 기준월 범위: 2025-02-01 ~ 2026-07-01 + +## 1. 배경 + +HMM 데모는 Oracle ADB의 직원·조직·권한 데이터와 AWS RDS PostgreSQL의 선사 실적을 +federation으로 결합한다. 사용자는 ADB에서 발급한 토큰으로 식별하고, 팀원은 자신에게 +배정된 선사만, 팀장은 팀원에게 배정된 선사 전체를 조회하는 흐름을 보여준다. + +이번 단계에서는 federation 연결 전에 RDS에 재현 가능한 가상 선사 실적 원장을 만든다. +실제 회사나 실제 재무 실적처럼 오인되지 않도록 모든 선사명과 수치는 데모 전용으로 +생성한다. + +## 2. 시스템별 책임 + +| 시스템 | 보관 정보 | 접근 제어 책임 | +|---|---|---| +| RDS PostgreSQL | 선사 기본정보, 선사별 월간 KPI | 전용 스키마와 조회 전용 역할 | +| Oracle ADB | 직원, 팀, 담당 선사 배정, 사용자 토큰 | VPD와 사용자 세션 context | +| Select AI/MCP | 자연어 질문, 보안 View 조회 | ADB에서 허용한 View만 사용 | + +직원 사번이나 팀 정보는 RDS에 복제하지 않는다. RDS 데이터에는 선사 코드만 두고, 직원과 +선사의 배정 관계는 후속 ADB 단계에서 `HMM_CARRIER_ASSIGNMENTS`로 관리한다. + +## 3. 데이터 모델 + +### 3.1 `hmm_demo.carriers` + +가상 선사 기준정보다. + +| 컬럼 | 형식 | 설명 | +|---|---|---| +| `carrier_code` | `varchar(10)` | 가상 선사 식별 코드, PK | +| `carrier_name` | `varchar(120)` | 가상 선사명, UNIQUE | +| `country_code` | `char(2)` | ISO 형식의 데모 국가 코드 | +| `alliance_name` | `varchar(80)` | 가상 협력 네트워크 | +| `service_region` | `varchar(40)` | 주력 운항 권역 | +| `performance_tier` | `varchar(10)` | `CORE`, `GROWTH`, `WATCH` | +| `active` | `boolean` | 사용 여부 | +| `created_at` | `timestamptz` | 최초 생성 시각 | + +### 3.2 `hmm_demo.carrier_monthly_performance` + +선사별 월간 실적 fact다. 한 선사·한 기준월에 한 행만 허용한다. + +| 컬럼 | 형식 | 설명 | +|---|---|---| +| `carrier_code` | `varchar(10)` | 선사 코드, FK | +| `performance_month` | `date` | 월의 첫째 날 | +| `contracted_teu` | `integer` | 계약 물동량 | +| `shipped_teu` | `integer` | 실제 운송 물동량 | +| `revenue_usd` | `numeric(16,2)` | 매출 | +| `gross_margin_usd` | `numeric(16,2)` | 매출총이익, 위험 패턴은 음수 허용 | +| `schedule_reliability_pct` | `numeric(5,2)` | 정시 운항률 | +| `vessel_utilization_pct` | `numeric(5,2)` | 선복 활용률 | +| `claim_rate_pct` | `numeric(5,2)` | 클레임 비율 | +| `yoy_growth_pct` | `numeric(7,2)` | 전년 동월 대비 성장률 | +| `risk_level` | `varchar(10)` | `GREEN`, `AMBER`, `RED` | +| `updated_at` | `timestamptz` | 적재 시각 | + +PK는 `(carrier_code, performance_month)`다. 퍼센트 값은 0~100, 물동량과 매출은 0 이상을 +CHECK 제약으로 보장한다. 월별 조회와 위험 선사 조회에 각각 인덱스를 둔다. + +### 3.3 `hmm_demo.carrier_performance_latest_v` + +가장 최근 기준월의 선사명과 KPI를 한 번에 조회하는 View다. 후속 federation 점검과 +백오피스 미리보기에서 사용한다. + +## 4. 가상 데이터 + +8개 가상 선사와 18개월 실적 144행을 만든다. + +| 코드 | 가상 선사 | 의도한 데모 패턴 | +|---|---|---| +| `C001` | Bluewave Maritime | 높은 정시율과 지속 성장 | +| `C002` | Pacific Star Lines | 안정적 실적 | +| `C003` | Korea Ocean Link | 정시율과 클레임률 회복 | +| `C004` | Nordic Seaways | 계절성 변동 | +| `C005` | Orient Bridge Shipping | 점진적 물동량·마진 하락 | +| `C006` | Atlantic Merchant Line | 정시율 저하와 클레임 증가 | +| `C007` | Global Horizon Cargo | 빠른 성장 | +| `C008` | Southern Cross Marine | 지속적인 고위험 패턴 | + +모든 수치는 프로파일 상수와 월 순번으로 계산한다. 난수를 사용하지 않으므로 어느 환경에서 +다시 실행해도 같은 결과가 만들어진다. + +## 5. 재실행과 변경 규칙 + +- `CREATE SCHEMA/TABLE IF NOT EXISTS`를 사용한다. +- 기준정보와 실적은 PK 기준 `ON CONFLICT DO UPDATE`로 현행화한다. +- 스크립트가 소유하지 않은 행은 삭제하지 않는다. +- 전체 실행은 하나의 transaction으로 처리한다. +- 오류가 발생하면 `ON_ERROR_STOP`과 transaction rollback으로 부분 적재를 방지한다. +- 비밀번호, endpoint, CA 파일 경로는 SQL에 넣지 않는다. + +## 6. 보안 + +- `hmm_federation_reader`는 `NOLOGIN` 그룹 역할로 만든다. +- 해당 역할에는 `hmm_demo`의 `USAGE`와 테이블/View `SELECT`만 부여한다. +- ADB database link용 LOGIN 역할과 비밀번호는 federation 단계에서 별도로 생성하고 + `hmm_federation_reader` 역할만 상속시킨다. +- DDL/DML 권한과 `public` 스키마 변경 권한은 federation 역할에 부여하지 않는다. +- RDS 연결은 AWS global CA bundle과 `sslmode=verify-full`로 검증한다. + +## 7. 적재 순서 + +1. RDS TLS와 현재 계정의 CREATE 권한을 확인한다. +2. `sql/source/postgres_hmm_carrier_performance.sql`을 실행한다. +3. `sql/source/postgres_hmm_carrier_performance_verify.sql`을 실행한다. +4. 재실행 후에도 동일한 선사 8개와 실적 144행인지 확인한다. +5. 후속 작업에서 ADB credential과 PostgreSQL database link를 만든다. + +## 8. 검증 기준 + +- 선사 수: `8` +- 월간 실적: `144` +- 선사별 월 수: 모두 `18` +- 기간: `2025-02-01` ~ `2026-07-01` +- 최신월 위험 분포에 `GREEN`, `AMBER`, `RED`가 모두 존재 +- `C001`의 최신 매출이 최초 매출보다 증가 +- `C005`, `C006`, `C008`의 최신 매출이 최초 매출보다 감소 +- 존재하지 않는 선사 코드, 잘못된 퍼센트, 월 첫째 날이 아닌 날짜 입력 거부 +- `hmm_federation_reader`에 `SELECT`만 부여 +- TLS 1.2 이상으로 실제 조회 성공 + +## 9. 제외 범위 + +- ADB의 `HMM_CARRIER_ASSIGNMENTS` 생성과 직원별 배정 +- ADB PostgreSQL database link 생성 +- ADB 보안 View와 VPD 정책 +- Select AI profile 및 MCP tool 변경 +- 실제 선사명이나 실제 경영 실적 적재 + +이 항목들은 RDS 가상 원장이 검증된 뒤 별도 단계로 진행한다. diff --git a/sql/source/postgres_hmm_carrier_performance.sql b/sql/source/postgres_hmm_carrier_performance.sql new file mode 100644 index 0000000..509895b --- /dev/null +++ b/sql/source/postgres_hmm_carrier_performance.sql @@ -0,0 +1,300 @@ +-- ============================================================ +-- HMM demo carrier performance source data for PostgreSQL. +-- +-- Target: AWS RDS for PostgreSQL, database postgres +-- Schema: hmm_demo +-- Redmine: #729 +-- +-- This script is idempotent. It owns only C001..C008 and their +-- 2025-02 through 2026-07 monthly demo rows. It never stores an +-- RDS endpoint, password, or certificate path. +-- +-- Example: +-- PGPASSWORD="$PG_PASSWORD" psql \ +-- "host=$PG_HOST port=5432 dbname=postgres user=$PG_USER \ +-- sslmode=verify-full sslrootcert=$PG_SSL_ROOT_CERT" \ +-- -X -f sql/source/postgres_hmm_carrier_performance.sql +-- ============================================================ + +\set ON_ERROR_STOP on +\echo === HMM carrier performance demo: begin === + +BEGIN; + +CREATE SCHEMA IF NOT EXISTS hmm_demo AUTHORIZATION postgres; +COMMENT ON SCHEMA hmm_demo IS + 'HMM federation demonstration data. All carrier names and KPI values are fictional.'; + +REVOKE CREATE ON SCHEMA hmm_demo FROM PUBLIC; + +CREATE TABLE IF NOT EXISTS hmm_demo.carriers ( + carrier_code VARCHAR(10) PRIMARY KEY, + carrier_name VARCHAR(120) NOT NULL UNIQUE, + country_code CHAR(2) NOT NULL CHECK (country_code ~ '^[A-Z]{2}$'), + alliance_name VARCHAR(80), + service_region VARCHAR(40) NOT NULL, + performance_tier VARCHAR(10) NOT NULL + CHECK (performance_tier IN ('CORE', 'GROWTH', 'WATCH')), + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE hmm_demo.carriers IS + 'Fictional carrier master data for the HMM ADB and PostgreSQL federation demo.'; +COMMENT ON COLUMN hmm_demo.carriers.carrier_code IS 'Fictional carrier identifier.'; +COMMENT ON COLUMN hmm_demo.carriers.performance_tier IS + 'Demo portfolio tier: CORE, GROWTH, or WATCH.'; + +CREATE TABLE IF NOT EXISTS hmm_demo.carrier_monthly_performance ( + carrier_code VARCHAR(10) NOT NULL + REFERENCES hmm_demo.carriers (carrier_code), + performance_month DATE NOT NULL + CHECK (performance_month = date_trunc('month', performance_month)::DATE), + contracted_teu INTEGER NOT NULL CHECK (contracted_teu >= 0), + shipped_teu INTEGER NOT NULL CHECK (shipped_teu >= 0), + revenue_usd NUMERIC(16,2) NOT NULL CHECK (revenue_usd >= 0), + gross_margin_usd NUMERIC(16,2) NOT NULL, + schedule_reliability_pct NUMERIC(5,2) NOT NULL + CHECK (schedule_reliability_pct BETWEEN 0 AND 100), + vessel_utilization_pct NUMERIC(5,2) NOT NULL + CHECK (vessel_utilization_pct BETWEEN 0 AND 100), + claim_rate_pct NUMERIC(5,2) NOT NULL + CHECK (claim_rate_pct BETWEEN 0 AND 100), + yoy_growth_pct NUMERIC(7,2) NOT NULL, + risk_level VARCHAR(10) NOT NULL + CHECK (risk_level IN ('GREEN', 'AMBER', 'RED')), + updated_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (carrier_code, performance_month) +); + +COMMENT ON TABLE hmm_demo.carrier_monthly_performance IS + 'Deterministic fictional monthly carrier KPIs for 2025-02 through 2026-07.'; +COMMENT ON COLUMN hmm_demo.carrier_monthly_performance.performance_month IS + 'First calendar day of the KPI month.'; +COMMENT ON COLUMN hmm_demo.carrier_monthly_performance.risk_level IS + 'Derived demo risk level based on margin, reliability, and claim rate.'; + +CREATE INDEX IF NOT EXISTS ix_carrier_perf_month + ON hmm_demo.carrier_monthly_performance (performance_month, carrier_code); +CREATE INDEX IF NOT EXISTS ix_carrier_perf_risk + ON hmm_demo.carrier_monthly_performance (risk_level, performance_month); + +INSERT INTO hmm_demo.carriers ( + carrier_code, + carrier_name, + country_code, + alliance_name, + service_region, + performance_tier, + active +) VALUES + ('C001', 'Bluewave Maritime', 'SG', 'North Pacific Network', 'ASIA-PACIFIC', 'CORE', TRUE), + ('C002', 'Pacific Star Lines', 'JP', 'East Ocean Connect', 'ASIA-PACIFIC', 'CORE', TRUE), + ('C003', 'Korea Ocean Link', 'KR', 'East Ocean Connect', 'ASIA-PACIFIC', 'GROWTH', TRUE), + ('C004', 'Nordic Seaways', 'DK', 'Northern Trade Group', 'EUROPE', 'CORE', TRUE), + ('C005', 'Orient Bridge Shipping', 'TW', 'North Pacific Network', 'ASIA-PACIFIC', 'WATCH', TRUE), + ('C006', 'Atlantic Merchant Line', 'DE', 'Atlantic Cargo Forum', 'EUROPE-AMERICAS', 'WATCH', TRUE), + ('C007', 'Global Horizon Cargo', 'US', 'Atlantic Cargo Forum', 'AMERICAS', 'GROWTH', TRUE), + ('C008', 'Southern Cross Marine', 'FR', 'Southern Ocean Group', 'OCEANIA', 'WATCH', TRUE) +ON CONFLICT (carrier_code) DO UPDATE SET + carrier_name = EXCLUDED.carrier_name, + country_code = EXCLUDED.country_code, + alliance_name = EXCLUDED.alliance_name, + service_region = EXCLUDED.service_region, + performance_tier = EXCLUDED.performance_tier, + active = EXCLUDED.active; + +WITH month_series AS ( + SELECT + month_index, + (DATE '2025-02-01' + make_interval(months => month_index))::DATE AS performance_month, + CASE month_index % 6 + WHEN 0 THEN -1 + WHEN 1 THEN 0 + WHEN 2 THEN 1 + WHEN 3 THEN 2 + WHEN 4 THEN 1 + ELSE -1 + END::NUMERIC AS seasonal_factor + FROM generate_series(0, 17) AS series(month_index) +), +profiles ( + carrier_code, + base_teu, + monthly_teu_step, + seasonal_teu, + utilization_base, + utilization_step, + revenue_per_teu, + revenue_step, + margin_pct_base, + margin_pct_step, + reliability_base, + reliability_step, + claim_rate_base, + claim_rate_step, + yoy_growth_base, + yoy_growth_step +) AS ( + VALUES + ('C001', 1800::NUMERIC, 25::NUMERIC, 80::NUMERIC, 91.5::NUMERIC, 0.10::NUMERIC, 1850::NUMERIC, 4::NUMERIC, 15.0::NUMERIC, 0.10::NUMERIC, 95.0::NUMERIC, 0.05::NUMERIC, 0.35::NUMERIC, -0.005::NUMERIC, 8.0::NUMERIC, 0.25::NUMERIC), + ('C002', 1500::NUMERIC, 5::NUMERIC, 60::NUMERIC, 88.5::NUMERIC, 0.02::NUMERIC, 1780::NUMERIC, 3::NUMERIC, 12.0::NUMERIC, 0.00::NUMERIC, 92.5::NUMERIC, 0.00::NUMERIC, 0.45::NUMERIC, 0.005::NUMERIC, 3.0::NUMERIC, 0.05::NUMERIC), + ('C003', 1050::NUMERIC, 15::NUMERIC, 50::NUMERIC, 83.0::NUMERIC, 0.20::NUMERIC, 1920::NUMERIC, 2::NUMERIC, 8.0::NUMERIC, 0.15::NUMERIC, 88.5::NUMERIC, 0.25::NUMERIC, 1.20::NUMERIC, -0.040::NUMERIC, -4.0::NUMERIC, 0.60::NUMERIC), + ('C004', 1350::NUMERIC, 0::NUMERIC, 180::NUMERIC, 86.0::NUMERIC, 0.00::NUMERIC, 1810::NUMERIC, 3::NUMERIC, 10.0::NUMERIC, 0.00::NUMERIC, 93.0::NUMERIC, 0.00::NUMERIC, 0.55::NUMERIC, 0.010::NUMERIC, 1.0::NUMERIC, 0.10::NUMERIC), + ('C005', 1300::NUMERIC, -20::NUMERIC, 70::NUMERIC, 88.0::NUMERIC, -0.10::NUMERIC, 1850::NUMERIC, -2::NUMERIC, 8.0::NUMERIC, -0.20::NUMERIC, 91.0::NUMERIC, -0.15::NUMERIC, 0.80::NUMERIC, 0.030::NUMERIC, -2.0::NUMERIC, -0.50::NUMERIC), + ('C006', 1250::NUMERIC, -10::NUMERIC, 90::NUMERIC, 84.0::NUMERIC, -0.12::NUMERIC, 1800::NUMERIC, -3::NUMERIC, 6.0::NUMERIC, -0.15::NUMERIC, 87.0::NUMERIC, -0.25::NUMERIC, 1.50::NUMERIC, 0.050::NUMERIC, -5.0::NUMERIC, -0.70::NUMERIC), + ('C007', 800::NUMERIC, 30::NUMERIC, 100::NUMERIC, 82.0::NUMERIC, 0.25::NUMERIC, 2050::NUMERIC, 5::NUMERIC, 9.0::NUMERIC, 0.18::NUMERIC, 90.5::NUMERIC, 0.15::NUMERIC, 0.70::NUMERIC, -0.020::NUMERIC, 12.0::NUMERIC, 0.50::NUMERIC), + ('C008', 1150::NUMERIC, -25::NUMERIC, 60::NUMERIC, 80.0::NUMERIC, -0.15::NUMERIC, 1750::NUMERIC, -4::NUMERIC, 3.0::NUMERIC, -0.20::NUMERIC, 85.0::NUMERIC, -0.30::NUMERIC, 2.00::NUMERIC, 0.080::NUMERIC, -8.0::NUMERIC, -0.80::NUMERIC) +), +base_metrics AS ( + SELECT + p.carrier_code, + m.performance_month, + ROUND( + p.base_teu + + p.monthly_teu_step * m.month_index + + p.seasonal_teu * m.seasonal_factor + )::INTEGER AS contracted_teu, + LEAST( + 98.0::NUMERIC, + GREATEST( + 60.0::NUMERIC, + p.utilization_base + + p.utilization_step * m.month_index + + m.seasonal_factor * 0.50 + ) + ) AS utilization_pct, + p.revenue_per_teu + p.revenue_step * m.month_index AS unit_revenue, + p.margin_pct_base + p.margin_pct_step * m.month_index AS margin_pct, + LEAST( + 99.5::NUMERIC, + GREATEST( + 60.0::NUMERIC, + p.reliability_base + + p.reliability_step * m.month_index + + m.seasonal_factor * 0.20 + ) + ) AS reliability_pct, + LEAST( + 100.0::NUMERIC, + GREATEST( + 0.0::NUMERIC, + p.claim_rate_base + p.claim_rate_step * m.month_index + ) + ) AS claim_pct, + p.yoy_growth_base + p.yoy_growth_step * m.month_index AS yoy_pct + FROM profiles p + CROSS JOIN month_series m +), +volume_metrics AS ( + SELECT + b.*, + ROUND(b.contracted_teu * b.utilization_pct / 100.0)::INTEGER AS shipped_teu + FROM base_metrics b +), +financial_metrics AS ( + SELECT + v.*, + ROUND(v.shipped_teu * v.unit_revenue, 2) AS revenue_usd + FROM volume_metrics v +), +final_metrics AS ( + SELECT + f.*, + ROUND(f.revenue_usd * f.margin_pct / 100.0, 2) AS gross_margin_usd + FROM financial_metrics f +) +INSERT INTO hmm_demo.carrier_monthly_performance ( + carrier_code, + performance_month, + contracted_teu, + shipped_teu, + revenue_usd, + gross_margin_usd, + schedule_reliability_pct, + vessel_utilization_pct, + claim_rate_pct, + yoy_growth_pct, + risk_level, + updated_at +) +SELECT + f.carrier_code, + f.performance_month, + f.contracted_teu, + f.shipped_teu, + f.revenue_usd, + f.gross_margin_usd, + ROUND(f.reliability_pct, 2), + ROUND(f.utilization_pct, 2), + ROUND(f.claim_pct, 2), + ROUND(f.yoy_pct, 2), + CASE + WHEN f.gross_margin_usd < 0 + OR f.reliability_pct < 87 + OR f.claim_pct >= 2 THEN 'RED' + WHEN f.margin_pct < 7 + OR f.reliability_pct < 92 + OR f.claim_pct >= 1 THEN 'AMBER' + ELSE 'GREEN' + END, + TIMESTAMPTZ '2026-07-24 00:00:00+00' +FROM final_metrics f +ON CONFLICT (carrier_code, performance_month) DO UPDATE SET + contracted_teu = EXCLUDED.contracted_teu, + shipped_teu = EXCLUDED.shipped_teu, + revenue_usd = EXCLUDED.revenue_usd, + gross_margin_usd = EXCLUDED.gross_margin_usd, + schedule_reliability_pct = EXCLUDED.schedule_reliability_pct, + vessel_utilization_pct = EXCLUDED.vessel_utilization_pct, + claim_rate_pct = EXCLUDED.claim_rate_pct, + yoy_growth_pct = EXCLUDED.yoy_growth_pct, + risk_level = EXCLUDED.risk_level, + updated_at = EXCLUDED.updated_at; + +CREATE OR REPLACE VIEW hmm_demo.carrier_performance_latest_v AS +SELECT + c.carrier_code, + c.carrier_name, + c.country_code, + c.alliance_name, + c.service_region, + c.performance_tier, + p.performance_month, + p.contracted_teu, + p.shipped_teu, + p.revenue_usd, + p.gross_margin_usd, + p.schedule_reliability_pct, + p.vessel_utilization_pct, + p.claim_rate_pct, + p.yoy_growth_pct, + p.risk_level +FROM hmm_demo.carriers c +JOIN hmm_demo.carrier_monthly_performance p + ON p.carrier_code = c.carrier_code +WHERE p.performance_month = ( + SELECT MAX(latest.performance_month) + FROM hmm_demo.carrier_monthly_performance latest +); + +COMMENT ON VIEW hmm_demo.carrier_performance_latest_v IS + 'Latest fictional monthly KPI for each active demo carrier.'; + +DO $role$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hmm_federation_reader') THEN + CREATE ROLE hmm_federation_reader NOLOGIN; + END IF; +END +$role$; + +GRANT USAGE ON SCHEMA hmm_demo TO hmm_federation_reader; +GRANT SELECT ON ALL TABLES IN SCHEMA hmm_demo TO hmm_federation_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA hmm_demo + GRANT SELECT ON TABLES TO hmm_federation_reader; + +COMMIT; + +\echo === HMM carrier performance demo: complete === +\echo Next: run sql/source/postgres_hmm_carrier_performance_verify.sql diff --git a/sql/source/postgres_hmm_carrier_performance_verify.sql b/sql/source/postgres_hmm_carrier_performance_verify.sql new file mode 100644 index 0000000..8d5b4f8 --- /dev/null +++ b/sql/source/postgres_hmm_carrier_performance_verify.sql @@ -0,0 +1,324 @@ +-- ============================================================ +-- Verification for HMM demo carrier performance data. +-- Redmine: #729 +-- ============================================================ + +\set ON_ERROR_STOP on +\pset pager off +\echo === HMM carrier performance demo: verification begin === + +DO $verify$ +DECLARE + carrier_count INTEGER; + performance_count INTEGER; + invalid_month_count INTEGER; + latest_month DATE; + earliest_month DATE; + wrong_series_count INTEGER; + latest_risk_count INTEGER; + wrong_trend_count INTEGER; + reader_can_login BOOLEAN; +BEGIN + SELECT COUNT(*) + INTO carrier_count + FROM hmm_demo.carriers + WHERE carrier_code BETWEEN 'C001' AND 'C008'; + + SELECT COUNT(*), MIN(performance_month), MAX(performance_month) + INTO performance_count, earliest_month, latest_month + FROM hmm_demo.carrier_monthly_performance + WHERE carrier_code BETWEEN 'C001' AND 'C008' + AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01'; + + SELECT COUNT(*) + INTO invalid_month_count + FROM hmm_demo.carrier_monthly_performance + WHERE performance_month <> date_trunc('month', performance_month)::DATE; + + SELECT COUNT(*) + INTO wrong_series_count + FROM ( + SELECT carrier_code, COUNT(*) AS month_count + FROM hmm_demo.carrier_monthly_performance + WHERE carrier_code BETWEEN 'C001' AND 'C008' + AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01' + GROUP BY carrier_code + HAVING COUNT(*) <> 18 + ) wrong_series; + + SELECT COUNT(DISTINCT risk_level) + INTO latest_risk_count + FROM hmm_demo.carrier_performance_latest_v; + + WITH boundaries AS ( + SELECT + carrier_code, + MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2025-02-01') + AS first_revenue, + MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2026-07-01') + AS latest_revenue + FROM hmm_demo.carrier_monthly_performance + WHERE carrier_code BETWEEN 'C001' AND 'C008' + GROUP BY carrier_code + ) + SELECT COUNT(*) + INTO wrong_trend_count + FROM boundaries + WHERE (carrier_code IN ('C001', 'C002', 'C003', 'C004', 'C007') + AND latest_revenue <= first_revenue) + OR (carrier_code IN ('C005', 'C006', 'C008') + AND latest_revenue >= first_revenue); + + SELECT rolcanlogin + INTO reader_can_login + FROM pg_roles + WHERE rolname = 'hmm_federation_reader'; + + IF carrier_count <> 8 THEN + RAISE EXCEPTION 'Expected 8 carriers, found %', carrier_count; + END IF; + IF performance_count <> 144 THEN + RAISE EXCEPTION 'Expected 144 monthly performance rows, found %', performance_count; + END IF; + IF earliest_month <> DATE '2025-02-01' OR latest_month <> DATE '2026-07-01' THEN + RAISE EXCEPTION 'Unexpected period: % through %', earliest_month, latest_month; + END IF; + IF invalid_month_count <> 0 OR wrong_series_count <> 0 THEN + RAISE EXCEPTION + 'Invalid monthly series: invalid dates %, wrong carrier series %', + invalid_month_count, + wrong_series_count; + END IF; + IF latest_risk_count <> 3 THEN + RAISE EXCEPTION 'Expected GREEN, AMBER, RED latest risks, found % levels', + latest_risk_count; + END IF; + IF wrong_trend_count <> 0 THEN + RAISE EXCEPTION 'Unexpected first-to-latest trend for % carriers', wrong_trend_count; + END IF; + IF reader_can_login IS DISTINCT FROM FALSE THEN + RAISE EXCEPTION 'hmm_federation_reader must remain a NOLOGIN role'; + END IF; + + IF NOT has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'USAGE') THEN + RAISE EXCEPTION 'hmm_federation_reader lacks schema USAGE'; + END IF; + IF has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'CREATE') THEN + RAISE EXCEPTION 'hmm_federation_reader unexpectedly has schema CREATE'; + END IF; + IF NOT has_table_privilege( + 'hmm_federation_reader', + 'hmm_demo.carrier_monthly_performance', + 'SELECT' + ) THEN + RAISE EXCEPTION 'hmm_federation_reader lacks SELECT'; + END IF; + IF has_table_privilege( + 'hmm_federation_reader', + 'hmm_demo.carrier_monthly_performance', + 'INSERT' + ) THEN + RAISE EXCEPTION 'hmm_federation_reader unexpectedly has INSERT'; + END IF; +END +$verify$; + +DO $constraint_tests$ +BEGIN + BEGIN + INSERT INTO hmm_demo.carrier_monthly_performance ( + carrier_code, + performance_month, + contracted_teu, + shipped_teu, + revenue_usd, + gross_margin_usd, + schedule_reliability_pct, + vessel_utilization_pct, + claim_rate_pct, + yoy_growth_pct, + risk_level, + updated_at + ) VALUES ( + 'C001', + DATE '2026-07-02', + 1, + 1, + 1, + 1, + 90, + 90, + 1, + 1, + 'GREEN', + CURRENT_TIMESTAMP + ); + RAISE EXCEPTION 'Month-first-day constraint did not reject invalid input'; + EXCEPTION + WHEN check_violation THEN + NULL; + END; + + BEGIN + INSERT INTO hmm_demo.carrier_monthly_performance ( + carrier_code, + performance_month, + contracted_teu, + shipped_teu, + revenue_usd, + gross_margin_usd, + schedule_reliability_pct, + vessel_utilization_pct, + claim_rate_pct, + yoy_growth_pct, + risk_level, + updated_at + ) VALUES ( + 'UNKNOWN', + DATE '2026-08-01', + 1, + 1, + 1, + 1, + 90, + 90, + 1, + 1, + 'GREEN', + CURRENT_TIMESTAMP + ); + RAISE EXCEPTION 'Foreign key constraint did not reject unknown carrier'; + EXCEPTION + WHEN foreign_key_violation THEN + NULL; + END; + + BEGIN + INSERT INTO hmm_demo.carrier_monthly_performance ( + carrier_code, + performance_month, + contracted_teu, + shipped_teu, + revenue_usd, + gross_margin_usd, + schedule_reliability_pct, + vessel_utilization_pct, + claim_rate_pct, + yoy_growth_pct, + risk_level, + updated_at + ) VALUES ( + 'C001', + DATE '2026-08-01', + 1, + 1, + 1, + 1, + 101, + 90, + 1, + 1, + 'GREEN', + CURRENT_TIMESTAMP + ); + RAISE EXCEPTION 'Percentage constraint did not reject invalid input'; + EXCEPTION + WHEN check_violation THEN + NULL; + END; +END +$constraint_tests$; + +\echo --- Row counts and period --- +SELECT + COUNT(DISTINCT carrier_code) AS carrier_count, + COUNT(*) AS monthly_row_count, + MIN(performance_month) AS first_month, + MAX(performance_month) AS latest_month +FROM hmm_demo.carrier_monthly_performance +WHERE carrier_code BETWEEN 'C001' AND 'C008' + AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01'; + +\echo --- Per-carrier series --- +SELECT + c.carrier_code, + c.carrier_name, + COUNT(p.*) AS month_count, + MIN(p.performance_month) AS first_month, + MAX(p.performance_month) AS latest_month +FROM hmm_demo.carriers c +JOIN hmm_demo.carrier_monthly_performance p + ON p.carrier_code = c.carrier_code +WHERE c.carrier_code BETWEEN 'C001' AND 'C008' +GROUP BY c.carrier_code, c.carrier_name +ORDER BY c.carrier_code; + +\echo --- Latest KPI and risk distribution --- +SELECT + carrier_code, + carrier_name, + performance_month, + shipped_teu, + revenue_usd, + gross_margin_usd, + schedule_reliability_pct, + claim_rate_pct, + risk_level +FROM hmm_demo.carrier_performance_latest_v +ORDER BY carrier_code; + +SELECT risk_level, COUNT(*) AS carrier_count +FROM hmm_demo.carrier_performance_latest_v +GROUP BY risk_level +ORDER BY risk_level; + +\echo --- First-to-latest revenue trend --- +WITH boundaries AS ( + SELECT + carrier_code, + MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2025-02-01') AS first_revenue, + MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2026-07-01') AS latest_revenue + FROM hmm_demo.carrier_monthly_performance + WHERE carrier_code BETWEEN 'C001' AND 'C008' + GROUP BY carrier_code +) +SELECT + carrier_code, + first_revenue, + latest_revenue, + CASE + WHEN latest_revenue > first_revenue THEN 'UP' + WHEN latest_revenue < first_revenue THEN 'DOWN' + ELSE 'FLAT' + END AS trend +FROM boundaries +ORDER BY carrier_code; + +\echo --- TLS session --- +SELECT + ssl, + version AS tls_version, + cipher, + bits +FROM pg_stat_ssl +WHERE pid = pg_backend_pid(); + +\echo --- Read-only role privileges --- +SELECT + (SELECT NOT rolcanlogin + FROM pg_roles + WHERE rolname = 'hmm_federation_reader') AS no_login, + has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'USAGE') AS schema_usage, + has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'CREATE') AS schema_create, + has_table_privilege( + 'hmm_federation_reader', + 'hmm_demo.carrier_monthly_performance', + 'SELECT' + ) AS can_select, + has_table_privilege( + 'hmm_federation_reader', + 'hmm_demo.carrier_monthly_performance', + 'INSERT' + ) AS can_insert; + +\echo === HMM carrier performance demo: verification complete ===