-- ============================================================================ -- Migration: Add controller_id columns for multi-controller support -- Run this once against the existing HC900 database. -- ============================================================================ BEGIN; -- 1. realtime_table: add controller_id + UNIQUE(controller_id, tagname) ALTER TABLE realtime_table ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'; -- Drop any existing unique constraint on tagname alone (idempotent wrapper) DO $$ BEGIN IF EXISTS ( SELECT 1 FROM pg_indexes WHERE tablename = 'realtime_table' AND indexname = 'idx_realtime_table_tagname_unique' ) THEN DROP INDEX IF EXISTS idx_realtime_table_tagname_unique; END IF; IF EXISTS ( SELECT 1 FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid WHERE t.relname = 'realtime_table' AND c.contype = 'u' ) THEN -- There's a unique constraint; we can't easily drop it by name unless we know it. -- Use a safe approach: drop any unique constraint on tagname END IF; END $$; -- Add the new composite UNIQUE (required for ON CONFLICT (controller_id, tagname) DO UPDATE) -- Skip if already exists DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE tablename = 'realtime_table' AND indexname = 'idx_realtime_table_ctrl_tag_unique' ) THEN CREATE UNIQUE INDEX idx_realtime_table_ctrl_tag_unique ON realtime_table(controller_id, tagname); END IF; END $$; -- 2. history_table: add controller_id with default 'HC1' ALTER TABLE history_table ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'; -- 3. event_history_table: add controller_id ALTER TABLE event_history_table ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'; -- 4. tag_metadata: add controller_id ALTER TABLE tag_metadata ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'; -- 5. hc900_map_master: add controller_id ALTER TABLE hc900_map_master ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'; -- 6. v_tag_summary: recreate with controller_id DROP VIEW IF EXISTS v_tag_summary CASCADE; CREATE VIEW v_tag_summary AS SELECT rt_base.base_tag, pv_rt.livevalue AS pv, sp_rt.livevalue AS sp, op_rt.livevalue AS op, instate0_rt.livevalue AS instate0, instate1_rt.livevalue AS instate1, instate2_rt.livevalue AS instate2, desc_md.value AS description, area_md.value AS area, sub_area_md.value AS sub_area, rt_base.controller_id FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag, controller_id FROM realtime_table) rt_base LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv' AND pv_rt.controller_id = rt_base.controller_id LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp' AND sp_rt.controller_id = rt_base.controller_id LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op' AND op_rt.controller_id = rt_base.controller_id LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0' AND instate0_rt.controller_id = rt_base.controller_id LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1' AND instate1_rt.controller_id = rt_base.controller_id LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2' AND instate2_rt.controller_id = rt_base.controller_id LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc' LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area' LEFT JOIN tag_metadata sub_area_md ON sub_area_md.base_tag = rt_base.base_tag AND sub_area_md.attribute = 'sub_area'; -- 7. v_plant_running_state: recreate (depends on v_tag_summary) DROP VIEW IF EXISTS v_plant_running_state; CREATE VIEW v_plant_running_state AS WITH pump_state AS ( SELECT trim(split_part(area, '|', 2)) AS area_code, area AS area_raw, base_tag, pv, controller_id FROM v_tag_summary WHERE area IS NOT NULL AND (base_tag LIKE 'p-%' OR base_tag LIKE 'vp-%') AND pv ~ '\|\s*(L-RUN|R-RUN|L-STOP|R-STOP|L-TRIP|R-TRIP)\s*\|' ) SELECT area_code, MAX(area_raw) AS area_raw, COUNT(*) AS total_pumps, COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') AS running_pumps, COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-TRIP\s*\|') AS tripped_pumps, COUNT(*) FILTER (WHERE pv ~ '\|\s*(L-STOP|R-STOP)\s*\|') AS stopped_pumps, CASE WHEN COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') > 0 THEN 'RUNNING' WHEN COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-TRIP\s*\|') > 0 THEN 'TRIPPED' ELSE 'STOPPED' END AS status, array_agg(base_tag) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') AS running_pump_tags FROM pump_state WHERE area_code IS NOT NULL AND area_code <> '' GROUP BY area_code ORDER BY area_code; COMMIT;