fix: hc900_map_master UNIQUE 인덱스 변경 + FF/Stream 컬럼 마이그레이션

- hc900_map_master: UNIQUE(tagname) → UNIQUE(controller_id, tagname)
  - 동일 태그명이 여러 컨트롤러에 존재 가능 (peer comms)
- ff_column_config: ALTER TABLE ADD COLUMN IF NOT EXISTS 10개 누락 컬럼
  (feed_tag, pressure_tag, level_tags, feed_filter_tau_sec 등)
- ff_stream_config: stream_key → key RENAME COLUMN + 12개 컬럼 추가
  (flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec, tau_sec,
   sp_min, sp_max, rate_up_per_min, rate_dn_per_min, reflux_from_product, grade)
- EF Core HasIndex(tagname).IsUnique() → HasIndex(controller_id, tagname).IsUnique()
- GetSubArea/UpdateSubArea/DeletePoint: ToLowerInvariant 제거
  → OrdinalIgnoreCase 비교로 대체
This commit is contained in:
windpacer
2026-06-04 09:43:48 +09:00
parent daeb5316a2
commit 78a532ae41

View File

@@ -44,7 +44,7 @@ public class Hc900DbContext : DbContext
modelBuilder.Entity<Hc900MapEntry>(e => modelBuilder.Entity<Hc900MapEntry>(e =>
{ {
e.HasKey(x => x.Id); e.HasKey(x => x.Id);
e.HasIndex(x => x.TagName).IsUnique(); e.HasIndex(x => new { x.ControllerId, x.TagName }).IsUnique();
e.HasIndex(x => x.Hc900Tag); e.HasIndex(x => x.Hc900Tag);
}); });
@@ -461,6 +461,30 @@ public class Hc900DbService : IExperionDbService
await _ctx.Database.ExecuteSqlRawAsync( await _ctx.Database.ExecuteSqlRawAsync(
"ALTER TABLE hc900_map_master ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'"); "ALTER TABLE hc900_map_master ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1'");
// hc900_map_master: tagname is unique PER controller, not globally — the same
// SignalTag name can exist on several controllers (peer comms). Replace the old
// UNIQUE(tagname) with UNIQUE(controller_id, tagname).
await _ctx.Database.ExecuteSqlRawAsync("""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'hc900_map_master'::regclass
AND conname = 'hc900_map_master_tagname_key'
) THEN
ALTER TABLE hc900_map_master DROP CONSTRAINT hc900_map_master_tagname_key;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'hc900_map_master'
AND indexname = 'ux_hc900_map_ctrl_tag'
) THEN
CREATE UNIQUE INDEX ux_hc900_map_ctrl_tag
ON hc900_map_master(controller_id, tagname);
END IF;
END $$;
""");
// realtime_table: UNIQUE(controller_id, tagname) for ON CONFLICT upsert // realtime_table: UNIQUE(controller_id, tagname) for ON CONFLICT upsert
await _ctx.Database.ExecuteSqlRawAsync(""" await _ctx.Database.ExecuteSqlRawAsync("""
DO $$ DO $$
@@ -1155,6 +1179,36 @@ public class Hc900DbService : IExperionDbService
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_tag TEXT; ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_flood_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9; ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_flood_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS temp_high_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9; ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS temp_high_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9;
-- migration: missing columns from the original CREATE TABLE (schema was json-based)
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS feed_tag TEXT NOT NULL DEFAULT '';
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS pressure_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS level_tags TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS feed_filter_tau_sec DOUBLE PRECISION NOT NULL DEFAULT 300;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS feed_move_thr_per_min DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS press_filter_tau_sec DOUBLE PRECISION NOT NULL DEFAULT 60;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS pressure_band DOUBLE PRECISION NOT NULL DEFAULT 1e9;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS settle_sec DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS stale_sec DOUBLE PRECISION NOT NULL DEFAULT 120;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS product_key TEXT NOT NULL DEFAULT 'P';
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='hc900' AND table_name='ff_stream_config' AND column_name='stream_key')
AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='hc900' AND table_name='ff_stream_config' AND column_name='key') THEN
ALTER TABLE hc900.ff_stream_config RENAME COLUMN stream_key TO key;
END IF;
END $$;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS flow_tag TEXT NOT NULL DEFAULT '';
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'Monitor';
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS target_coeff DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS theta_up_sec DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS theta_dn_sec DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS tau_sec DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS sp_min DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS sp_max DOUBLE PRECISION NOT NULL DEFAULT 1e9;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS rate_up_per_min DOUBLE PRECISION NOT NULL DEFAULT 1e9;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS rate_dn_per_min DOUBLE PRECISION NOT NULL DEFAULT 1e9;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS reflux_from_product BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS grade TEXT NOT NULL DEFAULT 'A';
"""); """);
// ── FF operator action audit log ──────────────────────────────── // ── FF operator action audit log ────────────────────────────────
@@ -1414,9 +1468,8 @@ public class Hc900DbService : IExperionDbService
var point = await _ctx.RealtimePoints.FindAsync(id); var point = await _ctx.RealtimePoints.FindAsync(id);
if (point == null) return new PointDeleteResult { Deleted = false }; if (point == null) return new PointDeleteResult { Deleted = false };
var tagName = point.TagName; // 예: "fi-6101.pv" var tagName = point.TagName; // 예: "FICQ-6101.pv"
var baseTag = (tagName.Contains('.') ? tagName[..tagName.IndexOf('.')] : tagName) var baseTag = tagName.Contains('.') ? tagName[..tagName.IndexOf('.')] : tagName;
.ToLowerInvariant(); // "fi-6101"
_ctx.RealtimePoints.Remove(point); _ctx.RealtimePoints.Remove(point);
await _ctx.SaveChangesAsync(); await _ctx.SaveChangesAsync();
@@ -1924,9 +1977,9 @@ public class Hc900DbService : IExperionDbService
public async Task<string?> GetSubAreaByTagNameAsync(string tagName) public async Task<string?> GetSubAreaByTagNameAsync(string tagName)
{ {
var baseTag = (tagName.Contains('.') ? tagName[..tagName.LastIndexOf('.')] : tagName).ToLowerInvariant(); var baseTag = tagName.Contains('.') ? tagName[..tagName.LastIndexOf('.')] : tagName;
return await _ctx.TagMetadata return await _ctx.TagMetadata
.Where(m => m.BaseTag == baseTag && m.Attribute == "sub_area") .Where(m => m.BaseTag.Equals(baseTag, StringComparison.OrdinalIgnoreCase) && m.Attribute == "sub_area")
.Select(m => m.Value) .Select(m => m.Value)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
@@ -1991,7 +2044,6 @@ public class Hc900DbService : IExperionDbService
public async Task<bool> UpdateSubAreaAsync(string baseTag, string? subArea) public async Task<bool> UpdateSubAreaAsync(string baseTag, string? subArea)
{ {
baseTag = baseTag.ToLowerInvariant();
if (string.IsNullOrWhiteSpace(subArea)) if (string.IsNullOrWhiteSpace(subArea))
{ {
var deleted = await _ctx.Database.ExecuteSqlRawAsync( var deleted = await _ctx.Database.ExecuteSqlRawAsync(