-- 012: 어구 그룹 모선 추론 저장소 + sub_cluster/resolution 스키마 정합성 SET search_path TO kcg, public; -- ── live lab과 repo 마이그레이션 정합성 맞추기 ───────────────────── ALTER TABLE kcg.group_polygon_snapshots ADD COLUMN IF NOT EXISTS sub_cluster_id SMALLINT NOT NULL DEFAULT 0; ALTER TABLE kcg.group_polygon_snapshots ADD COLUMN IF NOT EXISTS resolution VARCHAR(20) NOT NULL DEFAULT '6h'; CREATE INDEX IF NOT EXISTS idx_gps_type_res_time ON kcg.group_polygon_snapshots(group_type, resolution, snapshot_time DESC); CREATE INDEX IF NOT EXISTS idx_gps_key_res_time ON kcg.group_polygon_snapshots(group_key, resolution, snapshot_time DESC); CREATE INDEX IF NOT EXISTS idx_gps_key_sub_time ON kcg.group_polygon_snapshots(group_key, sub_cluster_id, snapshot_time DESC); ALTER TABLE kcg.gear_correlation_raw_metrics ADD COLUMN IF NOT EXISTS sub_cluster_id SMALLINT NOT NULL DEFAULT 0; CREATE INDEX IF NOT EXISTS idx_raw_metrics_group_sub_time ON kcg.gear_correlation_raw_metrics(group_key, sub_cluster_id, observed_at DESC); ALTER TABLE kcg.gear_correlation_scores ADD COLUMN IF NOT EXISTS sub_cluster_id SMALLINT NOT NULL DEFAULT 0; ALTER TABLE kcg.gear_correlation_scores DROP CONSTRAINT IF EXISTS gear_correlation_scores_model_id_group_key_target_mmsi_key; DROP INDEX IF EXISTS kcg.gear_correlation_scores_model_id_group_key_target_mmsi_key; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE connamespace = 'kcg'::regnamespace AND conrelid = 'kcg.gear_correlation_scores'::regclass AND conname = 'gear_correlation_scores_unique' ) THEN ALTER TABLE kcg.gear_correlation_scores ADD CONSTRAINT gear_correlation_scores_unique UNIQUE (model_id, group_key, sub_cluster_id, target_mmsi); END IF; END; $$ LANGUAGE plpgsql; CREATE INDEX IF NOT EXISTS idx_gc_model_group_sub ON kcg.gear_correlation_scores(model_id, group_key, sub_cluster_id, current_score DESC); -- ── 그룹 단위 모선 추론 저장소 ───────────────────────────────────── CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_candidate_snapshots ( id BIGSERIAL PRIMARY KEY, observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), group_key VARCHAR(100) NOT NULL, sub_cluster_id SMALLINT NOT NULL DEFAULT 0, parent_name TEXT NOT NULL, candidate_mmsi VARCHAR(20) NOT NULL, candidate_name VARCHAR(200), candidate_vessel_id INT REFERENCES kcg.fleet_vessels(id) ON DELETE SET NULL, rank INT NOT NULL, candidate_source VARCHAR(100) NOT NULL, model_id INT REFERENCES kcg.correlation_param_models(id) ON DELETE SET NULL, model_name VARCHAR(100), base_corr_score DOUBLE PRECISION DEFAULT 0, name_match_score DOUBLE PRECISION DEFAULT 0, track_similarity_score DOUBLE PRECISION DEFAULT 0, visit_score_6h DOUBLE PRECISION DEFAULT 0, proximity_score_6h DOUBLE PRECISION DEFAULT 0, activity_sync_score_6h DOUBLE PRECISION DEFAULT 0, stability_score DOUBLE PRECISION DEFAULT 0, registry_bonus DOUBLE PRECISION DEFAULT 0, final_score DOUBLE PRECISION DEFAULT 0, margin_from_top DOUBLE PRECISION DEFAULT 0, evidence JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (observed_at, group_key, sub_cluster_id, candidate_mmsi) ); CREATE INDEX IF NOT EXISTS idx_ggpcs_group_time ON kcg.gear_group_parent_candidate_snapshots(group_key, sub_cluster_id, observed_at DESC, rank ASC); CREATE INDEX IF NOT EXISTS idx_ggpcs_candidate ON kcg.gear_group_parent_candidate_snapshots(candidate_mmsi, observed_at DESC); CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_resolution ( group_key VARCHAR(100) NOT NULL, sub_cluster_id SMALLINT NOT NULL DEFAULT 0, parent_name TEXT NOT NULL, normalized_parent_name VARCHAR(200), status VARCHAR(40) NOT NULL, selected_parent_mmsi VARCHAR(20), selected_parent_name VARCHAR(200), selected_vessel_id INT REFERENCES kcg.fleet_vessels(id) ON DELETE SET NULL, confidence DOUBLE PRECISION, decision_source VARCHAR(40), top_score DOUBLE PRECISION DEFAULT 0, second_score DOUBLE PRECISION DEFAULT 0, score_margin DOUBLE PRECISION DEFAULT 0, stable_cycles INT DEFAULT 0, last_evaluated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_promoted_at TIMESTAMPTZ, approved_by VARCHAR(100), approved_at TIMESTAMPTZ, manual_comment TEXT, rejected_candidate_mmsi VARCHAR(20), rejected_at TIMESTAMPTZ, evidence_summary JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (group_key, sub_cluster_id) ); CREATE INDEX IF NOT EXISTS idx_ggpr_status ON kcg.gear_group_parent_resolution(status, last_evaluated_at DESC); CREATE INDEX IF NOT EXISTS idx_ggpr_parent ON kcg.gear_group_parent_resolution(selected_parent_mmsi); CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_review_log ( id BIGSERIAL PRIMARY KEY, group_key VARCHAR(100) NOT NULL, sub_cluster_id SMALLINT NOT NULL DEFAULT 0, action VARCHAR(20) NOT NULL, selected_parent_mmsi VARCHAR(20), actor VARCHAR(100) NOT NULL, comment TEXT, payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_ggprl_group_time ON kcg.gear_group_parent_review_log(group_key, sub_cluster_id, created_at DESC); -- ── copied schema 환경의 sequence 정렬 ───────────────────────────── SELECT setval( pg_get_serial_sequence('kcg.fleet_companies', 'id'), COALESCE((SELECT MAX(id) FROM kcg.fleet_companies), 1), TRUE ); SELECT setval( pg_get_serial_sequence('kcg.fleet_vessels', 'id'), COALESCE((SELECT MAX(id) FROM kcg.fleet_vessels), 1), TRUE ); SELECT setval( pg_get_serial_sequence('kcg.gear_identity_log', 'id'), COALESCE((SELECT MAX(id) FROM kcg.gear_identity_log), 1), TRUE ); SELECT setval( pg_get_serial_sequence('kcg.fleet_tracking_snapshot', 'id'), COALESCE((SELECT MAX(id) FROM kcg.fleet_tracking_snapshot), 1), TRUE ); SELECT setval( pg_get_serial_sequence('kcg.group_polygon_snapshots', 'id'), COALESCE((SELECT MAX(id) FROM kcg.group_polygon_snapshots), 1), TRUE ); SELECT setval( pg_get_serial_sequence('kcg.gear_correlation_scores', 'id'), COALESCE((SELECT MAX(id) FROM kcg.gear_correlation_scores), 1), TRUE );